Utilitzar fitxers separats
Separar el codi difícil de gestionar en diferents fitxers, i adjuntar senyals als components.
Per què i com
Per primera vegada, separarem alguns dels nostres components en els seus propis fitxers en QML. Si seguim afegint coses a Main.qml
, ràpidament serà difícil saber què fa què, i correm el risc d'enterbolir el nostre codi.
En aquesta guia d'aprenentatge, dividirem el codi de Main.qml
en Main.qml
, AddDialog.qml
i KountdownDelegate.qml
.
A més, fins i tot quan es dispersa el codi entre diversos fitxers QML, la quantitat de fitxers en projectes reals pot anar-se'n de les mans. Una solució habitual a aquest problema és separar lògicament fitxers en carpetes diferents. Farem una breu aproximació a tres enfocaments comuns vistos en projectes reals, i implementarem un d'ells:
- emmagatzematge de fitxers QML juntament amb fitxers C++
- emmagatzematge de fitxers en QML en un directori diferent dins del mateix mòdul
- emmagatzematge de fitxers QML en un directori diferent dins d'un mòdul diferent
Després de la divisió, tindrem separació de preocupacions entre cada fitxer, i els detalls d'implementació s'abstrauran, fent el codi més llegible.
Emmagatzematge de fitxers QML juntament amb fitxers C++
Consisteix a mantenir els fitxers QML del projecte juntament amb els fitxers C++ a src/
. Aquesta mena d'estructura es veuria així:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Això és el que vam fer anteriorment. En el cas anterior, només hauríeu de continuar afegint fitxers QML al kirigami-tutorial/src/CMakeLists.txt
existent. No hi ha cap separació lògica, i una vegada que el projecte obté més d'un parell de fitxers QML (i fitxers C++ que creen tipus que es faran servir en el QML), la carpeta es pot omplir ràpidament.
Emmagatzematge de fitxers en QML en un directori diferent dins del mateix mòdul
Consisteix a mantenir tots els fitxers QML en una carpeta separada, normalment src/qml/
. Aquesta mena d'estructura es veuria així:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
└── qml/
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Aquesta estructura és molt habitual en els projectes KDE, principalment per a evitar tenir un fitxer CMakeLists.txt extra per al directori src/qml/
i crear un mòdul separat. Aquest mètode manté els fitxers en una carpeta separada, però també hauríeu d'afegir-los a kirigami-tutorial/src/CMakeLists.txt
. Tots els fitxers QML creats pertanyen al mateix mòdul QML que Main.qml
.
A la pràctica, una vegada que el projecte té més d'una dotzena de fitxers QML, tot i que no s'omplirà el directori src/
, s'atapeirà el fitxer src/CMakeLists.txt
. Es farà difícil diferenciar entre fitxers C++ tradicionals i fitxers C++ que tenen tipus exposats al QML.
També trencarà el concepte de localitat (localització de detalls de dependència), on mantindríeu la descripció de les dependències en el mateix lloc que les pròpies dependències.
Emmagatzematge de fitxers QML en un directori diferent dins d'un mòdul diferent
Això consisteix a mantenir tots els fitxers QML en una carpeta separada amb el seu propi CMakeLists.txt i un mòdul QML propi separat. Aquesta mena d'estructura es veuria així:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── components/
├── CMakeLists.txt
├── AddDialog.qml
└── KountdownDelegate.qml
Aquesta estructura no és tan habitual en els projectes KDE i requereix escriure un CMakeLists.txt addicional, però és la més flexible. En el nostre cas, donem nom a la nostra carpeta «components» ja que estem creant dos components QML nous a partir del fitxer Main.qml
anterior, i mantenim la informació sobre ells a kirigami-tutorial/src/components/CMakeLists.txt
. El fitxer Main.qml
es manté a src/
de manera que s'utilitza automàticament quan s'executa l'executable, com abans.
Més tard, seria possible crear més carpetes amb diversos fitxers QML, tots agrupats per funcions, com ara «models» i «configuració», i els fitxers C++ que tenen tipus exposats al QML (com els models) es podrien mantenir juntament amb altres fitxers QML on tingui sentit.
Utilitzarem aquesta estructura en aquesta guia d'aprenentatge.
Preparar el CMake per als fitxers nous
Primer, creeu el fitxer kirigami-tutorial/src/components/CMakeLists.txt
amb el contingut següent:
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})
|
Creem un objectiu nou anomenat kirigami-hello-components
i després el convertim en un mòdul QML utilitzant ecm_add_qml_module() amb el nom d'importació org.kde.tutorial.components
i afegim els fitxers QML pertinents.
Com que l'objectiu és diferent de l'executable, funcionarà com un mòdul QML diferent, en aquest cas el necessitarem fer dues coses: fer que generi el codi perquè funcioni com a un connector Qt amb GENERATE_PLUGIN_SOURCE, i finalitzar-lo amb ecm_finalize_qml_module(). Després l'instal·lem exactament igual que en les lliçons anteriors.
Necessitàvem utilitzar add_library() perquè puguem enllaçar kirigami-hello-components
amb l'executable a la crida target_link_libraries() a 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)
|
També necessitem utilitzar add_subdirectory() perquè el CMake trobi el directori kirigami-tutorial/src/components/
.
En les lliçons anteriors, no calia afegir la importació org.kde.tutorial
al nostre Main.qml
perquè no era necessari: sent el punt d'entrada de l'aplicació, l'executable executaria el fitxer immediatament de totes maneres. Com que els nostres components es troben en un mòdul QML separat, és necessària una importació nova a kirigami-tutorial/src/Main.qml
, la mateixa definida anteriorment, 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
// La resta del codi...
I ja estem preparats per a continuar.
Divisió de Main.qml
Tornem a fer un cop d'ull a l'original 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
}
}
}
|
El delegat personalitzat amb id: kountdownDelegate
es pot dividir completament perquè ja està embolcallat en un
tipus de component QML
. Utilitzem un component per a poder definir-lo sense necessitat d'instanciar-lo; els fitxers QML separats funcionen de la mateixa manera.
Si movem el codi a fitxers separats, llavors no té sentit deixar-lo embolcallat en un component: podem dividir només la
Kirigami.AbstractCard
en el fitxer separat. Aquest és el KountdownDelegate.qml
resultant:
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")
}
}
}
}
|
El nostre diàleg amb id: addDialog
no està embolcallat en un component, i no és un component que sigui visible de manera predeterminada, així que el codi es pot copiar tal com està en el 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();
}
}
|
Amb la divisió del codi, Main.qml
es fa molt més curt:
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 {}
}
}
}
|
Ara tenim dos fitxers QML addicionals, AddDialog.qml
i KountdownDelegate
, i necessitem trobar alguna manera d'utilitzar-los a Main.qml
. La manera d'afegir el contingut dels fitxers nous a Main.qml
és instanciant-los.
AddDialog.qml
esdevé AddEditDialog {}
:
31
32
33
| AddDialog {
id: addDialog
}
|
KountdownDelegate.qml
esdevé KountdownDelegate {}
:
47
48
49
50
51
| Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: KountdownDelegate {}
}
|
La majoria dels casos que heu vist d'un component començat per majúscules i seguit de parèntesis eren instàncies d'un component QML. Per això els fitxers QML nous han de començar per una lletra en majúscula.
Compileu el projecte i executeu-lo, i hauríeu de tenir una finestra funcional que es comporti exactament igual que abans, però amb el codi dividit en parts separades, fent les coses molt més manejables.