pythonpyside2qtreeviewqstyleditemdelegate

Change foreground color of items based on the item's text itself


I have a QTreeView with 5 columns and I set up a QComboBox delegate for 2 of them (columns 1 and 2). Both of them have the same delegate and they must have the same drop down list. Let's say the list is always: ["Next", "Stop"]. So far so good: the delegate works perfectly.

Now the problem: I wanted items of column 1 and 2 to display text in different colors based on the text itself. For example: if the text is "Next", text's color should be green and if the text is "Stop", the color should be red.

After some searching I decided to use the delegate to set the color. I found different solutions but the only one almost working for me was this (see paint() function):

class ComboBoxDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        painter.save()
        text = index.data(Qt.DisplayRole)
        if text == 'Next':
            color = GREEN
        elif text == 'Stop':
            color = RED
        else:
            color = BLACK
        painter.setPen(QPen(color))
        painter.drawText(option.rect, Qt.AlignLeft | Qt.AlignVCenter, text)
        painter.restore()


    def createEditor(self, parent, option, index):
        editor = QtWidgets.QComboBox(parent)
        editor.currentIndexChanged.connect(self.commitEditor)
        return editor

    # @QtCore.Slot
    def commitEditor(self):
        editor = self.sender()
        self.commitData.emit(editor)
        if isinstance(self.parent(), QtWidgets.QAbstractItemView):
            self.parent().updateEditorGeometries()
        self.closeEditor.emit(editor)

    def setEditorData(self, editor, index):
        try:
            values = index.data(QtCore.Qt.UserRole + 100)
            val = index.data(QtCore.Qt.UserRole + 101).strip('<>')
            editor.clear()
            for i, x in enumerate(values):
                editor.addItem(x, x)
                if val == x:
                    editor.setCurrentIndex(i)
        except (TypeError, AttributeError):
            LOG.warning(f"No values in drop-down list for item at row: {index.row()} and column: {index.column()}")

    def setModelData(self, editor, model, index):
        values = index.data(QtCore.Qt.UserRole + 100)
        if values:
            ix = editor.currentIndex()
            model.setData(index, values[ix], QtCore.Qt.UserRole + 101)
            model.setData(index, values[ix], QtCore.Qt.DisplayRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

As you can see in the image below, the selection and the hovering are not working properly:

TreeView screenshot

What am I doing wrong?

Minimum reproducible example (remember to import ComboBoxDelegate):

import sys

from PySide2.QtGui import QStandardItemModel, QStandardItem
from PySide2.QtWidgets import QTreeView, QApplication, QMainWindow

from combo_box_delegate import ComboBoxDelegate

COLUMN_HEADER_LIST = ["0", "1", "2", "3", "4"]


class MyTreeView(QTreeView):
    def __init__(self):
        super(MyTreeView, self).__init__()
        self.model = QStandardItemModel()
        self.root = self.model.invisibleRootItem()
        self.setModel(self.model)
        delegate = ComboBoxDelegate(self)
        self.setItemDelegateForColumn(1, delegate)
        self.setItemDelegateForColumn(2, delegate)
        self.data = {
            "a": {
                "b": {
                    "c": ["Next", "Stop", "1", "Hello World!"],
                    "d": ["Next", "Stop", "2", "Hello World!"],
                    "e": ["Next", "Stop", "3", "Hello World!"],
                    "f": ["Next", "Stop", "4", "Hello World!"]
                }
            }
        }
        self.populate_tree(self.root, self.data)
        self._format_columns()

    def populate_tree(self, parent, data):
        for key, value in data.items():
            if isinstance(value, dict):
                child = QStandardItem(key)
                parent.appendRow(child)
                self.populate_tree(child, data[key])
            elif isinstance(value, list):
                items = [QStandardItem(key)] + [QStandardItem(str(val)) for val in value]
                parent.appendRow(items)

    def _format_columns(self):
        self.model.setHorizontalHeaderLabels(COLUMN_HEADER_LIST)
        self.expandAll()


class Main(QMainWindow):
    def __init__(self):
        super(Main, self).__init__()
        tree = MyTreeView()
        self.setCentralWidget(tree)
        self.setMinimumWidth(600)
        self.setMinimumHeight(400)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    main_window = Main()
    main_window.show()
    app.exec_()

Solution

  • The painting of the items is special so in general it is not recommended to override the paint() method but to change the properties of QStyleOptionViewItem in initStyleOption() method that are used for painting, for example changing the QPalette:

    from PySide2.QtCore import Qt
    from PySide2.QtGui import QBrush, QColor, QPalette
    from PySide2.QtWidgets import QAbstractItemView, QComboBox, QStyledItemDelegate
    
    RED = "red"
    BLACK = "black"
    GREEN = "green"
    
    
    class ComboBoxDelegate(QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            text = index.data(Qt.DisplayRole)
            if text == "Next":
                color = GREEN
            elif text == "Stop":
                color = RED
            else:
                color = BLACK
            option.palette.setBrush(QPalette.Text, QBrush(QColor(color)))
    
        def createEditor(self, parent, option, index):
            editor = QComboBox(parent)
            editor.currentIndexChanged.connect(self.commitEditor)
            return editor
    
        # @QtCore.Slot
        def commitEditor(self):
            editor = self.sender()
            self.commitData.emit(editor)
            if isinstance(self.parent(), QAbstractItemView):
                self.parent().updateEditorGeometries()
            self.closeEditor.emit(editor)
    
        def setEditorData(self, editor, index):
            try:
                values = index.data(Qt.UserRole + 100)
                val = index.data(Qt.UserRole + 101).strip("<>")
                editor.clear()
                for i, x in enumerate(values):
                    editor.addItem(x, x)
                    if val == x:
                        editor.setCurrentIndex(i)
            except (TypeError, AttributeError):
                pass
                # LOG.warning(f"No values in drop-down list for item at row: {index.row()} and column: {index.column()}")
    
        def setModelData(self, editor, model, index):
            values = index.data(Qt.UserRole + 100)
            if values:
                ix = editor.currentIndex()
                model.setData(index, values[ix], QtCore.Qt.UserRole + 101)
                model.setData(index, values[ix], QtCore.Qt.DisplayRole)
    
        def updateEditorGeometry(self, editor, option, index):
            editor.setGeometry(option.rect)