Skip to main content
Skip to content

Prekrývajúce listy

Rozdelenie nepraktického kódu do rôznych súborov a pripojenie signálov k vašim komponentom.

Prečo a ako

Prvýkrát budeme oddeľovať niektoré z našich komponentov do vlastných QML súborov. Ak budeme stále pridávať veci do Main.qml, rýchlo bude ťažké rozoznať, čo robí čo, a riskujeme zneprehľadnenie nášho kódu.

V tomto tutoriáli rozdelíme kód v Main.qml do Main.qml, AddDialog.qml a KountdownDelegate.qml.

Navyše, aj keď rozdelíte kód medzi viaceré QML súbory, množstvo súborov v reálnych projektoch sa môže vymknúť spod kontroly. Bežným riešením tohto problému je logické oddelenie súborov do rôznych priečinkov. Stručne sa pozrieme na tri bežné prístupy videné v reálnych projektoch a jeden z nich implementujeme:

  • ukladanie súborov QML spolu so súbormi C++
  • ukladanie súborov QML do iného priečinka pod rovnakým modulom
  • ukladanie súborov QML do iného priečinka pod iným modulom

Po rozdelení budeme mať oddelenie zodpovedností medzi každým súborom a implementačné detaily budú abstrahované, čím bude kód čitateľnejší.

Ukladanie súborov QML spolu so súbormi C++

Toto spočíva v ponechaní QML súborov projektu spolu s C++ súbormi v src/. Tento druh štruktúry by vyzeral takto:

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

Toto sme robili predtým. V uvedenom prípade by ste jednoducho museli pokračovať v pridávaní QML súborov do existujúceho kirigami-tutorial/src/CMakeLists.txt. Neexistuje žiadne logické oddelenie a keď projekt získa viac ako pár QML súborov (a C++ súborov, ktoré vytvárajú typy na použitie v QML), priečinok sa môže rýchlo preplniť.

Ukladanie súborov QML do iného priečinka pod rovnakým modulom

Toto spočíva v ponechaní všetkých QML súborov v samostatnom priečinku, zvyčajne src/qml/. Tento druh štruktúry by vyzeral takto:

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

Táto štruktúra je veľmi bežná v projektoch KDE, hlavne na vyhnutie sa potrebe mať extra súbor CMakeLists.txt pre adresár src/qml/ a vytvoreniu samostatného modulu. Táto metóda uchováva samotné súbory v samostatnom priečinku, ale museli by ste ich tiež pridať v kirigami-tutorial/src/CMakeLists.txt. Všetky vytvorené QML súbory by potom patrili do rovnakého QML modulu ako Main.qml.

V praxi, keď projekt získa viac ako tucet QML súborov, aj keď nepreplní adresár src/, preplní súbor src/CMakeLists.txt. Bude ťažké rozlíšiť medzi tradičnými C++ súbormi a C++ súbormi, ktoré majú typy vystavené do QML.

Tiež to naruší koncept lokality (lokalizácia detailov závislostí), kde by ste udržiavali popis vašich závislostí na rovnakom mieste ako samotné závislosti.

Ukladanie súborov QML do iného priečinka pod iným modulom

Toto spočíva v ponechaní všetkých QML súborov v samostatnom priečinku s vlastným CMakeLists.txt a vlastným samostatným QML modulom. Tento druh štruktúry by vyzeral takto:

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

Táto štruktúra nie je tak bežná v projektoch KDE a vyžaduje napísanie dodatočného CMakeLists.txt, ale je najflexibilnejšia. V našom prípade náš priečinok nazývame "components", pretože vytvárame dva nové QML komponenty z nášho predchádzajúceho súboru Main.qml, a informácie o nich uchovávame v kirigami-tutorial/src/components/CMakeLists.txt. Samotný súbor Main.qml zostáva v src/, aby bol automaticky použitý pri spustení spustiteľného súboru, ako predtým.

Neskôr by bolo možné vytvoriť viac priečinkov s viacerými QML súbormi, všetky zoskupené podľa funkcie, ako napríklad "models" a "settings", a C++ súbory, ktoré majú typy vystavené do QML (ako modely), by mohli byť uchovávané spolu s inými QML súbormi, kde to dáva zmysel.

V tomto tutoriáli budeme používať túto štruktúru.

Prekrývajúce listy

Najprv vytvorte súbor kirigami-tutorial/src/components/CMakeLists.txt s nasledujúcim obsahom:

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

Vytvoríme nový cieľ s názvom kirigami-hello-components a potom ho premeníme na modul QML pomocou ecm_add_qml_module() pod importným názvom org.kde.tutorial.components a pridáme príslušné súbory QML.

Pretože cieľ je odlišný od spustiteľného súboru, bude fungovať ako iný QML modul, v takom prípade musíme urobiť dve veci: nechať ho generovať kód, aby fungoval ako Qt plugin pomocou GENERATE_PLUGIN_SOURCE, a finalizovať ho pomocou ecm_finalize_qml_module(). Potom ho nainštalujeme presne ako v predchádzajúcich lekciách.

Museli sme použiť add_library(), aby sme mohli prepojiť kirigami-hello-components so spustiteľným súborom vo volaní target_link_libraries() 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
33
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
    KF6::IconThemes
    kirigami-hello-components
)

install(TARGETS kirigami-hello ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})

add_subdirectory(components)

Tiež musíme použiť add_subdirectory(), aby CMake našiel adresár kirigami-tutorial/src/components/.

V predchádzajúcich lekciách sme nemuseli pridávať import org.kde.tutorial do nášho Main.qml, pretože to nebolo potrebné: keďže bol vstupným bodom aplikácie, spustiteľný súbor by súbor aj tak okamžite spustil. Keďže naše komponenty sú v samostatnom QML module, je potrebný nový import v kirigami-tutorial/src/Main.qml, rovnaký ako ten definovaný skôr, 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

// Zvyšok kódu...

A sme pripravení.

Rozdelenie Main.qml

Pozrime sa ešte raz na pôvodný 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
        }
    }
}

Vlastný delegát s id: kountdownDelegate sa dá úplne oddeliť, pretože je už obalený v type QML Component. Component používame, aby sme ho mohli definovať bez potreby jeho inštancovania; samostatné QML súbory fungujú rovnakým spôsobom.

Ak presunieme kód do samostatného súboru, potom nemá zmysel ponechať ho obalený v Component: môžeme oddeliť len Kirigami.AbstractCard do samostatného súboru. Tu je výsledný 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")
            }
        }
    }
}

Náš dialóg s id: addDialog nie je obalený v Component a nie je to komponent, ktorý je predvolene viditeľný, takže kód sa dá skopírovať tak, ako je, do 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();
    }
}

S rozdelením kódu sa Main.qml stáva oveľa kratším:

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

Teraz máme dva extra QML súbory, AddDialog.qml a KountdownDelegate, a musíme nájsť spôsob, ako ich použiť v Main.qml. Spôsob, ako pridať obsah nových súborov do Main.qml, je ich inštancovaním.

AddDialog.qml sa stáva AddDialog {}:

31
32
33
    AddDialog {
        id: addDialog
    }

KountdownDelegate.qml sa stáva KountdownDelegate {}:

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

Väčšina prípadov, keď ste videli komponent začínajúci veľkým písmenom nasledovaný zátvorkami, boli inštancie QML komponentu. Preto musia naše nové QML súbory začínať veľkým písmenom.

Skompilujte projekt a spustite ho a mali by ste mať funkčné okno, ktoré sa správa presne rovnako ako predtým, ale s kódom rozdeleným do samostatných častí, čím sa veci stávajú omnoho spravovateľnejšie.