Lägga till en dialogruta

Att begripa Kirigami dialogs.

Göra vårt program användbart

Vi har ett fönster, vi har kort, och vi har åtgärder. Vi behöver ändå komma på något sätt att mata in ett namn, en beskrivning och ett datum vi väljer.

Ett sätt vi skulle kunna göra det är genom att skapa en nya sida där vi placerar inmatningselement. Dock verkar en hel sida tillägnad för att tillhandahålla namn, beskrivning och datum lite överdrivet.

Istället använder vi en dialogruta.

Dialogruta som visas mitt i programmet

Öppna dialogrutan

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

Först redigerar vi åtgärden från föregående handledning: bara en Kirigami.Action som aktiverar dialogens funktion open().

Nedräkning: lägga till dialogrutor

Den nya komponenten vi lägger till är Kirigami.Dialog. Dialogrutor dyker upp mitt på fönstret och kan användas för att tillhandahålla extra information relevant för det aktuella sammanhanget. De kan inte flyttas, men de anpassar sin egen storlek till fönstret.

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

        // Formulärlayouter hjälper till att justera och strukturera en layout med mer indata
        Kirigami.FormLayout {
            // Textfält låter dig mata in text i en smal textruta
            Controls.TextField {
                id: nameField
                // Tillhandahåller en beteckning kopplad till textfältet
                Kirigami.FormData.label: i18nc("@label:textbox", "Name*:")
                // Vad du ska göra efter att inmatning har accepterats (dvs. tryckt på
                // returtangenten), i det här fallet flyttas fokus till nästa fält
                onAccepted: descriptionField.forceActiveFocus()
            }
            Controls.TextField {
                id: descriptionField
                Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
                placeholderText: i18n("Optional")
                // Återigen flyttar det fokus till nästa fält
                onAccepted: dateField.forceActiveFocus()
            }
            Controls.TextField {
                id: dateField
                Kirigami.FormData.label: i18nc("@label:textbox", "ISO Date*:")
                // D betyder ett obligatoriskt värde mellan 1-9, 9 betyder ett obligatoriskt värde
                // mellan 0-9
                inputMask: "D999-99-99"
                // Här bekräftar vi operationen precis som att klicka på knappen Ok
                onAccepted: addDialog.onAccepted()
            }
            Controls.Label {
                text: "* = required fields"
            }
        }
        // Dialogrutans logik ska finnas här
    }
    // ...
}

Dialogrutor har normalt en header och en [footer](https://doc.qt .io/qt-6/qml-qtquick-controls-dialog.html#footer-prop), båda ärvda från Controls.Dialog.

Header innehåller normalt en title och en stängningsknapp som kan inaktiveras med [showCloseButton] (docs:kirigami2;Dialog::showCloseButton). Footer innehåller normalt en stängningsknapp, och den kan ignoreras med standardButtons.

Vi ställde först in den för att visa knapparna "Ok" och "Avbryt", lade till lite utfyllnad och en rimlig preferredWidth. Den föredragna bredden är den förväntade standardstorleken på dialogrutan, som kan öka vid behov. Vi kan använda standard Kirigami.Units som vi tittar vidare på igen senare.

Sedan kommer vi till en Kirigami.FormLayout. I motsats till en ColumnLayout, är layouten av ingående komponenter automatisk och centrerad, med valfria beteckningar. Som namnet antyder, används den för att skapa inmatningsformulär.

Formulärlayouter är konstruerade för att arbeta med ett stort antal typer av inmatning, även om vi håller oss till enkla Controls.Textfield, som ger oss enkla textrutor att skriva in saker i.

Vi har skapat elementet Textfield som fungerar som:

  1. Indata för namnet på vår nedräkning
  2. Indata för beskrivningen av vår nedräkning
  3. Indata för datumet vi räknar ner till, som måste anges på formatet ÅÅÅÅ-MM-DD.

Inom vart och ett av elementen i Controls.Textfield anger vi in egenskapen ​​Kirigami.FormData.label som låter oss definiera beteckningar för dem. Formuläret visar rätt beteckning till vänster om vart och ett av textinmatningsfälten.

Slutligen ställer vi också in egenskapen onAccepted för att använda metoden [forceActiveFocus()](https:/ /doc.qt.io/qt-6/qml-qtquick-item.html#forceActiveFocus-method) för följande fält. Det byter det aktiva fältet när användaren trycker på returtangenten, vilket förbättrar formulärets användbarhet.

Vi har också ställt in en egenskap som kallas inputMask i textfältet för vårt datum. Att ställa in den till `D999-99-99" förhindrar oss från att mata in någonting som kan förstöra programmets funktion (såsom text), och begränsar dem att bara mata in siffror som vi sedan kan försöka tolka som ett datumobjekt.

När användargränssnittet för dialogrutan är klart måste vi ändra hur det beter sig. För att göra det behöver vi tre saker:

  1. Visa bara knappen Ok när de obligatoriska fälten är ifyllda
  2. Lägga till den inmatade informationen i modellen
  3. Rensa inmatningsformuläret
Kirigami.Dialog {
    // ... När Kirigami.Dialog väl har initierats vill vi skapa en egen bindning för att endast göra
    // Ok-knappen synlig om de obligatoriska textfälten är ifyllda. För att göra det använder vi
    // Kirigami.Dialog.standardButton(button):
    Component.onCompleted: {
        const button = standardButton(Kirigami.Dialog.Ok);
        // () => är en pilfunktion i Javascript
        button.enabled = Qt.binding( () => requiredFieldsFilled() );
    }
    onAccepted: {
        // Bindningen är skapad, men vi måste fortfarande göra den oklickbar om fälten inte är
        // ifyllda
        if (!addDialog.requiredFieldsFilled()) return;
        appendDataToModel();
        clearFieldsAndClose();
    }
}

Det första som behöver göras är att skapa en bindning mellan Ok-knappens egenskap enabled och en kontroll av om fälten är ifyllda, vilket i detta fall måste göras med [Qt.binding()](https://doc.qt.io/qt-6/qtqml-syntax-propertybinding.html#creating- egenskapsbindningar-från-javascript) i JavaScript. I själva verket är raden:

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

som liknar de QML-bindningar vi har sett hittills, liksom i följande pseudokod:

enabled: requiredFieldsFilled()

Signalhanteraren som aktiverar Ok-knappen är onAccepted. Den förblir tom och utan att göra någonting om de obligatoriska fälten är ifyllda. Annars lägger den till indata i modellen och rensar dialogrutan till nästa gång den öppnas.

Kirigami.Dialog {
    // ... Vi kontrollerar att namnfältet inte är tomt och att datumfältet (som har en inputMask) är
    // helt ifyllt
    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();
    }
}

För vårt obligatoriska namnfält är allt vi behöver göra att kontrollera om fälttexten är en tom sträng. För datumfältet, eftersom det har en inmatningsmask, behöver vi istället använda acceptableInput, vilket endast är sant när hela fältet är ifyllt och endast innehåller acceptabla tecken.

Sedan lägger metoden append() i vår listmodell kountdownModel till ett Javascript-objekt som inkluderar egenskaperna vi har tillhandahållit.

Till sist, säkerställer vi att textfälten är tomma genom att ställa in deras egenskap text till en tom sträng, och sedan stänga den med close().

När vi har sparat våra filer och byggt vårt program kommer vi att kunna lägga till våra egna anpassade nedräkningar! Vi kan göra en sista touch för att förbättra gränssnittet, nämligen ta bort exempelnedräkningen vi hade i de tidigare lektionerna:

26
27
28
    ListModel {
        id: kountdownModel
    }

För det andra, nu när vi har ett verkligt datum att leka med, kan vi beräkna tiden fram till datumet:

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

Och för det tredje öka fönsterstorleken så att vi har mer plats för våra nya kort:

 9
10
    width: 600
    height: 400

Mycket trevligare.

Vårt program så långt

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

Skärmbild av programmet med fyra exempelkort