
Heterogeneous hierarchy of QML models
Introduction
It is often necessary to structure models as follows - at the same level of the model with one structure, and at another level, the structure of the model changes. For example, take a task in which you want to display a list of devices, each device has settings groups, and each settings group has a list of settings of various types. For simplicity, we will assume that the device has only a name and a list of groups. The group has only a name and a list of settings. The setting has only a name and type - a checkbox, text box or slider.

This pattern was systematized based on the article . The following is a description of the pattern, similar to GoF.
Appointment
A pattern structuring the use of complex models in C ++ using QML. Facilitates the use of nested model lists to form a hierarchical structure. At the same time, for use in QML, the complexity does not increase.
Applicability
Use the pattern when:
- you need to imagine a hierarchy of models in which at different levels different types of models
- models fill dynamically
Structure

Members
- ListModel - a list model class, delegates work with roles in the model to the ListItem list class
- ListItem - an abstract class of a list item, must define a method for finding a unique identifier and methods for accessing roles
- SubListedListModel - a list model class with sublists; the prototype is the interface of the SubListedListItem class; returns a submodel for the current element.
- SubListedListItem - an abstract class of a list item with a submodel, must define its submodel.
- ConcreteSubItem1 - a class of a list item with a submodel that contains a submodel list for ConcreteSubItem2 elements.
- ConcreteSubItem2 - a list item class with a submodel containing a list model for ConcreteSubItem3 items.
- ConcreteSubItem3 is the list item class.
Relations
The list object delegates role data to the class of the list item that was specified as the prototype in the constructor. It is required to be inherited either from an element with a submodel, or from a simple list item. In QML, to access the child model, you need to call the subModelFromId method, where the parameter is the id role for the current element.
C ++ code example
Add device model:
class DeviceModelItem : public Models::SubListedListItem
{
Q_OBJECT
public:
enum GroupModelItemRoles
{
deviceId = Qt::UserRole + 1,
deviceNameRole
};
DeviceModelItem(QObject* parent = 0);
int id() const;
QVariant data(int role) const;
QHash roleNames() const;
Models::ListModel* submodel() const;
private:
int _id;
static int g_id;
QString deviceName;
Models::ListModel* groupListModel;
};
To track identifiers, use the global counter g_id, the identifier of the current device is _id. In the constructor, add a submodel and initialize the names:
DeviceModelItem::DeviceModelItem(QObject *parent):SubListedListItem(parent),
_id(g_id++)
{
deviceName = QString("Device %1").arg(_id);
groupListModel = new Models::SubListedListModel(new GroupModelItem());
//Для примера задаем фиксированные подмодели
groupListModel->appendRow(new GroupModelItem());
groupListModel->appendRow(new GroupModelItem());
}
Role Processing:
QVariant DeviceModelItem::data(int role) const
{
switch (role)
{
case deviceId:
return this->id();
case deviceNameRole:
return this->deviceName;
default:
return QVariant();
}
}
QHash DeviceModelItem::roleNames() const
{
QHash roles;
roles[deviceId] = "deviceId";
roles[deviceNameRole] = "deviceName";
return roles;
}
The group model looks similar, except that in the constructor we create a list without submodels:
GroupModelItem::GroupModelItem(QObject *parent):SubListedListItem(parent),
_id(g_id++)
{
groupName = QString("Group %1").arg(_id);
settingsListModel = new Models::ListModel(new SettingsModelItem());
...
}
In the model, settings are already inherited from ListItem:
class SettingsModelItem : public Models::ListItem
{
Q_OBJECT
public:
enum SettingsModelItemRoles
{
settingsId = Qt::UserRole + 1,
settingsNameRole,
settingsTypeRole
}
...
}
We do not need a submodel in the settings. Now add the root device model to the context:
int main(int argc, char *argv[])
{
//Запуск как в примере с touch
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Models::ListModel* devicesModel = new Models::SubListedListModel(new DeviceModelItem());
//DEBUG
devicesModel->appendRow(new DeviceModelItem());
devicesModel->appendRow(new DeviceModelItem());
devicesModel->appendRow(new DeviceModelItem());
devicesModel->appendRow(new DeviceModelItem());
devicesModel->appendRow(new DeviceModelItem());
engine.rootContext()->setContextProperty("deviceModel",devicesModel);
...
}
QML sample code
As a navigation model, use StackView (main.qml):
StackView {
id: stackView
anchors.fill: parent
initialItem: Item {
width: parent.width
height: parent.height
ListView {
model: deviceModel
anchors.fill: parent
delegate: AndroidDelegate {
text: deviceName
onClicked: stackView.push({item:Qt.resolvedUrl("pages/GroupPage.qml"),
properties:{subModel:deviceModel.subModelFromId(model.deviceId)}})
}
}
}
}
For the groups page, a submodel was installed via subModelFromId. In the group model, we process in the same way:
ScrollView {
...
property variant subModel: null
ListView {
...
model: subModel
delegate: AndroidDelegate {
text: groupName
onClicked: stackView.push({item:Qt.resolvedUrl("SettingsPage.qml"),
properties:{subModel:subModel.subModelFromId(model.groupId)}})
}
}
...
}
For settings page only list:
ListView {
id: settingsView
...
model: subModel
delegate: Item {
CheckBox{
visible: settingsType == 0
...
}
Column{
...
visible: settingsType == 1
Text{text: settingsName}
TextField {text: "Text input"}
}
Column{
...
visible: settingsType == 2
Text{text: settingsName}
Slider {value: 1.0}
}
}
Screenshots of the result

Link to source: GitHub
Link to source article : Article