Issue
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
Treewidget after column resize
Solution
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()
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.