Введение
Очень часто необходимо структурировать модели следующим образом — на одном уровне модели с одной структурой, а на другом уровне структура модели изменяется. Для примера возьмем задачу, в которой требуется отобразить список устройств, у каждого устройства присутствуют группы настроек, а у каждой группы настроек есть список настроек различных типов. Для простоты будем полагать что у устройства есть только название и список групп. У группы есть только название и список настроек. У настройки есть только название и тип — чекбокс, текстовое поле или слайдер.

Данный паттерн был систематизирован на основе статьи. Далее идет описание паттерна, аналогично GoF.
Назначение
Паттерн, структурирующий использование сложных моделей в C++ с использованием QML. Облегчает использование вложенных списков моделей для образования иерархической структуры. При этом, для использования в QML, сложность не возрастает.
Применимость
Используйте паттерн, когда:
- нужно представить иерархию моделей, в которой на разных уровнях разные типы моделей
- модели заполняются динамически
Структура

Участники
- ListModel – класс модели списка, делегирует работу с ролями в модели классу списку ListItem
- ListItem – абстрактный класс элемента списка, должен определить метод нахождения уникального идентификатора и методы доступа к ролям
- SubListedListModel — класс модели списка с подсписками, прототипом является интерфейс класса SubListedListItem, возвращает подмодель для текущего элемента.
- SubListedListItem — абстрактный класс элемента списка с подмоделью, должен определить свою подмодель.
- ConcreteSubItem1 – класс элемента списка с подмоделью, содержащий подмодель-список для элементов ConcreteSubItem2.
- ConcreteSubItem2 – класс элемента списка с подмоделью, содержащий модель-список для элементов ConcreteSubItem3.
- ConcreteSubItem3 – класс элемента списка.
Отношения
Объект-список делегирует данные о ролях классу элементу списка, который был задан как прототип в конструкторе. Требуется наследоваться либо от элемента с подмоделью, либо от простого элемента списка. В QML для доступа к дочерней модели требуется вызвать метод subModelFromId, где параметром является роль-id для текущего элемента.
Пример кода С++
Добавляем модель устройства:
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<int, QByteArray> roleNames() const; Models::ListModel* submodel() const; private: int _id; static int g_id; QString deviceName; Models::ListModel* groupListModel; };
Для отслеживания идентификаторов служит глобальный счетчик g_id, идентификатор текущего устройства — _id. В конструкторе добавим подмодель и проинициализируем имена:
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()); }
Обработка ролей:
QVariant DeviceModelItem::data(int role) const { switch (role) { case deviceId: return this->id(); case deviceNameRole: return this->deviceName; default: return QVariant(); } } QHash<int, QByteArray> DeviceModelItem::roleNames() const { QHash<int, QByteArray> roles; roles[deviceId] = "deviceId"; roles[deviceNameRole] = "deviceName"; return roles; }
Аналогично выглядит модель группы, за исключением того, что в конструкторе создаем список без подмоделей:
GroupModelItem::GroupModelItem(QObject *parent):SubListedListItem(parent), _id(g_id++) { groupName = QString("Group %1").arg(_id); settingsListModel = new Models::ListModel(new SettingsModelItem()); ... }
В модели настройки уже наследуемся от ListItem:
class SettingsModelItem : public Models::ListItem { Q_OBJECT public: enum SettingsModelItemRoles { settingsId = Qt::UserRole + 1, settingsNameRole, settingsTypeRole } ... }
Нам не требуется подмодель в настройках. Теперь добавляем в контекст корневую модель устройств:
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
В качестве модели навигации используем 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)}}) } } } }
Для страницы групп установили подмодель через subModelFromId. В модели групп обрабатываем аналогично:
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)}}) } } ... }
Для страницы настроек только список:
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} } }
Скриншоты результата

Ссылка на исходники: GitHub
Ссылка на статью-источник: Статья
