A Model for all Ranges

Qt's model/view framework was one of the big additions to Qt 4 in 2005, replacing the previous item-based list, table, and tree widgets with a more general abstraction. QAbstractItemModel sits at the heart of this framework, and provides a virtual interface that allows implementers to make data available to the UI components. QAbstractItemModel is part of the Qt Core module, and it is also the interface through which Qt Quick's item views read and write data.

Qt includes some simple convenience model implementation, such as QStringListModel, various proxy models that provide transforming views on other models, and some more elaborate implementations, like the QSqlQueryModel.

For anything not covered by the built-in models, we can implement a custom QAbstractItemModel from scratch. While that is at best repetitious for simple use cases like a list of values, it can quickly become a rather complex task. This is also because the abstraction of QAbstractItemModel as a tree-of-tables is extremely generic. In theory, every row in a table could have different types of data for the respective columns; every cell can be a parent of a whole hierarchy of data, and every level of a tree can have completely different dimensions and underlying data structures. In practice however, it is rare to see a data structure that requires all this flexibility, and views generally assume a consistent layout. So, a tree is often just a hierarchy of rows, with the same set of columns on each level.

On the C++ side, a lot has changed in the last 20 years. The C++ standard template library in 2005 had few data structures and protocols to offer, and not much of it really worked reliably across the various (and often proprietary) tool chains. Today we have data structures for static and dynamic sizes, with solid support across all major compilers. We have standardized iterator APIs and algorithms operating on them, and with C++ 20 we got ranges and views that further generalize these basic principles. And we have class-template argument deduction, and a range of meta-programming tools and patterns that we can use to discover the capabilities of a given data structure at compile time.

Armed with those modern programming tools, and the assumption that not everything that can be done with QAbstractItemModel is also useful (and we are of course not taking QAbstractItemModel away from anyone!), we wanted to see if it is possible to implement a general purpose item model that can make any C++ range available to Qt's model/view framework, and in particular the item views in Qt Widgets and Qt Quick.

Spoiler: If you have played with the Qt 6.10 betas you might have noticed that there's a new class that implements this idea: QRangeModel

The code should be as simple as:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    std::vector<int> data = { 1, 2, 3, 4, 5 };
    QRangeModel model(data);

    QListView view;
    view.setModel(&model);
    view.show();

With a Qt Quick UI like this:


Window {
    id: window
    visible: true
    width: 500
    height: 500

    required property AbstractItemModel model

    ListView {
        id: list
        anchors.fill: parent
        model: window.model
        delegate: Text {
            required property string display

            width: list.width
            text: display
        }
    }
}

we'd then use the same model:

    QQmlApplicationEngine engine;
    engine.setInitialProperties({
        {
    	    "model", QVariant::fromValue(&model)
        }
    });

    engine.loadFromModule("Main", "Main");

    return app.exec();
} 

Put together, we get two windows, each showing a list with 5 numbers.

In the 6.10 betas, you find a manual test in qtbase/tests/manual/corelib/itemmodels/qrangemodel that does that, and many other things.

Ok, so far this doesn't seem to be particularly exciting. The only thing that's special so far is that we can have a model on a std::vector without writing a single line of std::vectors code. If you, however, pay attention to the widget UI created by the example above, then you'll notice that you can double click on an item, and edit it. And when doing that, then the Qt Quick UI will show the new value as well. So far no surprise - that's what the model/view framework gives us for any QAbstractItemModel. However, if you inspect the data variable in the main() function above, you'll see that it still holds the original 5 numbers. That's because we instantiated the QRangeModel with a copy of data.

Let's change that:

QRangeModel model(&data); // raw pointer

or

QRangeModel model(std::ref(data)); // reference wrapper

Now, modifying the model through the UI will modify the instance of data on the stack of the main function, giving us bi-directional integration between C++ data and both the widget and Qt Quick UI, with a single line of code. That we can use a std::vector<QString> or a QList<double> as the data type instead gives us a hint at what's possible with QRangeModel.

QStringList data = {"one", "two", "three"};
QRangeModel model(&data);

You can use a QList, or std::vector, or std::list, etc of anything that QVariant can convert into a string. If you are adventurous, try a C++ 20 range:

auto square = [](int i) { return i * i; };
QRangeModel model(std::views::iota(1, 5) | std::views::transform(square));

rangemodel-with-zipview

Sadly, some views, such as std::views::filter, cannot be used here; std::cbegin and std::cend are not available for all views in C++ 20, and while C++ 23 adds those, we still cannot iterate over a const filter view.

A single row of values is a good start. But let's beef our example up a bit to show a table:

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

    std::vector<std::vector<int>> data = {
        {1, 2, 3, 4, 5},
        {6, 7, 8, 9, 10},
        {11, 12, 13, 14, 15},
    };
    QRangeModel model(&data);

    QTableView view;
    view.setModel(&model);
    view.show();

    QQmlApplicationEngine engine;
    engine.setInitialProperties({
        {
            "model", QVariant::fromValue(&model)
        }
    });

    engine.loadFromModule("Main", "Main");

    return app.exec();
}
Window {
    id: window
    visible: true
    width: 500
    height: 500

    required property AbstractItemModel model

    TableView {
        id: table
        anchors.fill: parent
        model: window.model
        alternatingRows: true
        rowSpacing: 5
        columnSpacing: 5
        clip: true
        selectionModel: ItemSelectionModel {}
        delegate: TableViewDelegate {}
    }
}

Here we use a vector of vectors, which QRangeModel transforms into a table for us. And since we pass it in as a pointer, we can modify the entries in that data structure interactively. In fact, if we were to implement a UI that allows the user to insert, remove, or move rows or columns using the QAbstractItemModel API, then that would insert, remove, and move entries!

However, if we pass the range in as a const object, then modifications are not possible:

const std::vector<std::vector<int>> data = { ~~~ };
QRangeModel model(data); // const is also maintained when passing by value

Now the UI doesn't provide editing functionality, and programmatic calls to setData, moveRows etc will fail.

In addition to dynamically sized data structures like std::vector or QList, QRangeModel can also operate on fixed-sized data structures, like arrays:

std::array data = {1, 2, 3, 4, 5};

and tuples:

std::vector<std::tuple<int, QString>> data = {
    {1, "eins"},
    {2, "zwei"},
    {3, "drei"},
    {4, "vier"},
    {5, "fünf"},
};
QRangeModel model(std::ref(data));

This produces a table model with two columns, the first one being a list of numerical values, and the second being the German name of that value. We can edit the entries in the model, and we can insert and delete rows, and move them around; but we cannot make any modifications to the column structure: that's a compile time constant.

However, std::tuple is a bit messy to work with, especially if you have your own struct and have to implement the C++ tuple protocol for it. Qt's meta object system gives us an alternative:

struct Value
{
    Q_GADGET
    Q_PROPERTY(int display MEMBER m_display)
    Q_PROPERTY(QString toolTip MEMBER m_toolTip)
public:
    int m_display;
    QString m_toolTip;
};

std::vector<Value> data = {
    {1, "eins"},
    {2, "zwei"},
    {3, "drei"},
    {4, "vier"},
    {5, "fünf"},
};
QRangeModel model(&data);

This model can also modify entries and rows, but the column structure is still fixed (the amount and order of the properties is still a constant, although not a constexpr constant).

There's a reason for those property names: if you put that gadget into a table structure instead (or specialize QRangeModel::RowOptions to tag this gadget as a multi-role item - see the documentation), then it becomes a multi-role item:

std::vector<std::vector<Value>> data = {
    ~~~
};
QRangeModel model(&data);

This makes it very easy to build data structures in C++ that are fully accessible to Qt Quick item views using named required properties!

Ranges of multi-role items can also be realized using an associative container that maps from Qt::ItemDataRole, int, or QString to a QVariant.

QList<QMap<Qt::ItemDataRole, QVariant>> data = {
    {
        {Qt::DisplayRole, 1},
        {Qt::ToolTipRole, "eins"},
    },
    {
        {Qt::DisplayRole, 2},
        {Qt::ToolTipRole, "zwei"},
    },
};
QRangeModel model(&data);

Lastly, with some helper functions provided to navigate the hierarchy of parent and child rows, QRangeModel can also represent a C++ data type as a tree. This is a bit more involved, so I'll redirect to the documentation for that.

A note on the QRangeModel implementation

QRangeModel is designed to let the compiler generate a QAbstractItemModel implementation based on type of range it is constructed with. The constructor is a template, but QRangeModel itself is not a template class: it is a subclass of QAbstractItemModel, and - unrelated to moc's limitations in that respect - making template classes that are subclasses of polymorph types is ill-advised in C++, as we end up with weak virtual tables. This is particularly problematic in library code, as it can break dynamic_cast across library boundaries, and results in substantial bloat.

Instead, we are using sophisticated type erasure techniques in the implementation of QRangeModel. The curious reader might find some interesting approaches for accessing a tuple's element with a runtime index; routing of function calls through our own quasi-virtual-table machinery; and generally helpers that allows us to work with ranges, rows, and values that might be references, pointers, smart pointers, or plain values, all of which might be const.

Future plans

The main downside of letting a QAbstractItemModel of any form operate on a standard C++ data structure is that modifications done directly to the data structure are not going to be visible to the abstract item model. When changing values in a vector that is displayed by a view, then someone has to tell the view about that change. Obviously, a simple data[5] = 6 assignment won't, and there is no way for us to hook into e.g. std::vector to get informed about such changes. In the worst case, this results in crashes because QPersistentModelIndex instances won't be updated or invalidated. So one follow-up to QRangeModel is a convenient adapter class that gives us a C++-like API for modifying the range (without having to fiddle with QModelIndex), while speaking QAbstractItemModel protocol under the hood to inform the views about such changes, and to make sure that persistent indexes are updated.

Another larger follow-up is to make it easier to work with ranges that hold QObject instances. Connecting the notification signals of properties of those QObjects to the corresponding QAbstractItemView::dataChanged() signal emission makes it even easier to integrate C++ data with Qt Quick views.

As a smaller item, being able to set the header data for a range might be useful. QRangeModel already implements a default for tuples (the name of the element's type) and gadget rows (the name of the corresponding property). QAbstractItemModel conveniently provides setHeaderData() which does nothing. Perhaps we can provide a compile-time solution, at least for the horizontal header, in addition to a sensible default implementation of the setter.

And lastly, while we don't want to require anything more modern than C++17 to be able to use QRangeModel, we want to continue  to support modern C++ features: some std::ranges have no std::size implementation, for example unbounded ranges such as std::views::iota(0) where finding the end-iterator is an expensive operation. QAIM::fetchMore might come in handy for such cases, allowing us to search for the end one chunk at a time. And I'd really like to see if we can get std::views::filter and similar to work, without having to compromise on const-correctness in the QRangeModel code. That however will require at least C++ 23, where std::views::filter gets a cbegin(), which however still can't be called on a const view. And with reflections in C++ 26's, supporting structs might become a lot simpler than it is today, not requiring any tuple protocol implementation.

With Qt 6.10 you will be able to integrate standard C++ data structures into Qt Quick and Qt Widgets UIs without implementing your own QAbstractItemModel. And perhaps you can skip the next repetitive implementation of QAbstractItemModel entirely and let the compiler do the work for you. But if you come across a C++ range that you think should work, but doesn't - please let us know!


Blog Topics:

Comments