如何在 QTreeView 中的项目旁边添加红色圆圈(断点样式)

How to add red circles next to items in QTreeView (breakpoint style)

我有这样的东西:

我无法向您展示更多,但这是一个简单的 QTreeView,其中包含 QStandardItems。图中的项目有一个父项,父项也有一个父项。

当我在一个项目上激活 断点 时,我有这个:

没关系,但我也想像大多数 IDE 一样在其下添加一个 圆圈 (我以 PyCharm 为例):

问题是我不知道该怎么做。有人可以帮忙吗?

一个可能的解决方案是重写 QTreeView 的 drawRow 方法并使用 QModelIndex 中的信息进行绘制:

import sys

from PySide2.QtCore import Qt, QRect
from PySide2.QtGui import QColor, QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QApplication, QTreeView

IS_BREAKPOINT_ROLE = Qt.UserRole + 1


class TreeView(QTreeView):
    def drawRow(self, painter, option, index):
        super().drawRow(painter, option, index)
        if index.column() == 0:
            if not index.data(IS_BREAKPOINT_ROLE):
                return
            rect = self.visualRect(index)
            if not rect.isNull():
                margin = 4
                r = QRect(0, rect.top(), rect.height(), rect.height()).adjusted(
                    margin, margin, -margin, -margin
                )
                painter.setBrush(QColor("red"))
                painter.drawEllipse(r)


def main(args):
    app = QApplication(args)
    view = TreeView()
    view.setSelectionBehavior(QAbstractItemView.SelectRows)
    model = QStandardItemModel()
    model.setHorizontalHeaderLabels(["col1", "col2"])
    view.setModel(model)
    counter = 0
    for i in range(10):
        item1 = QStandardItem("Child 1-{}".format(i))
        item2 = QStandardItem("Child 2-{}".format(i))
        for j in range(10):
            child1 = QStandardItem("Child {}-1".format(counter))
            child2 = QStandardItem("Child {}-2".format(counter))
            child1.setData(counter % 2 == 0, IS_BREAKPOINT_ROLE)
            item1.appendRow([child1, child2])

            counter += 1

        model.appendRow([item1, item2])

    view.show()
    view.resize(320, 240)
    view.expandAll()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main(sys.argv)

我想提出一个基于 的替代解决方案,它会在视口中添加左边距,避免在层次结构线上绘制(这可能会隐藏父项的展开装饰箭头需要显示圆圈)。

一些重要说明:

  • 左边距是使用 setViewportMargins() 创建的,但是所有项目视图在调用 updateGeometries() 时都会自动重置这些边距(几乎每次布局更改时都会发生),因此需要重写该方法;
  • 在边距上绘画意味着绘画不会在视口中发生,因此我们无法实现 paintEvent()(默认情况下会调用视口更新);这导致在 event() 中执行绘图;
  • 更新必须在滚动条变化或项目为 expanded/collapsed 时显式调用,但 Qt 仅更新实际已“更改”的项目感兴趣的区域(因此可能排除其他“移位”项目) ;为了请求完整范围的更新,我们需要调用 QWidget 的基本实现(不是视图的实现,因为该方法已被覆盖);
class TreeView(QTreeView):
    leftMargin = 14
    def __init__(self, *args, **kwargs):
        super().__init__()
        self.leftMargin = self.fontMetrics().height()
        self.verticalScrollBar().valueChanged.connect(self.updateLeftMargin)
        self.expanded.connect(self.updateLeftMargin)
        self.collapsed.connect(self.updateLeftMargin)

    def updateLeftMargin(self):
        QWidget.update(self, 
            QRect(0, 0, self.leftMargin + self.frameWidth(), self.height()))

    def setModel(self, model):
        if self.model() != model:
            if self.model():
                self.model().dataChanged.disconnect(self.updateLeftMargin)
            super().setModel(model)
            model.dataChanged.connect(self.updateLeftMargin)

    def updateGeometries(self):
        super().updateGeometries()
        margins = self.viewportMargins()
        if margins.left() < self.leftMargin:
            margins.setLeft(margins.left() + self.leftMargin)
            self.setViewportMargins(margins)

    def event(self, event):
        if event.type() == event.Paint:
            pos = QPoint()
            index = self.indexAt(pos)
            qp = QPainter(self)
            border = self.frameWidth()
            bottom = self.height() - border * 2
            qp.setClipRect(QRect(border, border, self.leftMargin, bottom))
            top = .5
            if self.header().isVisible():
                top += self.header().height()
            qp.translate(.5, top)
            qp.setBrush(Qt.red)
            qp.setRenderHints(qp.Antialiasing)
            deltaY = self.leftMargin / 2 - border
            circle = QRect(
                border + 1, 0, self.leftMargin - 2, self.leftMargin - 2)
            row = 0
            while index.isValid():
                rect = self.visualRect(index)
                if index.data(IS_BREAKPOINT_ROLE):
                    circle.moveTop(rect.center().y() - deltaY)
                    qp.drawEllipse(circle)
                row += 1
                pos.setY(rect.bottom() + 2)
                if pos.y() > bottom:
                    break
                index = self.indexAt(pos)
        return super().event(event)