Conectar modelos a la interfaz de usuario QML
Como se muestra en el tutorial anterior, se puede conectar código C++ con QML creando una clase que se tratará como otro componente en QML. No obstante, es posible que quiera representar datos más complicados, como datos que deben actuar como un ListModel personalizado o que, de alguna manera, se deben delegar de un Repeater.
Podemos crear nuestros propios modelos desde el lado C++ y declarar cómo se deben representar los datos de este modelo en la interfaz QML.
Preparación de la clase
En este tutorial crearemos una clase que contiene un QMap, donde se usa un QString como clave y objetos QStringList como valores. La interfaz será capaz de leer y mostrar las claves y los valores, y será tan fácil de usar como una matriz de una dimensión. Debería parecerse a un ListModel de QML.
Para ello, necesitamos declarar una clase que herede de QAbstractListModel. También añadiremos datos al QMap. Estas declaraciones estarán en model.h
.
Nota
Si está siguiendo las instrucciones, recuerde actualizar su archivoCMakeLists.txt
.#pragma once
#include <QAbstractListModel>
class Model : public QAbstractListModel {
private:
QMap<QString, QStringList> m_list = {
{"Feline", {"Tigress", "Waai Fuu"}},
{"Fox", {"Carmelita", "Diane", "Krystal"}},
{"Goat", {"Sybil", "Toriel"}}
};
};
Por supuesto, no podemos mostrar esta clase como está. También necesitamos decirle a QML cómo se representan estos datos en la clase. Podemos hacerlo sobrescribiendo tres funciones virtuales que son esenciales para ello, cada una de las cuales realiza su propia tarea.
rowCount()
: Piense en esta función como un modo de decirle a QML cuántos elementos hay en el modelo que se va a representar.roleNames()
: Se puede decir que los nombres de los roles son nombres de propiedades adjuntados a los datos en QML. Esta función le permite crear dichos roles.data()
: Esta función se llama cuando necesite obtener los datos que corresponden a los nombres de roles del modelo.
Nota
Los nombres de roles personalizados creados porroleNames()
solo se pueden usar cuando un modelo está delegado, y no se pueden usar fuera del mismo. Consulte Modelos y vistas.Nota
Técnicamente, los modelos de Qt se representan como tablas, con filas y columnas. Así, lo que la sobrescritura derowCount()
hace es decirle a Qt cuántas filas hay en un modelo. Como solo estamos tratando con una matriz de una única dimensión en este tutorial, podemos pensar en las «filas» como «número de elementos».Sobrescritura e implementación de rowCount()
Sobrescribamos la función en el archivo de cabecera. rowCount()
viene con su propio parámetro, pero no se usará en este ejemplo, por lo que se excluye.
class Model : public QAbstractListModel {
...
public:
int rowCount(const QModelIndex &) const override;
};
A continuación declaramos cuántas filas hay en este modelo en model.cpp
.
#include "model.h"
int Model::rowCount(const QModelIndex &) const {
return m_list.count();
}
Sobrescritura e implementación de roleNames()
Antes de sobrescribir roleNames()
, necesitamos declarar que los roles están en el lado C++ usando una variable enum
pública. El motivo para ello es que dichos valores de la variable enum
se pasan a data()
cada vez que QML accede a un determinado rol y, como tal, podemos hacer que data()
devuelva lo que queremos.
Empecemos creando la variable enum
para los roles, donde cada valor es un rol para el lado C++.
class Model : public QAbstractListModel {
...
public:
enum Roles {
SpeciesRole = Qt::UserRole,
CharactersRole
};
...
QHash<int, QByteArray> roleNames() const override;
};
Una vez que tengamos esto resuelto, podremos crear lo que son dichos roles en el lado QML usando un QHash donde las claves son los valores de enumeración emparejados con QByteArrays. El texto del QByteArray es lo que se usa en el código QML real.
QHash<int, QByteArray> Model::roleNames() const {
return {
{SpeciesRole, "species"},
{CharactersRole, "characters"}
};
}
En el modelo de nuestro ejemplo, el rol «especies» se puede usar para obtener la clave QString «Felino», «Zorro», «Cabra», cada una en un delegado separado. Se puede hacer lo mismo con los valores QStringList para la lista de nombres de caracteres.
Sobrescritura e implementación de data()
Existen dos parámetros que se pasan en data()
: index
y role
. index
es la posición donde están los datos cuando se delegan. Como se ha dicho anteriormente, QML usa role
para devolver datos específicos cuando accede a un rol.
En data()
podemos usar una sentencia switch
para devolver los datos apropiados y el tipo de dato según el rol, lo que es posible porque data()
devuelve una QVariant. No obstante, todavía tenemos que asegurarnos de que obtenemos la posición apropiada de los datos. En el siguiente ejemplo se puede ver que se declara una nueva variable de iteración, que se establece al principio de la lista más la fila del índice y los datos a los que apunta el iterador son los que se devuelven.
Sin embargo, no podemos simplemente devolver los datos que queramos. Es posible que estemos intentando asociar datos a una propiedad con un tipo de datos incompatible, como una QStringList a una QString. Es posible que tenga que realizar una conversión de datos para que los datos se muestren correctamente.
QVariant Model::data(const QModelIndex &index, int role) const {
const auto it = m_list.begin() + index.row();
switch (role) {
case SpeciesRole:
return it.key();
case CharactersRole:
return formatList(it.value());
default:
return {};
}
}
QString Model::formatList(const QStringList& list) {
QString result;
for (const QString& character : list) {
result += character;
if (list.last() != character) {
result += ", ";
}
}
return result;
}
Permitir que la clase se declare en QML
No olvidemos hacer que nuestra clase se pueda usar en QML.
int main(int argc, char *argv[]) {
...
qmlRegisterType<Model>("CustomModel", 1, 0, "CustomModel");
...
}
Uso de clases en QML
El archivo QML que se usa contendrá solo tres componentes Kirigami.AbstractCard, donde la clave es la cabecera y el valor es el contenido. Estas tarjetas se crean delegando una AbstractCard que usa un Repeater, donde el modelo personalizado que hemos creado actúa como modelo. Se accede a los datos usando la palabra model
, seguida por los roles que hemos declarado en roleNames()
.
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import org.kde.kirigami as Kirigami
import CustomModel 1.0
Kirigami.ApplicationWindow {
id: root
title: "Tutorial"
CustomModel {
id: customModel
}
pageStack.initialPage: Kirigami.ScrollablePage {
ColumnLayout {
Repeater {
model: customModel
delegate: Kirigami.AbstractCard {
header: Kirigami.Heading {
text: model.species
level: 2
}
contentItem: Controls.Label {
text: model.characters
}
}
}
}
}
}
Modificación de datos
Edición de datos con dataChanged()
y setData()
Es posible que se encuentre con una situación en la que desee modificar datos en el modelo y que los cambios se reflejen en la interfaz. Cada vez que cambiamos datos en el modelo, debemos emitir la señal dataChanged()
, que aplicará los cambios en la interfaz en las celdas indicadas específicamente en sus argumentos. En este tutorial, podemos usar el argumento index
de setData()
.
setData()
es una función virtual que se puede sobrescribir para que, al intentar modificar los datos desde el lado de la interfaz, refleje de forma automática dichos cambios en el lado del motor. Requiere tres parámetros:
index
: La posición de los datos.value
: El contenido de los datos nuevos.role
: En este contexto, el rol se usa para indicar a las vistas cómo deben manejar los datos. Aquí, el rol debe serQt::EditRole
.
En este caso, el parámetro role
se usa para garantizar que setData()
se pueda editar mediante la entrada del usuario (Qt::EditRole). Mediante index
, podemos usarlo para determinar la ubicación donde se deben editar los datos con el contenido de value
.
bool Model::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!value.canConvert<QString>() && role != Qt::EditRole) {
return false;
}
auto it = m_list.begin() + index.row();
QString charactersUnformatted = value.toString();
QStringList characters = charactersUnformatted.split(", ");
m_list[it.key()] = characters;
emit dataChanged(index, index);
return true;
}
Nota
setData()
no emite dataChanged()
automáticamente, por lo que se tiene que hacer de forma manual.Actualicemos el código QML para que podamos abrir un mensaje que nos permita editar el modelo usando un Controls.Button adjunto a las tarjetas.
Kirigami.ApplicationWindow {
...
Kirigami.OverlaySheet {
id: editPrompt
property var model
property alias text: editPromptText.text
title: "Edit Characters"
Controls.TextField {
id: editPromptText
}
footer: Controls.DialogButtonBox {
standardButtons: Controls.DialogButtonBox.Ok
onAccepted: {
const model = editPrompt.model;
model.characters = editPromptText.text;
editPrompt.close();
}
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
ColumnLayout {
Repeater {
model: customModel
delegate: Kirigami.AbstractCard {
Layout.fillHeight: true
header: Kirigami.Heading {
text: model.species
level: 2
}
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
ColumnLayout {
id: delegateLayout
Controls.Label {
text: model.characters
}
Controls.Button {
text: "Edit"
onClicked: {
editPrompt.text = model.characters;
editPrompt.model = model;
editPrompt.open();
}
}
}
}
}
}
}
}
}
Ahora, cada vez que los valores del modelo cambian en la interfaz, los cambios deberían actualizarse automáticamente en el motor.
Añadir filas
Hemos añadido una forma de modificar los datos en las claves de QMap existentes y, en la interfaz, esto se refleja como una modificación del contenido dentro de AbstractCards. Pero, ¿qué pasa si necesitamos añadir una nueva entrada de clave en el QMap y reflejarla en el lado QML? Hagamos esto creando un nuevo método que se pueda llamar en el lado QML para realizar esta tarea.
Para hacer que el método sea visible en QML debemos usar la macro Q_OBJECT en la clase, y empezar la declaración del método con la macro Q_INVOKABLE. Este método también incluirá un parámetro de cadena, que está destinado a ser la nueva clave del QMap.
class Model : public QAbstractListModel {
Q_OBJECT;
...
public:
...
Q_INVOKABLE void addSpecies(const QString &species);
};
Dentro de este método, necesitamos decirle a Qt que queremos crear más filas en el modelo. Esto se hace llamando a beginInsertRows()
para empezar nuestra operación de añadir filas, seguido por la inserción de lo que necesitemos; termine la operación usando endInsertRows()
. No obstante, todavía necesitamos emitir dataChanged()
al final. Esta vez vamos a actualizar todas las filas, desde la primera hasta la última, ya que el QMap puede reorganizarlas alfabéticamente por sí mismo, y necesitamos capturar esto de todas las filas.
Al llamar a beginInsertRows()
debemos pasar primero una clase QModelIndex para indicar la posición donde se deben añadir las nuevas filas, seguido de los números de la primera y última fila. En este tutorial, el primer argumento será QModelIndex()
, puesto que no hay necesidad de usar el parámetro aquí. Podemos usar el tamaño de la fila actual para los números de la primera y de la última filas, ya que solo vamos a añadir una fila al final del modelo.
void Model::addSpecies(const QString& species) {
beginInsertRows(QModelIndex(), m_list.size() - 1, m_list.size() - 1);
m_list.insert(species, {});
endInsertRows();
emit dataChanged(index(0), index(m_list.size() - 1));
}
Nota
La funcióndataChanged()
usa QModelIndex como el tipo de dato para sus parámetros. No obstante, podemos convertir enteros en QModelIndex usando la función index()
.Actualicemos el código QML para tener la capacidad de añadir una nueva clave al QMap.
Kirigami.ApplicationWindow {
...
Kirigami.OverlaySheet {
id: addPrompt
title: "Add New Species"
Controls.TextField {
id: addPromptText
}
footer: Controls.DialogButtonBox {
standardButtons: Controls.DialogButtonBox.Ok
onAccepted: {
customModel.addSpecies(addPromptText.text);
addPromptText.text = ""; // Borrar TextField cada vez que termina
addPrompt.close();
}
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
actions: [
Kirigami.Action {
icon.name: "add"
text: "Add New Species"
onTriggered: {
addPrompt.open();
}
}
]
...
}
}
Ahora deberíamos tener una nueva acción en la parte superior de la aplicación que muestra un mensaje que permite añadir un nuevo elemento al modelo con nuestros propios datos personalizados.
Eliminar filas
La forma de eliminar filas es similar a la de añadirlas. Vamos a crear otro método que llamaremos en QML. Esta vez, usaremos un parámetro adicional, que es un entero que representa el número de fila. El nombre de la especie se usa para eliminar la clave del QMap, mientras que el número de fila se usa para eliminar la fila en la interfaz.
class Model : public QAbstractListModel {
Q_OBJECT;
...
public:
...
Q_INVOKABLE void deleteSpecies(const QString &speciesName, const int &rowIndex);
}
void Model::deleteSpecies(const QString &speciesName, const int& rowIndex) {
beginRemoveRows(QModelIndex(), rowIndex, rowIndex);
m_list.remove(speciesName);
endRemoveRows();
emit dataChanged(index(0), index(m_list.size() - 1));
}
Ahora, actualicemos la aplicación para que aparezca un botón «Borrar» junto al botón «Editar» y conectémoslo a nuestro método de borrado.
ColumnLayout {
Repeater {
model: customModel
delegate: Kirigami.AbstractCard {
...
contentItem: Item {
implicitWidth: delegateLayout.implicitWidth
implicitHeight: delegateLayout.implicitHeight
ColumnLayout {
id: delegateLayout
Controls.Label {
text: model.characters
}
RowLayout {
Controls.Button {
text: "Edit"
onClicked: {
editPrompt.text = model.characters;
editPrompt.model = model;
editPrompt.open();
}
}
Controls.Button {
text: "Delete"
onClicked: {
customModel.deleteSpecies(model.species, index);
}
}
}
}
}
}
}
}
Código completo
Main.qml
|
|
model.h
|
|
model.cpp
|
|
Más información
Para más información, consulte Uso de modelos en C++ con vistas de Qt Quick y Programación modelo/vista.