You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义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>•</span> #{}</b>
        </span>
        <br>
        <span style="font-size:11px;">
          <b>Status:</b> <span style='color:rgb(255,0,0);'>⬤</span> Active
            <br>
          <b>Position:</b> WR
            <br>
          <b>Team:</b> <span style='color:rgb(0,128,255);'>█</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位置,对应修改如下:

  1. 替换背景绘制逻辑:不要手动调用fillRect绘制选中背景,改用QStyle自带的drawPrimitive方法绘制PE_PanelItemViewItem元素,完全适配样式表定义的高亮效果
  2. 修正checkbox绘制参数:使用subElementRect获取标准的checkbox指示器位置,同时继承原视图条目的所有状态(hover、激活等),保证样式表规则可以正确命中自定义绘制的checkbox
  3. 调整文本绘制区域:计算文本区域时减去checkbox的宽度和边距,避免内容重叠
  4. 修正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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 16:24:03