A platform-independent QWidget to display tags written in Python/Pyside2.
The widget adapts to the QApplication style palette.
The need for this came from another Qt application that needed a tags display but Qt by default does not have anything like it and it needed to work with PySide2 in Maya.
Another requirement was, the styling needed to pick up the palette from any application the widget is a child of.
Currently tags can be added and removed. No duplicates are allowed and it is using a custom layout method to make the widget responsive to its parent container size.


Short video showing the adding and removing of widget as well as resizing the container.
Here is the code. It is still very work in progress. These are a few features and improvements I’d like to add in the future:
- Size Hint improvements
- Drag and Drop re-ordering
- Priority based on ordering
- Tight packed layout as alternative to fixed order layout
- Explore applicability of MVVM pattern
- Better styling possibilities
The imports. There is nothing special going on here. I could have used the Qt.py but since I am not actively using PyQt4 or PySide anywhere I didn’t see the need for that.
1 2 3 4 5 6 7 |
import os from collections import OrderedDict from math import ceil from PySide2 import QtCore from PySide2 import QtWidgets from PySide2 import QtGui |
The NQTagsView class that creates and manages the individual tags as well as the layout. This class is the one that would be added to a Qt application or window layout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
class NQTagsView(QtWidgets.QWidget): def __init__(self, parent=None): super(NQTagsView, self).__init__(parent=parent) self._size_hint = QtCore.QSize(0, 0) self.font_size = 8 self.font_metrics = QtGui.QFontMetrics(self.font()) self._tag_height = 0 self.add_tag_text = "Add Tag..." self.set_up() self._tags_widgets = OrderedDict(()) self._tags = [] self.current_tag_pos = QtCore.QPointF(0, 0) self._spacing = 4 self.setContentsMargins(4, 4, 4, 4) self.lineEdit_enter = self.line_edit() def set_up(self): font = self.font() font.setPointSize(self.font_size) self.setFont(font) self.font_metrics = QtGui.QFontMetrics(self.font()) self._tag_height = self.font_metrics.boundingRect('J').height() + 2.0 * 4.0 self.setCursor(QtCore.Qt.IBeamCursor) @property def tags(self): _tags = [widget.text for widget in self._tags_widgets] return _tags def line_edit(self): line_edit_enter = QtWidgets.QLineEdit(self) line_edit_enter.setStyleSheet('border: 0px; background-color: rgba(0, 0, 0, 0);') line_edit_enter.setPlaceholderText(self.add_tag_text) line_edit_enter.returnPressed.connect(self.line_edit_return_pressed) line_edit_enter.textChanged.connect(self.line_edit_text_changed) return line_edit_enter def line_edit_return_pressed(self): self.add_tag(self.lineEdit_enter.text()) self.lineEdit_enter.clear() self.line_edit_text_changed(self.add_tag_text) def line_edit_text_changed(self, text): if text == '': text = self.add_tag_text self.lineEdit_enter.setPlaceholderText(text) t_width = self.font_metrics.boundingRect(text).width() self.lineEdit_enter.setFixedWidth(t_width + 10) self.layout_tags() def add_tag(self, text): if text not in self._tags_widgets.keys(): widget = NQTagWidget(text, self) widget.close_clicked.connect(self.tag_close_clicked) widget.show() self._tags_widgets.update({text: widget}) self.layout_tags() def remove_tag(self, text): self._tags_widgets[text].close() self.layout_tags() def clear(self): for tags_widget in self._tags_widgets.values(): tags_widget.close() self._tags_widgets.clear() self.layout_tags() def tag_close_clicked(self, tag): tag.close() self._tags_widgets.pop(tag.text) self.layout_tags() def layout_tags(self): margins = self.contentsMargins() m_left = margins.left() m_top = margins.top() m_right = margins.right() m_bottom = margins.bottom() a_width = self.width() - m_left - m_right self.current_tag_pos = QtCore.QPointF(m_left, m_top) tag_index = 0 for tag_widget in self._tags_widgets.values(): tag = tag_widget if (self.current_tag_pos.x() + tag.size.width() - self._spacing < a_width) or tag_index is 0: tag.move(self.current_tag_pos.toPoint()) self.current_tag_pos = QtCore.QPointF(self.current_tag_pos.x() + tag.size.width() + self._spacing, self.current_tag_pos.y()) else: self.current_tag_pos = QtCore.QPointF(m_left, self.current_tag_pos.y() + tag.size.height() + self._spacing) tag.move(self.current_tag_pos.toPoint()) self.current_tag_pos.setX(self.current_tag_pos.x() + tag.size.width() + self._spacing) tag_index += 1 self.lineEdit_enter.setFixedHeight(self._tag_height) if self.current_tag_pos.x() + self.lineEdit_enter.width() - self._spacing < a_width: self.lineEdit_enter.setGeometry(QtCore.QRect(self.current_tag_pos.toPoint(), self.lineEdit_enter.rect().size())) else: self.current_tag_pos = QtCore.QPointF(m_left, self.current_tag_pos.y() + self._tag_height + self._spacing) self.lineEdit_enter.setGeometry(QtCore.QRect(self.current_tag_pos.toPoint(), self.lineEdit_enter.rect().size())) self._size_hint.setHeight(self.current_tag_pos.y() + self.lineEdit_enter.height() + 5) def resizeEvent(self, resize_event): self.layout_tags() self._size_hint.setWidth(self.parent().width()) return super(NQTagsView, self).resizeEvent(resize_event) def sizeHint(self): # TODO: return correctly calculated size hint here return QtCore.QSize(300, 200) def minimumSizeHint(self): return QtCore.QSize(80, 50) |

Here we have the NQTagWidget class. This is taking care of the drawing of the tag itself and is currently using a color scheme based on the QApplication Style Palette.
Some of the painting get’s a little bit unwieldy trying to stay responsive to the font size and spacing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
class NQTagWidget(QtWidgets.QWidget): close_clicked = QtCore.Signal(object) def __init__(self, text, parent=None): super(NQTagWidget, self).__init__(parent=parent) self.text = text self.parent = parent self.position = QtCore.QPoint(0, 0) self.pos = self.position self.margins = QtCore.QMargins() self.size = QtCore.QSizeF(0, 0) self._mouse_position = QtCore.QPoint(0, 0) self._mouse_over = False self._mouse_down = False self._close_mouse_over = False self._font_metrics = QtGui.QFontMetricsF(self.parent.font()) self._text_bounding_rect = self._font_metrics.boundingRect(self.text) self._style_option = QtWidgets.QStyleOption() self._close_x_width = 1.5 self._close_x_spacing = 2 self._path = QtGui.QPainterPath() self._init_elements() self.setMouseTracking(True) def _init_elements(self): self.margins.setLeft(10) self.margins.setTop(4) self.margins.setRight(0) self.margins.setBottom(0) self._text_rect = QtCore.QRectF(self.position.x() + self.margins.left(), self.position.y() + self.margins.top(), self._text_bounding_rect.width() + 4, self._text_bounding_rect.height()) self._close_rect = QtCore.QRectF(self.position.x() + 2.0 * self.margins.left() + self._text_bounding_rect.width(), self.position.y() + self.margins.top(), self._text_bounding_rect.height(), self._text_bounding_rect.height()) self._bounding_rect = QtCore.QRectF( self.position.x(), self.position.y(), self._text_bounding_rect.width() + self.margins.left() * 2.0 + self._close_rect.width() + self.margins.top(), self._text_bounding_rect.height() + self.margins.top() * 2.0) self._corner_radius = self._text_bounding_rect.height() / 2.0 + self.margins.top() self._path.addRoundedRect(self._bounding_rect, self._corner_radius, self._corner_radius) self._path.translate(0.5, 0.5) self.size = self._bounding_rect.size() self.resize(self.size.toSize() + QtCore.QSize(1, 1)) self.setCursor(QtCore.Qt.ArrowCursor) def enterEvent(self, event): self._mouse_over = True def leaveEvent(self, event): self._mouse_over = False def mousePressEvent(self, mouse_event): self._mouse_down = True return super(NQTagWidget, self).mousePressEvent(mouse_event) def mouseReleaseEvent(self, mouse_event): self._mouse_down = False if self._close_rect.contains(mouse_event.pos()): self.close_clicked.emit(self) return super(NQTagWidget, self).mouseReleaseEvent(mouse_event) def mouseMoveEvent(self, mouse_event): self._close_mouse_over = True if self._close_rect.contains(mouse_event.pos()) else False def paintEvent(self, painter_event, *args, **kwds): painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing, True) path_pen = QtGui.QPen(self._style_option.palette.dark(), 0) path_pen_highlight = QtGui.QPen(self._style_option.palette.highlight(), 0) painter.setPen(path_pen_highlight if self.underMouse() else path_pen) gradient = QtGui.QLinearGradient(self._bounding_rect.topLeft(), self._bounding_rect.bottomLeft()) gradient.setColorAt(0, self._style_option.palette.light().color()) gradient.setColorAt(1, self._style_option.palette.midlight().color()) painter.setBrush(gradient) painter.drawPath(self._path) pen = QtGui.QPen(self._style_option.palette.text().color()) painter.setPen(pen) painter.drawText(self._text_rect, QtCore.Qt.AlignLeft, self.text) self.draw_close_button(painter) painter.end() self.update() return super(NQTagWidget, self).paintEvent(painter_event) def draw_close_button(self, painter): old_pen = painter.pen() old_brush = painter.brush() painter.setPen(QtCore.Qt.NoPen) x_pen = QtGui.QPen(self._style_option.palette.windowText().color(), self._close_x_width) line_fraction = self._close_rect.height() * 0.15 + self._close_x_spacing if self._close_mouse_over: x_pen = QtGui.QPen(self._style_option.palette.highlightedText().color(), self._close_x_width) line_fraction = self._close_rect.height() * 0.2 + self._close_x_spacing brush = QtGui.QBrush(self._style_option.palette.highlight().color()) if self._mouse_down: brush = QtGui.QBrush(self._style_option.palette.highlight().color().lighter(150)) painter.setBrush(brush) painter.drawEllipse(self._close_rect) painter.setPen(x_pen) painter.drawLine(self._close_rect.left() + line_fraction, self._close_rect.top() + line_fraction, self._close_rect.right() - line_fraction + 1, self._close_rect.bottom() - line_fraction + 1) painter.drawLine(self._close_rect.right() - line_fraction + 1, self._close_rect.top() + line_fraction, self._close_rect.left() + line_fraction, self._close_rect.bottom() - line_fraction + 1) painter.setPen(old_pen) painter.setBrush(old_brush) |