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:
    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

لعرض النص، أضف 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
    }
}

الآن ترجم وابدأ برنامجك مرة أخرى. سترى أن الصفحة الجديدة تحتوي على عنوان وسطي يقول "مرحبًا بالعالم!".

تهانينا، تعلمت:

  • كيف تسجل أنواع الخلفية إلى QML
  • أضف عناصر جديدة إلى ملف QML
  • أنشئ صفوفًا فرعية جديدة من QObject
  • كيف تضيف خصائص وماذا تفعل
  • ما هي الإشارات

إذا أردت معرفة المزيد عن التكامل بين QML و C++، نوصي بقراءة وثائق Qt الرسمية.