QStyleItemDelegate 复选框与样式表不匹配

Posted

技术标签:

【中文标题】QStyleItemDelegate 复选框与样式表不匹配【英文标题】:QStyleItemDelegate checkbox doesn't match stylesheet 【发布时间】:2021-11-26 14:51:14 【问题描述】:

我制作了一个自定义 StyleItemDelegate,由于某种原因,当它绘制复选框指示器时,它与我的样式表中定义的不匹配。我怎样才能解决这个问题?您可以在应用程序的右侧看到样式表正确地影响复选框在默认列表视图绘制事件中的显示方式。

更新 #1

我制作了一个自定义样式项委托来支持富文本 html 渲染,这一切都很好。我需要重新实现复选框,因为我已经覆盖了绘制事件并确保该复选框仍然可用。但是,我的文本与复选框重叠,使其无法使用。结果,当尝试绘制复选框指示器时,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()

【问题讨论】:

【参考方案1】:

QStyle 绘画函数假定您使用该样式的默认行为作为控件/基元。这意味着,在大多数情况下,可以忽略最后一个 widget 参数,因为没有考虑覆盖。

当使用样式表时,情况会发生变化。如果小部件或其父级(包括 QApplication)的 任何 具有样式表,则小部件将使用内部 QStyleSheetStyle,继承 QApplication 样式的行为,并被为父母。 在这种情况下,该参数成为强制性的,因为底层 QStyleSheetStyle 将需要检查小部件是否具有(或继承)样式表并最终“爬上”小部件树以了解 if如何为其设置任何自定义样式。

您只需将其添加到函数的参数中:

    style.drawControl(QtWidgets.QStyle.CE_CheckBox, cbStyleOption, painter, 
                      option.widget)
                      ^^^^^^^^^^^^^

上面解决了样式的问题,但没有解决绘图的问题。

虽然项目视图中复选框的外观通常与 QCheckBox 相同(::indicator 伪选择器可以同时用于两者),但它们实际上根据样式绘制了不同的功能。

您的实现问题是您使用drawControlCE_CheckBox 进行绘图,但对于项目视图,您必须使用drawPrimitivePE_IndicatorItemViewItemCheck。此外,由于您只是在翻译原始选项 rect,因此结果是 drawControl 将在一个与 整个 项目矩形一样大的矩形上绘制(因此在选择上绘制)。

正确的解决方案是在现有的基础上创建一个new QStyleOptionViewItem,并将subElementRect返回的矩形与SE_ItemViewItemCheckIndicator一起使用。

    def paint(self, painter, option, index):
        if (index.flags() & QtCore.Qt.ItemIsUserCheckable):
            cbStyleOption = QtWidgets.QStyleOptionViewItem(opts)

            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 = style.subElementRect(
                style.SE_ItemViewItemCheckIndicator, opts, opts.widget)

            style.drawPrimitive(style.PE_IndicatorItemViewItemCheck, 
                cbStyleOption, painter, opts.widget)

请注意,您不应该在绘画中使用平移,因为这会使其与鼠标交互不一致。

要翻译元素,请改用样式表中的topleft 属性:

            QWidget::indicator 
                left: 10px;
                ...
            

另请注意,您使用原始选项获取文本矩形,该选项未初始化,因此可能返回无效矩形;然后,您应该使用实际初始化的选项,并使用上面解释的 widget 参数:

    textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, 
        opts, opts.widget)

【讨论】:

我补充说,但现在我的行​​选择完全不合时宜,我更新了原始帖子以显示问题。我该如何解决? 还注意到复选框不再起作用,所以我必须在某处更新用户交互,因为我重新绘制了复选框?感谢您的所有帮助:) @JokerMartini 澄清一下,您究竟想通过自定义绘画实现什么?因为在研究了您的代码之后,我并不太清楚。选择背景的问题在于您只是在平移选项的矩形并绘制一个完整的 QCheckBox 控件(它覆盖了 QSS 中为 QWidget 设置的默认背景),但该矩形包括 whole 项目区域,而你应该使用style.subControlRect得到SE_ItemViewItemCheckIndicator的矩形,然后使用drawPrimitivePE_IndicatorItemViewItemCheck 你之前的实现已经存在点击矩形的问题了(如果你之前没有注意到可能是因为默认的指示器比较大),那是因为你只是在翻译绘图,但是委托在检查鼠标事件时不知道这一点,因此它使用默认定位。要翻译控件,请使用QAbstractItemView::indicator margin-left: 10px; 。我也强烈不鼓励你使用通用的QWidget 样式,因为它经常会在复杂的小部件上产生问题,最常见的是任何滚动区域的滚动条;发生这种情况 @JokerMartini 看到更新,它应该修复点击和选择问题。

以上是关于QStyleItemDelegate 复选框与样式表不匹配的主要内容,如果未能解决你的问题,请参考以下文章

样式下拉列表与复选框

使用纯css3自定义单选框radio和复选框checkbox

使用easyui框架form控件,单选按钮radio或复选框checkbox样式问题

利用伪元素before实现自定义checkbox样式

在 PyQt4 中修改系统样式

[ HTML5 表单样式 checkbox | radio ] 自定义checkbox 与radio样式实现思路