[Из песочницы] Паттерн гетерогенная иерархия моделей QML
Введение
Очень часто необходимо структурировать модели следующим образом — на одном уровне модели с одной структурой, а на другом уровне структура модели изменяется. Для примера возьмем задачу, в которой требуется отобразить список устройств, у каждого устройства присутствуют группы настроек, а у каждой группы настроек есть список настроек различных типов. Для простоты будем полагать что у устройства есть только название и список групп. У группы есть только название и список настроек. У настройки есть только название и тип — чекбокс, текстовое поле или слайдер.
Данный паттерн был систематизирован на основе статьи. Далее идет описание паттерна, аналогично 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
Ссылка на статью-источник: Статья