Skip to content

MeldStudio/QTreeToTableModel

Repository files navigation

QTreeToTableModel

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.

QTreeToTableModel powered QML view in action

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.


Features

  • Flat table from any tree — Depth-first flattening of any QAbstractItemModel tree 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=1 shows only direct children, maxDepth=2 includes grandchildren, and so on.
  • Additional roles — Exposes DepthRole and SourceIndexRole on top of all source model roles.
  • QML-ready — Registered as a QML_ELEMENT with Q_INVOKABLE mapping 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 QAbstractItemModelTester validation.

Requirements

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 uses pragma ValueTypeBehavior: Addressable which requires Qt 6.7+.

Building

cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/gcc_64
cmake --build build

Running Tests

./build/tst_qtreetotablemodel

Running the QML Demo

./build/appQTreeToTableModel

Usage

C++

#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);

QML

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
    }
}

Additional Roles

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

API

Properties

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)

Mapping Functions

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

Utility Functions

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

How It Works

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.

Internal structure

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

Signal forwarding

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

Move preservation

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.

Root index and max depth filtering

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.

Testing

The test suite (tst_qtreetotablemodel.cpp) covers:

  • Model contract — key tests run with QAbstractItemModelTester to 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, hasChildren override, sibling behavior

Project Structure

├── 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

License

MIT — see LICENSE.

Attribution

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.

About

A QAbstractItemModel for converting tree models into a table structure.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors