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