4 min read
If you’ve worked with QTreeView in Qt, you may have run into
performance issues when updating or refreshing large tree models. Specifically,
saving and restoring the expanded/collapsed state of a tree can become a bottleneck
if you’re calling model()->match(...)
with a wildcard,
which scans the entire model multiple times.
In this post, I’ll show you how I discovered the problem, what caused it, and how
I optimized it—reducing refresh time from 800ms down to
200ms by replacing match(...)
calls with a single
recursive approach.
I first noticed that whenever my data model called
beginResetModel()
and endResetModel()
, the UI froze
for nearly a second. Since Qt’s painting and event processing happen on the main
thread, this lag caused my entire application to feel unresponsive.
To see where the time was being spent, I added simple time measurements (you could also use a profiler). It turned out that these two functions were the bottlenecks:
onSaveExpandedState()
onRestoreExpandedState()
They each used model()->match(...)
with a wildcard "*", plus
Qt::MatchRecursive
. This function was scanning all items in a
potentially large tree. Then I was iterating again to expand or collapse nodes.
The naive approach looked something like this (simplified):
void TreeView::onSaveExpandedState() {
// ...
QModelIndexList indexList =
model()->match(model()->index(0, 0), Qt::DisplayRole, "*", -1,
Qt::MatchWildcard | Qt::MatchRecursive);
foreach (const QModelIndex &idx, indexList) {
if (isExpanded(idx)) {
m_expandedItems.insert(model()->data(idx).toString());
}
}
// ...
}
A second model()->match(...)
call in
onRestoreExpandedState()
repeated the whole process. This works fine for
tiny trees. But in a large model, it’s extremely slow because you’re forcing Qt to
search the entire tree for a wildcard match, effectively re-traversing everything
multiple times.
To fix this, I removed all match(...)
calls and replaced them with a
single DFS (depth-first search) through the tree, using the standard
rowCount
and index
calls in a loop:
void TreeView::onSaveExpandedState() {
setUpdatesEnabled(false);
m_expandedItems.clear();
saveExpandedStateRecursively(QModelIndex()); // Start from the root
setUpdatesEnabled(true);
}
void TreeView::saveExpandedStateRecursively(const QModelIndex &parentIndex) {
for (int i = 0; i < model()->rowCount(parentIndex); ++i) {
QModelIndex childIndex = model()->index(i, 0, parentIndex);
if (isExpanded(childIndex)) {
m_expandedItems.insert(uniqueKeyForIndex(childIndex));
}
saveExpandedStateRecursively(childIndex);
}
}
The counterpart for restoring expansions is very similar:
void TreeView::onRestoreExpandedState() {
setUpdatesEnabled(false);
restoreExpandedStateRecursively(QModelIndex());
setUpdatesEnabled(true);
}
void TreeView::restoreExpandedStateRecursively(const QModelIndex &parentIndex) {
for (int i = 0; i < model()->rowCount(parentIndex); ++i) {
QModelIndex childIndex = model()->index(i, 0, parentIndex);
if (m_expandedItems.contains(uniqueKeyForIndex(childIndex))) {
setExpanded(childIndex, true);
}
restoreExpandedStateRecursively(childIndex);
}
}
Lastly, I needed a unique identifier for each QModelIndex
. I used a path
of row indices from the root:
QString TreeView::uniqueKeyForIndex(const QModelIndex &index) const {
if (!index.isValid()) return QString();
QStringList pathParts;
QModelIndex current = index;
while (current.isValid()) {
pathParts.prepend(QString::number(current.row()));
current = current.parent();
}
return pathParts.join("/");
}
By removing wildcard matching and scanning the tree exactly once for saving and once for restoring, the refresh time fell from 800ms to 200ms. That’s still not instant, but it’s a 75% improvement and enough to keep my UI responsive. In many projects, this alone is a huge win.
Some developers take it a step further by tracking expansions and collapses in real
time via the expanded(...)
and collapsed(...)
signals, so
they rarely need to do a full traversal after the model resets. In many cases, you can
also avoid calling beginResetModel()
entirely (e.g., using more
fine-grained updates), which can preserve expansions automatically. But the above
approach is simple and made a big difference for my use case.
If you find your Qt application struggling whenever a large tree is reset, check
whether you’re using model()->match(...)
with a wildcard. That call
can be surprisingly expensive on big trees. A straightforward recursive loop or
real-time expand/collapse tracking can solve the problem elegantly.
For me, this change took just a few lines of code but yielded a 75% performance improvement. If your users are seeing slow UIs, especially with big data sets in tree views, I highly recommend giving this approach a try!
© 2024 Milad Sharbati. All Rights Reserved. Designed by HTML Codex