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

Also popular now: