New in Qt 6.11: QRangeModel updates and QRangeModelAdapter
-
When introducing QRangeModel for Qt 6.10 I wrote that we'd try to tackle some limitations in future releases. With Qt 6.11 around the corner, it's time to give an update.
Model updates when properties change
For a
QRangeModelwhere all items or rows are backed by instances of the same QObject type, the model can now automatically emit thedataChanged()signal when properties of those objects change. This provides a convenient mechanism to keep things in sync without having to go through theQAbstractItemModelAPI for such changes. Let's say our data is backed by an item type like this:class Person : public QObject { Q_OBJECT Q_PROPERTY(QString firstName READ firstName WRITE setFirstName NOTIFY firstNameChanged) Q_PROPERTY(QString lastName READ lastName WRITE setLastName NOTIFY lastNameChanged) Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged) public: explicit Person( /* ~~~ */ ); // ~~~ Q_SIGNALS: // ~~~ void ageChanged(int age); private: int m_age = 0; };QRangeModel represents a list of such objects as a table with three columns, one for each property.
// backend code QList<Person *> data = { new Person("Max", "Mustermann", 37), new Person("Eva", "Nordmann", 39), new Person("Bob", "Mountainman", 42), }; model = new QRangeModel(&data); model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::Full); // frontend code tableView = new QTableView(this); tableView->setModel(model);By setting the
autoConnectPolicyof QRangeModel to eitherFullorOnRead, future changes to properties will make the model emitQAbstractItemModel::dataChanged()for the corresponding index and role. This in turn updates all views.// backend code data[0]->setAge(data[0]->age() + 1);The only requirement is that the property has a notification signal that the setter emits when the value changes.
Using
QObjectinstances as the data type backing a model is comparatively expensive, but this addition makes it easy to work with the data inside small models directly, without having to deal withQModelIndexorQVariant.Modify a model with QRangeModelAdapter
Making it unnecessary to deal with those
QAbstractItemModel'isms for trivial operations is also the idea behind the biggest item-models addition in Qt 6.11:QRangeModelAdapteris a new template class that makes it safe and convenient to work with the data structure aQRangeModeloperates on. Item data and row/column structure of the underlying range can be modified and accessed through the adapter without having to deal withQModelIndexorQVariant, and the adapter makes sure that the model emits signals, invalidates and updates persistent indexes, and informs views about the changes.struct Backend { QList<int> data { 1, 2, 3, 4 }; QRangeModelAdapter adapter(std::ref(data)); void updateData(); }; class Frontend { Frontend() { // ~~~ listView->setModel(backend.adapter.model()); // the adapter creates and owns the model } }; // ~~~ void Backend::updateData() { adapter[0] = 23; // emits dataChanged() adapter.insertRow(0, 78); // prepends 78, calls begin/endInsertRows adapter.removeRows(2, 2); // removes two rows, calls begin/endRemoveRows }If the underlying data structure is a table, then an individual item can be modified like this:
void Backend::updateData() { adapter.at(1, 0) = 44; // works with C++17 adapter[0, 1] = 55; // with C++23's multidimensional subscript operator }For tree-shaped data structures, the row can be accessed using a path of row indexes:
void Backend::updateData() { adapter.at({0, 1}, 1) = "zero/one:one"; // second column in the second child of the first toplevel row }In addition to index-based access to rows and columns,
QRangeModelAdapterprovides an iterator API:void Backend::storeData() const { for (const auto &item : adapter) storage << item; }With a table, clearing all cells while maintaining the dimensions of the table could be done like this:
void Backend::clearData() { for (auto row : adapter) { for (auto item : row) { item = {}; } } }QRangeModelAdapteris in technology preview for Qt 6.11; the API is a bit unorthodox, with different semantics depending on the underlying data structure. The heavy use of C++ 17 meta programming techniques also makes the documentation a bit unwieldy, which has given our qdoc developers a good reason to improve the rendering of template APIs. We'd very much like to hear your feedback to it!Customizing item access
We added a new customization point for implementing custom item access. Specialize
QRangeModel::ItemAccessfor your type and implement staticreadRoleandwriteRoleaccessors to read and write role-specific values to an item:template <> struct QRangeModel::ItemAccess<Person> { static QVariant readRole(const Person &item, int role) { switch (role) { case Qt::DisplayRole: return item.firstName() + " " + item.lastName(); case Qt::UserRole: return item.firstName(); case Qt::UserRole + 1: return item.lastName(); case Qt::UserRole + 2: return item.age(); } return {}; } static bool writeRole(Person &item, const QVariant &data, int role) { bool ok = true; switch (role) { case Qt::DisplayRole: case Qt::EditRole: { const QStringList names = data.toString().split(u' '); if (names.size() > 0) item.setFirstName(names.at(0)); if (names.size() > 1) item.setLastName(names.at(1)); break; } case Qt::UserRole: item.setFirstName(data.toString()); break; case Qt::UserRole + 1: item.setLastName(data.toString()); break; case Qt::UserRole + 2: item.setAge(data.toInt(&ok)); break; default: ok = false; break; } return ok; } };A specialization of
ItemAccesstakes precedence over built-in access mechanisms, and also implies thatQRangeModelwill interpret the type as a multi-role item, even if it could be treated as a multi-column row. So you can use this technique to customize and optimize access to tuple-like types, gadgets, or completely custom structs.Support for caching std::ranges
Last but not least, a minor improvement for users of
std::ranges:QRangeModelno longer requiresstd::being/endon constant ranges, so you can use e.g.std::views::filteras input to your model:const QDate today = QDate::currentDate(); model = new QRangeModel(std::views::iota(today.addYears(-100), today.addYears(100)) | std::views::filter([](QDate date) { return date.dayOfWeek() < 6; }) );And if
std::rangesis already old news for you and you want to read more about bleeding-edge C++ stuff, check the blog post about my hackathon project using C++26 reflections to eliminate more boiler plate for custom structs.Conclusion
With Qt 6.11 we make it easier than ever to work with C++ data structures as sources of UI data. Modifying data with
QRangeModelAdapteruses API concepts that are familiar for C++ developers, while making sure thatQAbstractItemModelclients are informed about relevant changes. Automatically binding thedataChanged()to QObject properties, support for more C++20 ranges, and a new customization point round of the additions to Qt's modern item model handling for C++ developers. -
When introducing QRangeModel for Qt 6.10 I wrote that we'd try to tackle some limitations in future releases. With Qt 6.11 around the corner, it's time to give an update.
Model updates when properties change
For a
QRangeModelwhere all items or rows are backed by instances of the same QObject type, the model can now automatically emit thedataChanged()signal when properties of those objects change. This provides a convenient mechanism to keep things in sync without having to go through theQAbstractItemModelAPI for such changes. Let's say our data is backed by an item type like this:class Person : public QObject { Q_OBJECT Q_PROPERTY(QString firstName READ firstName WRITE setFirstName NOTIFY firstNameChanged) Q_PROPERTY(QString lastName READ lastName WRITE setLastName NOTIFY lastNameChanged) Q_PROPERTY(int age READ age WRITE setAge NOTIFY ageChanged) public: explicit Person( /* ~~~ */ ); // ~~~ Q_SIGNALS: // ~~~ void ageChanged(int age); private: int m_age = 0; };QRangeModel represents a list of such objects as a table with three columns, one for each property.
// backend code QList<Person *> data = { new Person("Max", "Mustermann", 37), new Person("Eva", "Nordmann", 39), new Person("Bob", "Mountainman", 42), }; model = new QRangeModel(&data); model->setAutoConnectPolicy(QRangeModel::AutoConnectPolicy::Full); // frontend code tableView = new QTableView(this); tableView->setModel(model);By setting the
autoConnectPolicyof QRangeModel to eitherFullorOnRead, future changes to properties will make the model emitQAbstractItemModel::dataChanged()for the corresponding index and role. This in turn updates all views.// backend code data[0]->setAge(data[0]->age() + 1);The only requirement is that the property has a notification signal that the setter emits when the value changes.
Using
QObjectinstances as the data type backing a model is comparatively expensive, but this addition makes it easy to work with the data inside small models directly, without having to deal withQModelIndexorQVariant.Modify a model with QRangeModelAdapter
Making it unnecessary to deal with those
QAbstractItemModel'isms for trivial operations is also the idea behind the biggest item-models addition in Qt 6.11:QRangeModelAdapteris a new template class that makes it safe and convenient to work with the data structure aQRangeModeloperates on. Item data and row/column structure of the underlying range can be modified and accessed through the adapter without having to deal withQModelIndexorQVariant, and the adapter makes sure that the model emits signals, invalidates and updates persistent indexes, and informs views about the changes.struct Backend { QList<int> data { 1, 2, 3, 4 }; QRangeModelAdapter adapter(std::ref(data)); void updateData(); }; class Frontend { Frontend() { // ~~~ listView->setModel(backend.adapter.model()); // the adapter creates and owns the model } }; // ~~~ void Backend::updateData() { adapter[0] = 23; // emits dataChanged() adapter.insertRow(0, 78); // prepends 78, calls begin/endInsertRows adapter.removeRows(2, 2); // removes two rows, calls begin/endRemoveRows }If the underlying data structure is a table, then an individual item can be modified like this:
void Backend::updateData() { adapter.at(1, 0) = 44; // works with C++17 adapter[0, 1] = 55; // with C++23's multidimensional subscript operator }For tree-shaped data structures, the row can be accessed using a path of row indexes:
void Backend::updateData() { adapter.at({0, 1}, 1) = "zero/one:one"; // second column in the second child of the first toplevel row }In addition to index-based access to rows and columns,
QRangeModelAdapterprovides an iterator API:void Backend::storeData() const { for (const auto &item : adapter) storage << item; }With a table, clearing all cells while maintaining the dimensions of the table could be done like this:
void Backend::clearData() { for (auto row : adapter) { for (auto item : row) { item = {}; } } }QRangeModelAdapteris in technology preview for Qt 6.11; the API is a bit unorthodox, with different semantics depending on the underlying data structure. The heavy use of C++ 17 meta programming techniques also makes the documentation a bit unwieldy, which has given our qdoc developers a good reason to improve the rendering of template APIs. We'd very much like to hear your feedback to it!Customizing item access
We added a new customization point for implementing custom item access. Specialize
QRangeModel::ItemAccessfor your type and implement staticreadRoleandwriteRoleaccessors to read and write role-specific values to an item:template <> struct QRangeModel::ItemAccess<Person> { static QVariant readRole(const Person &item, int role) { switch (role) { case Qt::DisplayRole: return item.firstName() + " " + item.lastName(); case Qt::UserRole: return item.firstName(); case Qt::UserRole + 1: return item.lastName(); case Qt::UserRole + 2: return item.age(); } return {}; } static bool writeRole(Person &item, const QVariant &data, int role) { bool ok = true; switch (role) { case Qt::DisplayRole: case Qt::EditRole: { const QStringList names = data.toString().split(u' '); if (names.size() > 0) item.setFirstName(names.at(0)); if (names.size() > 1) item.setLastName(names.at(1)); break; } case Qt::UserRole: item.setFirstName(data.toString()); break; case Qt::UserRole + 1: item.setLastName(data.toString()); break; case Qt::UserRole + 2: item.setAge(data.toInt(&ok)); break; default: ok = false; break; } return ok; } };A specialization of
ItemAccesstakes precedence over built-in access mechanisms, and also implies thatQRangeModelwill interpret the type as a multi-role item, even if it could be treated as a multi-column row. So you can use this technique to customize and optimize access to tuple-like types, gadgets, or completely custom structs.Support for caching std::ranges
Last but not least, a minor improvement for users of
std::ranges:QRangeModelno longer requiresstd::being/endon constant ranges, so you can use e.g.std::views::filteras input to your model:const QDate today = QDate::currentDate(); model = new QRangeModel(std::views::iota(today.addYears(-100), today.addYears(100)) | std::views::filter([](QDate date) { return date.dayOfWeek() < 6; }) );And if
std::rangesis already old news for you and you want to read more about bleeding-edge C++ stuff, check the blog post about my hackathon project using C++26 reflections to eliminate more boiler plate for custom structs.Conclusion
With Qt 6.11 we make it easier than ever to work with C++ data structures as sources of UI data. Modifying data with
QRangeModelAdapteruses API concepts that are familiar for C++ developers, while making sure thatQAbstractItemModelclients are informed about relevant changes. Automatically binding thedataChanged()to QObject properties, support for more C++20 ranges, and a new customization point round of the additions to Qt's modern item model handling for C++ developers.@Volker-Hilsheimer said in New in Qt 6.11: QRangeModel updates and QRangeModelAdapter:
support for more C++20 ranges
Nice addition. Reading the linked article you say there "while we don't want to require anything more modern than C++17 to be able to use QRangeModel". To be clear: your
QRangeModel, now and for the foreseeable future, will only require that I use C++17? IF I use C++20+ I will get more support for certain types of models/ranges which I will not get if I only use C++17, so I could not use those in the backend, but I won't have to use C++20+ if I keep clear of models/ranges which require its features? -
@JonB that is correct; our tests compile and run with a C++ 17 only toolchain. But if you use it with a newer C++ standard, then you get some support for new language features (like multi-dimensional subscripts from C++23, or std::ranges). This is possible with QRangeModel(Adapter) as the implementation is mostly inline in the headers.