问题
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:
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
Qt Model-View update view?
PyQt - Automatically refresh a custom view when the model is updated?
http://www.qtcentre.org/threads/48230-QTreeView-How-to-refresh-the-view
http://www.qtcentre.org/threads/3145-QTableView-How-to-refresh-the-view-after-an-update-of-the-model
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()
回答1:
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
来源:https://stackoverflow.com/questions/33061249/refresh-view-when-model-data-has-not-changed-qt-pyside-pyqt