QTreeView animation updates, data updates, layout updates - definitive correct way and performance
-
Hi, using QTreeView with my custom QAbstractItemModel subclass.
What is the really correct way to conduct updates and animations, repaints?
There are 2 main cases - animation of icons (data is same), and other case: actual change of model's data.
Case 1:
The icons (displayed in some of the items) are animated.
This is done by heaving the ::data() on my subclass of QAbstractItemModel that returns different icons - different QIcon(...) depending on e.g. current time/clock. The code is like:if (role == Qt::DecorationRole) { if (clock%2) return QIcon(frame1); else return QIcon(frame2); }
This works, except that I do not know how to properly trigger the re-paint.
I have a timer, like:
QTimer * timer1 = new QTimer(nullptr); timer1->start(200); connect(timer1, &QTimer::timeout, this, &MainWindow::on_timer1); ... ::on_timer1() { ui->treeView->update(QRect(0,0,9000,9000)); }
the timer function fires, but repainting is not done.
What DOES work is to instead in the timer call fullui->treeView->model()->layoutChanged();
how ever this seems expensive (CPU-intensive).The tree might have even 1 million nodes (items) in it, of which e.g. 10,000 are expanded tree - but due to limited size of screen only e.g. 200 of them are actually visible on screen.
So, I do NOT want to call some data changed event on all 1,000,000 nodes, I instead want the QT to fully repaint the entire thing. Why the ->update() is not working? Tried also
ui->treeView->repaint(QRect(0,0,9000,9000));
but it, same as update, does nothing at all (nothing changes on screen).
Why this update and repaint do nothing? Only tree is repainted when I resize or move around the window manually with mouse as the user.
How to tell Qt to actually repaint all visible items?Case 1b:
small possible optimisation: say that only 10% of items have any animation in them, and the animation is only in tree column number 5 and 6. How to optimise this? E.g. I want Qt QTreeView to find out which items (AbstractItems - model nodes) are visible on screen right now, and for them call some function e.g. data() or otherwise and test which items need repainting, and which columns in them. How do I connect code that knows that into Qt so that Qt drawing code can do it?
Perhaps I should query QTreeView which items are visible (e.g. some list of QModelIndex), and tell Qt to re-run ::data() for each this item, and then notice it returns different QIcon this time?Case 2:
Besides animation, the tree data is actually dynamic (e.g. updated by background thread). My nodes have e.g. flag this->m_dirty so they know their data was updated (since last call to ::data on them). How all the changed items should tell QTreeView/Qt that their data actually changed and they might need repaint (if they are visible)? Call some Update() on them? OnDataChange...? -
@egorski said in QTreeView animation updates, data updates, layout updates - definitive correct way and performance:
But that doesn't seem right, if the tree is large and has also large structure (e.g. lots of subnodes).
For example a tree with typical fanout of 10, not to mention something like binary-tree, if 30% of items are animated, it means i would need to send 300,000 dataChanged events for each animation frame (few times a second).Then, as mentioned, just emit the
dataChaged
for a range (e.g.emit dataChanged(index(0,0),index(rowCount()-1,columnCount()-1),{Qt::DecorationRole})
) this is a single call and it will trigger a repaint of the entire viewport.so how to obtain list of currently visible items? emiting a dataChanged only for them might make more sense for such animation.
You shouldn't. That's a problem for the view, not the model.
QTreeView
already takes care of this without you having to do anything.Reads of logical_data from GUI (From ::data of QAbstractItemModel-subclass etc), and writes to it (done by background thread that process logical data of the tree) will be protected by mutex (std::mutex).
This should be fine as long as you don't change the shape of the tree (insert/remove/move/sort rows/columns) from a different thread
-
The reply for all the above is: emit
dataChanged
. When that signal is emitted from the modelQTreeView
is already smart enough to repaint only the item that changed and only if it's visible. If you emitdataChagend
for multiple items at once (i.e. thetopLeft
argument is not the same as thebottomRight
argument) the it will repaint all the items currently visible (so slightly inefficient here).Case 1:
different QIcon(...) depending on e.g. current time/clock.
This will also need to get paired with a timer to emit
dataChanged
. That's all.Case 1b:
To implement this optimisation (and only if it actually make sense) emit a differentdataChanged
signal for each item instead of indicating a range in the first 2 argumentsCase 2:
The model should emitdataChanged
to tell the view it needs repaintingP.S.
updated by background thread
Just so you are aware, models are not thread safe so model and view should both live on the GUI thread. It appears as they do in your case but wanted to leave a small footnote just in case
-
@VRonin said in QTreeView animation updates, data updates, layout updates - definitive correct way and performance:
This will also need to get paired with a timer to emit dataChanged. That's all.
But that doesn't seem right, if the tree is large and has also large structure (e.g. lots of subnodes).
For example a tree with typical fanout of 10, not to mention something like binary-tree, if 30% of items are animated, it means i would need to send 300,000 dataChanged events for each animation frame (few times a second).
If I would "pack" together events, so a typical node has 10 subnodes as assumed for my tree data heaving typical fanout=10 (and they have further subnodes) still it would be 300,000/10 = 30,000 events, right? Few times per second (e.g. 4 fps animation of the icons).
Also this seems semantically wrong, the data is not really updated in the model at all, it's just that it needs to be painted in other way. Perhaps a totally different approach should be used with overwritting custom painter/delegate somehow? And QDecoration returning list of QIcon...Unless, the data change works in recursive way, e.g. emitting it for the top node would signal need to repaint it's subnodes, subsubnodes, subsubsubnodes etc?
Since most or 30% of objects need repainting but only when visible, this is why sending dataChanged seems the bad way to approach it.
All this, versus just e.g. 100 visible objects that really need to be painted.
Case 1b:
To implement this optimisation (and only if it actually make sense) emit a different dataChanged signal for each item instead of indicating a range in the first 2 argumentsso how to obtain list of currently visible items? emiting a dataChanged only for them might make more sense for such animation.
Just so you are aware, models are not thread safe so model and view should both live on the GUI thread. It appears as they do in your case but wanted to leave a small footnote just in case
Right, the updates to actual data will be protected by mutex.
E.g. my QAbstractItemModel-subclass works by storing shared_ptr to some logical_data class inside of QModelIndex::internalPointer().
Reads of logical_data from GUI (From ::data of QAbstractItemModel-subclass etc), and writes to it (done by background thread that process logical data of the tree) will be protected by mutex (std::mutex).
-
@egorski said in QTreeView animation updates, data updates, layout updates - definitive correct way and performance:
But that doesn't seem right, if the tree is large and has also large structure (e.g. lots of subnodes).
For example a tree with typical fanout of 10, not to mention something like binary-tree, if 30% of items are animated, it means i would need to send 300,000 dataChanged events for each animation frame (few times a second).Then, as mentioned, just emit the
dataChaged
for a range (e.g.emit dataChanged(index(0,0),index(rowCount()-1,columnCount()-1),{Qt::DecorationRole})
) this is a single call and it will trigger a repaint of the entire viewport.so how to obtain list of currently visible items? emiting a dataChanged only for them might make more sense for such animation.
You shouldn't. That's a problem for the view, not the model.
QTreeView
already takes care of this without you having to do anything.Reads of logical_data from GUI (From ::data of QAbstractItemModel-subclass etc), and writes to it (done by background thread that process logical data of the tree) will be protected by mutex (std::mutex).
This should be fine as long as you don't change the shape of the tree (insert/remove/move/sort rows/columns) from a different thread
-
@VRonin said in QTreeView animation updates, data updates, layout updates - definitive correct way and performance:
Then, as mentioned, just emit the dataChaged for a range (e.g. emit dataChanged(index(0,0),index(rowCount()-1,columnCount()-1),{Qt::DecorationRole})) this is a single call and it will trigger a repaint of the entire viewport.
Allright, that seems to work.
I was under impression (apparently wrong) that in Tree (due to branches heaving different parent pointer) to select all elements you need to recurse yourself and select multitude of ranges.It seems that instead doing that thing once in the root element of tree does indeed "recurse" and selects all sub(sub*)nodes too.
So, now the timer does obtain pointer to my QAbstractItemModel-subclass (with some dynamic casts, there is a sorting proxy model in beteween), and calls its
->emit_all_data_changed();
which is implemented as:void c_node_model::emit_all_data_changed() { emit dataChanged(index(0,0),index(rowCount()-1,columnCount()-1),{Qt::DecorationRole}); }
the conversions&call in timer are done like:
auto * model_proxy1 = ui->treeView->model(); auto * model_proxy2 = dynamic_cast<QAbstractProxyModel*>(model_proxy1); auto * model_unproxy = model_proxy2->sourceModel(); auto * mymodel = dynamic_cast<n_tagsentry::n_qt::c_node_model *>( model_unproxy ); if (mymodel) { mymodel->emit_all_data_changed(); } else throw std::runtime_error("Can't convert to my model");
It seems to work fine (much less CPU intensive than calling full layoutChanged). I will later test how it behaves with large (and deep) trees (also with nodes mostly expanded on screen).
This should be fine as long as you don't change the shape of the tree (insert/remove/move/sort rows/columns) from a different thread
To insert/remove child nodes, I plan to:
Take exclusive mutex lock on the node (parent), then modify it (push_back into ->m_child), and then emit dataChanged event on the parent's model.As for changes to columns, sorting etc, they are never done from background logic, only are done as e.g. onClick and such from the GUI, so should be "in GUI thread".
Of course, all accesses to my node's logical data will be under mutexes still.
Thanks.
-
@egorski said in QTreeView animation updates, data updates, layout updates - definitive correct way and performance:
So, now the timer does obtain pointer to my QAbstractItemModel-subclass (with some dynamic casts, there is a sorting proxy model in beteween), and calls its ->emit_all_data_changed(); which is implemented as:
I would have moved the timer to be part of the model instead of having it external but the concept is the same
@egorski said in QTreeView animation updates, data updates, layout updates - definitive correct way and performance:
To insert/remove child nodes, I plan to:
Take exclusive mutex lock on the node (parent), then modify it (push_back into ->m_child), and then emit dataChanged event on the parent's model.That's not how it works. To insert child elements you need to call
beginInsertRows
/endInsertRows
. The view and the proxy models will "do stuff" as a consequences of calling those functions and your model must be in "pre-change" state untilbeginInsertRows
returns and must be in the "post-change" state when you callendInsertRows
.
It's very useful to have the model test run alongside to make sure everything behaves as it should