Skip to main content
Skip to content

Akcie

Prekrývajúce listy

Urobenie našej aplikácie užitočnou

Máme okno, máme karty a máme akcie. No stále musíme nájsť nejaký spôsob zadávania mena, popisu a dátumu podľa nášho výberu.

Jedným spôsobom, ako to urobiť, by bolo vytvorenie novej stránky, kde umiestnime požadované vstupné prvky. Avšak celá stránka venovaná poskytovaniu mena, popisu a dátumu sa zdá trochu prehnané.

Namiesto toho budeme používať dialóg.

Dialog appearing in the middle of the application

Otvorenie dialógu

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

Najprv upravíme akciu z predchádzajúceho tutoriálu: len Kirigami.Action, ktorá spúšťa funkciu open() dialógu.

Dialógy na pridávanie odpočítavania

Nový komponent, ktorý pridávame, je Kirigami.Dialog. Dialógy sa zobrazujú v strede okna a dajú sa použiť na poskytnutie ďalších informácií relevantných pre aktuálny obsah. Nedajú sa presúvať, ale prispôsobujú svoju vlastnú veľkosť oknu.

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

        // Rozloženia formulárov pomáhajú zarovnať a štrukturovať rozloženie s viacerými vstupmi
        Kirigami.FormLayout {
            // Textové polia vám umožňujú zadávať text v tenkom textovom poli
            Controls.TextField {
                id: nameField
                // Poskytuje štítok pripojený k textovému poľu
                Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
                // Čo robiť po prijatí vstupu (t.j. stlačení Enter). V tomto prípade presunie
                // zameranie na ďalšie pole
                onAccepted: descriptionField.forceActiveFocus()
            }
            Controls.TextField {
                id: descriptionField
                Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
                placeholderText: i18n("Optional")
                // Opäť presunie zameranie na ďalšie pole
                onAccepted: dateField.forceActiveFocus()
            }
            Controls.TextField {
                id: dateField
                Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
                // D znamená povinné číslo medzi 1-9, 9 znamená povinné číslo medzi 0-9
                inputMask: "D999-99-99"
                // Tu potvrdíme operáciu rovnako ako kliknutím na tlačidlo OK
                onAccepted: addDialog.onAccepted()
            }
            Controls.Label {
                text: "* = required fields"
            }
        }
        // Tu sa nachádza logika dialógu
    }
    // ...
}

Dialógy majú predvolene hlavičku a pätu, obe zdedené z Controls.Dialog.

Hlavička predvolene obsahuje nadpis a tlačidlo na zatvorenie, ktoré sa dá deaktivovať pomocou showCloseButton. Päta predvolene obsahuje tlačidlo na zatvorenie a dá sa prepísať pomocou standardButtons.

Najprv ho nastavíme na zobrazenie tlačidla "Ok" a tlačidla "Cancel", pridáme nejaké odsadenie a pridáme rozumnú preferredWidth. Preferovaná šírka je predvolená očakávaná veľkosť dialógu, ktorá sa môže v prípade potreby zväčšiť. Môžeme použiť štandardné Kirigami.Units, ku ktorým sa neskôr vrátime.

Potom sa dostávame k Kirigami.FormLayout. Na rozdiel od ColumnLayout je rozloženie jeho potomkovských komponentov automatické a vycentrované s voliteľnými popiskami. Ako názov napovedá, používa sa na vytváranie vstupných formulárov.

Tieto rozloženia formulárov sú navrhnuté na prácu s rôznymi typmi vstupov, aj keď my sa držíme jednoduchých vstupov Controls.Textfield, ktoré nám dávajú jednoduché textové polia na písanie.

Vytvorili sme elementy Textfield, ktoré fungujú ako:

  1. Vstup pre názov nášho odpočítavania
  2. Vstup pre popis nášho odpočítavania
  3. Vstup pre dátum, ku ktorému odpočítavame, ktorý musí byť zadaný vo formáte YYYY-MM-DD

V každom z týchto prvkov Controls.Textfield nastavujeme vlastnosť Kirigami.FormData.label, ktorá nám umožňuje definovať popisky pre ne. Formulár zobrazí správne popisky naľavo od každého z týchto textových vstupných polí.

Nakoniec tiež nastavujeme vlastnosť onAccepted na spustenie metódy forceActiveFocus() nasledujúceho poľa; toto prepne aktívne pole, keď používateľ stlačí kláves ENTER, čím sa zlepší použiteľnosť formulára.

Nastavili sme tiež vlastnosť inputMask na textovom poli pre náš dátum. Nastavenie na D999-99-99 bráni používateľom zadať niečo, čo by mohlo narušiť funkčnosť aplikácie (ako text), obmedzujúc ich na zadávanie iba číslic, ktoré potom môžeme skúsiť analyzovať na objekt dátumu.

Keď je používateľské rozhranie dialógu hotové, musíme zmeniť, ako sa správa. Na to potrebujeme tri veci:

  1. Zobraziť tlačidlo Ok iba keď sú vyplnené povinné polia
  2. Pridať vstupné informácie do modelu
  3. Vyčistiť vstupný formulár
Kirigami.Dialog {
    // ... Keď je Kirigami.Dialog inicializovaný, chceme vytvoriť vlastné prepojenie, aby bolo
    // tlačidlo Ok viditeľné iba ak sú vyplnené požadované textové polia. Na to používame
    // Kirigami.Dialog.standardButton(button):
    Component.onCompleted: {
        const button = standardButton(Kirigami.Dialog.Ok);
        // () => je šípková funkcia JavaScriptu
        button.enabled = Qt.binding( () => requiredFieldsFilled() );
    }
    onAccepted: {
        // Prepojenie je vytvorené, ale stále musíme urobiť tlačidlo neklikateľným, pokiaľ nie sú
        // polia vyplnené
        if (!addDialog.requiredFieldsFilled()) return;
        appendDataToModel();
        clearFieldsAndClose();
    }
}

Prvá vec, ktorú musíme urobiť, je vytvoriť prepojenie medzi vlastnosťou enabled tlačidla OK a kontrolou, či sú polia vyplnené, čo v tomto prípade musí byť vykonané pomocou Qt.binding() v JavaScripte. V skutočnosti riadok:

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

je podobný previazaniam QML, ktoré sme doteraz videli, ako v nasledujúcom pseudokóde:

enabled: requiredFieldsFilled()

Obsluha signálu, ktorá spúšťa tlačidlo Ok, je onAccepted. Zostáva prázdna a bez činnosti, ak sú vyplnené požadované polia; inak pridá vstup do modelu a vyčistí dialóg pre ďalšie otvorenie.

Kirigami.Dialog {
    // ... Kontrolujeme, že nameField nie je prázdne a že dateField (ktoré má inputMask) je úplne
    // vyplnené
    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();
    }
}

Pre naše povinné pole názvu stačí skontrolovať, či je text poľa prázdny reťazec. Pre pole dátumu, pretože má masku vstupu, musíme namiesto toho použiť acceptableInput, ktoré sa stane pravdivým iba vtedy, keď je celé pole vyplnené a obsahuje iba prijateľné znaky.

Potom metóda append() nášho modelu zoznamu kountdownModel pridá objekt JavaScript obsahujúci vlastnosti, ktoré sme poskytli.

Nakoniec sa uistíme, že vyčistíme textové polia nastavením ich vlastností text na prázdny reťazec, a potom dialóg zavrieme.

Keď uložíme naše súbory a zostavíme náš program, budeme schopní pridávať vlastné odpočítavania! Môžeme urobiť poslednú úpravu na zlepšenie rozhrania, konkrétne odstrániť figurantské odpočítavanie, ktoré sme mali v predchádzajúcich lekciách:

26
27
28
    ListModel {
        id: kountdownModel
    }

Po druhé, teraz keď máme skutočný dátum, s ktorým môžeme pracovať, môžeme vypočítať čas do tohto dátumu:

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

A po tretie zväčšiť veľkosť okna, aby sme mali viac priestoru pre naše nové karty:

 9
10
    width: 600
    height: 400

Oveľa lepšie.

Naša aplikácia doteraz

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

Screenshot of the application with four example cards