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 alsMain.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.