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, AddEditDialog.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
    ├── AddEditDialog.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
        ├── AddEditDialog.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
        ├── AddEditDialog.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
    AddEditDialog.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
add_executable(kirigami-hello)

ecm_add_qml_module(kirigami-hello
    URI "org.kde.tutorial"
)

ecm_target_qml_sources(kirigami-hello
    SOURCES
    Main.qml
)

target_sources(kirigami-hello
    PRIVATE
    main.cpp
)

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(qml)

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
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:")
                // 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"
                // 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 AddEditDialog.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
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

    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
    }

    AddEditDialog {
        id: addEditDialog
    }

    pageStack.initialPage: Kirigami.ScrollablePage {
        title: i18nc("@title", "Kountdown")

        actions: [
            Kirigami.Action {
                id: addAction
                icon.name: "list-add"
                text: i18nc("@action:button", "Add kountdown")
                onTriggered: addEditDialog.open()
            }
        ]

        Kirigami.CardsListView {
            id: cardsView
            model: kountdownModel
            delegate: KountdownDelegate {}
        }
    }
}

Ara tenim dos fitxers QML addicionals, AddEditDialog.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.

AddEditDialog.qml esdevé AddEditDialog {}:

28
29
30
    AddEditDialog {
        id: addEditDialog
    }

KountdownDelegate.qml esdevé KountdownDelegate {}:

44
45
46
47
48
        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.