Uso de archivos y señales separados

Separar código difícil de manejar en diferentes archivos y adjuntar señales a los componentes.

Pero ¿por qué?

Por primera vez, separaremos algunos de nuestros componentes en sus propios archivos QML. Si seguimos añadiendo cosas a main.qml, rápidamente se volverá difícil saber qué hace cada cosa, y corremos el riesgo de enturbiar nuestro código.

Primero necesitamos añadir los nuevos archivos a nuestro resources.qrc que creamos en la primera parte de este 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>

Uso de nuestros nuevos archivos

Necesitaremos encontrar algún modo de usar los nuevos archivos en main.qml. Afortunadamente, todo lo que tenemos que hacer es incluir una declaración de dichos componentes en nuestro main.qml del siguiente modo:

AddEditSheet {
	id: addEditSheet
}

Extendiendo nuestra hoja de añadir a una hoja de añadir/editar

Mientras que en el último tutorial hicimos que nuestro botón para añadir una cuenta atrás hiciera algo, el botón de edición de nuestras tarjetas de cuenta atrás sigue inactivo. También creamos una hoja para añadir que ahora podríamos reutilizar para que también sirva como una hoja de edición… Pero antes de esto necesitamos añadir un par de cosas adicionales a nuestro 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
import QtQuick 2.6
import QtQuick.Controls 2.0 as Controls
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.13 as Kirigami

// Base element, provides basic features needed for all kirigami applications
Kirigami.ApplicationWindow {
	// ID provides unique identifier to reference this element
	id: root
	
	// Window title
	title: i18nc("@title:window", "Day Kountdown")

	// Global drawer element with app-wide actions
	globalDrawer: Kirigami.GlobalDrawer {
		// Makes drawer a small menu rather than sliding pane
		isMenu: true
		actions: [
			Kirigami.Action {
				text: i18n("Quit")
				icon.name: "gtk-quit"
				shortcut: StandardKey.Quit
				onTriggered: Qt.quit()
			}
		]
	}
	
	// ListModel needed for ListView, contains elements to be displayed
	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
	function openPopulateSheet(mode, index = -1, listName = "", listDesc = "", listDate = "") {
		addEditSheet.mode = mode
		if(mode === "edit") {
			addEditSheet.index = index;
			addEditSheet.name = listName
			addEditSheet.description = listDesc
			addEditSheet.kdate = listDate
		}
		addEditSheet.open()
	}


	// Initial page to be loaded on app load
	pageStack.initialPage: Kirigami.ScrollablePage {
		// Title for the current page, placed on the toolbar
		title: i18nc("@title", "Kountdown")

		// Kirigami.Action encapsulates a UI action. Inherits from QQC2 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: openPopulateSheet("add")
		}
		
		// List view for card elements
		Kirigami.CardsListView {
			id: layout
			// Model contains info to be displayed
			model: kountdownModel
			// Delegate is how the information will be presented in the ListView
			delegate: KountdownDelegate {}
		}
	}
}

Los cambios claves que hemos realizado implican la adición de nuestra definición de componente AddEditSheet (y KountdownDelegate más abajo) y una nueva función llamada openPopulateSheet.

Veamos nuestra definición de AddEditSheet:

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

onAdded and onEdited son controladores de señales. Del mismo modo que onTriggered se llama cuando hacemos clic en una acción, también podemos usar controladores que responden a nuestras señales personalizadas. Si miramos nuestro nuevo AddEditSheet.qml (nuestra hoja para añadir reutilizada), podemos ver cómo funcionan estas señales.

 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
import QtQuick 2.6
import QtQuick.Controls 2.3 as Controls
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.13 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 string name: ""
	property string description: ""
	property string kdate: ""
	
	// 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
			text: mode === "add" ? "" : name
			onAccepted: descriptionField.forceActiveFocus()
		}
		Controls.TextField {
			id: descriptionField
			Kirigami.FormData.label: i18nc("@label:textbox", "Description:")
			placeholderText: i18n("Optional")
			text: mode === "add" ? "" : description
			onAccepted: dateField.forceActiveFocus()
		}
		Controls.TextField {
			id: dateField
			Kirigami.FormData.label: i18nc("@label:textbox", "Date:")
			inputMask: "0000-00-00"
			placeholderText: i18n("YYYY-MM-DD")
			text: mode === "add" ? "" : kdate
		}
		// 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();
			}
		}
	}
}

Las señales invocan a sus controladores cuando se las llama. En este caso, hemos creado dos señales que podemos invocar con diferentes resultados, y a las que podemos adjuntar información sobre la cuenta atrás que estamos añadiendo o creando. Una cosa interesante de las señales es que exponen las variables definidas en ellas a las funciones que las están escuchando, por lo que podemos simplemente llamar a esos nombres de variables en nuestros controladores onEdited y onAdded de main.qml. Nuestras señales son invocadas por el botón ‘Terminado’ dependiendo del contenido de la propiedad mode, definida en la parte superior de nuestra AddEditSheet.

La propiedad mode también controla varias cosas más: principalmente a qué está asignado el título de nuestra hoja y qué texto se incluirá en nuestros campos de texto. Sin embargo, de forma predeterminada, nuestra propiedad mode está asignada para añadir…

Lo que nos lleva de vuelta a main.qml y a nuestra nueva función openPopulateSheet. Es posible que haya notado que esto es lo que se llama ahora cuando se dispara la acción para añadir una cuenta atrás. Esta función tiene varios argumentos de entrada que se han proporcionado con valores por defecto. Esto resulta de utilidad cuando solo queremos añadir una nueva cuenta atrás, porque podemos hacer que la función concisa llame a openPopulateSheet("add"). Y, lo más importante, esta función asigna todas las propiedades relevantes en AddEditSheet.

function openPopulateSheet(mode, index = -1, listName = "", listDesc = "", listDate = "") {
	addEditSheet.mode = mode
	if(mode == "edit") {
		addEditSheet.index = index;
		addEditSheet.name = listName
		addEditSheet.description = listDesc
		addEditSheet.date = listDate
	}
	addEditSheet.open()
}
  • mode cambia la hoja de añadir/editar dependiendo de si este argumento está definido como "añadir" o como "editar"
  • El argumento index es necesario para que cuando guardemos nuestra cuenta atrás editada se modifique la correcta.
  • listName, listDesc y listDate son los detalles relevantes de la cuenta atrás que necesita poner en los campos de la hoja

Por supuesto, para usar nuestra hoja para cualquier cosa además de añadir cuentas atrás, primero debemos hacer que el botón de edición en nuestras tarjetas funcione. Pero si mira nuestro Kirigami.CardsListView en main.qml

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

Hemos sustituido nuestra Kirigami.AbstractCard con la definición de un 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
55
56
57
58
59
60
61
62
63
64
import QtQuick 2.6
import QtQuick.Controls 2.0 as Controls
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.13 as Kirigami

Kirigami.AbstractCard {
	id: kountdownDelegate
	// contentItem property includes the content to be displayed on the card
	contentItem: Item {
    	// implicitWidth/Height define the natural width/height of an item if no width or height is specified
		// The setting below defines a component's preferred size based on its content
		implicitWidth: delegateLayout.implicitWidth
		implicitHeight: delegateLayout.implicitHeight
		GridLayout {
			id: delegateLayout
			// QtQuick anchoring system allows quick definition of anchor points for positioning
			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 {
				// Heading will be as tall as possible while respecting constraints
				Layout.fillHeight: true
				// Level determines the size of the heading
				level: 1
				text: i18n("%1 days", Math.round((date-Date.now())/86400000))
			}
			
			// Layout for positioning elements vertically
			ColumnLayout {
				Kirigami.Heading {
					Layout.fillWidth: true
					level: 2
					text: name
				}
				// Horizontal rule
				Kirigami.Separator {
					Layout.fillWidth: true
					visible: description.length > 0
				}
				// Labels contain text
				Controls.Label {
					Layout.fillWidth: true
					// Word wrap makes text stay within box and shift with size
					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: openPopulateSheet("edit", index, name, description, date = new Date(date).toISOString().slice(0,10))
			}
		}
	}
}

La propiedad onClicked del botón ‘Editar’ de nuestras tarjetas llama ahora a la función openPopulateSheet, con las propiedades del elemento de la lista extraída de la tarjeta definidas como argumentos para esta función. Con ellos, la hoja se puede rellenar con el texto correcto.

Con eso, ¡tenemos una hoja completamente funcional con la que podemos añadir y editar nuestras cuentas atrás!