Conectar modelos a la interfaz de usuario QML

Conectar modelos del motor C++ a la interfaz de QML

As shown from the previous tutorial, you can connect C++ code to QML by creating a class that will be treated as just another component in QML. However, you may want to represent more complicated data, such as data that needs to act as a custom ListModel or in some way needs to be delegated from a 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.

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

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

Once we have that settled, we can finally create what these roles are in the QML side using a QHash where the keys are the enumerated values paired with QByteArrays . The text in the QByteArray is what's used in the actual QML code.

QHash<int, QByteArray> Model::roleNames() const {
    return {
        {SpeciesRole,   "species"},
        {CharactersRole, "characters"}
    };
}

In our example model, the role "species" can be used to retrieve the QString key "Feline", "Fox", "Goat", each in a separate delegate. The same can be done with the QStringList values for the character names list.

Sobrescritura e implementación de data()

There are two parameters that are passed in data(): index and role. index is the location of where the data is when being delegated. As previously stated, role is used by QML to get specific data returned when it's accessing a role.

In data(), we can use a switch statement to return the appropriate data and data type depending on the role, which is possible as data() returns a QVariant . We still need to make sure we get the appropriate location of the data, though. In this example below, you can see that a new iterator variable is being declared, which is set from the beginning of the list plus the row of the index and the data that the iterator is pointing to is what is being returned.

We can't just return whatever data we want though. We may be trying to bind data to a property with an incompatible data type, such as a QStringList to a QString. You may have to do data conversion in order for the data to be displayed properly.

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

The QML file that is used will just contain three Kirigami.AbstractCard components, where the key is the header and the value is the content. These cards are created by delegating an AbstractCard using a Repeater, where the custom model we created acts as the model. The data is accessed using word model, followed by the roles we declared in roleNames().

import QtQuick 2.15
import QtQuick.Controls 2.15 as Controls
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.20 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
                    }
                }
            }
        }
    }
}
Captura de pantalla de la aplicación

Modificación de datos

Edición de datos con dataChanged() y setData()

You may encounter a situation where you want to modify data in the model, and have the changes reflected on the frontend side. Every time we change data in the model, we must emit the dataChanged() signal which will apply those changes on the frontend side at the specific cells specified in its arguments. In this tutorial, we can just use the index argument of 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 ubicació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 ser Qt::EditRole.

The role parameter in this case is used to ensure setData() can be edited via user input (Qt::EditRole). Using index, we can use that to determine the location of where the data should be edited with the contents of 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;
}

Let's update the QML code so that we can open up a prompt that allows us to edit the model using a Controls.Button attached to the cards.

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.

app_screenshot_1.png
app_screenshot_2.png

Añadir filas

We added a way to modify the data in existing keys of the QMap, and in the front end, this is reflected as modifying the contents inside the AbstractCards. But what if we need to add a new key entry in the QMap and have that reflected on the QML side? Let's do this by creating a new method that is callable on the QML side to perform this task.

To make the method visible in QML, we must use the Q_OBJECT macro in the class, and begin the method declaration with the Q_INVOKABLE macro. This method will also include a string parameter, which is intended to be the new key in the QMap.

class Model : public QAbstractListModel {
Q_OBJECT;

    ...
public:
    ...
    Q_INVOKABLE void addSpecies(const QString &species);
};

Inside of this method, we need to tell Qt that we want to create more rows in the model. This is done by calling beginInsertRows() to begin our row adding operation, followed by inserting whatever we need, then use endInsertRows() to end the operation. We still need to emit dataChanged() at the end, however. This time, we are going to update all rows, from the first row to the last one as the QMap may alphabetically reorganize itself, and we need to catch that across all rows.

When calling beginInsertRows(), we need to first pass in a QModelIndex class to specify the location of where the new rows should be added, followed by what the new first and last row numbers are going to be. In this tutorial, the first argument will just be QModelIndex() as there is no need to use the parameter here. We can just use the current row size for the first and last row number, as we'll just be adding one row at the end of the model.

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

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 = ""; // Clear TextField every time it's done
                addPrompt.close();
            }
        }
    }

    pageStack.initialPage: Kirigami.ScrollablePage {
        actions.main: Kirigami.Action {
            icon.name: "add"
            text: "Add New Species"
            onTriggered: {
                addPrompt.open();
            }
        }
        ...
    }
}

Now, we should be given a new action at the top of the app that brings up a prompt that allows to add a new element to the model, with our own custom data.

app_screenshot_add_1.png
app_screenshot_add_2.png

Eliminar filas

The way remove rows is similar to adding rows. Let's create another method that we'll call in QML. This time, we will use an additional parameter, and that is an integer that is the row number. The species name is used to delete the key from the QMap, while the row number will be used to delete the row on the front end.

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

Now, let's update the application so a "Delete" button appears alongside the edit button, and hook it up to our delete method.

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);
                            }
                        }
                    }
                }
            }
        }
    }
}
app_screenshot_del_1.png
app_screenshot_del_2.png

Código completo

 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
#pragma once

#include <QAbstractListModel>

class Model : public QAbstractListModel {
Q_OBJECT;

private:
    QMap<QString, QStringList> m_list = {
            {"Feline", {"Tigress",   "Waai Fuu"}},
            {"Fox",    {"Carmelita", "Diane", "Krystal"}},
            {"Goat",   {"Sybil",     "Toriel"}}
    };

    static QString formatList(const QStringList &list);

public:
    enum Roles {
        SpeciesRole = Qt::UserRole,
        CharactersRole
    };

    int rowCount(const QModelIndex &) const override;

    QHash<int, QByteArray> roleNames() const override;

    QVariant data(const QModelIndex &index, int role) const override;

    bool setData(const QModelIndex &index, const QVariant &value, int role) override;

    Q_INVOKABLE void addSpecies(const QString &species);

    Q_INVOKABLE void deleteSpecies(const QString &species, const int &rowIndex);
};
 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
 
#include "model.h"
 
 int Model::rowCount(const QModelIndex &) const {
     return m_list.count();
 }
 
 QHash<int, QByteArray> Model::roleNames() const {
     QHash<int, QByteArray> map = {
             {SpeciesRole,   "species"},
             {CharactersRole, "characters"}
     };
     return map;
 }
 
 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;
 }
 
 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;
 }
 
 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));
 }
 
 void Model::deleteSpecies(const QString &species, const int& rowIndex) {
     beginRemoveRows(QModelIndex(), rowIndex, rowIndex);
     m_list.remove(species);
     endRemoveRows();
     emit dataChanged(index(0), index(m_list.size() - 1));
 }
 
  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
 
import QtQuick 2.15
 import QtQuick.Controls 2.15 as Controls
 import QtQuick.Layouts 1.15
 import org.kde.kirigami 2.20 as Kirigami
 import CustomModel 1.0
 
 Kirigami.ApplicationWindow {
     id: root
     title: "Tutorial"
 
     CustomModel {
         id: customModel
     }
 
     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();
             }
         }
     }
 
     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 = ""; // Clear TextField every time it's done
                 addPrompt.close();
             }
         }
     }
 
     pageStack.initialPage: Kirigami.ScrollablePage {
         actions.main: Kirigami.Action {
             icon.name: "add"
             text: "Add New Species"
             onTriggered: {
                 addPrompt.open();
             }
         }
 
         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
                             }
                             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);
                                     }
                                 }
                             }
                         }
                     }
                 }
             }
         }
     }
 }
 

Más información

For more information, see Using C++ Models with Qt Quick Views and Model/View Programming .