QStyleItemDelegate с пользовательскими кнопками

1

У меня есть 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, реализованный со стилями. Теперь, когда я "обновляю" это дерево, я не хочу, чтобы пользователь даже замечал разницу, но я не смог воспроизвести один и тот же точный вид с помощью силезских таблиц.

Простите меня, если вышеприведенный код имеет тупую ошибку, я протестировал полную версию, и она работает, и я просто разместил здесь соответствующие материалы.


РЕДАКТИРОВАТЬ:

В соответствии с запросом это код, который (по крайней мере, для меня) дает желаемый результат. Тем не менее, я задаюсь вопросом, правильно ли это делать то, что я делаю или нет...

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, и я быстро Hardcoded большую часть размеров, поэтому из коробки этот код будет создавать гораздо большие виджеты на мониторе HD.

  • 0
    пожалуйста, сэкономьте наше время и приведите минимальный воспроизводимый пример
  • 0
    что такое mode , что такое color и т. д.?
Показать ещё 12 комментариев
Теги:
pyside2

1 ответ

1
Лучший ответ

В вашем коде имеются следующие ошибки:

  • Нет необходимости использовать QPixmap.fromImage(QImage(path)), вы можете создать QPixmap напрямую с помощью пути: QPixmap(path)

  • Если это одни и те же изображения, лучше загрузить их один раз и повторно использовать, например, в моем решении я делаю это для кнопок QPixmap.

  • Не создавайте динамические атрибуты, потому что он генерирует кодовое связывание, в случае элементов вы должны использовать роли.

  • Чтобы узнать, расширяется ли элемент, или вы не должны использовать QStyle::State_Open мы избегаем связывания, и делегат может использоваться другими представлениями без внесения многих изменений.

  • Используйте QRect чтобы разграничить прямоугольник и, например, вы используете, чтобы увидеть, находится ли точка внутри прямоугольника.

Вышеприведенные основные замечания, в следующей части: решение:

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_())
  • 0
    Спасибо, что нашли время, чтобы просмотреть мой код и за все эти предложения. Таким образом, способ, которым я реализовал свое дерево, является правильным, с логической точки зрения? Можно ли будет повторить тот же результат с таблицей стилей или другими методами или делегатами, которые являются лучшим способом в этом сценарии?
  • 0
    @Yagalos ваша первоначальная логика частично верна, если вы проверите мой ответ, есть предложения по улучшению, которые применимы к моему решению, проверьте мое решение, чтобы получить более подробную информацию, если мой ответ поможет вам, не забудьте пометить его как правильный, если вы не знаю, как это сделать, проверьте [тур], это лучший способ поблагодарить.
Показать ещё 10 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню