Search code examples
pythonpyqt5qcomboboxqstyleditemdelegate

Custom ComboBox using custom model and delegate


I have a list of items in the form (string, integer, bool), like so:

[ ('Item 1', 12, True), ('Item 2', 156, True), ('Item 3', 19, False) ]

which I'd like to display in a QComboBox in the following way:

Custom QCombobox with a green rectangle, grey text to the left and a darker number to the right

  • The first element of the tuple is left-justified
  • The number is right-justified in a darker color
  • If the third element is set to True, a colored rectangle (will be replaced with an icon) is shown.
    If it is set to False, no rectangle is shown, but the space should remain empty, instead of the text being moved to the left

This is what I have so far:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *


class MyModel(QAbstractListModel):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.data = [
            ('Item 1', 12, True),
            ('Item 2', 156, True),
            ('Item 3', 19, False)
        ]

    def data(self, index, role=None):
        if role == Qt.DisplayRole:
            text = self.data[index.row()][0]
            return text

        if role == Qt.DecorationRole:
            status = self.data[index.row()][2]

            if status:
                return QColor('#22FF35')

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.data)


class MyDelegate(QStyledItemDelegate):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def paint(self, painter, option, index):
        painter.save()

        options = QStyleOptionViewItem(option)
        self.initStyleOption(options, index)

        style = options.widget.style()

        style.drawItemText(painter, options.widget.rect(), Qt.AlignLeft,
                           options.widget.palette(), True, options.text,
                           QPalette.Text)

        painter.restore()


class MyComboBox(QComboBox):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setFixedSize(200, 75)

        self.model = MyModel()
        self.setModel(self.model)

        self.setItemDelegate(MyDelegate())


if __name__ == '__main__':
    app = QApplication([])
    window = MyComboBox()
    window.show()
    app.exec()

The code obviously is incomplete and does not achieve what I have in mind.
A few questions arise, like:

How can I pass both the first and the second item to the delegate?
If the model returns a list or a tuple, options.text in the delegate will be empty.


Solution

  • In this case it is only necessary to paint the text on the right over the default one:

    TEXT_ROLE, VALUE_ROLE, STATUS_ROLE = ((Qt.UserRole + i + 1) for i in range(3))
    
    
    class MyModel(QAbstractListModel):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.data = [
                ("Item 1", 12, True),
                ("Item 2", 156, True),
                ("Item 3", 19, False),
                ("Item 4", 126, True),
                ("Item 5", 100, False),
            ]
    
        def data(self, index, role=None):
            if 0 <= index.row() < self.rowCount():
                item = self.data[index.row()]
                if role in (TEXT_ROLE, Qt.DisplayRole):
                    return item[0]
                elif role == VALUE_ROLE:
                    return item[1]
                elif role == STATUS_ROLE:
                    return item[2]
    
        def rowCount(self, parent=QModelIndex()):
            if parent.isValid():
                return 0
            return len(self.data)
    
    
    class MyDelegate(QStyledItemDelegate):
        RIGHT_MARGIN = 4
    
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            status = index.data(STATUS_ROLE)
            option.features |= QStyleOptionViewItem.HasDecoration
            pixmap = QPixmap(option.decorationSize)
            pixmap.fill(QColor("#22FF35" if status else "transparent"))
            option.icon = QIcon(pixmap)
    
        def paint(self, painter, option, index):
            value = index.data(VALUE_ROLE)
            super().paint(painter, option, index)
            painter.drawText(
                option.rect.adjusted(0, 0, -self.RIGHT_MARGIN, 0),
                Qt.AlignRight | Qt.AlignVCenter,
                str(value),
            )