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