Uporaba ločenih datotek
Ločevanje neusmiljene kode v različne datoteke in spajanje signalov komponentam.
Zakaj in kako
Prvič bomo nekatere naše komponente ločili v lastne datoteke QML. Če dodajamo stvari v Main.qml
, bo hitro postalo težko ugotoviti, kaj počne kaj, in tvegamo, da bomo umazali našo kodo.
V tem učbeniku bomo razdelili kodo v Main.qml
na Main.qml
, AddEditDialog.qml
in KountdownDelegate.qml
.
Poleg tega lahko tudi pri širjenju kode med več datotekami QML količina datotek v realnih projektih uide izpod nadzora. Običajna rešitev te težave je logično ločevanje datotek v različne mape. Na kratko si bomo ogledali tri pogoste pristope, ki jih vidimo v resničnih projektih, in implementirali enega od njih:
- shranjevanje datotek QML skupaj z datotekami C++
- shranjevanje datotek QML v drugem imeniku pod istim modulom
- shranjevanje datotek QML v drugem imeniku pod drugim modulom
Po razcepu bomo imeli ločitev pomislekov med vsako datoteko in [podrobnosti o izvedbi bodo povzete](https://en.wikipedia.org/ wiki/Abstraction_(computer_science)), zaradi česar je koda bolj berljiva.
Shranjevanje datotek QML skupaj z datotekami C++
To vključuje shranjevanje datotek QML projekta skupaj z datotekami C++ v src/
. Takšna struktura bi izgledala takole:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
To smo storili prej. V zgornjem primeru bi morali kar naprej dodajati datoteke QML v obstoječi kirigami-tutorial/src/CMakeLists.txt
. Sploh ni logičnega ločevanja in ko projekt dobi več kot nekaj datotek QML (in datotek C++, ki ustvarjajo tipe za uporabo v QML), lahko mapa hitro postane natrpana.
Shranjevanje datotek QML v drugem imeniku pod istim modulom
To pomeni, da se vse datoteke QML hranijo v ločeni mapi, običajno src/qml/
. Takšna struktura bi izgledala takole:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
└── qml/
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Ta struktura je zelo pogosta v projektih KDE, predvsem zato, da bi se izognili dodatni datoteki CMakeLists.txt za imenik src/qml/
in ustvarjanju ločenega modula. Ta metoda hrani same datoteke v ločeni mapi, vendar bi jih morali dodati tudi v kirigami-tutorial/src/CMakeLists.txt
. Vse ustvarjene datoteke QML bi potem pripadale istemu modulu QML kot Main.qml
.
V praksi, ko projekt prejme več kot ducat datotek QML, čeprav ne bo napolnil imenika src/
, bo napolnil datoteko src/CMakeLists.txt
. Težko bo razlikovati med tradicionalnimi datotekami C++ in datotekami C++, katerih vrste so izpostavljene QML.
Prav tako bo prekinil koncept lokalnosti (lokalizacija podrobnosti odvisnosti), kjer bi opis vaših odvisnosti obdržali na istem mestu kot same odvisnosti.
Shranjevanje datotek QML v drugem imeniku pod drugim modulom
To vključuje shranjevanje vseh datotek QML v ločeni mapi z lastnim CMakeLists.txt in lastnim ločenim modulom QML. Takšna struktura bi izgledala takole:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── components/
├── CMakeLists.txt
├── AddDialog.qml
└── KountdownDelegate.qml
Ta struktura ni tako pogosta v projektih KDE in zahteva pisanje dodatnega CMakeLists.txt, vendar je najbolj prilagodljiva. V našem primeru poimenujemo našo mapo "components", ker ustvarjamo dve novi komponenti QML iz naše prejšnje datoteke Main.qml
, informacije o njih pa hranimo v kirigami-tutorial/src/components/CMakeLists.txt
. Sama datoteka Main.qml
ostane v src/
, tako da se samodejno uporablja pri izvajanju izvršljive datoteke, kot prej.
Kasneje bi bilo mogoče ustvariti več map z več datotekami QML, vse združene po funkcijah, kot so "models" in "settings", in datoteke C++, katerih tipi so izpostavljeni QML (kot so modeli), bi lahko hranili skupaj z druge datoteke QML, kjer je to smiselno.
To strukturo bomo uporabili v tem učbeniku.
Priprava CMake za nove datoteke
Najprej ustvarite datoteko kirigami-tutorial/src/components/CMakeLists.txt
z naslednjo vsebino:
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})
|
Ustvarimo nov cilj z imenom kirigami-hello-components
in ga nato spremenimo v modul QML z uporabo ecm_add_qml_module() pod uvoženiim imenom org.kde.tutorial.components
in dodajte ustrezne datoteke QML.
Ker je cilj drugačen od izvršljive datoteke, bo deloval kot drug modul QML, v tem primeru bomo morali narediti dve stvari: omogočiti generiranje kode, da bo deloval kot vtičnik Qt z GENERATE_PLUGIN_SOURCE in ga finalizirajte z ecm_finalize_qml_module(). Nato ga namestimo natanko tako kot v prejšnjih lekcijah.
Uporabiti smo morali add_library(), da smo lahko povezali kirigami-hello-components
z izvršljivo datoteko v target_link_libraries( ) ter klic 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
| 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)
|
Uporabiti moramo tudi add_subdirectory(), da bo CMake našel imenik kirigami-tutorial/src/components/
.
V prejšnjih lekcijah nam ni bilo treba dodati uvoza org.kde.tutorial
v naš Main.qml
, ker ni bil potreben: ker je vstopna točka za aplikacijo, bi izvršljiva datoteka vseeno takoj zagnala datoteko. Ker so naše komponente v ločenem modulu QML, je potreben nov uvoz v kirigami-tutorial/src/Main.qml
, enak prej definiranemu, 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
// Preostanek kode...
In smo pripravljeni na odhod.
Razcepljanje Main.qml
Oglejmo si še enkrat originalni 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
}
}
}
|
Delegat po meri z id: kountdownDelegate
je mogoče v celoti razdeliti, ker je že zavit v vrsto komponente QML. Komponento uporabljamo, da jo lahko definiramo, ne da bi jo morali instancirati; ločene datoteke QML delujejo na enak način.
Če premaknemo kodo v ločene datoteke, potem nima smisla, da jo pustimo ovito v komponenti: lahko razdelimo samo Kirigami.AbstractCard v ločeno datoteko. Tukaj je nastala 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")
}
}
}
}
|
Naše pogovorno okno z id: addDialog
ni ovito v komponento in ni komponenta, ki je privzeto vidna, zato je kodo mogoče kopirati takšno, kot je, v 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();
}
}
|
Z razcepom kode postane Main.qml
tako veliko krajši:
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 {}
}
}
}
|
Zdaj imamo dve dodatni datoteki QML, AddDialog.qml
in KountdownDelegate
, in najti moramo način, kako ju uporabiti v Main.qml
. Način dodajanja vsebine novih datotek v Main.qml
je tako, da jih instanciiramo.
AddDialog.qml
postane `AddDialog {}``:
31
32
33
| AddDialog {
id: addDialog
}
|
KountdownDelegate.qml
postane `KountdownDelegate {}``:
47
48
49
50
51
| Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: KountdownDelegate {}
}
|
Večina primerov, ki ste jih videli, ko se je komponenta začela z velikimi črkami in ji sledijo oklepaji, je bila primerek komponente QML. Zato se morajo naše nove datoteke QML začeti z veliko začetnico.
Prevedite projekt in ga zaženite ter morali bi imeti funkcionalno okno, ki se obnaša popolnoma enako kot prej, vendar s kodo, razdeljeno na ločene dele, zaradi česar so stvari veliko bolj obvladljive.