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

712 views Asked by At

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_()
2

There are 2 answers

0
eyllanesc On BEST ANSWER

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)
1
Pavan Chandaka On

The weird selection issues which you notice are may be because of setting the current selection.

First set the root model index and then set the current index. And then set back the tree current item.

Roughly as said below (not tested. Also take care of syntax.)

yourComboBox.setRootModelIndex(yourTree.currentIndex())
yourComboBox.setCurrentIndex(index)
yourTree.setCurrentItem(yourTree.invisibleRootItem(),0)