Qt - 在委托 sizeHint 更改时更新视图大小

Qt - update view size on delegate sizeHint change

我有一个 QTreeView,里面有一个 QStyledItemDelegate。当委托发生某个动作时,它的大小应该会改变。但是我还没有弄清楚如何让 QTreeView 的行调整大小以响应委托编辑器大小的变化。我尝试了 QTreeView.updateGeometryQTreeView.repaint 以及其他一些方法,但它似乎不起作用。有人能给我指出正确的方向吗?

这里是一个最小的复制(注意:代码在一些地方是 hacky,它只是为了演示问题,而不是好的 MVC 的演示)。

步骤:

from PySide2 import QtCore, QtWidgets


_VALUE = 100


class _Clicker(QtWidgets.QWidget):

    clicked = QtCore.Signal()

    def __init__(self, parent=None):
        super(_Clicker, self).__init__(parent=parent)

        self.setLayout(QtWidgets.QVBoxLayout())

        self._button = QtWidgets.QPushButton("Add a label")
        self.layout().addWidget(self._button)

        self._button.clicked.connect(self._add_label)
        self._button.clicked.connect(self.clicked.emit)

    def _add_label(self):
        global _VALUE

        _VALUE += 10

        self.layout().addWidget(QtWidgets.QLabel("Add a label"))

        self.updateGeometry()  # Note: I didn't expect this to work but added it regardless


class _Delegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        widget = _Clicker(parent=parent)
        viewer = self.parent()

        widget.clicked.connect(viewer.updateGeometries)  # Note: I expected this to work

        return widget

    def paint(self, painter, option, index):
        super(_Delegate, self).paint(painter, option, index)

        viewer = self.parent()

        if not viewer.isPersistentEditorOpen(index):
            viewer.openPersistentEditor(index)

    def setEditorData(self, editor, index):
        pass

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

    def sizeHint(self, option, index):
        hint = index.data(QtCore.Qt.SizeHintRole)

        if hint:
            return hint

        return super(_Delegate, self).sizeHint(option, index)


class _Model(QtCore.QAbstractItemModel):
    def __init__(self, parent=None):
        super(_Model, self).__init__(parent=parent)

        self._labels = ["foo", "bar"]

    def columnCount(self, parent=QtCore.QModelIndex()):
        return 1

    def data(self, index, role):
        if role == QtCore.Qt.SizeHintRole:
            return QtCore.QSize(200, _VALUE)

        if role != QtCore.Qt.DisplayRole:
            return None

        return self._labels[index.row()]

    def index(self, row, column, parent=QtCore.QModelIndex()):
        child = self._labels[row]

        return self.createIndex(row, column, child)

    def parent(self, index):
        return QtCore.QModelIndex()

    def rowCount(self, parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0

        return len(self._labels)


application = QtWidgets.QApplication([])

view = QtWidgets.QTreeView()
view.setModel(_Model())
view.setItemDelegate(_Delegate(parent=view))
view.show()

application.exec_()

如何在已经应用了永久编辑器的 QTreeView 中获取一行,以告诉 Qt 调整大小以响应编辑器中的某些更改?

注意:一种可能的解决方案是关闭永久编辑器并重新打开它以强制 Qt 重绘编辑器小部件。这通常会很慢,并且在我的特定情况下不起作用。保持相同的持久编辑器很重要。

正如关于 updateGeometries() 的文档所解释的,它:

Updates the geometry of the child widgets of the view.

这用于根据当前视图状态更新小部件(编辑器、滚动条、headers 等)。它不考虑编辑器大小提示,因此调用或尝试更新大小提示是无用的(而且,不用说,为此使用全局是错误的)。

为了正确通知视图特定索引已更新其大小提示,您必须使用委托的 sizeHintChanged 信号,该信号也应在创建编辑器时发出,以确保view 为它留出足够的空间;请注意,标准编辑器通常不需要这样做(因为它们是临时的,它们应该 而不是 尝试更改视图的布局),但对于可能很大的持久性编辑器,它可能是必要的。

其他说明:

  • 在小部件上调用 updateGeometry() 在这种情况下毫无意义,因为将小部件添加到布局会自动导致 LayoutRequest 事件(这就是 updateGeometry() 所做的,等等事情);
  • createEditor() 中所述,“除非编辑器绘制自己的背景(例如,使用 setAutoFillBackground()),否则视图的背景会透出光来”;
  • 模型的 SizeHintRole 应始终 return 对模型重要的尺寸(如果有),不是 基于编辑器;这样做是委托人的责任,模型不应受到其任何观点的影响;
  • 在绘画事件中打开持久化编辑器是错误的;在绘制函数中只应该发生与绘制相关的方面,最重要的是因为它们被调用得非常频繁(对于项目视图甚至每秒数百次)所以它们应该尽可能快,但也因为做任何可能影响变化的事情在几何中会导致(至少)递归调用;
  • 信号可以在不使用 emit 的情况下“链接”:self._button.clicked.connect(self.clicked) 就足够了;

综上所述,有两种可能。问题在于编辑器小部件与其引用的索引之间没有直接关联,因此我们需要找到一种方法来在更新编辑器时使用正确的索引发出 sizeHintChanged
这只能通过为编辑器创建索引的引用来完成,但我们为此使用 QPersistentModelIndex 很重要,因为索引可能会在持久编辑器打开时发生变化(例如,排序或过滤时), 委托函数的参数中提供的索引无法跟踪这些更改。

发出自定义信号

在这种情况下,我们只使用一个自定义信号,每当我们知道布局发生变化时就会发出该信号,并且我们在 createEditor 中创建一个最终会发出 sizeHintChanged 信号的局部函数通过“重建”有效索引:

class _Clicker(QtWidgets.QWidget):
    sizeHintChanged = QtCore.Signal()
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAutoFillBackground(True)

        layout = QtWidgets.QVBoxLayout(self)

        self._button = QtWidgets.QPushButton("Add a label")
        layout.addWidget(self._button)

        self._button.clicked.connect(self._add_label)

    def _add_label(self):
        self.layout().addWidget(QtWidgets.QLabel("Add a label"))
        self.sizeHintChanged.emit()


class _Delegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        widget = _Clicker(parent)
        persistent = QtCore.QPersistentModelIndex(index)

        def emitSizeHintChanged():
            index = persistent.model().index(
                persistent.row(), persistent.column(), 
                persistent.parent())
            self.sizeHintChanged.emit(index)

        widget.sizeHintChanged.connect(emitSizeHintChanged)
        self.sizeHintChanged.emit(index)
        return widget

    # no other functions implemented here

使用委托的事件过滤器

我们可以在编辑器中为持久索引创建一个引用,然后每当从编辑器收到 LayoutRequest 事件时,就会在委托的事件过滤器中发出 sizeHintChanged 信号:

class _Clicker(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAutoFillBackground(True)

        layout = QtWidgets.QVBoxLayout(self)

        self._button = QtWidgets.QPushButton("Add a label")
        layout.addWidget(self._button)

        self._button.clicked.connect(self._add_label)

    def _add_label(self):
        self.layout().addWidget(QtWidgets.QLabel("Add a label"))


class _Delegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        widget = _Clicker(parent)
        widget.index = QtCore.QPersistentModelIndex(index)
        return widget

    def eventFilter(self, editor, event):
        if event.type() == event.LayoutRequest:
            persistent = editor.index
            index = persistent.model().index(
                persistent.row(), persistent.column(), 
                persistent.parent())
            self.sizeHintChanged.emit(index)
        return super().eventFilter(editor, event)

最后,您显然应该删除 data() 中的 SizeHintRole return,为了打开所有永久性编辑器,您可以这样做:

def openEditors(view, parent=None):
    model = view.model()
    if parent is None:
        parent = QtCore.QModelIndex()
    for row in range(model.rowCount(parent)):
        for column in range(model.columnCount(parent)):
            index = model.index(row, column, parent)
            view.openPersistentEditor(index)
            if model.rowCount(index):
                openEditors(view, index)

# ...
openEditors(view)