Qt - 在委托 sizeHint 更改时更新视图大小
Qt - update view size on delegate sizeHint change
我有一个 QTreeView
,里面有一个 QStyledItemDelegate
。当委托发生某个动作时,它的大小应该会改变。但是我还没有弄清楚如何让 QTreeView
的行调整大小以响应委托编辑器大小的变化。我尝试了 QTreeView.updateGeometry
和 QTreeView.repaint
以及其他一些方法,但它似乎不起作用。有人能给我指出正确的方向吗?
这里是一个最小的复制(注意:代码在一些地方是 hacky,它只是为了演示问题,而不是好的 MVC 的演示)。
步骤:
- 运行下面的代码
- 按任一“添加标签”按钮
- 请注意,无论单击哪个按钮多少次,
QTreeView
中行的高度都不会改变。
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)
我有一个 QTreeView
,里面有一个 QStyledItemDelegate
。当委托发生某个动作时,它的大小应该会改变。但是我还没有弄清楚如何让 QTreeView
的行调整大小以响应委托编辑器大小的变化。我尝试了 QTreeView.updateGeometry
和 QTreeView.repaint
以及其他一些方法,但它似乎不起作用。有人能给我指出正确的方向吗?
这里是一个最小的复制(注意:代码在一些地方是 hacky,它只是为了演示问题,而不是好的 MVC 的演示)。
步骤:
- 运行下面的代码
- 按任一“添加标签”按钮
- 请注意,无论单击哪个按钮多少次,
QTreeView
中行的高度都不会改变。
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)