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.