-1

I am trying to build a custom TreeWidget using a QStyledItemDelegate to draw custom checkboxes. Everything is working fine, except when I resize the TreeWidget columns. As you'll see below, when the "Age" column is moved all the way to the left, the "Name" checkbox from the first child item 'shows through' (even though all the text is properly elided and hidden).

Can anyone suggest why this is happening?

I've tried setting a size hint for the QStyledItemDelegate but this has no effect. Here is a minimum reproducible example:

import sys
from PyQt5 import QtCore, QtWidgets


class CustomTreeWidgetDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None) -> None:
        super().__init__(parent)

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

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

        if options.widget:
            style = option.widget.style()
        else:
            style = QtWidgets.QApplication.style()

        # lets only draw checkboxes for col 0
        if index.column() == 0:

            item_options = QtWidgets.QStyleOptionButton()
            item_options.initFrom(options.widget)

            if options.checkState == QtCore.Qt.Checked:
                item_options.state = (
                    QtWidgets.QStyle.State_On | QtWidgets.QStyle.State_Enabled
                )
            else:
                item_options.state = (
                    QtWidgets.QStyle.State_Off | QtWidgets.QStyle.State_Enabled
                )

            item_options.rect = style.subElementRect(
                QtWidgets.QStyle.SE_ViewItemCheckIndicator, options
            )

            QtWidgets.QApplication.style().drawControl(
                QtWidgets.QStyle.CE_CheckBox, item_options, painter
            )

        if index.data(QtCore.Qt.DisplayRole):

            rect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, options)
            painter.drawText(
                rect,
                QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
                options.fontMetrics.elidedText(
                    options.text, QtCore.Qt.ElideRight, rect.width()
                ),
            )


if __name__ == "__main__":

    class MyTree(QtWidgets.QTreeWidget):
        def __init__(self):
            super().__init__()

            self.setItemDelegate(CustomTreeWidgetDelegate())

            header = self.header()

            head = self.headerItem()
            head.setText(0, "Name")
            head.setText(1, "Age")

            parent = QtWidgets.QTreeWidgetItem(self)
            parent.setCheckState(0, QtCore.Qt.Unchecked)
            parent.setText(0, "Jack Smith")
            parent.setText(1, "30")

            child = QtWidgets.QTreeWidgetItem(parent)
            child.setCheckState(0, QtCore.Qt.Checked)
            child.setText(0, "Jane Smith")
            child.setText(1, "10")
            self.expandAll()

    # create pyqt5 app
    App = QtWidgets.QApplication(sys.argv)

    # create the instance of our Window
    myTree = MyTree()
    myTree.show()

    # start the app
    sys.exit(App.exec())

Treewidget before column resize

enter image description here

Treewidget after column resize

enter image description here

LozzerJP
  • 856
  • 1
  • 8
  • 23
  • 3
    We cannot know the reason of that if you don't show us what you do exactly, please provide a valid [mre]. – musicamante Oct 17 '22 at 07:26
  • Thanks for the comment. I was thinking that there is some obvious thing I forgot. I've now edited the question to include a minimal reproducible example and updated the images from that. Hopefully I'll get no more downvotes!! – LozzerJP Oct 18 '22 at 08:52
  • Well, for starters, both your `if`s are completely useless, since you're fundamentally doing something like `if True`, as those `options.ViewItemFeature` are *flags* (constants), while you should check agains the `options.features` (ie: `options.features & SomeFeature`). Then, you mentioned trying to draw custom checkboxes, but you're just drawing a standard checkbox which is fundamentally identical to the one already shown in item views. – musicamante Oct 19 '22 at 03:39
  • @musicamante Thanks for the comment. But, I don't think the two if statements are useless. The first checks what the state of the flag is and adjusts the state to add an enabled flag. Without this check, the checkboxes appear as disabled. Also, the first step to creating a custom checkbox is to get a normal checkbox working. As this is clearly not happening, a minimal reproducible example shouldn't clutter the problem with the custom icons etc. You can see that a standard checkbox has the problem, so I need to fix the display of that. Then I can look to customize it. – LozzerJP Oct 19 '22 at 11:22
  • Yes, they are useless: both `HasCheckIndicator` and `HasDisplay` are [`ViewItemFeature`](https://doc.qt.io/qt-5/qstyleoptionviewitem.html#ViewItemFeature-enum) flags, which are *constants*, and equal to 4 and 8 respectively: doing `if options.ViewItemFeature.HasCheckIndicator:` is **exactly** the same as doing `if 4:`, so both those if statements will always be `True` and will process their block. – musicamante Oct 19 '22 at 12:50
  • @musicamante I see. We were talking about different `if` statements. I thought you were talking about the `if options.checkState == QtCore.Qt.Checked` statement which also use flags. As you say, for this minimal example, the `HasCheckIndicator` and `HasDisplay ` will always be true. But, for the same reasons, they will not affect the display of the checkboxes, which are clearly problematic. So, do you have an answer to my original question? If you run the code, you should find the exact problem that I have. – LozzerJP Oct 19 '22 at 13:48
  • @musicamante I've now edited the code to remove the `HasCheckIndicator` and `HasDisplay` if statements. Of course, the problem is still there. Any suggestions for what the problem is? – LozzerJP Oct 19 '22 at 13:54

1 Answers1

1

Unlike widget painting (which is always clipped to the widget geometry), delegate painting is not restricted to the item bounding rect.

This allows to theoretically paint outside the item rect, for instance to display "expanded" decorations around items, but it's usually discouraged since the painting order is not guaranteed and might result in some graphical artifacts.

The solution is to always clip the painter to the option rect, which should always happen within a saved painter state:

class CustomTreeWidgetDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):
        painter.save()
        painter.setClipRect(option.rect)

        # ...

        painter.restore()

Note that:

  • the two if options.ViewItemFeature checks are useless, since those are constants (and being they greater than 0 they will always be "truthy ");
  • you should always draw the "base" of the item in order to consistently show its selected/hovered state;
  • while you stated that you want to draw a custom checkbox, be aware that you should always consider the state of the item (i.e. if it's selected or disabled);
  • the above is also valid for drawing the item text: most importantly, selected items have a different color, as it must have enough contrast against the selection background, so it's normally better to use QStyle drawItemText();

Considering the above, here's a revised version of your delegate:

class CustomTreeWidgetDelegate(QtWidgets.QStyledItemDelegate):
    def paint(self, painter, option, index):

        painter.save()
        painter.setClipRect(option.rect)

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

        widget = option.widget
        if widget:
            style = widget.style()
        else:
            style = QtWidgets.QApplication.style()

        # draw item base, including hover/selected highlighting
        style.drawPrimitive(
            style.PE_PanelItemViewItem, option, painter, widget
        )

        if option.features & option.HasCheckIndicator:

            item_option = QtWidgets.QStyleOptionButton()
            if widget:
                item_option.initFrom(widget)

            item_option.rect = style.subElementRect(
                QtWidgets.QStyle.SE_ViewItemCheckIndicator, option
            )

            item_option.state = option.state
            # disable focus appearance
            item_option.state &= ~QtWidgets.QStyle.State_HasFocus
            if option.checkState == QtCore.Qt.Checked:
                item_option.state |= QtWidgets.QStyle.State_On
            else:
                item_option.state |= QtWidgets.QStyle.State_Off

            QtWidgets.QApplication.style().drawControl(
                QtWidgets.QStyle.CE_CheckBox, item_option, painter
            )

        # "if index.data():" doesn't work if the value is a *numeric* zero
        if option.text:

            alignment = (
                index.data(QtCore.Qt.TextAlignmentRole) 
                or QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
            )

            rect = style.subElementRect(
                QtWidgets.QStyle.SE_ItemViewItemText, option, widget
            )

            margin = style.pixelMetric(
                style.PM_FocusFrameHMargin, None, widget) + 1
            rect.adjust(margin, 0, -margin, 0)

            text = option.fontMetrics.elidedText(
                option.text, QtCore.Qt.ElideRight, rect.width()
            )

            if option.state & style.State_Selected:
                role = QtGui.QPalette.HighlightedText
            else:
                role = QtGui.QPalette.Text

            style.drawItemText(painter, rect, 
                alignment, option.palette, 
                index.flags() & QtCore.Qt.ItemIsEnabled, 
                text, role
            )

        painter.restore()
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thank you so much! Your example was very insightful. On my Windows system, setting the role with `Gui.QPalette.HighlightedText` messes with the colors, with the highlighted text showing as white, whereas it should be black, so I needed to fix that. Also, the `disable focus appearance` part caused the checkboxes to turn on and off as I hovered and moved over them, which I didn't want so I disabled that part. But, my code now works perfectly. Very much appreciated! – LozzerJP Oct 19 '22 at 16:36
  • @LozzerJP You're welcome! The issue about the checkbox is probably due to the fact that you should not use `CE_CheckBox` to draw a check box of an item, but use the more correct `PE_IndicatorItemViewItemCheck` (with `drawPrimitive()`), as `CE_CheckBox is a bit more advanced and considers other aspects of the `option.state` that don't play well with the flags set for an item. The issue with the `HighlightedText` is because Windows always uses the `Text` role in views even for selected items, so use that role instead (and use `HighlightedText` if you target other platforms and detect them). – musicamante Oct 19 '22 at 22:54
  • Sorry, I thought it was all working. But, if I add a second child widget, things are still very odd. If the states (including that of the parent) are alternating (e.g. "on, off, on") then it's OK. But, if not, the second checkbox with the same state as the first will start showing through again. Any more ideas? – LozzerJP Oct 20 '22 at 07:48