A QAbstractTableModel that flattens any tree model into a flat table in depth-first order — extracted from Meld Studio and released under the MIT license.
Short gif of QTreeToTableModel powered QML view in action.
Takes any QAbstractItemModel with a tree structure and presents it as a flat table, where every visible tree item becomes a row. All items are always expanded — there is no collapse/expand toggle. Useful for presenting hierarchical data in flat views like ListView, TableView, or QListView.
- Flat table from any tree — Depth-first flattening of any
QAbstractItemModeltree into a flat table model. - Efficient signal forwarding — Inserts, removes, moves, data changes, layout changes, and column changes are all forwarded efficiently to minimize view updates.
- Move preservation — Move operations in the source model are preserved as move operations in the flat table where possible.
- Root index support — Optionally flatten only a subtree by setting a root index. Only descendants of the root are shown.
- Max depth limit — Optionally limit the depth of items included.
maxDepth=1shows only direct children,maxDepth=2includes grandchildren, and so on. - Additional roles — Exposes
DepthRoleandSourceIndexRoleon top of all source model roles. - QML-ready — Registered as a
QML_ELEMENTwithQ_INVOKABLEmapping helpers for direct use in QML. - Thoroughly tested — 165 tests covering construction, flattening, root index, max depth, insertions, removals, moves, data changes, layout changes, column changes, edge cases, persistent indexes, stress/performance, cross-feature interactions, and
QAbstractItemModelTestervalidation.
| Dependency | Version |
|---|---|
| C++ | 20 |
| Qt | 6.10+ |
| CMake | 3.16+ |
Note: The core library (
qtreetotablemodel.h/cpp) requires only Qt 6.4+. The QML demo usespragma ValueTypeBehavior: Addressablewhich requires Qt 6.7+.
cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/gcc_64
cmake --build build./build/tst_qtreetotablemodel./build/appQTreeToTableModel#include "qtreetotablemodel.h"
QStandardItemModel sourceModel;
// ... populate tree ...
QTreeToTableModel flatModel;
flatModel.setSourceModel(&sourceModel);
// Optionally show only a subtree
flatModel.setRootIndex(sourceModel.index(0, 0));
// Optionally limit depth
flatModel.setMaxDepth(2);
// Map between flat and source indices
QModelIndex flatIdx = flatModel.index(row, 0);
QModelIndex srcIdx = flatModel.mapToSource(flatIdx);import QTreeToTableModel
// Wrap any QAbstractItemModel tree — here 'myTreeModel' is
// exposed from C++ via a context property or QML_ELEMENT.
QTreeToTableModel {
id: flatModel
sourceModel: myTreeModel
maxDepth: 3 // 0 = unlimited
}
ListView {
model: flatModel
delegate: Text {
required property int depth
required property string display
text: " ".repeat(depth * 2) + display
}
}| Role | Name | Description |
|---|---|---|
Roles::Depth |
depth |
Depth of the item relative to the root (0 = direct child of root) |
Roles::SourceIndex |
sourceIndex |
QModelIndex of the item in the source model |
| Property | Type | Description |
|---|---|---|
sourceModel |
QAbstractItemModel* |
The source tree model to flatten |
rootIndex |
QModelIndex |
Optional root index; when set, only descendants are shown |
maxDepth |
int |
Maximum depth to include (0 = unlimited) |
count |
int |
Number of rows in the flattened table (read-only, QML convenience) |
| Method | Description |
|---|---|
mapToSource(proxyIndex) |
Map a flat table index to the source model |
mapFromSource(sourceIndex) |
Map a source index to the flat table |
sourceIndexForRow(row) |
Get the source index for a flat row |
rowForSourceIndex(sourceIndex) |
Get the flat row for a source index (-1 if not visible) |
depthForRow(row) |
Get the depth of an item at a given row (-1 if invalid) |
isSourceIndexVisible(sourceIndex) |
Check if a source index is currently visible |
| Method | Description |
|---|---|
resetRootIndex() |
Reset the root index to show the entire source tree |
isAncestorOf(ancestor, descendant) |
Check if one source index is an ancestor of another (or the same index) |
moveSourceRow(sourceIndex, destinationParent, destinationChild) |
Move a source item (and its subtree) to a new position via the source model's moveRows() |
dump() |
Print the current model state to the debug log |
QTreeToTableModel maintains a flat QList<FlatItem> where each entry stores a QPersistentModelIndex pointing back to the source tree and the item's depth relative to the root. The list is kept in depth-first order, matching what a fully expanded tree view would display.
Source tree: Flat list (m_items):
A [0] A depth=0
├─ A1 [1] A1 depth=1
│ ├─ A1a [2] A1a depth=2
│ └─ A1b [3] A1b depth=2
└─ A2 [4] A2 depth=1
B [5] B depth=0
└─ B1 [6] B1 depth=1
C [7] C depth=0
Rather than rebuilding the flat list on every source model change, the model intercepts source signals and emits targeted updates:
| Source signal | Flat model response |
|---|---|
rowsInserted |
Flatten the new subtree and emit beginInsertRows/endInsertRows at the correct position |
rowsAboutToBeRemoved |
Find the flat range (item + all descendants) and emit beginRemoveRows/endRemoveRows |
rowsAboutToBeMoved / rowsMoved |
Compute source and destination flat ranges; emit beginMoveRows/endMoveRows where possible, falling back to remove+insert when beginMoveRows rejects the operation |
dataChanged |
Map the changed source rows to flat rows and emit a single dataChanged |
layoutAboutToBeChanged / layoutChanged |
Save persistent indexes, rebuild the flat list, then remap persistent indexes to their new positions |
columnsInserted / columnsRemoved |
Forward column changes at the root level |
modelReset |
Full rebuild via beginResetModel/endResetModel |
When the source model emits rowsAboutToBeMoved, QTreeToTableModel computes the flat row ranges for the moved items and stores them in a PendingMove. On the subsequent rowsMoved signal, it applies the move with depth adjustments and emits beginMoveRows/endMoveRows. If the flat ranges overlap (making beginMoveRows reject), it falls back to a remove + insert pair.
When rootIndex is set, only descendants of that index appear in the flat list. The root item itself is excluded. When maxDepth is set to N > 0, only items with depth < N relative to the root are included. Both filters are applied during flattening and during incremental signal handling, so items outside the visible window are never inserted or tracked.
The test suite (tst_qtreetotablemodel.cpp) covers:
- Model contract — key tests run with
QAbstractItemModelTesterto validate index consistency, data roles, and parent/child relationships - Flattening — depth-first order, depth calculation, deeply nested and wide trees
- Root index — subtree filtering, depth recalculation, root removal, root from wrong model
- Max depth — depth limiting, boundary behavior, combined with root index
- Insertions — top-level, nested, mid-tree, with subtrees, at depth boundaries
- Removals — single items, subtrees, entire tree, root index subtree removal
- Moves — same parent, cross-parent, with subtrees, visible-to-invisible, invisible-to-visible, at depth boundaries, back-and-forth
- Data changes — single items, ranges, outside root subtree
- Layout changes — full rebuild with persistent index remapping
- Column changes — insert/remove at root level
- Signal pairing — every mutation verifies both the "aboutTo" and completion signals are emitted as matched pairs with correct arguments
- Persistent indexes — survival across inserts, removes, moves, and layout changes
- Stress and performance — large trees, rapid insert/remove cycles, multi-column models
- Edge cases — empty model, invalid inputs, foreign model indexes, no-op operations,
hasChildrenoverride,siblingbehavior
├── qtreetotablemodel.h # Public API — single header
├── qtreetotablemodel.cpp # Implementation (~1,180 lines)
├── simpletreemodel.h # Demo source model with moveRows() support
├── simpletreemodel.cpp # SimpleTreeModel implementation
├── tst_qtreetotablemodel.cpp # Test suite (~3,500 lines, 165 tests)
├── Main.qml # Demo app entry point
├── QTreeToTableView.qml # Interactive QML tree view demo
├── main.cpp # Demo app bootstrap
├── CMakeLists.txt # Build configuration
├── LICENSE # MIT license
└── .gitignore # Git ignore patterns
MIT — see LICENSE.
Extracted from Meld Studio, a live streaming and content creation application. Originally developed as a way to present hierarchical tree data in flat views and as an internal tool for QML integration with tree-structured models.
