Search code examples
pythonpyqtpyqt5qcombobox

Item separator in combo with model


I have a drop down which contains heatmaps and solid color names. It also has a 'Custom...' option which will open a color picker when selected. I would like to separate the 'Custom...' item from the hex colors.

enter image description here

When not using a model, adding a separator is easy:

if self.count() > 1:
    self.insertSeparator(self.count()-1)

How can I insert a separator when populating the combo using a model?

import sys
from PyQt5 import QtCore, QtWidgets, QtGui


HEATMAPS = [
    'viridis', 'inferno', 'ocean', 'hot', 'terrain', 'nipy_spectral',
]

COLORS = [
    '#ffffff', '#00ff00', '#0000ff', '#ff0000', '#ffff00',
]


class IconModel(QtCore.QAbstractListModel):

    def __init__(self, items=None, parent=None):
        super().__init__(parent=parent)

        if not items:
            self._items = []
        else:
            self._items = items

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self._items)

    def data(self, index, role):
        if not index.isValid():
            return None

        row = index.row()

        if role == QtCore.Qt.DisplayRole:
            return self._items[row]
        elif role == QtCore.Qt.DecorationRole:
            item = self._items[row]

            if item[0] != '#':
                return None
            else:
                h = item.lstrip('#')
                rgb = tuple(int(h[i:i+2], 16) for i in (0, 2, 4))

                color = QtGui.QColor(*rgb)

                pixmap = QtGui.QPixmap(16, 16)
                pixmap.fill(color)
                icon = QtGui.QIcon(pixmap)

                return icon


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    choices = [*HEATMAPS, 'Custom...', *COLORS]
    model = IconModel(choices)

    combo_box = QtWidgets.QComboBox()
    combo_box.setModel(model)

    def on_current_index_changed(index):
        text = combo_box.itemText(index)
        data = combo_box.itemData(index, QtCore.Qt.UserRole)
        print(index, text, data, flush=True)

    combo_box.currentIndexChanged[int].connect(on_current_index_changed)
    combo_box.show()
    sys.exit(app.exec_())


Solution

  • You have to implement the setData() and insertRow() method of the model since insertSeparator() inserts data (empty string and null QIcon). For example you can use QStandardItemModel for this.

    import sys
    from PyQt5 import QtCore, QtWidgets, QtGui
    
    
    HEATMAPS = [
        "viridis",
        "inferno",
        "ocean",
        "hot",
        "terrain",
        "nipy_spectral",
    ]
    
    COLORS = [
        "#ffffff",
        "#00ff00",
        "#0000ff",
        "#ff0000",
        "#ffff00",
    ]
    
    
    class ColorDelegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            value = index.data()
            if value.startswith("#"):
                option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
                
                pixmap = QtGui.QPixmap(option.decorationSize)
                pixmap.fill(QtGui.QColor(value))
                option.icon = QtGui.QIcon(pixmap)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
    
        model = QtGui.QStandardItemModel()
        for choice in HEATMAPS + ["Custom ..."] + COLORS :
          item = QtGui.QStandardItem(choice)
          model.appendRow(item)
    
    
        combo_box = QtWidgets.QComboBox()
        combo_box.setModel(model)
        combo_box.setItemDelegate(ColorDelegate(model))
        combo_box.insertSeparator(len(HEATMAPS))
        combo_box.insertSeparator(len(HEATMAPS) + 2)
    
        def on_current_index_changed(index):
            text = combo_box.itemText(index)
            data = combo_box.itemData(index, QtCore.Qt.UserRole)
            print(index, text, data, flush=True)
    
        combo_box.currentIndexChanged[int].connect(on_current_index_changed)
        combo_box.show()
        sys.exit(app.exec_())
    

    On the other hand, the model is not necessary since the logic of the icon can be handled by the delegate:

    app = QtWidgets.QApplication(sys.argv)
    
    combo_box = QtWidgets.QComboBox()
    combo_box.addItems(HEATMAPS + ["Custom..."] + COLORS)
    combo_box.setItemDelegate(ColorDelegate(combo_box))
    combo_box.insertSeparator(len(HEATMAPS))
    combo_box.insertSeparator(len(HEATMAPS) + 2)
    
    def on_current_index_changed(index):
        text = combo_box.itemText(index)
        data = combo_box.itemData(index, QtCore.Qt.UserRole)
        print(index, text, data, flush=True)
    
    combo_box.currentIndexChanged[int].connect(on_current_index_changed)
    combo_box.show()
    sys.exit(app.exec_())
    

    If you want to show the icon on the combobox display then a solution could modify the data method of the QStandardItemModel but a more efficient option is to create the icon and set it when the QStandardItem is created:

    app = QtWidgets.QApplication(sys.argv)
    
    model = QtGui.QStandardItemModel()
    for choice in HEATMAPS + ["Custom ..."]:
        item = QtGui.QStandardItem(choice)
        model.appendRow(item)
    
    for color in COLORS:
        item = QtGui.QStandardItem(choice)
        pixmap = QtGui.QPixmap(16, 16)
        pixmap.fill(QtGui.QColor(color))
        icon = QtGui.QIcon(pixmap)
        item.setIcon(icon)
        model.appendRow(item)
    
    combo_box = QtWidgets.QComboBox()
    combo_box.setModel(model)
    combo_box.insertSeparator(len(HEATMAPS))
    combo_box.insertSeparator(len(HEATMAPS) + 2)
    
    def on_current_index_changed(index):
        text = combo_box.itemText(index)
        data = combo_box.itemData(index, QtCore.Qt.UserRole)
        print(index, text, data, flush=True)
    
    combo_box.currentIndexChanged[int].connect(on_current_index_changed)
    combo_box.show()
    sys.exit(app.exec_())