Home > Net >  Why are checkboxes showing through in qt (pyqt) QStyledItemDelegate for TreeWidget
Why are checkboxes showing through in qt (pyqt) QStyledItemDelegate for TreeWidget

Time:10-20

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

CodePudding user response:

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()
  • Related