Aparte bestanden gebruiken
Onpraktische code in verschillende bestanden scheiden en signalen aan uw componenten hangen.
Hoe en waarom
Voor de eerste keer, zullen we enige van onze componenten in hun eigen QML bestanden scheiden. Als we dingen aan Main.qml
blijven toevoegen, wordt het snel moeilijk te vertellen wat wat doet en riskeren we modderige code.
In deze instructie zullen we de code in Main.qml
uitsplitsen naar Main.qml
, AddDialog.qml
en KountdownDelegate.qml
.
Daarnaast, zelfs als we de code over meerdere QML-bestanden verspreiden, loopt het aantal bestanden in echte projecten al snel uit de hand. Een veel voorkomende oplossing voor dit probleem is het logisch opdelen van de bestanden in meerdere mappen. We kijken kort naar drie voorkomende oplossingen in echte projecten, en implementeren een daarvan:
- QML-bestanden samen met C++ bestanden opslaan
- QML-bestanden in een andere map in dezelfde module opslaan
- QML-bestanden in een andere map in een andere module opslaan
Na het uitsplitsen zullen we separation of concerns tussen elk bestand hebben, en implementation details will be abstracted, wat de code leesbaarder maakt.
QML-bestanden samen met C++ bestanden opslaan
Dit bestaat uit bewaren van de QML-bestanden samen met de C++ bestanden in src/
. Deze manier van structureren zou er als volgt moeten zien:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Dit is wat we tot nu toe deden. In het geval hierboven, hoefde u alleen QML-bestanden aan de bestaande kirigami-tutorial/src/CMakeLists.txt
toe te voegen. Er is helemaal geen logische separatie, en als het project eenmaal meer dan een paar QML-bestanden (en C++ bestanden die de typen creëert die worden gebruikt in QML) krijgt, dan kan de map al snel overbevolkt raken.
QML-bestanden in een andere map in dezelfde module opslaan
Dit bestaat uit bewaren van de QML-bestanden in een aparte map, meestal src/qml/
. Deze manier van structureren zou er als volgt moeten zien:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
└── qml/
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Deze structuur wordt veel gebruikt bij KDE projecten, voornamelijk om te voorkomen dat voor de src/qml/-map een extra CMakeLists.txt-bestand met daarbij de creatie van een separate module nodig is. Deze methode houd de bestanden zelf in een separate map, maar ze moeten ook aan
kirigami-tutorial/src/CMakeLists.txttoegevoegd worden. Alle gecreëerde QML-bestanden horen bij dezelfde QML module als
Main.qml`.
In de praktijk, als het project meer dan een dozijn QML-bestanden heeft, zal het de src/
-map niet overbevolken, zal het wel de src/CMakeLists.txt
-map overbevolken. Het zal moeilijk om nog de traditionele C++ bestanden te onderscheiden van de C++ bestanden die de typen blootstellen aan QML.
Het zal ook het concept van lokaliteit (localisation of dependency details) breken, waar u de omschrijving van uw afhankelijkheden (dependencies) op dezelfde plaats als de afhankelijkheden zelf bewaart.
QML-bestanden in een andere map in een andere module opslaan
Dit bestaat uit bewaren van de QML-bestanden in een aparte map met zijn eigen CMakeLists.txt en zijn eigen QML-module. Deze manier van structureren zou er als volgt moeten zien:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── components/
├── CMakeLists.txt
├── AddDialog.qml
└── KountdownDelegate.qml
Deze structuur wordt niet zoveel veel gebruikt bij KDE projecten en het vereist de creatie van een extra CMakeLists.txt, maar het is het meest flexibel. In ons geval noemen we onze map "components" omdat we twee nieuwe QML-componenten uit onze vorig Main.qml
-bestand creëren, en we bewaren de informatie daarover in kirigami-tutorial/src/components/CMakeLists.txt
. Het bestand Main.qml
zelf blijft in src/
zodat het automatisch net als voorheen word gebruikt als de uitvoerbare bestand opstarten.
Later is het mogelijk om meer mappen te creëren met meerdere QML-bestanden, allen gegroepeerd bij functie, zoals "models" en "settings", en C++ bestanden die types blootstellen aan QML (zoals modellen) kunnen waar het zinvol is samen met andere QML-bestanden worden gehouden.
We zullen in deze instructie deze structuur gebruiken.
CMake voorbereiden voor de nieuwe bestanden
Creëer eerst het bestand kirigami-tutorial/src/components/CMakeLists.txt
met de volgende inhoud:
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})
|
We creëren een nieuw doel kirigami-hello-components
genaamd en zetten het om naar een QML module met de hulp van ecm_add_qml_module() met de importnaam org.kde.tutorial.components
en voegen daar de relevante QML-bestanden toe.
Omdat het doel afwijkt van het uitvoerbarebestand, zal het functioneren als een andere QML module, zodat in dat geval we twee dingen moeten doen: er voor zorgen dat het code genereert met [GENERATE_PLUGIN_SOURCE] (https://api.kde.org/ecm/module/ECMQmlModule.html) zodat het werkt als een Qt plugin, en het afmaken met ecm_finalize_qml_module(). Daarna installeren we het op precies dezelfde manier als in de vorige lessen.
We moeten add_library() gebruiken zodat we kirigami-hello-components
kunnen koppelen met het uitvoerbare bestand in de target_link_libraries() aanroep call in 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)
|
We moeten ook add_subdirectory() gebruiken zodat CMake de kirigami-tutorial/src/components/
map kan vinden.
In de eerdere lessen, hoefden we de org.kde.tutorial
import naar onze Main.qml
toe te voegen omdat dit niet nodig was: omdat dat het startpunt voor het programma is, zal het uitvoerbare bestand dit bestand toch onmiddellijk uitvoeren. Omdat onze componenten in een separate QML module geplaatst zijn, is de nieuwe import in kirigami-tutorial/src/Main.qml
noodzakelijk, dezelfde eerder gedefinieerd, 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
// De rest van de code...
En we zijn klaar.
Main.qml opsplitsen
Laten we nog een keer kijken naar de originele 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
}
}
}
|
De eigengemaakte gedelegeerde met id: kountdownDelegate
kan compleet gesplitst worden omdat het al ingepakt is in een QML Component type. We gebruiken een Component zodat we het kunnen definiëren zonder dat het hoeven te creëren; separate QML-bestanden werken op dezelfde manier.
Als we de code in separate bestanden verplaatsen, dan is er geen reden om ze ingepakt in een Component te laten: we kunnen gewoon de Kirigami.AbstractCard in het separate bestand uitsplitsen. Hier is het resulterende 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")
}
}
}
}
|
Onze dialoogvenster met id: addDialog
is niet ingepakt in een Component, en het is niet een component dat standaard zichtbaar is, zodat de code zoals het gekopieerd kan worden naar 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();
}
}
|
Nu de code is uitgesplitst, wordt Main.qml
dus veel korter:
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 {}
}
}
}
|
We hebben nu twee extra QML-bestanden, AddDialog.qml
en KountdownDelegate
, en we moeten een manier vinden om ze in Main.qml
te gebruiken. De manier om de inhoud van de nieuwe bestanden aan Main.qml
toe te voegen is door ze aan te roepen.
AddDialog.qml
wordt AddDialog {}
:
31
32
33
| AddDialog {
id: addDialog
}
|
KountdownDelegate.qml
wordt KountdownDelegate {}
:
47
48
49
50
51
| Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: KountdownDelegate {}
}
|
De meeste gevallen die u heeft gezien van een component die startte met een hoofdletter gevolgd door haakjes waren aanroepingen van een QML component. Dit is waarom onze nieuwe QML-bestanden met een hoofdletter moeten beginnen.
Compileer het project en start het op, en u zou een werkend venster moeten hebben dat zich precies hetzelfde gedraagt als voorheen, maar met de code uitgesplitst in separate stukken, zodat het veel beheersbaar is.