З'єднання моделей C++ з вашим інтерфейсом користувача QML
Дані з модуля обробки C++ до оболонки QML
Як показано у попередньому розділі підручника, ви можете з'єднати код мовою C++ із кодом QML шляхом створення класу, який буде оброблено як ще один компонент у QML. Втім, може виникнути потреба у представленні складніших даних, зокрема даних, які мають працювати як нетипова ListModel, або даних, які має бути делеговано з Repeater.
Ми можемо створювати власні моделі на боці C++ і оголошувати, як дані з відповідної моделі має бути представлено в оболонці QML.
Наполегливо рекомендуємо вам ознайомитися перед цим із настановами щодо панелей List.
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. Оболонка зможе читати і показувати ключі і значення і буде простою у використанні, зовсім як одновимірний масив. Усе це має виглядати подібним до ListModel у QML.
Для цього нам потрібно створити клас, який успадковує властивості від 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++ за допомогою відкритого enum. Причиною цього є те, що ці значення з enum буде передано до data() під час кожного доступу QML до відповідної ролі, а тому ми можемо зробити так, що data() повертала потрібні нам дані.
Почнімо зі створення enum для ролей у 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, де ключі будуть нумерованими значеннями, які буде пов'язано із QByteArrays. Цей код має бути у 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. Цього разу ми скористаємося додатковим параметром, і це буде ціле число, номер рядка. Назву species буде використано для вилучення ключа з QMap, а номер рядка буде використано для вилучення рядка в оболонці.
Додайте нову функцію Q_INVOKABLE із назвою deleteSpecies() до src/components/model.h:
#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 з'явилася кнопка «Delete» поряд із кнопкою редагування у нашому 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);};