Подключение моделей C++ к пользовательскому интерфейсу QML
Передача данных из бэкенда на C++ в интерфейс QML
Как показано в предыдущем руководстве, код C++ можно подключить к QML, создав класс, который будет восприниматься как ещё один компонент QML. Однако может потребоваться представить более сложные данные, например данные, которые должны выступать в роли пользовательской ListModel или каким-либо образом делегироваться из Repeater.
Можно создать собственные модели со стороны C++ и объявить, как данные из этой модели должны быть представлены в интерфейсе QML.
Настоятельно рекомендуется перед этим руководством прочитать руководство Представления списков.
globalDrawer:Kirigami.GlobalDrawer{isMenu:trueactions:[Kirigami.Action{text:i18n("Exposing to QML")icon.name:"kde"onTriggered:pageStack.push(Qt.createComponent("org.kde.tutorial.components","ExposePage"))},Kirigami.Action{text:i18n("C++ models in QML")icon.name:"kde"onTriggered:pageStack.push(Qt.createComponent("org.kde.tutorial.components","ModelsPage"))},Kirigami.Action{text:i18n("Quit")icon.name:"application-exit-symbolic"shortcut:StandardKey.QuitonTriggered:Qt.quit()}]}
Затем создайте новый файл src/components/ModelsPage.qml со следующим содержимым:
1
2
3
4
5
6
7
8
9
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"// ...
}
И наконец, добавьте его в src/components/CMakeLists.txt:
Чтобы в этом руководстве было проще понять, как заполняется модель, будет отключена функция, которую приложения KDE, использующие extra-cmake-modules (ECM), применяют по умолчанию и которая оптимизирует код строк. Это позволит обойтись без необходимости писать QStringLiteral() каждый раз при введении строки в коде C++, что пригодится для кода в следующем заголовочном файле.
В корневом файле CMakeLists.txt добавьте следующий код:
Отключение этого флага CMake преследует исключительно учебные цели. В рабочем коде следует по возможности использовать QStringLiteral() или пространство имён строковых литералов Qt.
Подготовка класса
Будет создан класс, содержащий QMap, в котором в качестве ключа используется QString, а в качестве значений — объекты QStringList. Интерфейс сможет читать и показывать ключи и значения, а работать с ним будет так же просто, как с одномерным массивом. Он должен быть похож на QML ListModel.
Для этого создайте класс, наследующий от QAbstractListModel. Также добавьте некоторые данные в QMap. Эти объявления будут находиться в model.h.
Создайте два новых файла: src/components/model.h и src/components/model.cpp.
Добавьте эти два новых файла в src/components/CMakeLists.txt:
Добавьте в файл src/components/model.h следующий код в качестве начального содержимого:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
Конечно, нельзя просто показать этот класс как есть. Кроме того, нужно сообщить QML, как представлять эти данные в классе. Это делается переопределением трёх основных виртуальных функций:
rowCount() — эту функцию можно рассматривать как способ сообщить QML, сколько элементов должна представить модель.
roleNames() — имена ролей можно рассматривать как имена свойств, привязанных к данным в QML. Эта функция позволяет создавать эти роли.
data() — эта функция вызывается, когда требуется получить данные, соответствующие именам ролей из модели.
Примечание
Пользовательские имена ролей, созданные функцией roleNames(), доступны только при делегировании модели и недоступны вне этого. См. Модели и представления.
Примечание
Технически модели в Qt представляются в виде таблиц со строками и столбцами. Переопределение rowCount() сообщает Qt, сколько строк содержится в модели. Поскольку в этом руководстве рассматривается лишь одномерный массив, «строки» можно понимать просто как «количество элементов».
Переопределение и реализация rowCount()
Переопределим функцию в заголовочном файле src/components/model.h. Функция rowCount() принимает собственный параметр, но в этом примере он не используется, поэтому его можно не именовать.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:introwCount(constQModelIndex&)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
Затем объявите в файле src/components/model.cpp, сколько строк содержит эта модель:
Прежде чем переопределять roleNames(), нужно объявить роли на стороне C++ с помощью открытого перечисления. Причина в том, что эти значения перечисления передаются в data() каждый раз, когда QML обращается к соответствующей роли, и благодаря этому data() можно заставить возвращать нужные данные.
Начнём с создания перечисления ролей в src/components/model.h, где каждое значение — роль для стороны C++.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};QHash<int,QByteArray>roleNames()constoverride;introwCount(constQModelIndex&)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
После того как с этим разобрались, можно наконец создать то, что представляют собой эти роли на стороне QML, с помощью QHash, где ключами служат значения перечисления, сопоставленные с QByteArray. Это должно находиться в src/components/model.cpp. Именно текст в QByteArray используется в самом коде QML.
В модели из нашего примера роль «species» позволяет получить ключ QString «Feline», «Fox», «Goat», каждый в отдельном делегате. То же можно сделать со значениями QStringList для списка имён персонажей.
Переопределение и реализация data()
В data() передаются два параметра: index и role. index — это позиция данных в модели. Как уже отмечалось, role используется QML для получения конкретных данных при обращении к роли.
В data() можно использовать оператор switch, чтобы возвращать подходящие данные и тип данных в зависимости от роли, что возможно, поскольку data() возвращает QVariant. Однако всё же нужно убедиться, что получено нужное местоположение данных. В приведённом ниже примере видно, что объявляется новая переменная-итератор, которая задаётся как начало списка плюс строка индекса, и возвращаются данные, на которые указывает итератор.
Однако нельзя просто вернуть любые данные. Может возникнуть попытка привязать данные к свойству с несовместимым типом данных, например QStringList к QString. Чтобы данные показывались правильно, может потребоваться преобразование данных. Для этого создадим новую закрытую статическую функцию с именем formatList().
В результате в файле src/components/model.cpp получается следующий код:
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
Использование класса в QML
Используемый файл QML будет содержать всего три компонента Kirigami.AbstractCard, где ключ — это заголовок, а значение — содержимое. Эти карточки создаются путём делегирования AbstractCard с помощью Repeater, где в качестве модели выступает созданная ранее пользовательская модель. Доступ к данным осуществляется с помощью слова model, за которым следуют роли, объявленные в roleNames().
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{header:Kirigami.Heading{text:model.specieslevel:2}contentItem:Controls.Label{text:model.characters}}}}}
Изменение данных
Редактирование данных с помощью dataChanged() и setData()
Может возникнуть ситуация, когда требуется изменить данные в модели и отразить эти изменения на стороне интерфейса. При каждом изменении данных в модели необходимо испустить сигнал dataChanged(), который применит эти изменения на стороне интерфейса в конкретных ячейках, указанных в его аргументах. В этом руководстве можно просто использовать аргумент index функции setData().
setData() — виртуальная функция, которую можно переопределить, чтобы изменение данных со стороны интерфейса автоматически отражалось на стороне бэкенда. Она принимает три параметра:
index — расположение данных.
value — содержимое новых данных.
role — в этом контексте роль служит для того, чтобы сообщить представлениям, как обрабатывать данные. Здесь роль должна быть Qt::EditRole.
В этом случае параметр role используется, чтобы setData() можно было редактировать через пользовательский ввод (Qt::EditRole). С помощью index можно определить местоположение данных, которые следует изменить содержимым value.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;boolsetData(constQModelIndex&index,constQVariant&value,introle)override;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
Обновим код QML, чтобы можно было открыть диалог, позволяющий редактировать модель с помощью Controls.Button, прикреплённой к карточкам.
Добавьте следующий Kirigami.PromptDialog в src/components/ModelsPage.qml вместе с новой кнопкой редактирования:
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{Layout.fillHeight:trueheader:Kirigami.Heading{text:model.specieslevel:2}contentItem:Item{implicitWidth:delegateLayout.implicitWidthimplicitHeight:delegateLayout.implicitHeightColumnLayout{id: delegateLayoutControls.Label{text:model.characters}Controls.Button{text:"Edit"onClicked:{editPrompt.text=model.characters;editPrompt.model=model;editPrompt.open();}}}}}}}Kirigami.PromptDialog{id: editPromptpropertyvarmodelpropertyaliastext:editPromptText.texttitle:"Edit Characters"standardButtons:Kirigami.Dialog.Ok|Kirigami.Dialog.CancelonAccepted:{constmodel=editPrompt.model;model.characters=editPromptText.text;editPrompt.close();}Controls.TextField{id: editPromptTextonAccepted:editPrompt.accept()}}}
Теперь при изменении значений модели в интерфейсе изменения должны автоматически обновляться в бэкенде.
Добавление строк
Добавлена возможность изменять данные в существующих ключах QMap, и на стороне интерфейса это отражается как изменение содержимого внутри AbstractCards. Но что, если потребуется добавить в QMap новую запись ключа и отразить её на стороне QML? Для этого создадим новый метод, вызываемый со стороны QML и выполняющий эту задачу.
Чтобы сделать метод доступным в QML, объявление метода нужно начинать с макроса Q_INVOKABLE. Этот метод также будет содержать строковый параметр, который станет новым ключом в QMap.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;Q_INVOKABLEvoidaddSpecies(constQString&species);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
Внутри этого метода нужно сообщить Qt о необходимости создать дополнительные строки в модели. Для этого вызывается beginInsertRows() — так начинается операция добавления строк, — затем вставляется всё необходимое, после чего вызывается endInsertRows() для завершения операции. Однако в конце всё ещё необходимо испустить сигнал dataChanged(). На этот раз предстоит обновить все строки, с первой по последнюю, поскольку QMap может переупорядочиться в алфавитном порядке, и это нужно отследить по всем строкам.
При вызове beginInsertRows() сначала нужно передать класс QModelIndex, чтобы указать место добавления новых строк, а затем — номера первой и последней новых строк. В этом руководстве первым аргументом будет просто QModelIndex(), так как использовать этот параметр здесь не требуется. Для номеров первой и последней строк можно просто взять текущее количество строк, поскольку в конец модели добавляется всего одна строка.
Функция dataChanged() использует QModelIndex в качестве типа данных для своих параметров. Однако целые числа можно преобразовать в типы данных QModelIndex с помощью функции index().
Обновим код QML, чтобы можно было добавить новый ключ в QMap.
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"actions:[Kirigami.Action{icon.name:"list-add-symbolic"text:"Add New Species"onTriggered:{addPrompt.open();}}]Model{id: customModel}ColumnLayout{// ...
}Kirigami.PromptDialog{id: addPrompttitle:"Add New Species"standardButtons:Kirigami.Dialog.OkonAccepted:{customModel.addSpecies(addPromptText.text);addPromptText.text="";// Clear TextField every time it's done
addPrompt.close();}Controls.TextField{id: addPromptTextLayout.fillWidth:trueonAccepted:addPrompt.accept()}}Kirigami.PromptDialog{id: editPrompt// ...
}}
Теперь в верхней части приложения должно появиться новое действие, открывающее диалог, позволяющий добавить в модель новый элемент с собственными данными.
Удаление строк
Удаление строк выполняется так же, как и добавление строк. Создайте ещё один метод, который будет вызываться в QML. На этот раз будет использоваться дополнительный параметр — целое число, являющееся номером строки. Название вида используется для удаления ключа из QMap, а номер строки — для удаления строки в интерфейсе.
Добавьте в файл src/components/model.h новую функцию Q_INVOKABLE с именем deleteSpecies():
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;Q_INVOKABLEvoidaddSpecies(constQString&species);Q_INVOKABLEvoiddeleteSpecies(constQString&speciesName,constint&rowIndex);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
Соответствующую реализацию добавьте в файл src/components/model.cpp:
Теперь обновим приложение, чтобы кнопка «Удалить» появлялась в RowLayout рядом с кнопкой редактирования внутри AbstractCard, и подключим её к методу удаления.
importorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.Page{title:"Exposing to QML Tutorial"Kirigami.Heading{anchors.centerIn:parenttext:Backend.introductionText}}
src/components/backend.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include<QObject>#include<qqmlintegration.h>classBackend:publicQObject{Q_OBJECTQML_ELEMENTQML_SINGLETONQ_PROPERTY(QStringintroductionTextREADintroductionTextWRITEsetIntroductionTextNOTIFYintroductionTextChanged)public:explicitBackend(QObject*parent=nullptr);QStringintroductionText()const;voidsetIntroductionText(constQString&introductionText);Q_SIGNALvoidintroductionTextChanged();private:QStringm_introductionText=QStringLiteral("Hello World!");};
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"actions:[Kirigami.Action{icon.name:"list-add-symbolic"text:"Add New Species"onTriggered:{addPrompt.open();}}]Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{Layout.fillHeight:trueheader:Kirigami.Heading{text:model.specieslevel:2}contentItem:Item{implicitWidth:delegateLayout.implicitWidthimplicitHeight:delegateLayout.implicitHeightColumnLayout{id: delegateLayoutControls.Label{text:model.characters}RowLayout{Layout.fillWidth:trueControls.Button{text:"Edit"onClicked:{editPrompt.text=model.characters;editPrompt.model=model;editPrompt.open();}}Controls.Button{text:"Delete"onClicked:{customModel.deleteSpecies(model.species,index);}}}}}}}}Kirigami.PromptDialog{id: addPrompttitle:"Add New Species"standardButtons:Kirigami.Dialog.OkonAccepted:{customModel.addSpecies(addPromptText.text);addPromptText.text="";// Clear TextField every time it's done
addPrompt.close();}Controls.TextField{id: addPromptTextLayout.fillWidth:trueonAccepted:addPrompt.accept()}}Kirigami.PromptDialog{id: editPromptpropertyvarmodelpropertyaliastext:editPromptText.texttitle:"Edit Characters"standardButtons:Kirigami.Dialog.Ok|Kirigami.Dialog.CancelonAccepted:{constmodel=editPrompt.model;model.characters=editPromptText.text;editPrompt.close();}Controls.TextField{id: editPromptTextonAccepted:editPrompt.accept()}}}
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;boolsetData(constQModelIndex&index,constQVariant&value,introle)override;Q_INVOKABLEvoidaddSpecies(constQString&species);Q_INVOKABLEvoiddeleteSpecies(constQString&speciesName,constint&rowIndex);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};