Search code examples
pythonpyqt5qtreeviewqstandarditemmodelqstyleditemdelegate

QStyledItemDelegate subclass implementation of paint()


I have a QStandardItemModel being displayed in two QTreeViews. The first TreeView uses the standard QStyledItemDelegate without any alterations and shows the items with a checkbox, an icon and their display text. The second view is supposed to show the same tree, although a custom implementation of QStyledItemDelegate is supposed to do the following:

  • Show some items' display text in italics
  • Show some items' display text in red
  • Remove the checkbox for every item
  • Make only those items selectable that are not italics

Whether items are in italics/not clickable or not and whether they're displayed in red or not is saved in a custom DataRole and accessed using index.data(custom_role).

My problem is: I can't get text to be shown in red and I have no idea how to remove the checkbox, or how to make items selectable or not.

Here's what I have:

class PostProcessedDelegate(QtWidgets.QStyledItemDelegate):

    def __init__(self):
        super().__init__()

    def paint(self, painter, option, index): 
        custom_option = QtWidgets.QStyleOptionViewItem(option)
        custom_painter = painter

        if not index.data(Item.IS_PROCESSED):
            custom_option.font.setItalic(True)
            #ALSO MAKE ITEM NON-SELECTABLE HERE 
    
        if index.data(Item.HAS_PROCESSING_ERROR):
            custom_painter.setPen(QtGui.QColor(255,0,0)) #THIS DOESNT HAVE ANY EFFECT
            
        #REMOVE CHECKBOX BEFORE PAINTING
        super().paint(custom_painter, custom_option, index) 

This screenshot shows the first TreeView to the left (the one without any alterations) and the second to the right, with only some of the required changes in effect. enter image description here


Solution

  • It is not necessary to override the paint method but to modify the QStyleOptionViewItem in initStyleOption, for selection you must override the selectionCommand method of the QTreeView:

    from enum import IntEnum, auto
    import random
    import sys
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class Item(IntEnum):
        IS_PROCESSED = QtCore.Qt.UserRole
        HAS_PROCESSING_ERROR = auto()
    
    
    def create_icon():
        color = QtGui.QColor(*random.sample(range(255), 3))
        pixmap = QtGui.QPixmap(128, 128)
        pixmap.fill(color)
        return QtGui.QIcon(pixmap)
    
    
    class StyledItemDelegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            if not index.data(Item.IS_PROCESSED):
                option.font.setItalic(True)
    
            if index.data(Item.HAS_PROCESSING_ERROR):
                option.palette.setBrush(QtGui.QPalette.Text, QtGui.QColor(255, 0, 0))
    
            option.features &= ~QtWidgets.QStyleOptionViewItem.HasCheckIndicator
    
    
    class TreeView(QtWidgets.QTreeView):
        def __init__(self, parent=None):
            super().__init__(parent)
            delegate = StyledItemDelegate(self)
            self.setItemDelegate(delegate)
    
        def selectionCommand(self, index, event):
            if not index.data(Item.IS_PROCESSED):
                return QtCore.QItemSelectionModel.NoUpdate
            return super().selectionCommand(index, event)
    
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            model = QtGui.QStandardItemModel(self)
    
            left_view = QtWidgets.QTreeView()
            right_view = TreeView()
    
            lay = QtWidgets.QHBoxLayout(self)
            lay.addWidget(left_view)
            lay.addWidget(right_view)
    
            left_view.setModel(model)
            right_view.setModel(model)
    
            root_item = QtGui.QStandardItem("Root")
            model.appendRow(root_item)
    
            self.populate(root_item, 3)
            left_view.expandAll()
            right_view.expandAll()
    
        def populate(self, root_item, level):
            for i in range(random.randint(2, 4)):
                it = QtGui.QStandardItem("item {}".format(i))
                it.setIcon(create_icon())
                it.setCheckable(True)
                it.setData(random.choice([True, False]), Item.IS_PROCESSED)
                it.setData(random.choice([True, False]), Item.HAS_PROCESSING_ERROR)
                root_item.appendRow(it)
                next_level = level - 1
                if next_level > 0:
                    self.populate(it, next_level)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
    
        w = Widget()
        w.show()
    
        sys.exit(app.exec_())