Optimizing QTreeView’s Expand/Collapse Performance in Qt

4 min read

Introduction

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.

Identifying the Problem

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 and Its Costs

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.

A Better Recursive Method

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("/");
}

Performance Gains

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.

Conclusion

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!

Further Reading & References:

© 2024 Milad Sharbati. All Rights Reserved. Designed by HTML Codex