Usar ficheiros separados e sinais

Separar código gigantesco em vários ficheiros e associar sinais/eventos aos seus componentes.

Mas porquê?

Pela primeira vez, iremos começar a separar alguns dos nossos componentes nos seus próprios ficheiros QML. Se continuarmos a adicionar tudo ao main.qml, irá começar a ficar cada vez mais difícil de saber o que faz cada coisa, e sujeitamo-nos a enlamear o nosso código.

Primeiro precisamos de adicionar os nossos novos ficheiros ao ficheiro resources.qrc que foi criado na primeira parte deste tutorial.

<RCC>
    <qresource prefix="/">
        <file alias="main.qml">contents/ui/main.qml</file>
        <file alias="AddEditSheet.qml">contents/ui/AddEditSheet.qml</file>
        <file alias="KountdownDelegate.qml">contents/ui/KountdownDelegate.qml</file>
    </qresource>
</RCC>

Usar os nossos novos ficheiros

Teremos de arranjar alguma forma de usar os nossos novos ficheiros no main.qml. Felizmente, tudo o que precisamos de fazer é incluir uma declaração destes componentes no nosso main.qml, da seguinte forma:

AddEditSheet {
    id: addEditSheet
}

Estender a nossa folha de adição para uma folha de adição/edição

Embora no último tutorial tenhamos feito com que o nosso botão de criação de contagens fizesse algo, o botão de edição continua inactivo. Também criámos uma folha de criação que poderemos agora reaproveitar para uma folha de edição... mas antes de o fazermos, precisamos de adicionar algumas coisas extra ao nosso 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
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami

Kirigami.ApplicationWindow {
    id: root

    title: i18nc("@title:window", "Day Kountdown")

    globalDrawer: Kirigami.GlobalDrawer {
        isMenu: true
        actions: [
            Kirigami.Action {
                text: i18n("Quit")
                icon.name: "gtk-quit"
                shortcut: StandardKey.Quit
                onTriggered: Qt.quit()
            }
        ]
    }

    ListModel {
        id: kountdownModel
    }

    // Fetches item from addEditSheet.qml and does action on signal
    AddEditSheet {
        id: addEditSheet
        onAdded: kountdownModel.append({
            "name": name,
            "description": description,
            "date": Date.parse(kdate)
        });
        onEdited: kountdownModel.set(index, {
            "name": name,
            "description": description,
            "date": Date.parse(kdate)
        });
        onRemoved: kountdownModel.remove(index, 1)
    }

    // Function called by 'edit' button on card and by 'add'-Action
    function openPopulatedSheet(mode, index = -1, listName = "", listDesc = "", listDate = "") {
        addEditSheet.mode = mode
        addEditSheet.index = index;
        addEditSheet.name = listName
        addEditSheet.description = listDesc
        addEditSheet.kdate = listDate

        addEditSheet.open()
    }

    pageStack.initialPage: Kirigami.ScrollablePage {
        title: i18nc("@title", "Kountdown")

        // Kirigami.Action encapsulates a UI action. Inherits from Controls.Action
        actions.main: Kirigami.Action {
            id: addAction
            // Name of icon associated with the action
            icon.name: "list-add"
            // Action text, i18n function returns translated string
            text: i18nc("@action:button", "Add kountdown")
            // What to do when triggering the action
            onTriggered: openPopulatedSheet("add")
        }

        Kirigami.CardsListView {
            id: layout
            model: kountdownModel
            delegate: KountdownDelegate {}
        }
    }
}

As mudanças-chave que fizemos envolvem a adição da definição do nosso componente AddEditSheet (e o KountdownDelegate mais abaixo), assim como uma nova função chamada openPopulateSheet().

Vejamos a definição do nosso AddEditSheet:

AddEditSheet { 
    id: addEditSheet
    onEdited: kountdownModel.set(index, {
        name,
        description,
        date,
    });
    onAdded: kountdownModel.append({
        name,
        description,
        date,
    });
}

O onAdded e o onEdited são rotinas de tratamento de sinais. Assim como o onTriggered é invocado quando carregamos numa acção, também iremos usar rotinas que respondem aos nossos sinais personalizados.

AddEditSheet.qml

Se olhar para o nosso novo AddEditSheet.qml— a nossa folha de adição remodelada — podemos ver como funcionarão esses eventos:

 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
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami

// Overlay sheets appear over a part of the window
Kirigami.OverlaySheet {
    id: addEditSheet

    // Sheet mode
    property string mode: "add"

    property int index: -1
    property alias name: nameField.text
    property alias description: descriptionField.text
    property alias kdate: dateField.text

    // Signals can be read and certain actions performed when these happen
    signal added (string name, string description, var kdate)
    signal edited(int index, string name, string description, var kdate)
    signal removed(int index)

    header: Kirigami.Heading {
        // i18nc is useful for adding context for translators
        text: mode === "add" ? i18nc("@title:window", "Add kountdown") :
            i18nc("@title:window", "Edit kountdown")
    }
    // 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 label attached to the textfield
            Kirigami.FormData.label: i18nc("@label:textbox", "Name:")
            // Placeholder text is visible before you enter anything
            placeholderText: i18n("Event name (required)")
            // 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")
            onAccepted: dateField.forceActiveFocus()
        }
        Controls.TextField {
            id: dateField
            Kirigami.FormData.label: i18nc("@label:textbox", "Date:")
            inputMask: "0000-00-00"
            placeholderText: i18n("YYYY-MM-DD")
        }
        // This is a button.
        Controls.Button {
            id: deleteButton
            Layout.fillWidth: true
            text: i18nc("@action:button", "Delete")
            visible: mode === "edit"
            onClicked: {
                addEditSheet.removed(addEditSheet.index)
                close();
            }
        }
        Controls.Button {
            id: doneButton
            Layout.fillWidth: true
            text: i18nc("@action:button", "Done")
            // Button is only enabled if the user has entered something into the nameField
            enabled: nameField.text.length > 0
            onClicked: {
                // Add a listelement to the kountdownModel ListModel
                if(mode === "add") {
                    addEditSheet.added(
                        nameField.text,
                        descriptionField.text,
                        dateField.text
                    );
                }
                else {
                    addEditSheet.edited(
                        index,
                        nameField.text,
                        descriptionField.text,
                        dateField.text
                    );
                }
                close();
            }
        }
    }
}

Os 'signals' (sinais ou eventos) invocam as suas rotinas de tratamento sempre que são invocados. Neste caso, criámos dois sinais, added e edited, que poderemos invocar com diferentes resultados, e aos quais poderemos associar informações sobre as contagens decrescentes que estamos a criar ou alterar. Uma coisa boa sobre os sinais é que eles expõem as variáveis definidas neles às funções que estão à escuta deles, sendo por isso que podemos invocar esses nomes de variáveis nas nossas rotinas onEdited e onAdded no main.qml. Os nossos sinais são invocados pelo botão "Terminar", dependendo do valor com que está configurada a propriedade mode, definida no topo da nossa AddEditSheet.

A propriedade mode também controla várias outras coisas: principalmente, qual o título que a nossa folha tem, qual o texto a incluir nos nossos campos de texto. Contudo, por omissão, a nossa propriedade mode está apenas configurada para add (adição)...

O que nos traz de volta ao main.qml e à nossa nova função openPopulateSheet(). Poderá ter reparado que é esta que agora é invocada quando é desencadeada a acção de adição de contagens. Esta função recebe vários argumentos que foram passados com os valores predefinidos. Isto é útil quando simplesmente queremos criar uma nova contagem decrescente, dado que podemos ter a chamada à função mais concisa openPopulateSheet("add"). Mais importante, esta função configura todas as propriedades relevantes em AddEditSheet.

function openPopulatedSheet(mode, index = -1, listName = "", listDesc = "", listDate = "") {
    addEditSheet.mode = mode
    addEditSheet.index = index;
    addEditSheet.name = listName
    addEditSheet.description = listDesc
    addEditSheet.kdate = listDate

    addEditSheet.open()
}
  • O mode muda a folha de adição/edição, dependendo se este argumento está configurado como "add" ou como "edit"
  • O argumento index é necessário para que, quando gravarmos a nossa contagem editada, seja modificada a correcta
  • O listName, listDesc e listDate são os detalhes relevantes da contagem que precisam de ser colocados nos campos da folha

Obviamente, para usarmos de facto a nossa folha para algo que não seja apenas adicionar contagens, primeiro precisamos de criar o botão de edição no nosso trabalho com os cartões. Porém, se olhar para o nosso Kirigami.CardsListView no main.qml...

Kirigami.CardsListView {
    id: layout
    model: kountdownModel
    delegate: KountdownDelegate {}
}

KountdownDelegate.qml

Substituímos o nosso Kirigami.AbstractCard pela definição de um componente-delegado de 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
51
52
53
54
import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 as Kirigami

Kirigami.AbstractCard {
    id: kountdownDelegate
    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 {
                Layout.fillHeight: true
                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
                // Column spanning within grid layout (vertically in this case)
                Layout.columnSpan: 2
                text: i18n("Edit")
                onClicked: openPopulatedSheet("edit", index, name, description, new Date(date).toISOString().slice(0,10))
            }
        }
    }
}

A propriedade onClicked do botão "Editar" nos nossos cartões agora invoca a função openPopulateSheet com o elemento da lista de cartões obtidos configurada como argumentos desta função. Com estes, a folha poderá ser preenchida com o texto correcto.

Com isto, já temos uma folha totalmente funciona onde poderemos adicionar e editar as nossas contagens decrescentes!