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

Добавление диалога

Знакомство с диалогами Kirigami

Делаем приложение полезным

Окно есть, карточки есть, действия есть. Однако по-прежнему нужно найти способ ввода имени, описания и даты по своему выбору.

Один из способов сделать это — создать новую страницу, на которой размещаются необходимые элементы ввода. Однако целая страница, отведённая под ввод имени, описания и даты, кажется несколько избыточной.

Вместо этого будет использоваться диалог.

Диалог, появляющийся в центре приложения

Открытие диалога

pageStack.initialPage: Kirigami.ScrollablePage {
    // …
    actions: [
        Kirigami.Action {
            id: addAction
            icon.name: "list-add"
            text: i18nc("@action:button", "Add kountdown")
            onTriggered: addDialog.open()
        }
    ]
}

Сначала отредактируем действие из предыдущего руководства: это просто Kirigami.Action, вызывающее функцию open() диалога.

Диалог добавления обратных отсчётов

Добавляемый компонент — это Kirigami.Dialog. Диалоги появляются в центре окна и позволяют предоставить дополнительную информацию, относящуюся к текущему содержимому. Переместить их нельзя, но они подстраивают свой размер под окно.

Kirigami.ApplicationWindow {
    // …
    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*:")
                // Что делать после подтверждения ввода (то есть нажатия клавиши Enter). В этом
                // случае фокус перемещается на следующее поле.
                onAccepted: descriptionField.forceActiveFocus()
            }
            Controls.TextField {
                id: descriptionField
                Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
                placeholderText: i18n("Optional")
                // Снова фокус перемещается на следующее поле
                onAccepted: dateField.forceActiveFocus()
            }
            Controls.TextField {
                id: dateField
                Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
                // D означает обязательную цифру от 1 до 9, 9 означает обязательную цифру от 0 до 9
                inputMask: "D999-99-99"
                // Здесь операция подтверждается так же, как при нажатии кнопки «ОК»
                onAccepted: addDialog.accepted()
            }
            Controls.Label {
                text: "* = required fields"
            }
        }
        // Здесь размещается логика диалога
    }
    // …
}

По умолчанию диалоги имеют header и footer, оба унаследованы от Controls.Dialog.

Заголовок по умолчанию содержит title и кнопку закрытия, которую можно отключить с помощью showCloseButton. Нижний колонтитул по умолчанию не содержит кнопок. Её можно переопределить с помощью standardButtons.

Сначала задаётся показ кнопок «ОК» и «Отмена», добавляются отступы и разумное значение preferredWidth. Предпочитаемая ширина — это ожидаемый размер диалога по умолчанию, который при необходимости может увеличиваться. Можно использовать стандартные Kirigami.Units, которые будут рассмотрены позже.

Далее следует Kirigami.FormLayout. В отличие от ColumnLayout, расположение его дочерних компонентов задаётся автоматически и по центру, а подписи необязательны. Как следует из названия, он предназначен для создания форм ввода.

Эти компоновки форм предназначены для работы с различными типами ввода, однако здесь используются простые поля Controls.Textfield, предоставляющие простые текстовые поля для ввода.

Созданы элементы Textfield, которые выполняют роль:

  1. Поле ввода названия обратного отсчёта
  2. Поле ввода описания обратного отсчёта
  3. Поле ввода даты, до которой ведётся обратный отсчёт; дата должна быть указана в формате YYYY-MM-DD

В каждом из этих элементов Controls.Textfield задаётся свойство Kirigami.FormData.label, позволяющее определить для них надписи. Форма покажет правильные надписи слева от каждого из этих полей ввода текста.

Наконец, свойство onAccepted также задаётся для вызова метода forceActiveFocus() следующего поля; это переключит активное поле, когда пользователь нажмёт клавишу ENTER, что повышает удобство работы с формой.

Также для текстового поля даты задано свойство inputMask. Задание этому свойству значения D999-99-99 не позволяет пользователям вводить что-либо, что может нарушить работу приложения (например, текст), ограничивая ввод только цифрами, которые затем можно попытаться преобразовать в объект даты.

Когда пользовательский интерфейс диалога готов, необходимо изменить его поведение. Для этого потребуется три вещи:

  1. Показывать кнопку «ОК» только тогда, когда заполнены обязательные поля
  2. Добавлять вводимую информацию в модель
  3. Очищать форму ввода
Kirigami.Dialog {
    // ... После инициализации Kirigami.Dialog требуется создать пользовательскую привязку, чтобы
    // кнопка «ОК» показывалась только при заполнении обязательных текстовых полей. Для этого
    // используется Kirigami.Dialog.standardButton(button):
    Component.onCompleted: {
        const button = standardButton(Kirigami.Dialog.Ok);
        // () => — это стрелочная функция JavaScript
        button.enabled = Qt.binding( () => requiredFieldsFilled() );
    }
    onAccepted: {
        // Привязка создана, но ещё требуется сделать кнопку недоступной для нажатия, пока поля не
        // заполнены
        if (!addDialog.requiredFieldsFilled()) return;
        appendDataToModel();
        clearFieldsAndClose();
    }
}

Первое, что нужно сделать, — создать привязку между свойством enabled кнопки «ОК» и проверкой того, заполнены ли поля; в этом случае привязку нужно создать с помощью Qt.binding() в JavaScript. По сути, строка:

button.enabled = Qt.binding( () => requiredFieldsFilled() );

аналогично привязкам QML, рассмотренным ранее, например в следующем псевдокоде:

enabled: requiredFieldsFilled()

Обработчик сигнала, срабатывающий при нажатии кнопки «ОК», — это onAccepted. Если обязательные поля заполнены, он остаётся пустым и ничего не делает; в противном случае он добавит введённые данные в модель и очистит диалог при следующем открытии.

Kirigami.Dialog {
    // … Проверяется, что поле nameField не пустое и что поле dateField (для которого задана маска
    // ввода) заполнено полностью
    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();
    }
}

Для обязательного поля имени достаточно проверить, является ли текст поля пустой строкой. Для поля даты, поскольку у него есть маска ввода, вместо этого нужно использовать acceptableInput, которое становится истинным только после того, как поле заполнено целиком и содержит только допустимые символы.

Затем метод append() модели списка kountdownModel добавляет объект JavaScript, содержащий предоставленные свойства.

Наконец, текстовые поля очищаются присвоением их свойствам text пустой строки, после чего вызывается close().

После сохранения файлов и сборки приложения можно будет добавить собственные обратные отсчёты! Осталось внести последний штрих для улучшения интерфейса — удалить фиктивный обратный отсчёт из предыдущих руководств:

26
27
28
    ListModel {
        id: kountdownModel
    }

Во-вторых, теперь, когда есть настоящая дата, с которой можно работать, можно вычислить время до этой даты:

47
48
49
50
                    Kirigami.Heading {
                        level: 1
                        text: i18n("%1 days", Math.round((date-Date.now())/86400000))
                    }

И в-третьих, увеличьте размер окна, чтобы было больше места для новых карточек:

 9
10
    width: 600
    height: 400

Гораздо лучше.

Наше приложение на данный момент

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

Снимок экрана приложения с четырьмя примерами карточек