带有自定义按钮的 QStyledItemDelegate

QStyleItemDelegate with Custom Buttons

我有一个 QTreeWidget,我想使用样式委托完全自定义项目的外观。

我的主要问题是我想在我的项目右侧创建一个自定义按钮,它允许我折叠和展开该项目的子项。通常可以在大多数树的左侧找到经典的“+”按钮。

我可以毫无问题地绘制按钮本身,并根据项目是否展开来更改其图标。问题是让它表现得像一个按钮(按下时激活命令,悬停时改变颜色等)

我想到的是使用editorEvent来检查鼠标是否被按下到与我绘制当前项目按钮的位置相同的位置。

为了获得悬停效果,我编辑了树的 mouseMoveEvent 并检查鼠标是否在项目按钮的顶部,如果是,则重新绘制悬停的项目。

我的实现完成了工作,但我担心我做的完全错了,效率不高,而且我的树会因为这种计算而变慢。所以我想知道,是否有人对如何改进下面的代码有一些建议。

代表

class styleDelegate(QtWidgets.QStyledItemDelegate):


    def __init__(self, parent=None, treeWidget = None):
        super(styleDelegate, self).__init__(parent)
        self.tree = treeWidget      

    def paint(self, painter, option, index):

        painter.save()
        rect = option.rect

        # set the pen to draw an outline around the item to divide them.
        pen = QPen()
        pen.setBrush(QtGui.QColor(43, 43, 43))
        pen.setWidthF(1)
        painter.setPen(pen)
        item = self.tree.itemFromIndex(index)

        # set the background color based on the item or if it is selected
        if option.state & QStyle.State_Selected:
            painter.setBrush(option.palette.highlight())
        else:
            color = item.color
            painter.setBrush(QtGui.QColor(color[0] * 255, color[1] * 255, color[2] * 255))

        #draw the colored background 
        painter.drawRect(rect)

        #draw the image
        imageScale = 0
        margin = 4
        imageScale = rect.height() - margin * 2 + 1
        painter.drawPixmap(rect.x() + margin, rect.y() + margin , imageScale, imageScale, item.image.scaled(imageScale, imageScale, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))

        # draw the text 

        painter.setPen(QtGui.QColor(255, 255, 255))
        font = painter.font()
        font.setPointSize(9)
        painter.setFont(font)

        painter.drawText(rect.x() + imageScale + margin * 3, rect.y(), 100, item.scale, Qt.AlignVCenter, item.name)

        # draw the expander button only if the item has children
        if item.childCount():

            # choose the appropriate icon to draw depending on the state of the item.
            if item.isExpanded():
                path = "checked.png"
                if item.hover:
                    path = "checked_hover.png"
            else:
                path = "unchecked.png"
                if item.hover:
                    path = "unchecked_hover.png"
            image = QtGui.QPixmap.fromImage(QtGui.QImage(path))
            size = 20 

            # define the position of the expander button

            positionX = rect.x() + rect.width() - 20 
            positionY = rect.y() + item.scale / 2 - size/2

            painter.drawPixmap(positionX, positionY, size, size, image.scaled(size, size, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation))

            item.expanderStart = QPoint(positionX, positionY)
            item.expanderEnd = QPoint(positionX + 20, positionY + 20)

        painter.restore() 

   def editorEvent(self, event, model, option, index):


        # if an item is clicked, check if the click happened in the area whee the expander button is drawn.
        if event.type() == QEvent.MouseButtonPress:
            item = self.tree.itemFromIndex(index)

            rect = option.rect
            clickX = event.x()
            clickY = event.y()

            # set the expanded expanded if it was clicked
            if  clickX > x and clickX < x + w:
                if clickY > y and clickY < y + h:
                    item.setExpanded(not item.isExpanded())

class myTree(QtWidgets.QTreeWidget):
    def __init__(self, parent):
        super(myTree, self).__init__(parent)
        self.setMouseTracking(True)

    def mouseMoveEvent(self, event)
        item = self.itemAt(event.pos())
        if item:
            if item.childCount():
                # get the current hovering state. if the item is already hovered, there is no need to repaint it. 
                hover = item.hover
                if (event.pos.x() > item.expanderStart.x()
                    and event.pos.x() < item.expanderEnd.x()
                    and event.pos.y() > item.expanderStart.y()
                    and event.pos.y() < item.expanderEnd.y())
                    item.hover = True
                else:
                    item.hover = False
                if item.hover != hover:
                    self.viewport().update(event.pos().x(), event.pos().y(), 20, 20)

我知道这可以在不使用委托的情况下完全实现,只需使用样式表或将小部件分配给项目即可。然而,我并没有深入了解这些方法,因为我遇到了几个问题。

我花了很多时间试图达到我想要的结果,但没有成功。也许我让我的物品看起来接近我想要的东西,但从来没有像我想象的那样。

我如此挑剔的原因是这个 QTreeWidget 曾经是一个 QListWidget,用样式表实现。现在我 "updgrading" 它变成了一棵树,我什至不希望用户注意到差异,但我无法单独使用 sylesheets 复制相同的外观。

如果上面的代码有愚蠢的错误,请原谅我,我已经测试了完整版本并且可以正常工作,我只是在这里发布了相关内容。


编辑:

根据要求,这是一些(至少对我而言)产生预期结果的代码。但是,我想知道这是否是我做或不做的事情的正确方法...

from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *

class styleDelegate(QStyledItemDelegate):


    def __init__(self, parent=None, treeWidget = None):
        super(styleDelegate, self).__init__(parent)
        self.tree = treeWidget      

    def paint(self, painter, option, index):

        painter.save()
        rect = option.rect

        # set the pen to draw an outline around the item to divide them.
        pen = QPen()
        pen.setBrush(QColor(43, 43, 43))
        pen.setWidthF(1)
        painter.setPen(pen)
        item = self.tree.itemFromIndex(index)

        # set the background color based on the item or if it is selected
        if option.state & QStyle.State_Selected:
            painter.setBrush(option.palette.highlight())
        else:
            color = item.color
            painter.setBrush(QColor(color[0], color[1], color[2]))

        #draw the colored background 
        painter.drawRect(rect)

        #draw the image
        margin = 4
        imageScale = rect.height() - margin * 2 + 1
        painter.drawPixmap(rect.x() + margin, rect.y() + margin , imageScale, imageScale, item.image.scaled(imageScale, imageScale, Qt.KeepAspectRatio, Qt.SmoothTransformation))

        # draw the text 

        painter.setPen(QColor(255, 255, 255))
        font = painter.font()
        font.setPointSize(9)
        painter.setFont(font)

        painter.drawText(rect.x() + imageScale + margin * 3, rect.y(), 300, item.scale, Qt.AlignLeft|Qt.AlignVCenter, item.name)

        # draw the expander button only if the item has children
        if item.childCount():

            # choose the appropriate icon to draw depending on the state of the item.
            if item.isExpanded():
                path = "c:\test.png"
                if item.hover:
                    path = "c:\test.png"
            else:
                path = "c:\test.png"
                if item.hover:
                    path = "c:\test.png"
            image = QPixmap.fromImage(QImage(path))
            size = self.tree.expanderSize

            # define the position of the expander button

            positionX = rect.x() + rect.width() - size - 10
            positionY = rect.y() + item.scale / 2 - size/2

            painter.drawPixmap(positionX, positionY, size, size, image.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation))

            item.expanderStart = QPoint(positionX, positionY)
            item.expanderEnd = QPoint(positionX + size, positionY + size)

        painter.restore() 

    def editorEvent(self, event, model, option, index):


        # if an item is clicked, check if the click happened in the area whee the expander button is drawn.
        if event.type() == QEvent.MouseButtonPress:
            item = self.tree.itemFromIndex(index)
            if item.childCount():
                rect = option.rect
                clickX = event.x()
                clickY = event.y()
                size = self.tree.expanderSize
                # this is the rect of the expander button
                x = rect.x() + rect.width() - 20 
                y = rect.y() + item.scale / 2 - size/2
                w = size # expander width
                h = size # expander height
                # set the expanded expanded if it was clicked
                if (clickX > item.expanderStart.x()
                    and clickX < item.expanderEnd.x()
                    and clickY > item.expanderStart.y()
                    and clickY < item.expanderEnd.y()):
                    print "expand"
                    item.setExpanded(not item.isExpanded())


class myTree(QTreeWidget):
    def __init__(self, parent = None):
        super(myTree, self).__init__(parent)
        self.setMouseTracking(True)
        self.setHeaderHidden(True)
        self.setRootIsDecorated(False)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    def mouseMoveEvent(self, event):
        item = self.itemAt(event.pos())
        if item:
            if item.childCount():
                # get the current hovering state. if the item is already hovered, there is no need to repaint it. 
                hover = item.hover
                if (event.pos() .x() > item.expanderStart.x()
                    and event.pos() .x() < item.expanderEnd.x()
                    and event.pos() .y() > item.expanderStart.y()
                    and event.pos() .y() < item.expanderEnd.y()):
                    item.hover = True
                else:
                    item.hover = False
                if item.hover != hover:
                    self.viewport().update(event.pos().x(), event.pos().y(), 20, 20)     
                    print "Hover", item.hover  
    def closeEvent(self, event):
        self.deleteLater()

def generateTree():
    tree = myTree()
    tree.setGeometry(500, 500, 1000, 500)
    tree.expanderSize = 50
    delegate = styleDelegate(tree, treeWidget = tree)
    tree.setItemDelegate(delegate)

    for object in ["Aaaaaaa", "Bbbbbbb", "Ccccccc"]:
        item = QTreeWidgetItem()
        item.name = object
        item.image = QPixmap.fromImage(QImage("c:\test.png"))
        item.color = [150, 150, 150]
        item.hover = False
        item.scale = 100
        tree.addTopLevelItem(item)
        item.setSizeHint(0, QSize(item.scale, item.scale ))
        for child in ["Eeeeee", "Fffffff"]:
            childItem = QTreeWidgetItem()
            childItem.name = child
            childItem.image = QPixmap.fromImage(QImage("c:\test.png"))
            childItem.color = [150, 150, 150]
            childItem.scale = 90

            item.addChild(childItem)
            childItem.setSizeHint(0, QSize(childItem.scale, childItem.scale))
    return tree

tree = generateTree()    
tree.show()

请注意,我的显示器是 4k,我很快就对大部分尺寸进行了硬编码,因此开箱即用该代码将在高清显示器上生成更大的小部件。

您的代码存在以下错误:

  • 不一定要用QPixmap.fromImage(QImage(path)),可以直接用路径创建一个QPixmapQPixmap(path)

  • 如果它们是相同的图像,最好加载一次并重新使用它,例如在我的解决方案中我为按钮 QPixmap 这样做。

  • 不要创建动态属性,因为它会产生代码耦合,在项目的情况下你必须使用角色。

  • 要知道一个项目是否被扩展或者你不应该使用QStyle::State_Open所以我们避免了耦合并且委托可以被其他视图使用而不需要做很多改变。

  • 使用 QRect 来分隔矩形,例如您使用 contains 来查看点是否在矩形内。

以上是主要观察,下面是解决方案:

from PySide2 import QtCore, QtGui, QtWidgets
from enum import Enum  

ScaleRole= QtCore.Qt.UserRole + 1
expanderSize = 50


class TreeDelegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None):
        super(TreeDelegate, self).__init__(parent)

        self.pixmap_collapsed =  QtGui.QPixmap("collapsed.png")
        self.pixmap_collapsed_hover = QtGui.QPixmap("collapsed_hover.png")
        self.pixmap_expanded = QtGui.QPixmap("expanded.png")
        self.pixmap_expanded_hover = QtGui.QPixmap("expanded_hover.png")

    def paint(self, painter, option, index):
        image = index.data(QtCore.Qt.DecorationRole)
        scale = index.data(ScaleRole)
        name = index.data()

        painter.save()
        rect = option.rect
        painter.setPen(QtGui.QPen(brush=QtGui.QColor(43, 43, 43), widthF=1))
        if option.state & QtWidgets.QStyle.State_Selected:
            painter.setBrush(option.palette.highlight())
        else:
            painter.setBrush(index.data(QtCore.Qt.BackgroundRole))
        painter.drawRect(rect)

        margin = 4
        image_scale = (rect.height() - margin * 2 + 1)*QtCore.QSize(1, 1)
        if image is not None and not image.isNull():
            image = image.scaled(image_scale, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
            painter.drawPixmap(rect.topLeft() + margin*QtCore.QPoint(1, 1), image)

        painter.setPen(QtGui.QColor(255, 255, 255))
        font = painter.font()
        font.setPointSize(9)
        painter.setFont(font)
        painter.drawText(QtCore.QRect(rect.topLeft() + QtCore.QPoint(image_scale.width() + 3*margin, 0) , QtCore.QSize(300, scale)), 
            QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, name)

        if index.model().hasChildren(index):
            pixmap = self.pixmap_collapsed
            if option.state & QtWidgets.QStyle.State_Open:
                if option.state & QtWidgets.QStyle.State_MouseOver:
                    pixmap = self.pixmap_expanded_hover
                else:
                    pixmap = self.pixmap_expanded
            else :
                if option.state & QtWidgets.QStyle.State_MouseOver:
                    pixmap = self.pixmap_collapsed_hover
            size = expanderSize
            pixmap = pixmap.scaled(size*QtCore.QSize(1, 1), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
            pos = rect.topRight() - QtCore.QPoint(size+10, (size-scale)/2)
            painter.drawPixmap(pos, pixmap)
        painter.restore()


class MyTreeItem(QtWidgets.QTreeWidgetItem):
    def __init__(self, name, image, color, scale):
        super(MyTreeItem, self).__init__([name])
        self.setData(0, ScaleRole, scale)
        self.setData(0, QtCore.Qt.BackgroundRole, color)
        self.setData(0, QtCore.Qt.DecorationRole, image)


class MyTree(QtWidgets.QTreeWidget):
    def __init__(self, parent=None):
        super(MyTree, self).__init__(parent)
        self.setMouseTracking(True)
        self.setHeaderHidden(True)
        self.setRootIsDecorated(False)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)

    def mousePressEvent(self, event):

        if not self.itemsExpandable(): return
        index = self.indexAt(event.pos())
        if not index.isValid(): return
        # restore state
        is_expanded = self.isExpanded(index)
        QtWidgets.QAbstractItemView.mousePressEvent(self, event)
        self.setExpanded(index, is_expanded)

        if not self.model().hasChildren(index): return
        rect = self.visualRect(index)
        size = expanderSize
        scale = index.data(ScaleRole)
        pos = rect.topRight() - QtCore.QPoint(size+10, (size-scale)/2)
        r = QtCore.QRect(pos, size*QtCore.QSize(1, 1))
        if r.contains(event.pos()):
            self.setExpanded(index, not self.isExpanded(index))


def generate_tree():
    tree = MyTree()
    scale = 100
    delegate = TreeDelegate(tree)
    tree.setItemDelegate(delegate)

    for text in ["Aaaaaaa", "Bbbbbbb", "Ccccccc"]:
        item = MyTreeItem(text, QtGui.QPixmap("image.png"), QtGui.QColor(150, 150, 150), scale)
        item.setSizeHint(0, QtCore.QSize(scale, scale))
        tree.addTopLevelItem(item)
        for child in ["Eeeeee", "Fffffff"]:
            childItem = MyTreeItem(child, QtGui.QPixmap("image.png"), QtGui.QColor(150, 150, 150), scale)
            childItem.setSizeHint(0, QtCore.QSize(scale, scale))
            item.addChild(childItem)
    return tree


if __name__ == '__main__':
    import sys

    app = QtWidgets.QApplication(sys.argv)
    tree = generate_tree()    
    tree.show()
    sys.exit(app.exec_())