Skip to main content
Перейти до вмісту

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

З'єднання модуля для виконання обчислень і надання інтерфейсу даних для показу

Щоб інтегрувати логіку до самої програми нам знадобляться класи модуля обробки мовою C++, які зможуть виконувати важливі обчислення. Не варто вписувати логіку до самих файлів QML — слід переносити якомога більшу її частину до модуля обробки так, щоб код QML використовувався лише для показу інтерфейсу користувача.

Як основу, ми використаємо код з підручника Використання окремих файлів у проєкті C++.

Структура проєкту

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    ├── Main.qml ----------------------- # Modified
    └── components/
        ├── CMakeLists.txt ------------- # Modified
        ├── AddDialog.qml
        ├── KountdownDelegate.qml
        ├── ExposePage.qml ------------- # New
        ├── backend.h ------------------ # New
        └── backend.cpp ---------------- # New

Зміни у наявному коді

src/components/backend.h

Спочатку створіть файл заголовків, який міститиме код, відкритий QML, а саме типу Backend:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#pragma once

#include <QObject>
#include <qqmlintegration.h>

class Backend : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON

public:
    explicit Backend(QObject *parent = nullptr);
};

Для доступу до коду C++ до QML потрібні дві речі, і одна з них — це просте використання макроса QML_ELEMENT, доступного з файла заголовків <QtQml/qqmlregistration.h>.

Модуль обробки буде створено як синглтон, тобто він буде створений лише один раз та існуватиме протягом усього терміну роботи програми. Для цього ми використовуємо макрос QML_SINGLETON.

src/components/backend.cpp

Ми можемо додати наш початковий код для конструктора до backend.cpp:

1
2
3
4
5
#include "backend.h"

Backend::Backend(QObject *parent)
    : QObject(parent)
{}

src/components/ExposePage.qml

Відтепер модуль обробки буде відомий QML як тип QML Backend. Він міститься у модулі під назвою org.kde.tutorial.components.

Це означає, що просто додавши рядок імпортування у файл QML, можна використовувати код, розкритий з C++:

import org.kde.tutorial.components

Щоб почати з чистого аркуша, використовуючи наявний код програми, створіть новий компонент QML, який містить порожню сторінку:

1
2
3
4
5
6
7
import org.kde.kirigami as Kirigami
import org.kde.tutorial.components

Kirigami.Page {
    title: "Exposing to QML Tutorial"
    // ...
}

src/Main.qml

Щоб фактично показати нову сторінку, давайте створимо пункт меню, який надсилатиме нову сторінку до програми, щоб ми могли легше протестувати модель.

Для цього ми можемо скористатися новим Kirigami.Action разом з Qt.createComponent() для створення сторінки на льоту:

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    globalDrawer: Kirigami.GlobalDrawer {
        isMenu: true
        actions: [
            Kirigami.Action {
                text: i18n("Exposing to QML")
                icon.name: "kde"
                onTriggered: pageStack.push(Qt.createComponent("org.kde.tutorial.components", "ExposePage"))
            },
            Kirigami.Action {
                text: i18n("Quit")
                icon.name: "application-exit-symbolic"
                shortcut: StandardKey.Quit
                onTriggered: Qt.quit()
            }
        ]
    }

src/components/CMakeLists.txt

Нарешті, додайте новостворені backend.h, backend.cpp і ExposePage.qml до CMake:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
add_library(kirigami-hello-components)

ecm_add_qml_module(kirigami-hello-components
    URI "org.kde.tutorial.components"
    GENERATE_PLUGIN_SOURCE
)

ecm_target_qml_sources(kirigami-hello-components
    SOURCES
    AddDialog.qml
    KountdownDelegate.qml
    ExposePage.qml
)

target_sources(kirigami-hello-components
    PRIVATE
    backend.cpp backend.h
)

ecm_finalize_qml_module(kirigami-hello-components)

install(TARGETS kirigami-hello-components ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})

Додавання файлів до наявного модуля QML у CMake — це друга частина розкриття коду для QML.

Зверніть увагу, що модуль QML слід було створити за допомогою ecm_add_qml_module() або qt_add_qml_module(), оскільки вони необхідні для декларативної реєстрації.

Тепер ви зможете зібрати і встановити проєкт, виконавши ті ж кроки, що й раніше.

Надання функціональності

src/components/backend.h

Тепер клас, який містить майбутню логіку, з'єднано із програмою, але він усе ще не виконує ніяких корисних дій. Щоб змінити це, давайте додамо до класу властивість. Призначення властивостей є набагато ширшим за прості змінні. Вони можуть інформувати інтерфейс користувача щодо змін, щоб той міг оновлювати належні області.

Одразу під макросом QML_SINGLETON додайте Q_PROPERTY.

Q_PROPERTY(QString introductionText READ introductionText WRITE setIntroductionText NOTIFY introductionTextChanged)

Здається, забагато коду для простого читання і написання коду модуля обробки, чи не так? Але прискіпливіший погляд показує, що читання властивості з інтерфейсу вже може запускати логіку. Те саме стосується запису. У цьому випадку код автоматично інформує оболонку і модуль обробки про зміни.

Читання і запис засновано на концепції функцій отримання та встановлення значень, тому додайте новий закритий (приватний) атрибут, який міститиме дані, до вашого класу у описаний спосіб, а також додайте відповідні функції отримання та встановлення значень.

Додайте новий приватний розділ з таким кодом:

private:
    QString m_introductionText = QStringLiteral("Hello World!");

І додайте наведений нижче код до наявного розділу public:

public:
    QString introductionText() const;
    void setIntroductionText(const QString &introductionText);
    Q_SIGNAL void introductionTextChanged();

Перша функція є отримувачем даних, друга — встановлювачем, а третя — сигналом, який видається, коли змінюється значення властивості. Вони відповідають частинам READ, WRITE і NOTIFY для вищої Q_PROPERTY.

Результат може виглядати десь так:

 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>

class Backend : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON
    Q_PROPERTY(QString introductionText READ introductionText WRITE setIntroductionText NOTIFY introductionTextChanged)
public:
    explicit Backend(QObject *parent = nullptr);
    QString introductionText() const;
    void setIntroductionText(const QString &introductionText);
    Q_SIGNAL void introductionTextChanged();
private:
    QString m_introductionText = QStringLiteral("Hello World!");
};

src/components/backend.cpp

Сигнал не потребує реалізації у файлі backend.cpp, оскільки він не виконує майже нічого, окрім видання, але функції отримання та встановлення значення має бути реалізовано десь так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include "backend.h"

Backend::Backend(QObject *parent)
    : QObject(parent)
{}

QString Backend::introductionText() const
{
    return m_introductionText;
}

void Backend::setIntroductionText(const QString &introductionText)
{
    m_introductionText = introductionText;
    Q_EMIT introductionTextChanged();
}

Як можна бачити, при виклику функції встановлення значення буде видано сигнал, отже про зміну буде проінформовано інтерфейс користувача і модуль обробки.

src/components/ExposePage.qml

Щоб показати текст у main.qml додайте Kirigami.Heading до `src/components/ExposePage.qml під властивістю title компонента Kirigami.Page, який ми додали до коду.

Код-результат у тій частині файла має виглядати ось так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.kde.kirigami as Kirigami
import org.kde.tutorial.components

Kirigami.Page {
    title: "Exposing to QML Tutorial"
    Kirigami.Heading {
        anchors.centerIn: parent
        text: Backend.introductionText
    }
}

Наша програма на поточний момент

Наявний код:

CMakeLists.txt
 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
cmake_minimum_required(VERSION 3.20)
project(kirigami-tutorial)

find_package(ECM 6.0.0 REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})

include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMFindQmlModule)
include(ECMQmlModule)

find_package(Qt6 REQUIRED COMPONENTS
    Core
    Quick
    Test
    Gui
    QuickControls2
    Widgets
)

find_package(KF6 REQUIRED COMPONENTS
    Kirigami
    I18n
    CoreAddons
    QQC2DesktopStyle
    IconThemes
)

ecm_find_qmlmodule(org.kde.kirigami REQUIRED)

add_subdirectory(src)

install(PROGRAMS org.kde.tutorial.desktop DESTINATION ${KDE_INSTALL_APPDIR})

feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
src/main.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
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QUrl>
#include <QQuickStyle>
#include <KLocalizedContext>
#include <KLocalizedString>
#include <KIconTheme>

int main(int argc, char *argv[])
{
    KIconTheme::initTheme();
    QApplication app(argc, argv);
    KLocalizedString::setApplicationDomain("tutorial");
    QApplication::setOrganizationName(QStringLiteral("KDE"));
    QApplication::setOrganizationDomain(QStringLiteral("kde.org"));
    QApplication::setApplicationName(QStringLiteral("Kirigami Tutorial"));
    QApplication::setDesktopFileName(QStringLiteral("org.kde.tutorial"));

    QApplication::setStyle(QStringLiteral("breeze"));
    if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
        QQuickStyle::setStyle(QStringLiteral("org.kde.desktop"));
    }

    QQmlApplicationEngine engine;

    engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
    engine.loadFromModule("org.kde.tutorial", "Main");

    if (engine.rootObjects().isEmpty()) {
        return -1;
    }

    return app.exec();
}
src/CMakeLists.txt
 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
add_executable(kirigami-hello)

ecm_add_qml_module(kirigami-hello
    URI
    org.kde.tutorial
)

target_sources(kirigami-hello
    PRIVATE
    main.cpp
)

ecm_target_qml_sources(kirigami-hello
    SOURCES
    Main.qml
)

target_link_libraries(kirigami-hello
    PRIVATE
    Qt6::Quick
    Qt6::Qml
    Qt6::Gui
    Qt6::QuickControls2
    Qt6::Widgets
    KF6::I18n
    KF6::CoreAddons
    KF6::IconThemes
    kirigami-hello-components
)

install(TARGETS kirigami-hello ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})

add_subdirectory(components)
src/components/KountdownDelegate.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
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami

Kirigami.AbstractCard {
    contentItem: Item {
        implicitWidth: delegateLayout.implicitWidth
        implicitHeight: delegateLayout.implicitHeight
        GridLayout {
            id: delegateLayout
            anchors {
                left: parent.left
                top: parent.top
                right: parent.right
            }
            rowSpacing: Kirigami.Units.largeSpacing
            columnSpacing: Kirigami.Units.largeSpacing
            columns: root.wideScreen ? 4 : 2

            Kirigami.Heading {
                level: 1
                text: i18n("%1 days", Math.round((date-Date.now())/86400000))
            }

            ColumnLayout {
                Kirigami.Heading {
                    Layout.fillWidth: true
                    level: 2
                    text: name
                }
                Kirigami.Separator {
                    Layout.fillWidth: true
                    visible: description.length > 0
                }
                Controls.Label {
                    Layout.fillWidth: true
                    wrapMode: Text.WordWrap
                    text: description
                    visible: description.length > 0
                }
            }
            Controls.Button {
                Layout.alignment: Qt.AlignRight
                Layout.columnSpan: 2
                text: i18n("Edit")
            }
        }
    }
}
src/components/AddDialog.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
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami

Kirigami.Dialog {
    id: addDialog
    title: i18nc("@title:window", "Add kountdown")
    standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
    padding: Kirigami.Units.largeSpacing
    preferredWidth: Kirigami.Units.gridUnit * 20

    Kirigami.FormLayout {
        Controls.TextField {
            id: nameField
            Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
            onAccepted: descriptionField.forceActiveFocus()
        }
        Controls.TextField {
            id: descriptionField
            Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
            onAccepted: dateField.forceActiveFocus()
        }
        Controls.TextField {
            id: dateField
            Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
            inputMask: "D999-99-99"
            onAccepted: addDialog.accepted()
        }
        Controls.Label {
            text: "* = required fields"
        }
    }
    Component.onCompleted: {
        const button = standardButton(Kirigami.Dialog.Ok);
        button.enabled = Qt.binding( () => requiredFieldsFilled() );
    }
    onAccepted: {
        if (!addDialog.requiredFieldsFilled()) return;
        appendDataToModel();
        clearFieldsAndClose();
    }
    function requiredFieldsFilled() {
        return (nameField.text !== "" && dateField.acceptableInput);
    }
    function appendDataToModel() {
        kountdownModel.append({
            name: nameField.text,
            description: descriptionField.text,
            date: new Date(dateField.text)
        });
    }
    function clearFieldsAndClose() {
        nameField.text = ""
        descriptionField.text = ""
        dateField.text = ""
        addDialog.close();
    }
}

Код, який дописано або змінено на цій сторінці:

src/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
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami
import org.kde.tutorial.components

Kirigami.ApplicationWindow {
    id: root

    width: 600
    height: 400

    title: i18nc("@title:window", "Day Kountdown")

    globalDrawer: Kirigami.GlobalDrawer {
        isMenu: true
        actions: [
            Kirigami.Action {
                text: i18n("Exposing to QML")
                icon.name: "kde"
                onTriggered: pageStack.push(Qt.createComponent("org.kde.tutorial.components", "ExposePage"))
            },
            Kirigami.Action {
                text: i18n("Quit")
                icon.name: "application-exit-symbolic"
                shortcut: StandardKey.Quit
                onTriggered: Qt.quit()
            }
        ]
    }

    ListModel {
        id: kountdownModel
    }

    AddDialog {
        id: addDialog
    }

    pageStack.initialPage: Kirigami.ScrollablePage {
        title: i18nc("@title", "Kountdown")

        actions: [
            Kirigami.Action {
                id: addAction
                icon.name: "list-add-symbolic"
                text: i18nc("@action:button", "Add kountdown")
                onTriggered: addDialog.open()
            }
        ]

        Kirigami.CardsListView {
            id: cardsView
            model: kountdownModel
            delegate: KountdownDelegate {}
        }
    }
}
src/components/CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
add_library(kirigami-hello-components)

ecm_add_qml_module(kirigami-hello-components
    URI "org.kde.tutorial.components"
    GENERATE_PLUGIN_SOURCE
)

ecm_target_qml_sources(kirigami-hello-components
    SOURCES
    AddDialog.qml
    KountdownDelegate.qml
    ExposePage.qml
)

target_sources(kirigami-hello-components
    PRIVATE
    backend.cpp backend.h
)

ecm_finalize_qml_module(kirigami-hello-components)

install(TARGETS kirigami-hello-components ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
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>

class Backend : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON
    Q_PROPERTY(QString introductionText READ introductionText WRITE setIntroductionText NOTIFY introductionTextChanged)
public:
    explicit Backend(QObject *parent = nullptr);
    QString introductionText() const;
    void setIntroductionText(const QString &introductionText);
    Q_SIGNAL void introductionTextChanged();
private:
    QString m_introductionText = QStringLiteral("Hello World!");
};
src/components/backend.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include "backend.h"

Backend::Backend(QObject *parent)
    : QObject(parent)
{}

QString Backend::introductionText() const
{
    return m_introductionText;
}

void Backend::setIntroductionText(const QString &introductionText)
{
    m_introductionText = introductionText;
    Q_EMIT introductionTextChanged();
}
src/components/ExposePage.qml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.kde.kirigami as Kirigami
import org.kde.tutorial.components

Kirigami.Page {
    title: "Exposing to QML Tutorial"
    Kirigami.Heading {
        anchors.centerIn: parent
        text: Backend.introductionText
    }
}

Тепер зберіть і знову запустіть свою програму. Ви побачите, що нова сторінка має заголовок, розташований у центрі, з написом «Hello World!».

Вітаємо, ви навчилися:

  • Як реєструвати типи модулів обробки у QML
  • Додавати нові елементи до файла QML
  • Створювати підкласи QObject
  • Як додавати властивості і для чого вони потрібні
  • Що таке сигнали

Якщо ви хочете дізнатися про інтеграцію між QML і C++, ми рекомендуємо ознайомитися із офіційною документацією до Qt.