У меня есть 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.
В вашем коде имеются следующие ошибки:
Нет необходимости использовать 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_())
mode
, что такоеcolor
и т. д.?