Connectar models a la vostra interfície d'usuari en QML

Connectar models des d'un dorsal C++ a un frontal QML

Com es mostra a la guia d'aprenentatge anterior, podeu connectar codi C++ al QML creant una classe que es tractarà com un altre component en QML. No obstant això, és possible que vulgueu representar dades més complicades, com ara les dades que han d'actuar com un ListModel personalitzat o, d'alguna manera, han de ser delegades des d'un Repeater.

Podem crear els nostres propis models des de la banda del C++, i declarar com s'han de representar les dades d'aquest model en el frontal QML.

Preparant la classe

En aquesta guia d'aprenentatge, crearem una classe que conté un QMap, on s'utilitza un QString com a clau i objectes QStringList com a valors. El frontal podrà llegir i mostrar les claus i els valors i serà senzill d'utilitzar com una matriu unidimensional. Hauria de ser semblant a un ListModel en QML.

Per a fer-ho, necessitem declarar una classe que hereti de QAbstractListModel. Afegim també algunes dades addicionals al QMap. Aquestes declaracions s'ubicaran a 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"}}
    };
};

Per descomptat, no podem simplement mostrar aquesta classe com és. També hem de dir-li al QML com representar aquestes dades a la classe. Podem fer-ho sobreescrivint tres funcions virtuals que són essencials per a fer-ho, totes les quals fan les seves pròpies tasques.

  • rowCount() - Penseu en aquesta funció com una manera de dir al QML quants elements hi ha en el model a representar.
  • roleNames() - Podeu pensar en els noms dels rols com a noms de propietats adjunts a les dades en QML. Aquesta funció permet crear aquests rols.
  • data(): Aquesta funció es crida quan voleu recuperar les dades que corresponen als noms dels rols a partir del model.

Sobreescriure i implementar rowCount()

Sobreescrivim la funció al fitxer de capçalera. El rowCount() ve amb el seu paràmetre propi, però no s'utilitzarà en aquest exemple i s'exclou.

class Model : public QAbstractListModel {
...
public:
    int rowCount(const QModelIndex &) const override;
};

Llavors, declarem quantes files hi ha en aquest model a model.cpp.

#include "model.h"

int Model::rowCount(const QModelIndex &) const {
    return m_list.count();
}

Sobreescriure i implementar roleNames()

Abans de substituir roleNames(), necessitem declarar quins són els rols a la banda del C++ utilitzant una variable pública enum. El motiu d'això és perquè aquests valors de la variable enum es passen a data() cada vegada que el QML accedeix a un rol corresponent, i com a tal podem fer que data() retorni el que volem.

Comencem per crear la variable enum per als rols, a on cada valor és un rol de la banda del C++.

class Model : public QAbstractListModel {
...
public:
    enum Roles {
        SpeciesRole = Qt::UserRole,
        CharactersRole
    };

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

Una vegada que hàgim resolt això, finalment podrem crear quins són aquests rols en la banda del QML utilitzant un QHash on les claus són els valors enumerats aparellats amb QByteArrays. El text del QByteArray és el que s'utilitza en el codi QML real.

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

En el nostre model d'exemple, el rol «espècies» es pot utilitzar per a recuperar la clau del QString «Feline», «Fox», «Goat», cadascuna en un delegat separat. El mateix es pot fer amb els valors de QStringList per a la llista de noms de caràcters.

Sobreescriure i implementar data()

Hi ha dos paràmetres que es passen a data(): index i role. index és la ubicació d'on es troben les dades quan es deleguen. Com s'ha indicat anteriorment, el QML utilitza el role per a obtenir dades específiques que es retornen quan s'està accedint a un rol.

A data(), podem utilitzar una declaració switch per a retornar les dades i el tipus de dades adequats depenent del rol, el qual és possible ja que data() retorna una QVariant. No obstant això, encara hem d'assegurar-nos d'aconseguir la ubicació adequada de les dades. En aquest exemple a continuació, podeu veure que s'està declarant una variable nova de l'iterador, que s'estableix des del principi de la llista més la fila de l'índex i les dades a les quals apunta l'iterador que és el que s'està retornant.

Però no podem retornar les dades que vulguem. És possible que estiguem intentant vincular dades a una propietat amb un tipus de dades incompatible, com ara una QStringList a una QString. És possible que hàgiu de fer la conversió de dades perquè les dades es mostrin correctament.

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

Permetre que la classe es declari en el QML

No oblidem fer la nostra classe utilitzable en el QML.

int main(int argc, char *argv[]) {
    ...
    qmlRegisterType<Model>("CustomModel", 1, 0, "CustomModel");
    ...
}

Ús de la classe en el QML

El fitxer QML que s'utilitza només contindrà tres components Kirigami.AbstractCard, on la clau és la capçalera i el valor és el contingut. Aquestes targetes es creen delegant una AbstractCard utilitzant un repetidor, on el model personalitzat que hem creat actua com a model. S'accedeix a les dades utilitzant la paraula model, seguida dels rols que hem declarat a 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
                    }
                }
            }
        }
    }
}

Captura de pantalla de l'aplicació

Modificació de dades

Edició de dades utilitzant dataChanged() i setData()

Podeu trobar-vos amb una situació en la qual vulgueu modificar les dades del model, i tenir els canvis reflectits a la banda del frontal. Cada vegada que canviem dades en el model, hem d'emetre el senyal dataChanged() que aplicarà aquests canvis a la banda del frontal a les cel·les específiques especificades en els seus arguments. En aquesta guia d'aprenentatge, només podem utilitzar l'argument index de setData().

setData() és una funció virtual que podeu sobreescriure de manera que intentar modificar les dades des de la banda del frontal reflecteix automàticament aquests canvis a la banda del dorsal. Requereix tres paràmetres:

  • index - La ubicació de les dades.
  • value - El contingut de les dades noves.
  • role - En aquest context, el rol aquí s'utilitza per a dir a les vistes com haurien de gestionar les dades. El rol aquí hauria de ser Qt::EditRole.

El paràmetre role en aquest cas s'utilitza per a assegurar que setData() es pot editar mitjançant l'entrada de l'usuari (Qt::EditRole). Utilitzant index, podem utilitzar això per a determinar la ubicació d'on s'han d'editar les dades amb el contingut 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;
}

Actualitzem el codi QML de manera que puguem obrir un indicador que ens permeti editar el model utilitzant un Controls.Button adjunt a les targetes.

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

Ara, sempre que els valors del model canviïn al frontal, els canvis s'hauran d'actualitzar automàticament al dorsal.

app_screenshot_1.png
app_screenshot_2.png

Afegir files

S'ha afegit una manera de modificar les dades a les claus existents del QMap, i al frontal, això es reflecteix com modificar el contingut dins de les AbstractCards. Però, i si necessitem afegir una entrada nova de clau al QMap i tenir això reflectit a la banda del QML? Fem-ho creant un mètode nou que es pot cridar a la banda del QML per a realitzar aquesta tasca.

Per a fer visible el mètode en el QML, hem d'utilitzar la macro Q_OBJECT a la classe, i començar la declaració del mètode amb la macro Q_INVOKABLE. Aquest mètode també inclourà un paràmetre de cadena, que es pretén que sigui la clau nova en el QMap.

class Model : public QAbstractListModel {
Q_OBJECT;

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

Dins d'aquest mètode, hem de dir a les Qt que volem crear més files en el model. Això es fa cridant a beginInsertRows() per a començar la nostra operació d'addició de files, seguida d'inserir el que necessitem, i després utilitzeu endInsertRows() per a acabar l'operació. No obstant això, encara hem d'emetre dataChanged() al final. Aquesta vegada, actualitzarem totes les files, des de la primera fila fins a l'última, ja que el QMap pot reorganitzar-se alfabèticament, i hem d'agafar-ho a totes les files.

En cridar beginInsertRows(), primer hem de passar en una classe QModelIndex per a especificar la ubicació d'on s'han d'afegir les files noves, seguit de quins seran els números nous de primera i última fila. En aquesta guia d'aprenentatge, el primer argument serà QModelIndex(), ja que no cal utilitzar el paràmetre aquí. Només podem utilitzar la mida de fila actual per al primer i últim número de fila, ja que només afegirem una fila al final del 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));
}

Actualitzem el codi en QML, de manera que se'ns dona la possibilitat d'afegir una clau nova 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 = ""; // Netejar el TextField cada vegada que s'hagi fet
                addPrompt.close();
            }
        }
    }

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

Ara, se'ns ha de donar una acció nova a la part superior de l'aplicació que obri un indicador que permeti afegir un element nou al model, amb les nostres pròpies dades personalitzades.

app_screenshot_add_1.png
app_screenshot_add_2.png

Eliminar files

La manera d'eliminar files és similar a afegir files. Creem un altre mètode que cridarem en el QML. Aquesta vegada, utilitzarem un paràmetre addicional, i és un enter que és el número de fila. El nom de l'espècie s'utilitza per a suprimir la clau del QMap, mentre que el número de fila s'utilitzarà per a suprimir la fila en el frontal.

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

Ara, actualitzem l'aplicació de manera que apareixerà un botó «Elimina» al costat del botó d'edició, i enganxeu-lo al nostre mètode d'eliminació.

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

Codi complet

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

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

model.h
 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);
};

model.cpp
 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));
}

Més informació

Per a més informació, vegeu Using C++ Models with Qt Quick Views i Model/View Programming.