Skip to main content
Passa al contenuto

Utilizzo di file separati in un progetto C++

Separare il codice ingombrante in file diversi e collegare i segnali ai componenti.

Perché e come

Per la prima volta separeremo alcuni dei nostri componenti nei loro file QML: continuando ad aggiungere delle cose al main.qml, presto sarà difficile dire chi fa cosa. Rischiamo quindi di sporcare il codice.

In questo tutorial, divideremo il codice in "Main.qml" in "Main.qml", "AddDialog.qml" e "KountdownDelegate.qml".

Inoltre, anche quando si distribuisce il codice tra più file QML, la quantità di file nei progetti reali può sfuggire di mano. Una soluzione comune a questo problema è separare logicamente i file in cartelle diverse. Daremo un breve sguardo a tre approcci comuni osservati nei progetti reali e ne implementeremo uno:

  • memorizzare file QML insieme a file C++
  • memorizzare file QML in una directory diversa nello stesso modulo
  • memorizzare file QML in una directory diversa in un modulo diverso

Dopo la divisione, avremo separazione delle preoccupazioni tra ciascun file e i dettagli di implementazione verranno astratti, rendendo il codice più leggibile.

Memorizzazione di file QML insieme a file C++

Ciò consiste nel mantenere i file QML del progetto insieme ai file C++ in src/. Questo tipo di struttura sarebbe simile a questa:

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

Questo è quello che abbiamo fatto in precedenza. Nel caso precedente, dovresti semplicemente continuare ad aggiungere file QML al kirigami-tutorial/src/CMakeLists.txt esistente. Non esiste alcuna separazione logica e una volta che il progetto ottiene più di un paio di file QML (e file C++ che creano tipi da utilizzare in QML), la cartella può diventare rapidamente affollata.

Memorizzazione dei file QML in una directory diversa nello stesso modulo

Ciò consiste nel conservare tutti i file QML in una cartella separata, solitamente src/qml/. Questo tipo di struttura sarebbe simile a questa:

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

Questa struttura è molto comune nei progetti KDE, soprattutto per evitare di avere un file CMakeLists.txt aggiuntivo per la directory src/qml/ e creare un modulo separato. Questo metodo mantiene i file stessi in una cartella separata, ma dovresti anche aggiungerli in kirigami-tutorial/src/CMakeLists.txt. Tutti i file QML creati apparterrebbero quindi allo stesso modulo QML di "Main.qml".

In pratica, una volta che il progetto ottiene più di una dozzina di file QML, anche se non affollerà la directory src/, affollerà il file src/CMakeLists.txt. Diventerà difficile distinguere tra file C++ tradizionali e file C++ con tipi esposti a QML.

Inoltre romperà il concetto di località (localizzazione dei dettagli delle dipendenze), in cui manterrai la descrizione delle tue dipendenze nello stesso posto delle dipendenze stesse.

Memorizzazione di file QML in una directory diversa in un modulo diverso

Ciò consiste nel mantenere tutti i file QML in una cartella separata con il proprio CMakeLists.txt e il proprio modulo QML separato. Questo tipo di struttura sarebbe simile a questa:

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

Questa struttura non è così comune nei progetti KDE e richiede la scrittura di un CMakeLists.txt aggiuntivo, ma è la più flessibile. Nel nostro caso, chiamiamo la nostra cartella "components" poiché stiamo creando due nuovi componenti QML dal nostro precedente file "Main.qml" e conserviamo le informazioni su di essi in "kirigami-tutorial/src/components/CMakeLists.txt". Il file Main.qml stesso rimane in src/ quindi viene utilizzato automaticamente durante l'esecuzione dell'eseguibile, come prima.

Successivamente, sarebbe possibile creare più cartelle con più file QML, tutti raggruppati insieme per funzione, come "modelli" e "impostazioni", e i file C++ che hanno tipi esposti a QML (come i modelli) potrebbero essere conservati insieme ad altri file QML dove ha senso.

Utilizzeremo questa struttura in questo tutorial.

Preparazione CMake per i nuovi file

Innanzitutto, crea il file kirigami-tutorial/src/components/CMakeLists.txt con il seguente contenuto:

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

Creiamo un nuovo target chiamato kirigami-hello-components e poi lo trasformiamo in un modulo QML utilizzando ecm_add_qml_module() con il nome di importazione org.kde.tutorial.components e aggiungiamo i file QML rilevanti.

Poiché il target è diverso dall'eseguibile, funzionerà come un modulo QML diverso, nel qual caso dovremo fare due cose: fargli generare codice affinché funzioni come plugin Qt con GENERATE_PLUGIN_SOURCE, e finalizzarlo con ecm_finalize_qml_module(). Lo installiamo quindi esattamente come nelle lezioni precedenti.

Avevamo bisogno di usare add_library() in modo da poter collegare kirigami-hello-components all'eseguibile nella chiamata target_link_libraries() 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)

Dobbiamo anche usare add_subdirectory() in modo che CMake trovi la directory kirigami-tutorial/src/components/.

Nelle lezioni precedenti non avevamo bisogno di aggiungere l'importazione org.kde.tutorial al nostro Main.qml perché non era necessaria: essendo il punto di ingresso dell'applicazione, l'eseguibile eseguiva comunque immediatamente il file. Dato che i nostri componenti si trovano in un modulo QML separato, è necessaria una nuova importazione in kirigami-tutorial/src/Main.qml, la stessa definita in precedenza, 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

// Il resto del codice...

E siamo pronti a partire.

Divisione Main.qml

Diamo un'occhiata ancora una volta al Main.qml originale:

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

Il delegato personalizzato con id: kountdownDelegate può essere diviso completamente perché è già racchiuso in un tipo componente QML. Usiamo un Componente per poterlo definire senza bisogno di istanziarlo; file QML separati funzionano allo stesso modo.

Se spostiamo il codice in un file separato, allora, non ha senso lasciarlo racchiuso in un Componente: possiamo dividere solo la Kirigami.AbstractCard nel file separato. Ecco il risultato 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")
            }
        }
    }
}

La nostra finestra di dialogo con id: addDialog non è racchiusa in un componente e non è un componente visibile per impostazione predefinita, quindi il codice può essere copiato così com'è in 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();
    }
}

Con la suddivisione del codice, Main.qml diventa quindi molto più breve:

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

Ora abbiamo due file QML aggiuntivi, AddDialog.qml e KountdownDelegate, e dobbiamo trovare un modo per usarli in Main.qml. Il modo per aggiungere il contenuto dei nuovi file a Main.qml è istanziarli.

AddDialog.qml diventa AddDialog {}:

31
32
33
    AddDialog {
        id: addDialog
    }

KountdownDelegate.qml diventa KountdownDelegate {}:

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

La maggior parte dei casi che hai visto di un componente iniziato con la lettera maiuscola e seguito da parentesi erano istanze di un componente QML. Questo è il motivo per cui i nostri nuovi file QML devono iniziare con una lettera maiuscola.

Compila il progetto ed eseguilo e dovresti avere una finestra funzionale che si comporta esattamente come prima, ma con il codice suddiviso in parti separate, rendendo le cose molto più gestibili.