Користування окремими файлами

Розподіл громіздкого коду між різними файлами і долучення сигналів до ваших компонентів.

Чому і як

Ми вперше відокремлюємо деякі з наших компонентів у окремі файли 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

Така структура є доволі типовою для проєктів KDE. Здебільшого, причиною є бажання уникнути використання додаткового файла 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

Ця структура не є аж надто поширеною у проєктах KDE і потребує написання додаткового файла 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 за допомогою using 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
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
    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 можна поділити повністю, оскільки його вже загорнуто у тип Component QML. Ми використовуємо Component, щоб мати змогу визначити його без потреби у створенні екземпляра; окремі файли QML працюють так само.

Якщо ми пересуваємо код до окремих файлів, немає сенсу в огортанні у Component: ми можемо просто вирізати 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 не загорнуто у Component, і не є компонентом, який є типово видимим, отже код можна скопіювати без змін до 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 мають починатися з великої літери.

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