PDA

View Full Version : Best way to refresh view for cosmetic (non-model) changes?



neuronet
15th October 2015, 06:37
I change the row height of a tree view from within a delegate's sizeHint method. It is within sizeHint that I get the value from a spinbox in a toolbar, and set the row height to the value in the spinbox. To make sure the size hint is actually called, I aim to refresh the view when the spinbox value is changed.

My question is this: in such cases of purely cosmetic changes, what is the recommended way to tell the view to refresh? Is there some method built specifically for such cases? Obviously, this question assumes my general strategy for adjusting row height is sound, which I am also open to correction on. There are a few methods for telling the view that it is time to refetch the data and redraw things: layoutChanged, reset, setModel, dataChanged. Hell, I found that even just calling expandAll on the tree was enough to update my view to show the new row height.

In practice, I found using layoutChanged works extremely well:


QtGui.QStandardItemModel.layoutChanged.emit()

It is sort of uncommon usage, as that is more for when you have rearranged your data (e.g., by sorting).

I also tried following the more commonly suggested practice of emitting dataChanged:


QtGui.QStandardItemModel.dataChanged.emit(QtCore.Q ModelIndex(), QtCore.QModelIndex())

This does not work. Even if it did, it would also be something of a hack, because it is telling the view that the data has changed in the model. When it hasn't. Interestingly, I found that while layoutChanged does call sizeHint, calling dataChanged only calls paint, but not sizeHint. So the suggestion here:

http://www.qtcentre.org/threads/48230-QTreeView-How-to-refresh-the-view

Doesn't work for purely cosmetic changes like changing row height, it seems.

Perhaps I'm missing a better technique, but layoutChanged() seems to work well in my standard item model. I'm wondering if I'm missing an obvious better way.

----------------------

Note I posed this question at Stack Overflow:
http://stackoverflow.com/questions/33061249/updating-view-when-model-data-has-not-changed-qt-pyside-pyqt

anda_skoa
15th October 2015, 08:41
So you change the value that is later returned by SizeHintRole in all cells in your model, then emit the dataChanged and the view doesn't pick it up?

Cheers,
_

neuronet
15th October 2015, 14:22
So you change the value that is later returned by SizeHintRole in all cells in your model, then emit the dataChanged and the view doesn't pick it up?


Yes. Strangely, the paint method is called when I invoke dataChanged, but sizeHint is not.

Note I'm using QStandardItemModel, not QAbstractItemModel. Here is my code:

import sys
from PySide import QtGui, QtCore

class MainTree(QtGui.QMainWindow):
def __init__(self, parent = None):
QtGui.QMainWindow.__init__(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.createRowHeightSpinbox() #create first otherwise get errors
self.tree = SimpleTree(self)
self.setCentralWidget(self.tree)
#Add spinbox to toolbar
self.rowHeightAction = QtGui.QAction("Change row height", self)
self.toolbar = self.addToolBar("rowHeight")
self.toolbar.addWidget(QtGui.QLabel("Row height "))
self.toolbar.addWidget(self.rowHeightSpinBox)
#Expand and resize tree
self.tree.expandAll()
self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)

def createRowHeightSpinbox(self):
self.rowHeightSpinBox = QtGui.QSpinBox()
self.rowHeightSpinBox.setRange(10, 50)
self.rowHeightSpinBox.setValue(18)
self.rowHeightSpinBox.valueChanged.connect(self.re freshView) #showimage uses the spinbox attribute to scale image

def refreshView(self):
self.tree.model.layoutChanged.emit() #this works
###self.tree.model.dataChanged.emit(QtCore.QModelI ndex(), QtCore.QModelIndex()) #this does not work


class SimpleTree(QtGui.QTreeView):
def __init__(self, parent = None):
QtGui.QTreeView.__init__(self, parent)
self.setUniformRowHeights(True) #optimize
self.model = QtGui.QStandardItemModel()
self.rootItem = self.model.invisibleRootItem()
item0 = [QtGui.QStandardItem('Sneeze'), QtGui.QStandardItem('You have been blocked up')]
item00 = [QtGui.QStandardItem('Tickle nose'), QtGui.QStandardItem('Key first step')]
item1 = [QtGui.QStandardItem('Get a job'), QtGui.QStandardItem('Do not blow it')]
self.rootItem.appendRow(item0)
item0[0].appendRow(item00)
self.rootItem.appendRow(item1)
self.setModel(self.model)
self.setItemDelegate(ExpandableRows(self))


class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent

def sizeHint(self, option, index):
rowHeight = self.parent.window().rowHeightSpinBox.value()
text = index.model().data(index)
document = QtGui.QTextDocument()
document.setDefaultFont(option.font)
document.setPlainText(text) #for html use setHtml
return QtCore.QSize(document.idealWidth() + 5, rowHeight)


def main():
app = QtGui.QApplication(sys.argv)
#myTree = SimpleTree()
myMainTree = MainTree()
myMainTree.show()
sys.exit(app.exec_())


if __name__ == "__main__":
main()

Added after 7 minutes:

Just did some experimenting trying to get sizeHint called with dataChanged by editing data in the model, and that only worked when I edited data in the first row (i.e., the first child of the invisible root item). Any other rows I edit, it repaints but does not invoke sizeHint so the row doesn't track the spinbox. But when I turn off 'setUniformRowHeight', this no longer is the case: upon editing an item in a row, when the editor is closed the row height for that row is adjusted.

So I seem to be screwing up something in my delegate, in that when I have uniform row height set to True, it only applies size hint when I edit the first row.

But this "insight" doesn't fix the problem: even turning off setUniformRowHeight doesn't make the row height update with the spinbox. Only editing data in the model does that.

Note all of this addendum only applies when I try to use dataChanged. Emitting layoutChanged seems to work fine.

Not sure which of these behaviors I've found are bugs and which are features: I assume all of them are features. I am pretty new to delegates.

anda_skoa
15th October 2015, 15:13
Your posted code seems to be missing the lines that set the sizehint values for the cells in the model.

So when dataChanged() triggers getting new data, the values will be the same as before.

Cheers,
_

neuronet
15th October 2015, 18:37
Your posted code seems to be missing the lines that set the sizehint values for the cells in the model.

So when dataChanged() triggers getting new data, the values will be the same as before.

Cheers,
_

Line 53:


def sizeHint(self, option, index):
rowHeight = self.parent.window().rowHeightSpinBox.value()
....
return QtCore.QSize(document.idealWidth() + 5, rowHeight)


I thought this was returning the size, and it would be automatically called when the view refreshed. It works great when I use 'layoutChanged' (which is what I include in the SSCCE). Again, I'm new to delegates I'm sure I'm doing something wrong, but not sure why you are saying I am not setting the height, I thought I was returning the new values right there in sizeHint.

anda_skoa
16th October 2015, 08:31
Line 53:


def sizeHint(self, option, index):
rowHeight = self.parent.window().rowHeightSpinBox.value()
....
return QtCore.QSize(document.idealWidth() + 5, rowHeight)


Ah.
That is in the delegate.
dataChanged() tells the view that the model's data has changed, so it will retrieve the data again.
But it could very well check if the data has actually changed and not do anything if it hasn't.
For example if the model does not return anything for SizeHintRole and, again after dataChanged(), then the view could decide to not redraw.

If the delegate wants the view to consider a new size hint, it has a signal on its own for that.

Using layoutChanged() does not appropriate too.

Cheers,
_

neuronet
16th October 2015, 13:16
Ah.
That is in the delegate.
dataChanged() tells the view that the model's data has changed, so it will retrieve the data again.
But it could very well check if the data has actually changed and not do anything if it hasn't.
For example if the model does not return anything for SizeHintRole and, again after dataChanged(), then the view could decide to not redraw.

If the delegate wants the view to consider a new size hint, it has a signal on its own for that.

Using layoutChanged() does not appropriate too.

Cheers,
_

So, what is the appropriate thing to use then? Is there a more direct way to force sizeHint to be invoked? While it may seem weird for a tree view, it is pretty common for tables to need to change the row height. What would people use for a spinbox to change row height for a table? layoutChanged() may be a hack, but it seems to work well, and is very fast. I guess I'll use that until something better comes along.

I guess if it were a table we'd have 'resizeRowsToContents: Resizes all rows based on the size hints of the delegate used to render each item in the rows.' Unfortunately we don't have that for trees. Or hell, we'd just use `setRowHeight` for a table view, another method we don't have for tree views.

I found a similar problem someone encountered:
http://www.qtcentre.org/threads/44019-Row-Height-Resizing-with-QAbstractItemModel-amp-QTreeView

ChrisW67
16th October 2015, 22:07
QAbstractItemDelegate::sizeHintChanged()

neuronet
17th October 2015, 00:56
QAbstractItemDelegate::sizeHintChanged()

Interesting. I added the following within the paint reimplementation (I specified just the first row.column index so it wouldn't be invoked for every item):

if index.row()==0 and index.column() == 0:
self.sizeHintChanged.emit(QtCore.QModelIndex())
It seems to work well. Not sure if I'm using it as I should: does using it this way seem appropriate? Seems a good way to force the issue.

That said, I fear something is amiss, as now I don't even need to call dataChanged and when I add that line to paint my app is actually calling paint continuously, even with zero interactions with the window.

So something is amiss....

Here is my revised paint function:

def paint(self, painter, option, index):
print 'paint'
if index.row()==0 and index.column() == 0:
self.sizeHintChanged.emit(QtCore.QModelIndex())
QtGui.QStyledItemDelegate.paint(self, painter, option, index)

ChrisW67
17th October 2015, 04:55
At a guess:
When the view paints you tell the view that the size might have changed, so it queues a repaint...
Then, when the view paints you tell the view that the size might have changed, so it queues a repaint...

You should emit the sizeHintChanged() signal only if the size hint actually changes, and also only for the indexes that the size change applies to (i.e. not an invalid index)

neuronet
17th October 2015, 05:18
At a guess:
When the view paints you tell the view that the size might have changed, so it queues a repaint...
Then, when the view paints you tell the view that the size might have changed, so it queues a repaint...

You should emit the sizeHintChanged() signal only if the size hint actually changes....

The problem is emitting it from within the main window when the qspinbox value changes. That's what I want to do. I'll think about how to do it. Maybe I can construct the valueChanged connection to sizeHint within the delegate?

Must sleep, but this seems to work, even when I don't worry about the index (after all I want all rows to change so this is convenient :)):

class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
self.parent.window().rowHeightSpinBox.valueChanged .connect(self.emitSizeChange)

def emitSizeChange(self):
self.sizeHintChanged.emit(QtCore.QModelIndex())

This looks a little ugly, but it actually seems elegant. Maybe my syntax is a little off, but it seems pretty.

ChrisW67
17th October 2015, 21:20
You could try this approach.

Give your delegate a slot to receive the value of the spin box when sent by the spin box's valueChanged(), e.g. setRowHeight(int).
When received, store the value in a delegate member variable and use that when sizeHint() is called.
Connect the spin box valueChanged() signal to your custom delegate's setRowHeight().
Connect the spin box valueChanged() signal to your view's update() slot (You should be able to ignore the parameter mismatch)

neuronet
20th October 2015, 15:33
From your suggestion, I connect spin box valueChanged() directly to the delegate's sizeHintChanged.emit(), and it works great. I posted my full solution in more detail here:
http://stackoverflow.com/a/33193340/1886357



The principled solution is to emit QAbstractItemDelegate.sizeHintChanged when the spinbox value changes. This is because you only want to call sizeHint of your delegate, and that's exactly what this method does.

In the example in the OP, the size hint is intended to change when the value in the spinbox is changed. You can connect the valueChanged signal from the spinbox to the delegate's sizeHintChanged signal as follows:


class ExpandableRows(QtGui.QStyledItemDelegate):
def __init__(self, parent=None):
QtGui.QStyledItemDelegate.__init__(self, parent)
self.parent = parent
self.parent.window().rowHeightSpinBox.valueChanged .connect(self.emitSizeChange)

def emitSizeChange(self):
self.sizeHintChanged.emit(QtCore.QModelIndex())

As indicated in the OP, you don't want to call dataChanged because it doesn't actually work, and because your data hasn't changed. And while calling layoutChanged works (indeed, it works very well: see below), it is less principled because it is technically meant to be used to tell the view that the models items have been rearranged, which isn't the case here.

Note when you do it the principled way, it actually is a tiny bit slower than using the layoutChanged trick, so it isn't clear this way is objectively better in any way! Specifically running layoutChanged takes about 70 microseconds, while emitting sizeHintChanged took about 100 microseconds. This didn't depend on the size of my models (up to 1000 row trees). This difference of 30 microseconds is so small as to be negligible in most applications, but if someone really wants to fully optimize for speed, they might go with the layoutChanged trick.

One caveat: technically I believe that sizeHintChanged expects to receive a valid index, but it seems to work with the invalid index, so I'm not sure what is going on there. Because it works, I'm leaving it with the invalid QtCore.QModelIndex() but am open to improvements on that.