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.