Skip to main content
Перейти к содержимому

Использование отдельных файлов в проекте на 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 должны начинаться с прописной буквы.

Соберите проект и запустите его — должно появиться работающее окно, которое ведёт себя точно так же, как и раньше, но код разделён на отдельные части, что значительно упрощает работу.