Skip to main content
Ir para o conteúdo

Usando arquivos separados em um projeto C++

Separando código pesado em arquivos diferentes e anexando sinais aos seus componentes.

Por que e como

Pela primeira vez, separaremos alguns dos nossos componentes em seus próprios arquivos QML. Se continuarmos adicionando coisas ao Main.qml, rapidamente ficará difícil dizer o que faz o quê, e corremos o risco de confundir nosso código.

Neste tutorial, dividiremos o código em Main.qml em Main.qml, AddDialog.qml e KountdownDelegate.qml.

Além disso, mesmo ao distribuir código entre vários arquivos QML, a quantidade de arquivos em projetos reais pode ficar fora de controle. Uma solução comum para esse problema é separar os arquivos logicamente em pastas diferentes. Analisaremos brevemente três abordagens comuns em projetos reais e implementaremos uma delas:

  • armazenar arquivos QML junto com arquivos C++
  • armazenar arquivos QML em uma pasta diferente no mesmo módulo
  • armazenar arquivos QML em uma pasta diferente em um módulo diferente

Após a divisão, teremos separação de interesses entre cada arquivo, e os detalhes da implementação serão abstraídos, tornando o código mais legível.

Armazenar arquivos QML junto com arquivos C++

Isso consiste em manter os arquivos QML do projeto junto com os arquivos C++ em src/. Esse tipo de estrutura ficaria assim:

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

Foi o que fizemos anteriormente. No caso acima, você só precisaria continuar adicionando arquivos QML ao arquivo kirigami-tutorial/src/CMakeLists.txt existente. Não há separação lógica alguma, e quando o projeto recebe mais do que alguns arquivos QML (e arquivos C++ que criam tipos a serem usados ​​em QML), a pasta pode ficar rapidamente lotada.

Armazenar arquivos QML em uma pasta diferente no mesmo módulo

Isso consiste em manter todos os arquivos QML em uma pasta separada, geralmente src/qml/. Esse tipo de estrutura ficaria assim:

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

Essa estrutura é muito comum em projetos KDE, principalmente para evitar a necessidade de um arquivo CMakeLists.txt extra para o diretório src/qml/ e a criação de um módulo separado. Este método mantém os arquivos em uma pasta separada, mas você também precisa adicioná-los em kirigami-tutorial/src/CMakeLists.txt. Todos os arquivos QML criados pertencerão ao mesmo módulo QML que Main.qml.

Na prática, quando o projeto recebe mais de uma dúzia de arquivos QML, embora não ocupe a pasta src/, ocupará o arquivo src/CMakeLists.txt. Será difícil diferenciar entre arquivos C++ tradicionais e arquivos C++ que possuem tipos expostos ao QML.

Isso também quebrará o conceito de localidade (localização de detalhes de dependência), onde você manteria a descrição de suas dependências no mesmo lugar que as próprias dependências.

Armazenar arquivos QML em uma pasta diferente em um módulo diferente

Isso consiste em manter todos os arquivos QML em uma pasta separada com seu próprio CMakeLists.txt e seu próprio módulo QML. Esse tipo de estrutura ficaria assim:

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

Essa estrutura não é tão comum em projetos KDE e requer a criação de um arquivo CMakeLists.txt adicional, mas é a mais flexível. No nosso caso, nomeamos nossa pasta "components", pois estamos criando dois novos componentes QML a partir do nosso arquivo anterior Main.qml e mantemos as informações sobre eles em kirigami-tutorial/src/components/CMakeLists.txt. O arquivo Main.qml permanece em src/, sendo usado automaticamente ao executar o executável, como antes.

Posteriormente, seria possível criar mais pastas com vários arquivos QML, todos agrupados por função, como "modelos" e "configurações", e arquivos C++ que tenham tipos expostos ao QML (como modelos) poderiam ser mantidos junto com outros arquivos QML quando fizer sentido.

Usaremos essa estrutura neste tutorial.

Preparando o CMake para os novos arquivos

Primeiro, crie o arquivo kirigami-tutorial/src/components/CMakeLists.txt com o seguinte conteúdo:

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

Criamos um novo alvo chamado kirigami-hello-components e o transformamos em um módulo QML usando ecm_add_qml_module() com o nome de importação org.kde.tutorial.components e adicionamos os arquivos QML relevantes.

Como o alvo é diferente do executável, ele funcionará como um módulo QML diferente. Nesse caso, precisaremos fazer duas coisas: fazê-lo gerar código para funcionar como um plugin Qt com GENERATE_PLUGIN_SOURCE e finalizá-lo com ecm_finalize_qml_module(). Em seguida, o instalaremos exatamente como nas lições anteriores.

Precisávamos usar add_library() para podermos vincular kirigami-hello-components ao executável na chamada target_link_libraries() em 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)

Também precisamos usar add_subdirectory() para que o CMake encontre o diretório kirigami-tutorial/src/components/.

Nas lições anteriores, não precisamos adicionar a importação org.kde.tutorial ao nosso Main.qml porque ela não era necessária: sendo o ponto de entrada para o aplicativo, o executável executaria o arquivo imediatamente de qualquer maneira. Como nossos componentes estão em um módulo QML separado, é necessária uma nova importação em kirigami-tutorial /src/Main.qml, a mesma definida anteriormente, 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

// O resto do código...

E estamos prontos para continuar.

Dividindo o Main.qml

Vamos dar uma olhada mais uma vez no Main.qml original:

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

O delegado personalizado com id: kountdownDelegate pode ser completamente dividido porque já está encapsulado em um tipo de Componente QML. Usamos um Componente para poder defini-lo sem precisar instanciá-lo; arquivos QML separados funcionam da mesma maneira.

Se movermos o código para arquivos separados, não há sentido em deixá-lo encapsulado em um Componente: podemos dividir apenas o Kirigami.AbstractCard no arquivo separado. Aqui está o KountdownDelegate.qml resultante:

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

Nosso diálogo com id: addDialog não está contido em um Componente e não é um componente visível por padrão, portanto, o código pode ser copiado como está para o 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();
    }
}

Com a divisão do código, Main.qml se torna muito mais curto:

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

Agora temos dois arquivos QML extras, AddDialog.qml e KountdownDelegate, e precisamos encontrar uma maneira de usá-los em Main.qml. A maneira de adicionar o conteúdo dos novos arquivos a Main.qml é instanciando-os.

AddDialog.qml se torna AddDialog {}:

31
32
33
    AddDialog {
        id: addDialog
    }

KountdownDelegate.qml se torna KountdownDelegate {}:

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

A maioria dos casos que você viu de um componente iniciado em letras maiúsculas e seguido por colchetes eram instanciações de um componente QML. É por isso que nossos novos arquivos QML precisam começar com uma letra maiúscula.

Compile o projeto e execute-o, e você deverá ter uma janela funcional que se comporta exatamente da mesma forma que antes, mas com o código dividido em partes separadas, tornando as coisas muito mais gerenciáveis.