4

I have a tree view of a standard item model in which I can use a spinbox to change the row height in the view (see SSCCE below). This doesn't change the content of the view, only its appearance, sort of like resizing the main window except I have to do it myself:

enter image description here

I change the row height from within the delegate's sizeHint method. It is within sizeHint that I get the value from the spinbox and set the row height to that value. To make sure the size hint is actually called, I 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:

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). This is what I include in the SSCCE below, because it works. I also tried following the more commonly suggested practice of emitting dataChanged:

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

This does not work for me. 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.

At any rate, there is a lot of discussion online about what to do when you change the data in your model (see Relevant Posts), but none I have found about what to do when you just want to simply refresh the view for purely cosmetic reasons.

Cross post

I posted the same question at Qt Centre:

http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes

I got an answer there that have incorporated into the accepted answer below.

Relevant posts

SSCCE

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.refreshView)  #showimage uses the spinbox attribute to scale image

    def refreshView(self):
        self.tree.model.layoutChanged.emit()


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()
Community
  • 1
  • 1
eric
  • 7,142
  • 12
  • 72
  • 138
  • 1
    I usually just use `dataChanged`. – 101 Oct 13 '15 at 04:54
  • @figs strangely that was one thing that didn't work, when instead of layoutchanged I tried `self.tree.model.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())` nothing actually happens. Maybe I'm doing it wrong? I've updated my original Q to make clear what I tried and that it didn't work. – eric Oct 13 '15 at 12:24
  • If I could get that to work, at least I could start doing comparison/metrics. I've got like 4 other ways that work it would be fun to simply compare them for speed for models of different sizes, and answer based on that. After all, *the delegate doesn't care if the data has changed or not: it uses `paint` to repaint the data even if you are just making cosmetic changes* like changing background color, row height, whatever. That is likely the right answer. So if we get `dataChanged` to work I'll be able to do the appropriate metrics. – eric Oct 13 '15 at 12:44
  • @figs so it is strange: `dataChanged` causes my delegate to repaint, but not invoke `sizeHint`. When I use `layoutChanged` sizeHint is indeed called, which suggests this is why it works. At any rate, this is getting more complicated, it is not obvious what is going on. Would be very curious if someone could get `dataChanged` to work in my SSCCE. – eric Oct 13 '15 at 18:09
  • @figs If you look here (http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes) you will find description of very odd behavior with dataChanged. Will add more details probably separate question later, just wanted to highlight it. – eric Oct 15 '15 at 13:29

1 Answers1

2

Solution

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())

Analysis

As indicated in the OP, you don't want to call dataChanged because it doesn't actually work, and because your data hasn't actually changed. Further, while calling layoutChanged works, it is less principled because it is technically meant to be used to tell the view that the model's items have been rearranged, which they have not.

Caveat: I believe that sizeHintChanged expects to use a valid index, but my solution is working with the invalid index. Because it works, I'm leaving it with the invalid QtCore.QModelIndex(). Perhaps someone can find an improvement on that and edit this answer.

Is principled better than fast?

Note when you do it the principled way, it actually is a little bit slower than using the layoutChanged trick. Specifically running layoutChanged takes about 70 microseconds, while emitting sizeHintChanged takes about 100 microseconds. This didn't depend on the size of the models I tested (up to 1000 rows). 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.

The layoutChanged trick also has the benefit of being simpler: it doesn't involve messing around with the delegate, but uses intuitively simple methods on the main window class. Also because it doesn't depend on any methods being implemented in the delegate, the trick (arguably) seems to be more modular. The "proper" way depends on creating a more brittle dependence between the delegate and the main window, which means it will be easier to break the application when developers modify one or the other.

In sum, a case could be made that in just about every measurable way, the hack from the OP is better than the "principled" solution here.

Acknowledgment

I got turned on to the existence of sizeHintChanged from answers at the same question that I cross-posted at QtCentre:

http://www.qtcentre.org/threads/63982-Best-way-to-refresh-view-for-cosmetic-%28non-model%29-changes

eric
  • 7,142
  • 12
  • 72
  • 138