Использование отдельных файлов в проекте на C++
Разделение громоздкого кода на разные файлы и подключение сигналов к компонентам.
Зачем и как
Впервые некоторые компоненты будут вынесены в отдельные файлы QML. Если продолжать добавлять всё в Main.qml, быстро станет трудно понять, что за что отвечает, и есть риск запутать код.
В этом руководстве код в Main.qml разделяется на Main.qml, AddDialog.qml и KountdownDelegate.qml.
Кроме того, даже при распределении кода между несколькими файлами QML количество файлов в реальных проектах может выйти из-под контроля. Обычное решение этой проблемы — логическое разделение файлов по разным папкам. Ниже кратко рассматриваются три распространённых подхода, встречающихся в реальных проектах, и реализуется один из них:
- Хранение файлов QML вместе с файлами C++
- Хранение файлов QML в другой папке в рамках одного модуля
- Хранение файлов QML в другой папке в составе другого модуля
После разбиения между каждым файлом будет разделение ответственности, а детали реализации будут абстрагированы, что сделает код более читаемым.
Хранение файлов QML вместе с файлами C++
Он заключается в том, чтобы хранить файлы QML проекта вместе с файлами C++ в src/. Такая структура выглядит так:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Это то, что было сделано ранее. В описанном выше случае достаточно продолжать добавлять файлы QML в существующий kirigami-tutorial/src/CMakeLists.txt. Логического разделения здесь нет вовсе, и как только в проекте становится больше пары файлов QML (а также файлов C++, создающих типы для использования в QML), папка быстро переполняется.
Хранение файлов QML в другой папке в рамках одного модуля
Это подразумевает хранение всех файлов QML в отдельной папке, обычно src/qml/. Такая структура выглядит так:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
└── qml/
├── Main.qml
├── AddDialog.qml
└── KountdownDelegate.qml
Такая структура очень распространена в проектах KDE, в основном чтобы избежать дополнительного файла CMakeLists.txt для папки src/qml/ и создания отдельного модуля. При этом способе файлы остаются в отдельной папке, но их также потребуется добавить в kirigami-tutorial/src/CMakeLists.txt. Тогда все созданные файлы QML будут относиться к тому же модулю QML, что и Main.qml.
На практике, как только в проекте становится больше десятка файлов QML, они не загромоздят папку src/, но загромоздят файл src/CMakeLists.txt. Станет сложно различать традиционные файлы C++ и файлы C++, в которых типы доступны для QML.
Это также нарушит принцип локальности (локальное размещение сведений о зависимостях), при котором описание зависимостей хранится в том же месте, что и сами зависимости.
Хранение файлов QML в другой папке другого модуля
Это подразумевает хранение всех файлов QML в отдельной папке с собственным CMakeLists.txt и отдельным модулем QML. Такая структура выглядит так:
kirigami-tutorial/
├── CMakeLists.txt
├── org.kde.tutorial.desktop
└── src/
├── CMakeLists.txt
├── main.cpp
├── Main.qml
└── components/
├── CMakeLists.txt
├── AddDialog.qml
└── KountdownDelegate.qml
Такая структура не так распространена в проектах KDE и требует написания дополнительного файла CMakeLists.txt, но при этом она наиболее гибкая. В нашем случае папка называется «components», поскольку из предыдущего файла Main.qml создаются два новых компонента QML, а сведения о них хранятся в kirigami-tutorial/src/components/CMakeLists.txt. Сам файл Main.qml остаётся в src/, поэтому, как и прежде, он автоматически используется при запуске исполняемого файла.
В дальнейшем можно будет создавать больше папок с несколькими файлами QML, сгруппированными по назначению, например «models» и «settings», а файлы C++ с типами, доступными для QML (например, модели), можно будет хранить вместе с другими файлами QML, где это целесообразно.
В этом руководстве будет использоваться эта структура.
Подготовка CMake для новых файлов
Сначала создайте файл kirigami-tutorial/src/components/CMakeLists.txt со следующим содержимым:
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})
|
Создаётся новая цель с именем kirigami-hello-components, которая затем преобразуется в модуль QML с помощью ecm_add_qml_module() под именем импорта org.kde.tutorial.components, и добавляются соответствующие файлы QML.
Поскольку цель отличается от исполняемого файла, она будет работать как другой QML-модуль. В этом случае потребуется выполнить два действия: заставить её генерировать код с помощью GENERATE_PLUGIN_SOURCE, чтобы она работала как модуль Qt, и завершить её с помощью ecm_finalize_qml_module(). Затем она устанавливается точно так же, как в предыдущих уроках.
Требовалось использовать add_library(), чтобы можно было связать kirigami-hello-components с исполняемым файлом в вызове 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)
|
Также необходимо использовать add_subdirectory(), чтобы CMake нашёл папку kirigami-tutorial/src/components/.
В предыдущих уроках не требовалось добавлять импорт org.kde.tutorial в Main.qml, поскольку он был не нужен: будучи точкой входа приложения, исполняемый файл всё равно сразу запускал бы этот файл. Так как компоненты находятся в отдельном модуле QML, в kirigami-tutorial/src/Main.qml необходимо добавить новый импорт — тот же, что был определён ранее, 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
// Остальная часть кода…
И можно приступать.
Разделение Main.qml
Вернёмся ещё раз к исходному файлу 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.accepted()
}
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
}
}
}
|
Пользовательский делегат с id: kountdownDelegate можно полностью разделить, поскольку он уже обёрнут в тип Component QML. Компонент применяется, чтобы определить его, не создавая экземпляр; отдельные файлы QML работают так же.
Если код перенесён в отдельные файлы, то нет смысла оставлять его внутри Component: в отдельный файл можно вынести только Kirigami.AbstractCard. Вот полученный 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")
}
}
}
}
|
Наш диалог с id: addDialog не заключён в Component и не является компонентом, видимым по умолчанию, поэтому код можно скопировать как есть в 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();
}
}
|
После разделения кода 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
| 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 {}
}
}
}
|
Теперь есть два дополнительных QML-файла, AddDialog.qml и KountdownDelegate, и нужно найти способ использовать их в Main.qml. Способ добавить содержимое новых файлов в Main.qml заключается в создании экземпляров этих файлов.
AddDialog.qml становится AddDialog {}:
31
32
33
| AddDialog {
id: addDialog
}
|
KountdownDelegate.qml становится KountdownDelegate {}:
47
48
49
50
51
| Kirigami.CardsListView {
id: cardsView
model: kountdownModel
delegate: KountdownDelegate {}
}
|
В большинстве случаев компонент, имя которого начиналось с прописной буквы и после которого стояли скобки, представлял собой экземпляр компонента QML. Поэтому имена новых файлов QML должны начинаться с прописной буквы.
Соберите проект и запустите его — должно появиться работающее окно, которое ведёт себя точно так же, как и раньше, но код разделён на отдельные части, что значительно упрощает работу.