[Из песочницы] Паттерн гетерогенная иерархия моделей QML

Введение


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

ontnMKwwnygDwE8gZVVKKotgogYpBQ.png

Данный паттерн был систематизирован на основе статьи. Далее идет описание паттерна, аналогично GoF.

Назначение


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

Применимость


Используйте паттерн, когда:

  • нужно представить иерархию моделей, в которой на разных уровнях разные типы моделей
  • модели заполняются динамически


Структура


drzo1P5LRFFaki3QInnoVTUoFiyF74.png

Участники


  • 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}
            }
        }


Скриншоты результата


U3WudyyZpxFFzHwOujnPM1VwLJmufi.png

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

© Habrahabr.ru