Uzante apartajn dosierojn

Disigante neoportunan kodon en malsamajn dosierojn, kaj aligu signalojn al viaj komponantoj.

Kial kaj kiel

Por la unua fojo, ni apartigos iujn niajn komponantojn en siajn proprajn QML-dosierojn. Se ni daŭre aldonas aferojn al Main.qml, rapide fariĝos malfacile diri kio faras kion, kaj ni riskas ŝlimigi nian kodon.

In this tutorial, we will be splitting the code in Main.qml into Main.qml, AddDialog.qml and KountdownDelegate.qml.

Aldone, eĉ dum disvastigo de kodo inter pluraj QML-dosieroj, la kvanto de dosieroj en realaj projektoj povas malaperi. Ofta solvo al ĉi tiu problemo estas logike apartigi dosierojn en malsamajn dosierujojn. Ni rigardos tri oftajn alirojn viditajn en realaj projektoj, kaj efektivigos unu el ili:

  • konservante QML-dosierojn kune kun C++-dosieroj
  • konservante QML-dosierojn en malsama dosierujo sub la sama modulo
  • konservante QML-dosierojn en malsama dosierujo sub malsama modulo

Post la disigo, ni havos disigo de zorgoj inter ĉiu dosiero, kaj [efektivigaj detaloj estos abstraktitaj](https://en.wikipedia.org/ wiki/Abstraction_(komputiko_scienco)), farante la kodon pli legebla.

Konservante QML-dosierojn kune kun C++-dosieroj

Ĉi tio konsistas el konservi la QML-dosierojn de la projekto kune kun C++-dosieroj en src/. Tia strukturo aspektus jene:

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    ├── Main.qml
    ├── AddDialog.qml
    └── KountdownDelegate.qml

Jen kion ni faris antaŭe. En la ĉi-supra kazo, vi nur bezonus daŭre aldoni QML-dosierojn al la ekzistanta kirigami-tutorial/src/CMakeLists.txt. Tute ne ekzistas logika disiĝo, kaj post kiam la projekto ricevas pli ol kelkajn QML-dosierojn (kaj C++-dosieroj, kiuj kreas tipojn por esti uzataj en QML), la dosierujo povas rapide pleniĝi.

Konservante QML-dosierojn en malsama dosierujo sub la sama modulo

Ĉi tio konsistas el konservi ĉiujn QML-dosierojn en aparta dosierujo, kutime src/qml/. Tia strukturo aspektus jene:

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    └── qml/
        ├── Main.qml
        ├── AddDialog.qml
        └── KountdownDelegate.qml

Ĉi tiu strukturo estas tre ofta en KDE-projektoj, plejparte por eviti havi kroman CMakeLists.txt-dosieron por la dosierujo src/qml/ kaj krei apartan modulon. Ĉi tiu metodo konservas la dosierojn mem en aparta dosierujo, sed vi ankaŭ bezonus aldoni ilin en kirigami-tutorial/src/CMakeLists.txt. Ĉiuj kreitaj QML-dosieroj tiam apartenus al la sama QML-modulo kiel Main.qml.

En praktiko, post kiam la projekto ricevas pli ol dekduon QML-dosierojn, dum ĝi ne plenigos la dosierujon src/, ĝi plenigos la dosieron src/CMakeLists.txt. Malfaciliĝos diferenci inter tradiciaj C++-dosieroj kaj C++-dosieroj, kiuj havas tipojn elmontritajn al QML.

Ĝi ankaŭ rompos la koncepton de loko (lokigo de dependecaj detaloj), kie vi konservus la priskribon de viaj dependecoj en la sama loko kiel la dependecoj mem.

Konservi QML-dosierojn en malsama dosierujo sub malsama modulo

Ĉi tio konsistas el konservi ĉiujn QML-dosierojn en aparta dosierujo kun sia propra CMakeLists.txt kaj propra aparta QML-modulo. Tia strukturo aspektus jene:

kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
    ├── CMakeLists.txt
    ├── main.cpp
    ├── Main.qml
    └── components/
        ├── CMakeLists.txt
        ├── AddDialog.qml
        └── KountdownDelegate.qml

Ĉi tiu strukturo ne estas tiel ofta en KDE-projektoj kaj postulas verki plian CMakeLists.txt, sed ĝi estas la plej fleksebla. En nia kazo, ni nomas nian dosierujon "components" ĉar ni kreas du novajn QML-komponentojn el nia antaŭa dosiero Main.qml, kaj konservas informojn pri ili en kirigami-tutorial/src/components/CMakeLists.txt. . La dosiero Main.qml mem restas en src/ do ĝi estas aŭtomate uzata dum rulado de la rulebla, kiel antaŭe.

Pli poste, eblus krei pliajn dosierujojn kun pluraj QML-dosieroj, ĉiuj grupigitaj laŭ funkcio, kiel "modeloj" kaj "agordoj", kaj C++-dosieroj kiuj havas tipojn elmontritajn al QML (kiel modeloj) povus. estu konservita kune kun aliaj QML-dosieroj kie ĝi havas sencon.

Ni uzos ĉi tiun strukturon en ĉi tiu lernilo.

Preparante CMake por la novaj dosieroj

Unue, kreu la dosieron kirigami-tutorial/src/components/CMakeLists.txt kun la jena enhavo:

 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})

Ni kreas novan celon nomitan kirigami-hello-components kaj poste turnas ĝin en QML-modulon uzante ecm_add_qml_module() sub la importnomo. org.kde.tutorial.components kaj aldonu la koncernajn QML-dosierojn.

Ĉar la celo estas diferenca de la rulebla, ĝi funkcios kiel malsama QML-modulo, en kiu kazo ni devos fari du aferojn: fari ĝin generi kodon por ke ĝi funkciu kiel Qt-kromaĵo kun [GENERATE_PLUGIN_SOURCE](https: //api.kde.org/ecm/module/ECMQmlModule.html), kaj finaligu ĝin per ecm_finalize_qml_module(). Ni tiam instalas ĝin ĝuste kiel en antaŭaj lecionoj.

Ni bezonis uzi add_library() por ke ni povu ligi kirigami-hello-components al la plenumebla en la target_link_libraries( ) voku en 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)

Ni ankaŭ devas uzi add_subdirectory() do CMake trovos la dosierujon kirigami-tutorial/src/components/.

En la antaŭaj lecionoj, ni ne bezonis aldoni la importon org.kde.tutorial al nia Main.qml ĉar ĝi ne estis bezonata: estante la enirpunkto por la aplikaĵo, la efektivigebla ĉiuokaze rulus la dosieron tuj. Ĉar niaj komponantoj estas en aparta QML-modulo, la nova importo en kirigami-tutorial/src/Main.qml estas necesa, la sama difinita antaŭe, 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 resto de la kodo...

Kaj ni estas pretaj iri.

Spliting Main.qml

Ni rigardu denove la originalan 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
        }
    }
}

La propra delegito kun id: kountdownDelegate povas esti tute dividita ĉar ĝi jam estas envolvita en QML Component-tipo. Ni uzas Komponaĵon por povi difini ĝin sen bezoni instantiigi ĝin; apartaj QML-dosieroj funkcias same.

Se ni movas la kodon al apartaj dosieroj, do ne utilas lasi ĝin envolvita en Komponento: ni povas dividi nur la Kirigami.AbstractCard en la aparta dosiero. Jen la rezulta 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")
            }
        }
    }
}

Our dialog with id: addDialog is not enveloped in a Component, and it is not a component that is visible by default, so the code can be copied as is into the 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();
    }
}

Kun la koddivido, Main.qml tiel fariĝas multe pli mallonga:

 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 now have two extra QML files, AddDialog.qml and KountdownDelegate, and we need to find some way of using them in Main.qml. The way to add the contents of the new files to Main.qml is by instantiating them.

AddDialog.qml becomes AddDialog {}:

31
32
33
    AddDialog {
        id: addDialog
    }

KountdownDelegate.qml fariĝas KountdownDelegate {}:

47
48
49
50
51
        Kirigami.CardsListView {
            id: cardsView
            model: kountdownModel
            delegate: KountdownDelegate {}
        }

Plej multaj kazoj, kiujn vi vidis de komponanto komencita per majuskla kaj sekvita per krampoj, estis instancoj de QML-komponento. Jen kial niaj novaj QML-dosieroj devas komenci per majuskla litero.

Kompilu la projekton kaj rulu ĝin, kaj vi devus havi funkcian fenestron kiu kondutas ekzakte same kiel antaŭe, sed kun la kodo dividita en apartajn partojn, farante aferojn multe pli regeblaj.