Använda separata filer
Separera tungrodd kod i olika filer, och koppla signaler till dina komponenter.
Varför och hur
För första gången separerar vi vissa av våra komponenter i sina egna QML-filer. Om vi fortsätter att lägga till saker i Main.qml
, blir det snart svårt att säga vad som gör vad, och vi riskerar att trassla till vår kod.
I den här handledningen delar vi upp koden i Main.qml
i Main.qml
, AddDialog.qml
och KountdownDelegate.qml
.
Dessutom, även när kod sprids mellan flera QML-filer, kan antalet filer i verkliga projekt gå överstyr. En vanlig lösning på problemet är att separera filer logiskt i olika kataloger. Vi tar en snabb titt på tre vanliga tillvägagångssätt i verkliga projekt och implementera ett av dem:
- lagra QML-filer tillsammans med C++ filer
- lagra QML-filer i en annan katalog under samma modul
- lagra QML-filer i en annan katalog under en annan modul
Efter uppdelningen har vi inkapsling mellan varje fil, och implementeringsdetaljer blir abstraherade, vilket gör koden mer läsbar.
Lagra QML-filer tillsammans med C++ filer
Det består av att låta projektets QML-filer vara tillsammans med C++-filer i src/
. Den här typen av struktur skulle se ut så här:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Det är vad vi gjorde tidigare. I ovanstående fall skulle man bara behöva fortsätta lägga till QML-filer i den befintliga kirigami-tutorial/src/CMakeLists.txt
. Det finns ingen logisk separation alls, och när projektet väl får fler än ett par QML-filer (och C++-filer som skapar typer som ska användas i QML), kan katalogen snabbt bli full.
Lagra QML-filer i en annan katalog under samma modul
Det består av att hålla alla QML-filer i en separat katalog, vanligtvis src/qml/
. Den här typen av struktur skulle se ut så här:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
└── qml/
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Strukturen är mycket vanlig i KDE-projekt, mest för att undvika att ha en extra CMakeLists.txt-fil för katalogen src/qml/
och skapa en separat modul. Metoden har själva filerna i en separat katalog, men man måste också lägga till dem i kirigami-tutorial/src/CMakeLists.txt
. Alla skapade QML-filer tillhör då samma QML-modul som Main.qml
.
I praktiken, när projektet väl får mer än ett dussintal QML-filer, även om det inte skapar trängsel i katalogen src/
, leder det till trängsel i filen src/CMakeLists.txt
. Det blir svårt att skilja mellan traditionella C++ filer och C++ filer som har typer exponerade för QML.
Det bryter också mot begreppet lokalitet (lokalisering av beroendedetaljer), där man håller beskrivningen av beroenden på samma plats som beroendena själva.
Lagra QML-filer i en annan katalog under en annan modul
Det består av att hålla alla QML-filer i en separat katalog med sin egen CMakeLists.txt och en egen separat QML-modul. Den här typen av struktur skulle se ut så här:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── components/
├── CMakeLists.txt
├── AddDialog.qml
└── KountdownDelegate.qml
Strukturen är inte lika vanlig i KDE-projekt och kräver att man skriver ytterligare en CMakeLists.txt, men den är den mest flexibla. I vårt fall kallar vi vår katalog "components" eftersom vi skapar två nya QML-komponenter från vår tidigare Main.qml
, och behåller information om dem i kirigami-tutorial/src/components/CMakeLists.txt
. Själva filen Main.qml
förblir i src/
så den används automatiskt när den körbara filen körs, som tidigare.
Senare vore det möjligt att skapa fler kataloger med flera QML-filer, alla grupperade enligt funktion, såsom "modeller" och "inställningar", och C++ filer som har typer exponerade i QML (som modeller) skulle kunna vara tillsammans med andra QML-filer när det är vettigt.
Vi använder den här strukturen i handledningen.
Förbereda CMake för de nya filerna
Skapa först filen kirigami-tutorial/src/components/CMakeLists.txt
med följande innehåll:
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})
|
Vi skapar ett nytt mål som heter kirigami-hello-components
och gör det sedan till en QML-modul med hjälp av ecm_add_qml_module() under importnamnet org.kde.tutorial.components
och lägger till relevanta QML-filer.
Eftersom målet skiljer sig från den körbara filen fungerar det som en annan QML-modul, vilket leder till att vi måste göra två saker: få det att generera kod för att det ska fungera som ett Qt-insticksprogram med [GENERATE_PLUGIN_SOURCE](https: //api.kde.org/ecm/module/ECMQmlModule.html), och slutföra det med ecm_finalize_qml_module(). Vi installerar det sedan precis som i tidigare lektioner.
Vi behövde använda add_library() så att vi kan länka kirigami-hello-components
till den körbara filen med anropet target_link_libraries( ) i 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)
|
Vi behöver också använda add_subdirectory() så CMake hittar katalogen kirigami-tutorial/src/components/
.
I de tidigare lektionerna behövde vi inte lägga till import av org.kde.tutorial
i vår Main.qml
, beroende på att det inte behövdes. Eftersom det är programmets startpunkten startades den körbara filen omedelbart ändå. Nu när våra komponenter finns i en separat QML-modul, blir en ny import nödvändig i kirigami-tutorial/src/Main.qml
, likadan som definierats tidigare, 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
// Resten av koden ...
Och vi är redo att starta.
Dela Main.qml
Låt oss återigen ta en titt på den ursprungliga 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
}
}
}
|
Den anpassade delegaten med id: kountdownDelegate
kan delas helt eftersom den redan är inkapslad i en QML Component typ. Vi använder en komponent för att kunna definiera den utan att behöva instansiera den. Separata QML-filer fungerar på samma sätt.
Om vi flyttar koden till en separat fil, är det ingen mening att lämna den inkapslad i en komponent: vi kan bara dela upp Kirigami.AbstractCard i den separata filen. Här är den resulterande 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")
}
}
}
}
|
Vår dialogruta med id: addDialog
är inte inkapslad i en komponent, och det är inte en komponent som är normalt synlig, så koden kan kopieras som den är till 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();
}
}
|
Med den delade koden blir Main.qml
således mycket kortare:
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 {}
}
}
}
|
Vi har nu två extra QML-filer, AddDialog.qml
och KountdownDelegate
, och vi måste hitta något sätt att använda dem i Main.qml
. Sättet att lägga till innehållet i de nya filerna i Main.qml
är genom att instansiera dem.
AddDialog.qml
blir AddDialog {}
:
31
32
33
| AddDialog {
id: addDialog
}
|
KountdownDelegate.qml
blir KountdownDelegate {}
:
47
48
49
50
51
| Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: KountdownDelegate {}
}
|
De flesta fall du har sett av en komponent som inleds med stor bokstav och följs av parenteser var instansieringar av en QML-komponent. Det är därför våra nya QML-filer måste börja med en stor bokstav.
Kompilera projektet och kör det, så bör ett funktionellt fönster som beter sig exakt som tidigare visas, men med koden uppdelad i separata delar, vilket gör saker och ting mycket mer hanterbara.