Skip to main content
تخط المحتوى

استخدام ملفات منفصلة في مشروع C++

فصل الشيفرة غير العملية إلى ملفات مختلفة، وإرفاق الإشارات بمكوناتك.

لماذا وكيف

لأول مرة، سنفصل بعض مكوناتنا إلى ملفات QML خاصة بها. إذا استمررنا في إضافة أشياء إلى Main.qml، سيصبح من الصعب سريعًا تمييز ما يفعل كل شيء، وسنخاطر بتشويش شفرتنا.

في هذا الدرس، سنقسم الشفرة في Main.qml إلى Main.qml وAddDialog.qml وKountdownDelegate.qml.

بالإضافة إلى ذلك، حتى عند توزيع الشفرة بين ملفات QML متعددة، يمكن أن يخرج عدد الملفات في المشاريع الحقيقية عن السيطرة. الحل الشائع لهذه المشكلة هو فصل الملفات منطقيًا إلى مجلدات مختلفة. سنلقي نظرة سريعة على ثلاثة أساليب شائعة تُرى في المشاريع الحقيقية، وسننفذ أحدها:

  • تخزين ملفات QML مع ملفات C++
  • تخزين ملفات QML في دليل مختلف تحت نفس الوحدة
  • تخزين ملفات QML في دليل مختلف تحت وحدة مختلفة

بعد التقسيم، سيكون لدينا فصل الاهتمامات بين كل ملف، وستُجرّد تفاصيل التنفيذ، مما يجعل الشفرة أكثر قابلية للقراءة.

تخزين ملفات QML مع ملفات C++

يتكون هذا من الاحتفاظ بملفات QML الخاصة بالمشروع مع ملفات C++ في src/. هذا النوع من الهيكل سيبدو هكذا:

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    ├── Main.qml
    ├── AddDialog.qml
    └── KountdownDelegate.qml

هذا ما فعلناه سابقًا. في الحالة أعلاه، ستحتاج فقط إلى الاستمرار في إضافة ملفات QML إلى kirigami-tutorial/src/CMakeLists.txt الموجود. لا يوجد فصل منطقي على الإطلاق، وبمجرد أن يحتوي المشروع على أكثر من ملفي QML (وملفات C++ التي تنشئ أنواعًا لاستخدامها في QML)، يمكن أن يصبح المجلد مزدحمًا بسرعة.

تخزين ملفات QML في دليل مختلف تحت نفس الوحدة

يتكون هذا من الاحتفاظ بجميع ملفات QML في مجلد منفصل، عادةً src/qml/. هذا النوع من الهيكل سيبدو هكذا:

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    └── qml/
        ├── Main.qml
        ├── AddDialog.qml
        └── KountdownDelegate.qml

هذا الهيكل شائع جدًا في مشاريع كيدي، وذلك لتجنب وجود ملف CMakeLists.txt إضافي لدليل src/qml/ وإنشاء وحدة منفصلة. تحتفظ هذه الطريقة بالملفات نفسها في مجلد منفصل، لكنك ستحتاج أيضًا إلى إضافتها في kirigami-tutorial/src/CMakeLists.txt. ستنتمي جميع ملفات QML المنشأة بعد ذلك إلى نفس وحدة QML مثل Main.qml.

عمليًا، بمجرد أن يتجاوز المشروع عشرات ملفات QML، فإنه لن يزدحم في دليل src/، بل سيزحم ملف src/CMakeLists.txt. سيصبح من الصعب التفريق بين ملفات C++ التقليدية وملفات C++ التي تحتوي على أنواع مكشوفة لـ QML.

سيكسر أيضًا مفهوم المحلية (توطين تفاصيل التبعيات)، حيث ستحتفظ بوصف تبعياتك في نفس مكان التبعيات نفسها.

تخزين ملفات QML في دليل مختلف تحت وحدة مختلفة

يتكون هذا من الاحتفاظ بجميع ملفات QML في مجلد منفصل مع ملف CMakeLists.txt الخاص به ووحدة QML منفصلة خاصة به. ستبدو هذه البنية هكذا:

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    ├── Main.qml
    └── components/
        ├── CMakeLists.txt
        ├── AddDialog.qml
        └── KountdownDelegate.qml

هذه البنية ليست شائعة في مشاريع كيدي وتتطلب كتابة ملف CMakeLists.txt إضافي، لكنها الأكثر مرونة. في حالتنا، نسمي مجلدنا "components" لأننا ننشئ مكوني QML جديدين من ملف Main.qml السابق، ونحتفظ بمعلومات عنهما في kirigami-tutorial/src/components/CMakeLists.txt. يبقى ملف Main.qml نفسه في src/ ليُستخدم آليًا عند تشغيل المنفذ، كما في السابق.

لاحقًا، سيكون من الممكن إنشاء المزيد من المجلدات بملفات QML متعددة، مجمعة جميعًا حسب الوظيفة، مثل "models" و"settings"، ويمكن الاحتفاظ بملفات C++ التي تحتوي على أنواع مكشوفة لـ QML (مثل النماذج) مع ملفات QML الأخرى حيثما كان ذلك منطقيًا.

سنستخدم هذه البنية في هذا الدليل.

تحضير CMake للملفات الجديدة

أولاً، أنشئ الملف kirigami-tutorial/src/components/CMakeLists.txt بالمحتويات التالية:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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
)

ecm_finalize_qml_module(kirigami-hello-components)

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

ننشئ هدفًا جديدًا يُدعى kirigami-hello-components ثم نحوله إلى وحدة QML باستعمال ecm_add_qml_module() تحت اسم الاستيراد org.kde.tutorial.components ونضيف ملفات QML ذات الصلة.

لأن الهدف مختلف عن المنفذ، سيعمل كوحدة QML مختلفة، وفي هذه الحالة سنحتاج إلى فعل شيئين: جعله يُولّد كودًا ليعمل كملحق Qt باستخدام GENERATE_PLUGIN_SOURCE، وإنهاؤه باستخدام ecm_finalize_qml_module(). ثم نُثبته تمامًا كما في الدروس السابقة.

احتجنا إلى استخدام add_library() لنتمكن من ربط kirigami-hello-components بالمنفذ في استدعاء target_link_libraries() في kirigami-tutorial/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)

نحتاج أيضًا إلى استخدام add_subdirectory() ليجد CMake الدليل kirigami-tutorial/src/components/.

في الدروس السابقة، لم نحتج إلى إضافة الاستيراد org.kde.tutorial إلى ملف Main.qml لأنه لم يكن ضروريًا: بكونه نقطة الدخول للتطبيق، كان المنفذ سيشغل الملف فورًا على أي حال. بما أن مكوناتنا في وحدة QML منفصلة، فإن استيرادًا جديدًا في kirigami-tutorial/src/Main.qml ضروري، وهو نفسه المُعرّف سابقًا، org.kde.tutorial.components:

import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami
import org.kde.tutorial.components

// بقية الكود...

ونحن مستعدون للانطلاق.

تقسيم Main.qml

دعنا نلقي نظرة مرة أخرى على 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami

Kirigami.ApplicationWindow {
    id: root

    width: 600
    height: 400

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

    globalDrawer: Kirigami.GlobalDrawer {
        isMenu: true
        actions: [
            Kirigami.Action {
                text: i18n("Quit")
                icon.name: "application-exit-symbolic"
                shortcut: StandardKey.Quit
                onTriggered: Qt.quit()
            }
        ]
    }

    ListModel {
        id: kountdownModel
    }

    Component {
        id: kountdownDelegate
        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")
                    }
                }
            }
        }
    }

    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

        // Form layouts help align and structure a layout with several inputs
        Kirigami.FormLayout {
            // Textfields let you input text in a thin textbox
            Controls.TextField {
                id: nameField
                // Provides a label attached to the textfield
                Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
                // What to do after input is accepted (i.e. pressed Enter)
                // In this case, it moves the focus to the next field
                onAccepted: descriptionField.forceActiveFocus()
            }
            Controls.TextField {
                id: descriptionField
                Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
                placeholderText: i18n("Optional")
                // Again, it moves the focus to the next field
                onAccepted: dateField.forceActiveFocus()
            }
            Controls.TextField {
                id: dateField
                Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
                // D means a required number between 1-9,
                // 9 means a required number between 0-9
                inputMask: "D999-99-99"
                // Here we confirm the operation just like
                // clicking the OK button
                onAccepted: addDialog.onAccepted()
            }
            Controls.Label {
                text: "* = required fields"
            }
        }
        // Once the Kirigami.Dialog is initialized,
        // we want to create a custom binding to only
        // make the Ok button visible if the required
        // text fields are filled.
        // For this we use Kirigami.Dialog.standardButton(button):
        Component.onCompleted: {
            const button = standardButton(Kirigami.Dialog.Ok);
            // () => is a JavaScript arrow function
            button.enabled = Qt.binding( () => requiredFieldsFilled() );
        }
        onAccepted: {
            // The binding is created, but we still need to make it
            // unclickable unless the fields are filled
            if (!addDialog.requiredFieldsFilled()) return;
            appendDataToModel();
            clearFieldsAndClose();
        }
        // We check that the nameField is not empty and that the
        // dateField (which has an inputMask) is completely filled
        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();
        }
    }

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

        // Kirigami.Action encapsulates a UI action. Inherits from Controls.Action
        actions: [
            Kirigami.Action {
                id: addAction
                // Name of icon associated with the action
                icon.name: "list-add-symbolic"
                // Action text, i18n function returns translated string
                text: i18nc("@action:button", "Add kountdown")
                // What to do when triggering the action
                onTriggered: addDialog.open()
            }
        ]

        Kirigami.CardsListView {
            id: cardsView
            model: kountdownModel
            delegate: kountdownDelegate
        }
    }
}

يمكن تقسيم المفوض المخصص ذو id: kountdownDelegate بالكامل لأنه مُغلف بالفعل في نوع مكون QML. نستخدم مكونًا لنتمكن من تعريفه دون الحاجة إلى إنشاء مثيل له؛ تعمل ملفات QML المنفصلة بنفس الطريقة.

إذا نقلنا الكود إلى ملفات منفصلة، فلا فائدة من تركه مُغلفًا في مكون: يمكننا تقسيم Kirigami.AbstractCard فقط في الملف المنفصل. إليك 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")
            }
        }
    }
}

حوارنا ذو id: addDialog ليس مُغلفًا في مكون، وليس مكونًا مرئيًا مبدئيًا، لذا يمكن نسخ الكود كما هو إلى 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();
    }
}

مع تقسيم الكود، يصبح 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
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("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 {}
        }
    }
}

لدينا الآن ملفا QML إضافيان، AddDialog.qml وKountdownDelegate، ونحتاج إلى إيجاد طريقة لاستخدامهما في Main.qml. طريقة إضافة محتويات الملفات الجديدة إلى Main.qml هي عن طريق إنشاء مثيل لها.

يصبح AddDialog.qml هو AddDialog {}:

31
32
33
    AddDialog {
        id: addDialog
    }

يصبح KountdownDelegate.qml هو KountdownDelegate {}:

47
48
49
50
51
        Kirigami.CardsListView {
            id: cardsView
            model: kountdownModel
            delegate: KountdownDelegate {}
        }

معظم الحالات التي رأيتها لمكون يبدأ بحرف كبير متبوعًا بأقواس كانت إنشاءات مثيل لمكون QML. لهذا تحتاج ملفات QML الجديدة إلى البدء بحرف كبير.

ترجم المشروع وشغّله، ويجب أن تحصل على نافذة وظيفية تتصرف تمامًا كما في السابق، ولكن مع تقسيم الكود إلى أجزاء منفصلة، مما يجعل الأمور أكثر قابلية للإدارة.