Prekrývajúce listy
Rozdelenie nepraktického kódu do rôznych súborov a pripojenie signálov k vašim komponentom.
Prečo a ako
Prvýkrát budeme oddeľovať niektoré z našich komponentov do vlastných QML súborov. Ak budeme stále pridávať veci do Main.qml, rýchlo bude ťažké rozoznať, čo robí čo, a riskujeme zneprehľadnenie nášho kódu.
V tomto tutoriáli rozdelíme kód v Main.qml do Main.qml, AddDialog.qml a KountdownDelegate.qml.
Navyše, aj keď rozdelíte kód medzi viaceré QML súbory, množstvo súborov v reálnych projektoch sa môže vymknúť spod kontroly. Bežným riešením tohto problému je logické oddelenie súborov do rôznych priečinkov. Stručne sa pozrieme na tri bežné prístupy videné v reálnych projektoch a jeden z nich implementujeme:
- ukladanie súborov QML spolu so súbormi C++
- ukladanie súborov QML do iného priečinka pod rovnakým modulom
- ukladanie súborov QML do iného priečinka pod iným modulom
Po rozdelení budeme mať oddelenie zodpovedností medzi každým súborom a implementačné detaily budú abstrahované, čím bude kód čitateľnejší.
Ukladanie súborov QML spolu so súbormi C++
Toto spočíva v ponechaní QML súborov projektu spolu s C++ súbormi v src/. Tento druh štruktúry by vyzeral takto:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Toto sme robili predtým. V uvedenom prípade by ste jednoducho museli pokračovať v pridávaní QML súborov do existujúceho kirigami-tutorial/src/CMakeLists.txt. Neexistuje žiadne logické oddelenie a keď projekt získa viac ako pár QML súborov (a C++ súborov, ktoré vytvárajú typy na použitie v QML), priečinok sa môže rýchlo preplniť.
Ukladanie súborov QML do iného priečinka pod rovnakým modulom
Toto spočíva v ponechaní všetkých QML súborov v samostatnom priečinku, zvyčajne src/qml/. Tento druh štruktúry by vyzeral takto:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
└── qml/
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Táto štruktúra je veľmi bežná v projektoch KDE, hlavne na vyhnutie sa potrebe mať extra súbor CMakeLists.txt pre adresár src/qml/ a vytvoreniu samostatného modulu. Táto metóda uchováva samotné súbory v samostatnom priečinku, ale museli by ste ich tiež pridať v kirigami-tutorial/src/CMakeLists.txt. Všetky vytvorené QML súbory by potom patrili do rovnakého QML modulu ako Main.qml.
V praxi, keď projekt získa viac ako tucet QML súborov, aj keď nepreplní adresár src/, preplní súbor src/CMakeLists.txt. Bude ťažké rozlíšiť medzi tradičnými C++ súbormi a C++ súbormi, ktoré majú typy vystavené do QML.
Tiež to naruší koncept lokality (lokalizácia detailov závislostí), kde by ste udržiavali popis vašich závislostí na rovnakom mieste ako samotné závislosti.
Ukladanie súborov QML do iného priečinka pod iným modulom
Toto spočíva v ponechaní všetkých QML súborov v samostatnom priečinku s vlastným CMakeLists.txt a vlastným samostatným QML modulom. Tento druh štruktúry by vyzeral takto:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── components/
├── CMakeLists.txt
├── AddDialog.qml
└── KountdownDelegate.qml
Táto štruktúra nie je tak bežná v projektoch KDE a vyžaduje napísanie dodatočného CMakeLists.txt, ale je najflexibilnejšia. V našom prípade náš priečinok nazývame "components", pretože vytvárame dva nové QML komponenty z nášho predchádzajúceho súboru Main.qml, a informácie o nich uchovávame v kirigami-tutorial/src/components/CMakeLists.txt. Samotný súbor Main.qml zostáva v src/, aby bol automaticky použitý pri spustení spustiteľného súboru, ako predtým.
Neskôr by bolo možné vytvoriť viac priečinkov s viacerými QML súbormi, všetky zoskupené podľa funkcie, ako napríklad "models" a "settings", a C++ súbory, ktoré majú typy vystavené do QML (ako modely), by mohli byť uchovávané spolu s inými QML súbormi, kde to dáva zmysel.
V tomto tutoriáli budeme používať túto štruktúru.
Prekrývajúce listy
Najprv vytvorte súbor kirigami-tutorial/src/components/CMakeLists.txt s nasledujúcim obsahom:
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})
|
Vytvoríme nový cieľ s názvom kirigami-hello-components a potom ho premeníme na modul QML pomocou ecm_add_qml_module() pod importným názvom org.kde.tutorial.components a pridáme príslušné súbory QML.
Pretože cieľ je odlišný od spustiteľného súboru, bude fungovať ako iný QML modul, v takom prípade musíme urobiť dve veci: nechať ho generovať kód, aby fungoval ako Qt plugin pomocou GENERATE_PLUGIN_SOURCE, a finalizovať ho pomocou ecm_finalize_qml_module(). Potom ho nainštalujeme presne ako v predchádzajúcich lekciách.
Museli sme použiť add_library(), aby sme mohli prepojiť kirigami-hello-components so spustiteľným súborom vo volaní target_link_libraries() v 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)
|
Tiež musíme použiť add_subdirectory(), aby CMake našiel adresár kirigami-tutorial/src/components/.
V predchádzajúcich lekciách sme nemuseli pridávať import org.kde.tutorial do nášho Main.qml, pretože to nebolo potrebné: keďže bol vstupným bodom aplikácie, spustiteľný súbor by súbor aj tak okamžite spustil. Keďže naše komponenty sú v samostatnom QML module, je potrebný nový import v kirigami-tutorial/src/Main.qml, rovnaký ako ten definovaný skôr, 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
// Zvyšok kódu...
A sme pripravení.
Rozdelenie Main.qml
Pozrime sa ešte raz na pôvodný 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
}
}
}
|
Vlastný delegát s id: kountdownDelegate sa dá úplne oddeliť, pretože je už obalený v type QML Component. Component používame, aby sme ho mohli definovať bez potreby jeho inštancovania; samostatné QML súbory fungujú rovnakým spôsobom.
Ak presunieme kód do samostatného súboru, potom nemá zmysel ponechať ho obalený v Component: môžeme oddeliť len Kirigami.AbstractCard do samostatného súboru. Tu je výsledný 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")
}
}
}
}
|
Náš dialóg s id: addDialog nie je obalený v Component a nie je to komponent, ktorý je predvolene viditeľný, takže kód sa dá skopírovať tak, ako je, do 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();
}
}
|
S rozdelením kódu sa Main.qml stáva oveľa kratším:
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 {}
}
}
}
|
Teraz máme dva extra QML súbory, AddDialog.qml a KountdownDelegate, a musíme nájsť spôsob, ako ich použiť v Main.qml. Spôsob, ako pridať obsah nových súborov do Main.qml, je ich inštancovaním.
AddDialog.qml sa stáva AddDialog {}:
31
32
33
| AddDialog {
id: addDialog
}
|
KountdownDelegate.qml sa stáva KountdownDelegate {}:
47
48
49
50
51
| Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: KountdownDelegate {}
}
|
Väčšina prípadov, keď ste videli komponent začínajúci veľkým písmenom nasledovaný zátvorkami, boli inštancie QML komponentu. Preto musia naše nové QML súbory začínať veľkým písmenom.
Skompilujte projekt a spustite ho a mali by ste mať funkčné okno, ktoré sa správa presne rovnako ako predtým, ale s kódom rozdeleným do samostatných častí, čím sa veci stávajú omnoho spravovateľnejšie.