I have a QTreeView
with a QStyledItemDelegate
inside of it. When a certain action occurs to the delegate, its size is supposed to change. However I haven’t figured out how to get the QTreeView
‘s rows to resize in response to the delegate’s editor size changing. I tried QTreeView.updateGeometry
and QTreeView.repaint
and a couple other things but it doesn’t seem to work. Could someone point me in the right direction?
Here’s a minimal reproduction (note: The code is hacky in a few places, it’s just meant to be a demonstration of the problem, not a demonstration of good MVC).
Steps:
- Run the code below
- Press either “Add a label” button
- Note that the height of the row in the
QTreeView
does not change no matter how many times either button is clicked.
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_()
How do I get a single row in a QTreeView
, which has a persistent editor applied already to it, to tell Qt to resize in response to some change in the editor?
Note: One possible solution would be to close the persistent editor and re-open it to force Qt to redraw the editor widget. This would be generally very slow and not work in my specific situation. Keeping the same persistent editor is important.
Advertisement
Answer
As the documentation about updateGeometries()
explains, it:
Updates the geometry of the child widgets of the view.
This is used to update the widgets (editors, scroll bars, headers, etc) based on the current view state. It doesn’t consider the editor size hints, so that call or the attempt to update the size hint is useless (and, it should go without saying, using global for this is wrong).
In order to properly notify the view that a specific index has updated its size hint, you must use the delegate’s sizeHintChanged
signal, which should also be emitted when the editor is created in order to ensure that the view makes enough room for it; note that this is normally not required for standard editors (as, being they temporary, they should not try to change the layout of the view), but for persistent editors that are potentially big, it may be necessary.
Other notes:
- calling
updateGeometry()
on the widget is pointless in this case, as adding a widget to a layout automatically results in aLayoutRequest
event (which is whatupdateGeometry()
does, among other things); - as explained in
createEditor()
, “the view’s background will shine through unless the editor paints its own background (e.g., with setAutoFillBackground())”; - the
SizeHintRole
of the model should always return a size important for the model (if any), not based on the editor; it’s the delegate responsibility to do that, and the model should never be influenced by any of its views; - opening a persistent editor in a paint event is wrong; only drawing related aspects should ever happen in a paint function, most importantly because they are called very often (even hundreds of times per second for item views) so they should be as fast as possible, but also because doing anything that might affect a change in geometry will cause (at least) a recursive call;
- signals can be “chained” without using
emit
:self._button.clicked.connect(self.clicked)
would have sufficed;
Considering all the above, there are two possibilities. The problem is that there is no direct correlation between the editor widget and the index it’s referred to, so we need to find a way to emit sizeHintChanged
with its correct index when the editor is updated.
This can only be done by creating a reference of the index for the editor, but it’s important that we use a QPersistentModelIndex for that, as the indexes might change while a persistent editor is opened (for example, when sorting or filtering), and the index provided in the arguments of delegate functions is not able to track these changes.
Emit a custom signal
In this case, we only use a custom signal that is emitted whenever we know that the layout is changed, and we create a local function in createEditor
that will eventually emit the sizeHintChanged
signal by “reconstructing” the valid index:
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
Use the delegate’s event filter
We can create a reference for the persistent index in the editor, and then emit the sizeHintChanged
signal in the event filter of the delegate whenever a LayoutRequest
event is received from the editor:
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)
Finally, you should obviously remove the SizeHintRole
return in data()
, and in order to open all persistent editors you could do something like this:
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)