自定义QStyledItemDelegate绘制的checkbox与Qt样式表不匹配如何解决
问题描述
自定义QStyledItemDelegate实现富文本HTML渲染功能时,发现自行绘制的checkbox指示器与样式表中定义的样式不匹配,应用右侧默认ListView通过原生paint事件绘制的checkbox可以正常响应样式表效果。
更新#1
自定义样式项委托的核心需求是支持富文本HTML渲染,该部分功能运行正常。但由于重写了paint事件,需要自行实现checkbox逻辑保证功能可用,目前存在两个问题:
- 文本与checkbox重叠,无法正常交互
- 绘制checkbox指示器时,ListItem的高亮效果被破坏,仅左侧显示一条细蓝条
截图

原问题代码
################################################################################ # imports ################################################################################ import os import sys from PySide2 import QtGui, QtWidgets, QtCore ################################################################################ # QStyledItemDelegate ################################################################################ class MyDelegate(QtWidgets.QStyledItemDelegate): MARGINS = 10 def __init__(self, parent=None, *args): QtWidgets.QStyledItemDelegate.__init__(self, parent, *args) # overrides def sizeHint(self, option, index): ''' Description: Since labels are stacked we will take whichever is the widest ''' options = QtWidgets.QStyleOptionViewItem(option) self.initStyleOption(options, index) # draw rich text doc = QtGui.QTextDocument() doc.setHtml(index.data(QtCore.Qt.DisplayRole)) doc.setDocumentMargin(self.MARGINS) doc.setDefaultFont(options.font) doc.setTextWidth(option.rect.width()) return QtCore.QSize(doc.idealWidth(), doc.size().height()) # methods def paint(self, painter, option, index): painter.save() painter.setClipping(True) painter.setClipRect(option.rect) opts = QtWidgets.QStyleOptionViewItem(option) self.initStyleOption(opts, index) style = QtGui.QApplication.style() if opts.widget is None else opts.widget.style() # Draw background if option.state & QtWidgets.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight().color()) else: painter.fillRect(option.rect, QtGui.QBrush(QtCore.Qt.NoBrush)) # Draw checkbox if (index.flags() & QtCore.Qt.ItemIsUserCheckable): cbStyleOption = QtWidgets.QStyleOptionButton() if index.data(QtCore.Qt.CheckStateRole): cbStyleOption.state |= QtWidgets.QStyle.State_On else: cbStyleOption.state |= QtWidgets.QStyle.State_Off cbStyleOption.state |= QtWidgets.QStyle.State_Enabled cbStyleOption.rect = option.rect.translated(self.MARGINS, 0) style.drawControl(QtWidgets.QStyle.CE_CheckBox, cbStyleOption, painter, option.widget) # Draw Title doc = QtGui.QTextDocument() doc.setHtml(index.data(QtCore.Qt.DisplayRole)) doc.setTextWidth(option.rect.width()) doc.setDocumentMargin(self.MARGINS) doc.setDefaultFont(opts.font) ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() # highlight text if option.state & QtWidgets.QStyle.State_Selected: ctx.palette.setColor(option.palette.Text, option.palette.color(option.palette.Active, option.palette.HighlightedText)) else: ctx.palette.setColor(option.palette.Text, option.palette.color(option.palette.Active, option.palette.Text)) textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, option) painter.translate(textRect.topLeft()) painter.setClipRect(textRect.translated(-textRect.topLeft())) doc.documentLayout().draw(painter, ctx) # end painter.restore() ################################################################################ # Widgets ################################################################################ class ListViewExample(QtWidgets.QWidget): ''' Description: Extension of listview which supports searching ''' def __init__(self, parent=None): super(ListViewExample, self).__init__(parent) self.setStyleSheet(''' QListView { color: rgb(255,255,255); background-color: rgb(60,60,60); } QCheckBox, QCheckBox:disabled { background: transparent; } QWidget::indicator { width: 12px; height: 12px; border: 2px solid rgb(90,90,90); border-radius: 3px; background: rgb(30,30,30); } QWidget::indicator:checked { border: 2px solid rgb(76,175,80); background: rgb(0,255,40); } ''') self.itemModel = QtGui.QStandardItemModel() self.checkbox = QtWidgets.QCheckBox('Sample') self.listView = QtWidgets.QListView() self.listView.setIconSize(QtCore.QSize(128,128)) self.listView.setModel(self.itemModel) self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.checkboxA = QtWidgets.QCheckBox('Sample') self.listViewA = QtWidgets.QListView() self.listViewA.setIconSize(QtCore.QSize(128,128)) self.listViewA.setModel(self.itemModel) self.listViewA.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) # layout self.mainLayout = QtWidgets.QGridLayout() self.mainLayout.addWidget(self.checkbox,0,0) self.mainLayout.addWidget(self.listView,1,0) self.mainLayout.addWidget(self.checkboxA,0,1) self.mainLayout.addWidget(self.listViewA,1,1) self.setLayout(self.mainLayout) ################################################################################ # Widgets ################################################################################ def main(): app = QtWidgets.QApplication(sys.argv) window = ListViewExample() window.resize(600,400) window.listView.setItemDelegate(MyDelegate()) window.itemModel.clear() for i in range(10): html = ''' <span style="font-size:12px;"> <b> Player <span>&#8226;</span> #{}</b> </span> <br> <span style="font-size:11px;"> <b>Status:</b> <span style='color:rgb(255,0,0);'>&#11044;</span> Active <br> <b>Position:</b> WR <br> <b>Team:</b> <span style='color:rgb(0,128,255);'>&#9608;</span> Wings </span> '''.format(i) item = QtGui.QStandardItem() item.setData(html, QtCore.Qt.DisplayRole) item.setCheckable(True) window.itemModel.appendRow(item) window.show() app.exec_() if __name__ == '__main__': pass main()
修复方案
问题核心是三个错误处理导致的:手动绘制背景没有适配样式表规则、checkbox绘制的参数未继承视图状态、文本区域没有预留checkbox位置,对应修改如下:
- 替换背景绘制逻辑:不要手动调用fillRect绘制选中背景,改用QStyle自带的
drawPrimitive方法绘制PE_PanelItemViewItem元素,完全适配样式表定义的高亮效果 - 修正checkbox绘制参数:使用
subElementRect获取标准的checkbox指示器位置,同时继承原视图条目的所有状态(hover、激活等),保证样式表规则可以正确命中自定义绘制的checkbox - 调整文本绘制区域:计算文本区域时减去checkbox的宽度和边距,避免内容重叠
- 修正sizeHint计算:返回尺寸时新增checkbox的宽度预留,保证条目尺寸计算正确
修改后的委托代码
class MyDelegate(QtWidgets.QStyledItemDelegate): MARGINS = 10 def __init__(self, parent=None, *args): QtWidgets.QStyledItemDelegate.__init__(self, parent, *args) def sizeHint(self, option, index): options = QtWidgets.QStyleOptionViewItem(option) self.initStyleOption(options, index) # 计算checkbox宽度 check_width = 0 if index.flags() & QtCore.Qt.ItemIsUserCheckable: check_rect = options.widget.style().subElementRect( QtWidgets.QStyle.SE_ItemViewItemCheckIndicator, options, options.widget ) check_width = check_rect.width() + self.MARGINS # 计算富文本尺寸 doc = QtGui.QTextDocument() doc.setHtml(index.data(QtCore.Qt.DisplayRole)) doc.setDocumentMargin(self.MARGINS) doc.setDefaultFont(options.font) doc.setTextWidth(option.rect.width() - check_width) return QtCore.QSize(doc.idealWidth() + check_width, doc.size().height()) def paint(self, painter, option, index): painter.save() painter.setClipping(True) painter.setClipRect(option.rect) opts = QtWidgets.QStyleOptionViewItem(option) self.initStyleOption(opts, index) style = opts.widget.style() # 绘制条目背景,适配样式表高亮 style.drawPrimitive(QtWidgets.QStyle.PE_PanelItemViewItem, opts, painter, opts.widget) check_width = 0 # 绘制checkbox if index.flags() & QtCore.Qt.ItemIsUserCheckable: cb_opt = QtWidgets.QStyleOptionButton() # 继承原条目所有状态 cb_opt.state = opts.state if index.data(QtCore.Qt.CheckStateRole) == QtCore.Qt.Checked: cb_opt.state |= QtWidgets.QStyle.State_On else: cb_opt.state |= QtWidgets.QStyle.State_Off # 获取标准checkbox位置 cb_opt.rect = style.subElementRect( QtWidgets.QStyle.SE_ItemViewItemCheckIndicator, opts, opts.widget ) style.drawControl(QtWidgets.QStyle.CE_CheckBox, cb_opt, painter, opts.widget) check_width = cb_opt.rect.width() + self.MARGINS # 绘制富文本 doc = QtGui.QTextDocument() doc.setHtml(index.data(QtCore.Qt.DisplayRole)) doc.setDocumentMargin(self.MARGINS) doc.setDefaultFont(opts.font) # 调整文本可用宽度 doc.setTextWidth(option.rect.width() - check_width) ctx = QtGui.QAbstractTextDocumentLayout.PaintContext() if opts.state & QtWidgets.QStyle.State_Selected: ctx.palette.setColor(QtGui.QPalette.Text, opts.palette.highlightedText().color()) else: ctx.palette.setColor(QtGui.QPalette.Text, opts.palette.text().color()) # 调整文本绘制位置,避开checkbox text_rect = QtCore.QRectF( option.rect.left() + check_width, option.rect.top(), option.rect.width() - check_width, option.rect.height() ) painter.translate(text_rect.topLeft()) painter.setClipRect(text_rect.translated(-text_rect.topLeft())) doc.documentLayout().draw(painter, ctx) painter.restore()
另外可以同步优化样式表规则,把QWidget::indicator改为QListView::indicator,避免样式作用范围过大影响其他控件。
内容的提问来源于stack exchange,提问作者JokerMartini
相关产品推荐
相关产品推荐

