Modellen verbinden naar uw QML gebruikersinterface

Modellen verbinden uit de C++-backend naar de QML-frontend

Zoals getoond in de vorige inleiding, kunt u C++ code verbinden met QML door een klasse aan te maken die behandeld zal worden als nog een component in QML. U zou echter meer gecompliceerde gegevens willen representeren, zoals gegevens die moeten acteren als een klant ListModel of op de een of andere manier gedelegeerd moeten worden uit een Repeater.

We kunnen onze eigen modellen aanmaken vanaf de C++ kant en declareren hoe de gegevens uit dat model gerepresenteerd moeten worden op de QML-frontend.

De klasse voorbereiden

In deze inleiding zullen we een klasse aanmaken die een QMap bevat, waar een QString wordt gebruikt als een sleutel en QStringList objecten gebruikt worden als waarden. De frontend zal in staat zijn de sleutels en de waarden te lezen en te tonen en eenvoudig te gebruiken net als een een-dimensionaal array. Het zou er uit moeten zien als een QML-ListModel.

Om dit te doen moeten we een klasse declareren die erft van QAbstractListModel. Laten we ook enige toegevoegde gegevens invoegen in de QMap. Deze declaraties zullen gelokaliseerd zijn in 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"}}
    };
};

Natuurlijk kunnen we deze klasse niet gewoon tonen zoals deze is. We moeten aan QML ook vertellen hoe deze gegevens in de klasse te representeren. We kunnen dit doen door drie virtuele functies te overschrijven die essentieel zijn om dit te doen, allen zullen hun eigen taken doen.

  • rowCount() - denk aan deze functie als een manier om QML te vertellen hoeveel items er in het model zijn om te representeren.
  • roleNames() - u kunt denken aan rolnamen als eigenschapnamen gekoppeld aan gegevens in QML. Deze functie biedt u het aanmaken van die rollen.
  • data() - deze functie wordt aangeroepen wanneer u de gegevens, die corresponderen met de rolnamen uit het model, wilt ophalen.

Overschrijven en implementeren rowCount()

Laten we de functie in het kopbestand overschrijven. De rowCount() komt met zijn eigen parameter, maar zal niet gebruikt worden in dit voorbeeld en wordt uitgesloten.

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

Laten we daarna declareren hoeveel rijen er in dit model in model.cpp zijn.

#include "model.h"

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

Overschrijven en implementeren roleNames()

Voordat we roleNames() overschrijven, moeten we declareren wat de rollen zijn aan de C++ kan met gebruik van een publieke enum variabele. De reden hiervoor is omdat deze waarden uit de variabele enum doorgegeven worden in data() elke keer dat QML toegang pakt tot een bijbehorende rol en als zodanig kunnen we data() terug laten geven wat we willen.

Laten we beginnen met de variabele enum voor rollen aan te maken, waar elke waarde is een rol is voor de C++ kant.

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

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

Nadat we dat hebben vastgesteld kunnen we tenslotte aanmaken wat deze rollen zijn aan de kant an QML met een QHash waar de sleutels de enumerated waarden zijn gepaard met QByteArrays. De tekst in de QByteArray is wat wordt gebruikt in de actuele QML code.

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

In ons voorbeeldmodel kan de rol "soorten" gebruikt worden om de QString-sleutel "Feline", "Fox", "Goat" op te halen, elk in een gescheiden gedelegeerde. Hetzelfde kan gedaan worden met de QStringList waarden voor de karakternamenlijst.

Overschrijven en implementeren data()

Er zijn twee parameters die doorgegeven worden in data(): index en role. index is de locatie waar de gegevens zijn wanneer ze gedelegeerd worden. Zoals eerder gesteld, role wordt gebruikt door QML om specifieke gegevens terug te krijgen wanneer er toegang toe wordt gevraagd als rol.

In data() kunnen we een switch statement gebruiken om de toepasselijke gegevens en gegevenstype terug te geven afhankelijk van de rol, wat mogelijk is als data() een QVariant teruggeeft. We moeten echter nog steeds zeker maken dat we de toepasselijke locatie van de gegevens krijgen. In dit onderstaande voorbeeld kunt u zien dat een nieuwe iterator-variabele gedeclareerd wordt, die is gezet vanaf het begin van de lijst plus de rij van de index en de gegevens waar de iterator naar wijst is wat wordt teruggegeven.

We kunnen echter niet gewoon teruggeven welke gegevens dan ook die we willen. We kunnen proberen gegevens te binden aan een eigenschap met een niet compatibel type gegeven, zoals een QStringList aan een QString. U moet misschien conversie van gegevens doen om het gegeven juist weer te geven.

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

De klasse toestaan gedeclareerd te worden in QML

Laten we niet vergeten om onze klasse bruikbaar te maken in QML.

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

Gebruik van klasse in QML

Het QML-bestand dat wordt gebruikt zal slechts drie Kirigami.AbstractCard componenten bevatten, waar de sleutel de kop is en de waarde de inhoud. Deze kaarten worden aangemaakt door het delegeren van AbstractCard met gebruik van een Repeater, waar het aangepaste model dat we aanmaakten als het model acteert. De toegang tot gegevens is via het woord model, gevolgd door de rollen gedeclareerd in 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
                    }
                }
            }
        }
    }
}

App Screenshot

Modificatie van gegevens

Gegevens bewerken met gebruik van dataChanged() en setData()

U kunt een situatie tegenkomen waar u de gegevens in het model wilt wijzigen en de wijzigingen gereflecteerd wilt zien aan de kant van de frontend. Elke keer dat we gegevens in het model wijzigen, moeten we het signaal dataChanged() uitzenden die die wijzigingen toepast aan de kant van het frontend in de specifieke cellen gespecificeerd in zijn argumenten. In deze inleiding, kunnen we gewoon het argument index van setData() gebruiken.

setData() is een virtuele functie die u kunt overschrijven zo dat pogen de gegevens vanuit de kant van de frontend te wijzigen automatisch deze wijzigingen aan de kant van de backend reflecteert. Het vereist drie parameters:

  • index - de locatie van de gegevens.
  • waarde - de inhoud van de nieuwe gegevens.
  • role - in deze context, de rol hier wordt gebruikt om weergaven te vertellen hoe ze de gegevens zouden moeten behandelen. De rol hier zou moeten zijn Qt::EditRole.

De parameter role in dit geval wordt gebruikt om te verzekeren dat setData() kan worden bewerkt via invoer van de gebruiker (Qt::EditRole). Door index te gebruiken kunnen we dat gebruiken om de locatie te bepalen waar de gegevens zouden worden bewerkt met de inhoud van 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;
}

Laten we de QML-code bijwerken zodat we een prompt kunnen openen die ons biedt het model te bewerken met een Controls.Button aangekoppeld aan de kaarten.

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

Nu, wanneer de waarden van het model in de frontend wijzigen, zouden de automatisch in de backend moeten worden bijgewerkt.

app_screenshot_1.png
app_screenshot_2.png

Het toevoegen van rijen

We hebben een manier toegevoegd om de gegevens te wijzigen in bestaande sleutels van de QMap en in de frontend, dit wordt gereflecteerd als wijzigen van de inhoud in de AbstractCards. Maar wat als we een nieuwe sleutelitem in de QMap willen toevoegen en dat gereflecteerd moet worden aan the QML-kant? Laten we dat doen door een nieuwe methode aan te maken die op te roepen is aan the QML-kant om deze taak uit te voeren.

Om de methode zichtbaar te maken in QML moeten we de Q_OBJECT macro gebruiken in de klasse en de declaratie van de methode beginnen met de macro Q_INVOKABLE. Deze methode zal ook een tekenreeksparameter omvatten, die bedoeld is om de nieuwe sleutel in de QMap te zijn.

class Model : public QAbstractListModel {
Q_OBJECT;

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

Binnen deze methode moeten we aan Qt vertellen dat we meer rijen in het model aan willen maken. Dit wordt gedaan door beginInsertRows() aan te roepen om onze rij toevoegen te beginnen, gevolgd door in te voegen wat we nodig hebben, gebruik daarna endInsertRows() om de bewerking te beëindigen. We moeten echter nog steeds dataChanged() aan het einde uitsturen. Deze keer gaan we alle rijen bijwerken, vanaf de eerste rij tot de laatste omdat de QMap zichzelf alfabetisch zal reorganiseren en we dat over alle rijen moeten vangen.

Bij aanroepen van beginInsertRows() moeten we eerst een QModelIndex klasse doorgeven om te de locatie te specificeren waar de nieuwe rijen toegevoegd zouden moeten worden, gevolgd door wat de nieuwe eerste en laatste rijnummers gaan worden. In deze inleiding zal het eerste argument gewoon QModelIndex() zijn omdat er geen noodzaak is de parameter hier te gebruiken. We kunnen gewoon de huidige rijgrootte voor het eerste en laatste rijnummer gebruiken, omdat we eenvoudig één rij aan het eind van het model zullen toevoegen.

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

Laten we de QML code bijwerken zodat we de mogelijkheid geven om een nieuwe sleutel aan de QMap toe te voegen.

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 = ""; // Wis het tekstveld elke keer dat het is gedaan
                addPrompt.close();
            }
        }
    }

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

Nu zouden een nieuwe actie bovenaan de toepassing moeten geven die een prompt laat verschijnen waarmee een nieuw element aan het model kan worden toegevoegd, met onze eigen aangepaste gegevens.

app_screenshot_add_1.png
app_screenshot_add_2.png

Rijen verwijderen

De manier om rijen te verwijderen is gelijk aan rijen toevoegen. Laten we een andere methode aanmaken die we in QML zullen aanroepen. Deze keer zullen we een extra parameter gebruiken en dat is een geheel getal die het rijnummer is. De naam ervan wordt gebruikt om de sleutel uit de QMap te verwijderen, terwijl het rijnummer gebruikt zal worden om de rij in de frontend te verwijderen.

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

Laten we nu de toepassing bijwerken zodat een knop "Verwijderen" verschijnt naast de knop Bewerken en verbind het met uw methode voor verwijderen.

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

Volledige code

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

Meer informatie

Voor meer informatie, zie Using C++ Models with Qt Quick Views en Model/Weergave programmeren.