З'єднання моделей з вашим інтерфейсом користувача QML

З'єднання моделей з модуля обробки на C++ із оболонкою на QML

Як показано у попередньому розділі підручника, ви можете з'єднати код мовою C++ із кодом QML шляхом створення класу, який буде оброблено як ще один компонент у QML. Втім, може виникнути потреба у представленні складніших даних, зокрема даних, які мають працювати як нетипова ListModel, або даних, які має бути делеговано з Repeater.

Ми можемо створювати власні моделі на боці C++ і оголошувати, як дані з відповідної моделі має бути представлено в оболонці QML.

Приготування класу

У цих настановах ми створимо клас, який містить QMap, де як ключ буде використано QString, а як значення буде використано об'єкти QStringList. Оболонка зможе читати і показувати ключі і значення і буде простою у використанні, зовсім як одновимірний масив. Усе це має виглядати подібним до ListModel у QML.

Для цього нам потрібно оголосити клас, який успадковує властивості від QAbstractListModel. Давайте також додамо якісь дані до QMap. Ці оголошення будуть зберігатися у model.h.

#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() — цю функцію буде викликано, коли ви захочете отримати дані, які відповідають назвам ролей з моделі.

Перевизначення і реалізація 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;
}

Давайте оновимо код 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();
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Тепер, кожного разу, коли значення моделі буде змінено в оболонці, зміни мають автоматично оновити дані модуля обробки.

app_screenshot_1.png
app_screenshot_2.png

Додавання рядків

Нами додано спосіб внесення змін до даних у наявних ключах 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));
}

Оновімо код 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();
                }
            }
        ]
        ...
    }
}

Тепер у нас буде новий пункт дії у верхній частині вікна програми, який викликатиме запит, за допомогою якого можна буде додати до моделі новий елемент із нашими власними нетиповими даними.

app_screenshot_add_1.png
app_screenshot_add_2.png

Вилучення рядків

Спосіб вилучення рядків є подібним до способу додавання рядків. Створімо ще один метод, який ми викликатимемо з 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);
                            }
                        }
                    }
                }
            }
        }
    }
}
app_screenshot_del_1.png
app_screenshot_del_2.png

Код повністю

Main.qml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
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
    }

    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();
            }
        }
    }

    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 = ""; // Clear TextField every time it's done
                addPrompt.close();
            }
        }
    }

    pageStack.initialPage: Kirigami.ScrollablePage {
        actions: [
            Kirigami.Action {
                icon.name: "add"
                text: "Add New Species"
                onTriggered: {
                    addPrompt.open();
                }
            }
        ]

        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
                            }
                            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);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

model.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#pragma once

#include <QAbstractListModel>

class Model : public QAbstractListModel {
Q_OBJECT;

private:
    QMap<QString, QStringList> m_list = {
            {"Feline", {"Tigress",   "Waai Fuu"}},
            {"Fox",    {"Carmelita", "Diane", "Krystal"}},
            {"Goat",   {"Sybil",     "Toriel"}}
    };

    static QString formatList(const QStringList &list);

public:
    enum Roles {
        SpeciesRole = Qt::UserRole,
        CharactersRole
    };

    int rowCount(const QModelIndex &) const override;

    QHash<int, QByteArray> roleNames() const override;

    QVariant data(const QModelIndex &index, int role) const override;

    bool setData(const QModelIndex &index, const QVariant &value, int role) override;

    Q_INVOKABLE void addSpecies(const QString &species);

    Q_INVOKABLE void deleteSpecies(const QString &species, const int &rowIndex);
};

model.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "model.h"

int Model::rowCount(const QModelIndex &) const {
    return m_list.count();
}

QHash<int, QByteArray> Model::roleNames() const {
    QHash<int, QByteArray> map = {
            {SpeciesRole,   "species"},
            {CharactersRole, "characters"}
    };
    return map;
}

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

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

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));
}

void Model::deleteSpecies(const QString &species, const int& rowIndex) {
    beginRemoveRows(QModelIndex(), rowIndex, rowIndex);
    m_list.remove(species);
    endRemoveRows();
    emit dataChanged(index(0), index(m_list.size() - 1));
}

Докладніше

Щоб дізнатися більше, ознайомтеся із розділами Використання моделей C++ на панелях Qt Quick та Програмування на основі моделі-панелі перегляду.