З'єднання моделей з вашим інтерфейсом користувача QML
Як показано у попередньому розділі підручника, ви можете з'єднати код мовою C++ із кодом QML шляхом створення класу, який буде оброблено як ще один компонент у QML. Втім, може виникнути потреба у представленні складніших даних, зокрема даних, які мають працювати як нетипова ListModel, або даних, які має бути делеговано з Repeater.
Ми можемо створювати власні моделі на боці C++ і оголошувати, як дані з відповідної моделі має бути представлено в оболонці QML.
Приготування класу
У цих настановах ми створимо клас, який містить QMap, де як ключ буде використано QString, а як значення буде використано об'єкти QStringList. Оболонка зможе читати і показувати ключі і значення і буде простою у використанні, зовсім як одновимірний масив. Усе це має виглядати подібним до ListModel у QML.
Для цього нам потрібно оголосити клас, який успадковує властивості від QAbstractListModel. Давайте також додамо якісь дані до QMap. Ці оголошення будуть зберігатися у model.h
.
Нотатка
Якщо ви послідовно виконуєте наші настанови, будь ласка, не забудьте оновити ваш файлCMakeLists.txt
!#pragma once
#include <QAbstractListModel>
class Model : public QAbstractListModel {
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()
Давайте перевизначимо функцію у файлів заголовків. rowCount()
має власний параметр, але його не буде використано у цьому прикладі, і його буде виключено.
class Model : public QAbstractListModel {
...
public:
int rowCount(const QModelIndex &) const override;
};
Тепер, давайте оголосимо, скільки рядків буде у цій моделі у model.cpp
.
#include "model.h"
int Model::rowCount(const QModelIndex &) const {
return m_list.count();
}
Перевизначення і реалізація roleNames()
До того, як ми перевизначимо roleNames()
, нами потрібно оголосити ролі на боці C++ за допомогою відкритої змінної enum
. Причиною цього є те, що ці значення зі змінної enum
буде передано до data()
під час кожного доступу QML до відповідної ролі, а тому ми можемо зробити так, що data()
повертала потрібні нам дані.
Почнімо зі створення змінної enum
для ролей, де кожне значення буде роллю на боці C++.
class Model : public QAbstractListModel {
...
public:
enum Roles {
SpeciesRole = Qt::UserRole,
CharactersRole
};
...
QHash<int, QByteArray> roleNames() const override;
};
Щойно код буде написано, ми нарешті можемо визначити ці ролі на боці QML за допомогою QHash, де ключі будуть нумерованими значеннями, які буде пов'язано із QByteArrays. Текстом у QByteArray буде те, що буде використано у самому коді QML.
QHash<int, QByteArray> Model::roleNames() const {
return {
{SpeciesRole, "species"},
{CharactersRole, "characters"}
};
}
У нашому прикладі моделі роллю «species» можна скористатися для отримання ключа QString «Feline», «Fox», «Goat», кожного в окремому делегаті. Те саме можна зробити за допомогою значень QStringList для списку назв персонажів.
Перевизначення і реалізація data()
У data()
передаються два параметри: index
і role
. index
— місце, де перебувають дані, коли їх делеговано. Як ми вже зазначали, role
використовується у QML для отримання специфічних даних, які програма повертає, коли відбувається доступ до ролі.
У data()
ми можемо скористатися інструкцією switch
для повернення відповідних даних, а тип даних залежатиме від ролі, що є можливим, оскільки data()
повертає QVariant. Втім, нам усе ще потрібно переконатися, що ми отримуємо належне місце у даних. У прикладі нижче можна бачити, що оголошено нову змінну-ітератор, значення якої встановлюється на початку списку з додаванням індексу рядка, а дані, на які цей ітератор вказує, — це те, що буде повернуто.
Втім, ми не можемо просто повернути будь-які бажані дані. Можлива спроба прив'язування даних до властивості із несумісним типом даних, зокрема QStringList до QString. Можливо, слід виконати перетворення даних, щоб їх можна було показати належним чином.
QVariant Model::data(const QModelIndex &index, int role) const {
const auto it = m_list.begin() + index.row();
switch (role) {
case SpeciesRole:
return it.key();
case CharactersRole:
return formatList(it.value());
default:
return {};
}
}
QString Model::formatList(const QStringList& list) {
QString result;
for (const QString& character : list) {
result += character;
if (list.last() != character) {
result += ", ";
}
}
return result;
}
Уможливлення оголошення класу у QML
Не забуваймо уможливити використання нашого класу у QML.
int main(int argc, char *argv[]) {
...
qmlRegisterType<Model>("CustomModel", 1, 0, "CustomModel");
...
}
Використання класів у QML
Використаний файл QML буде містити лише три компоненти Kirigami.AbstractCard, де ключем буде заголовок, а значенням — вміст. На цей момент ці картки створено делегуванням AbstractCard з використанням Repeater, де створена нами нетипова модель працює як модель. Доступ до моделі здійснюється за допомогою слова model
, за яким слід вказати ролі, які ми оголосили у roleNames()
.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami
import CustomModel 1.0
Kirigami.ApplicationWindow {
id: root
title: "Tutorial"
CustomModel {
id: customModel
}
pageStack.initialPage: Kirigami.ScrollablePage {
ColumnLayout {
Repeater {
model: customModel
delegate: Kirigami.AbstractCard {
header: Kirigami.Heading {
text: model.species
level: 2
}
contentItem: Controls.Label {
text: model.characters
}
}
}
}
}
}
Внесення змін до даних
Редагування даних за допомогою dataChanged()
і setData()
Може так статися, що вам потрібно буде змінити дані у моделі та відтворити зміни на боці оболонки. Кожного разу, коли ми змінюємо дані у моделі, нам слід надсилати сигнал dataChanged()
, який застосує ці зміни на боці оболонки у вказаних у її аргументах комірках. У цьому підручнику ми можемо просто скористатися аргументом index
у setData()
.
setData()
є віртуальною функцією, яку ви можете перевизначити так, щоб спроба внесення змін до даних з боку оболонки автоматично призводила до внесення цих змін на боці модуля обробки. Функції слід передати три аргументи:
index
— місце даних.value
— вміст нових даних.role
— у цьому контексті роль використовується для повідомлення панелям перегляду про те, як слід обробляти дані. Роллю тут має бутиQt::EditRole
.
Параметр role
у цьому випадку використано для того, щоб забезпечити можливість редагувати setData()
за введеними користувачем даних (Qt::EditRole). За допомогою index
ми можемо скористатися цим для визначення місця, де має бути змінено дані за вмістом value
.
bool Model::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!value.canConvert<QString>() && role != Qt::EditRole) {
return false;
}
auto it = m_list.begin() + index.row();
QString charactersUnformatted = value.toString();
QStringList characters = charactersUnformatted.split(", ");
m_list[it.key()] = characters;
emit dataChanged(index, index);
return true;
}
Нотатка
setData()
не надсилає dataChanged()
автоматично, це слід зробити вручну.Давайте оновимо код QML так, щоб він міг відкривати запит, за допомогою якого ми зможемо редагувати модель з використанням кнопки Controls.Button, яку пов'язано із картками.
Kirigami.ApplicationWindow {
...
Kirigami.OverlaySheet {
id: editPrompt
property var model
property alias text: editPromptText.text
title: "Edit Characters"
Controls.TextField {
id: editPromptText
}
footer: Controls.DialogButtonBox {
standardButtons: Controls.DialogButtonBox.Ok
onAccepted: {
const model = editPrompt.model;
model.characters = editPromptText.text;
editPrompt.close();
}
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
ColumnLayout {
Repeater {
model: customModel
delegate: Kirigami.AbstractCard {
Layout.fillHeight: true
header: Kirigami.Heading {
text: model.species
level: 2
}
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
ColumnLayout {
id: delegateLayout
Controls.Label {
text: model.characters
}
Controls.Button {
text: "Edit"
onClicked: {
editPrompt.text = model.characters;
editPrompt.model = model;
editPrompt.open();
}
}
}
}
}
}
}
}
}
Тепер, кожного разу, коли значення моделі буде змінено в оболонці, зміни мають автоматично оновити дані модуля обробки.
Додавання рядків
Нами додано спосіб внесення змін до даних у наявних ключах QMap і в оболонці — це відповідає зміні вмісту в AbstractCards. Але що, якщо нам потрібно додати новий запис ключа до QMap і відтворити дані на боці QML? Давайте зробимо це шляхом створення нового методу, який можна буде викликати на боці QML для виконання цього завдання.
Щоб зробити цей метод видимим у QML, нам слід скористатися у класі макросом Q_OBJECT і почати оголошення методу за допомогою макроса Q_INVOKABLE. Цей метод також включатиме рядковий параметр, який має бути новим ключем у QMap.
class Model : public QAbstractListModel {
Q_OBJECT;
...
public:
...
Q_INVOKABLE void addSpecies(const QString &species);
};
Всередині цього методу нам слід повідомити Qt про те, що ми хочемо створити додаткові рядки у моделі. Зробити це можна за допомогою виклику beginInsertRows()
для започаткування нашої дії з додавання рядка, після чого слід виконати вставляння потрібного пункту, а потім скористатися endInsertRows()
для завершення дії. Наприкінці, нам, як і раніше, слід надіслати dataChanged()
. Цього разу ми збираємося оновити усі рядки, від першого до останнього, оскільки QMap може оновлювати список за абеткою, і нам потрібно зробити це для усіх рядків.
При виклику beginInsertRows()
нам спочатку слід передати клас QModelIndex для визначення місця, куди слід додавати нові рядки, а потім вказати, якими будуть номери першого і останнього рядка. У цьому підручнику першим аргументом буде просто QModelIndex()
, оскільки тут немає потреби у використанні параметра. Ми можемо просто скористатися розміром поточного рядка для номерів першого і останнього рядка, оскільки ми просто додаємо один рядок наприкінці моделі.
void Model::addSpecies(const QString& species) {
beginInsertRows(QModelIndex(), m_list.size() - 1, m_list.size() - 1);
m_list.insert(species, {});
endInsertRows();
emit dataChanged(index(0), index(m_list.size() - 1));
}
Нотатка
У функціїdataChanged()
використано як тип даних для параметрів QModelIndex. Втім, ми можемо перетворити цілі числа у типах даних QModelIndex за допомогою функції index()
.Оновімо код QML так, щоб надати йому можливість додавати новий ключ до QMap.
Kirigami.ApplicationWindow {
...
Kirigami.OverlaySheet {
id: addPrompt
title: "Add New Species"
Controls.TextField {
id: addPromptText
}
footer: Controls.DialogButtonBox {
standardButtons: Controls.DialogButtonBox.Ok
onAccepted: {
customModel.addSpecies(addPromptText.text);
addPromptText.text = ""; // Спорожнення TextField при кожному завершенні обробки
addPrompt.close();
}
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
actions: [
Kirigami.Action {
icon.name: "add"
text: "Add New Species"
onTriggered: {
addPrompt.open();
}
}
]
...
}
}
Тепер у нас буде новий пункт дії у верхній частині вікна програми, який викликатиме запит, за допомогою якого можна буде додати до моделі новий елемент із нашими власними нетиповими даними.
Вилучення рядків
Спосіб вилучення рядків є подібним до способу додавання рядків. Створімо ще один метод, який ми викликатимемо з QML. Цього разу ми скористаємося додатковим параметром, і це буде ціле число, номер рядка. Назву species буде використано для вилучення ключа з QMap, а номер рядка буде використано для вилучення рядка в оболонці.
class Model : public QAbstractListModel {
Q_OBJECT;
...
public:
...
Q_INVOKABLE void deleteSpecies(const QString &speciesName, const int &rowIndex);
}
void Model::deleteSpecies(const QString &speciesName, const int& rowIndex) {
beginRemoveRows(QModelIndex(), rowIndex, rowIndex);
m_list.remove(speciesName);
endRemoveRows();
emit dataChanged(index(0), index(m_list.size() - 1));
}
Тепер оновімо програму так, щоб з'явилася кнопка «Delete» поряд із кнопкою редагування, і пов'яжімо цю кнопку із нашим методом вилучення.
ColumnLayout {
Repeater {
model: customModel
delegate: Kirigami.AbstractCard {
...
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
ColumnLayout {
id: delegateLayout
Controls.Label {
text: model.characters
}
RowLayout {
Controls.Button {
text: "Edit"
onClicked: {
editPrompt.text = model.characters;
editPrompt.model = model;
editPrompt.open();
}
}
Controls.Button {
text: "Delete"
onClicked: {
customModel.deleteSpecies(model.species, index);
}
}
}
}
}
}
}
}
Код повністю
Main.qml
|
|
model.h
|
|
model.cpp
|
|
Докладніше
Щоб дізнатися більше, ознайомтеся із розділами Використання моделей C++ на панелях Qt Quick та Програмування на основі моделі-панелі перегляду.