Ansluta modeller till QML-användargränssnittet

Ansluta modeller från C++ bakgrundsprogrammet till QML-gränssnittet

Som framgår av föregående handledning, kan C++-kod anslutas till QML genom att skapa en klass som behandlas som bara en annan komponent i QML. Däremot kanske man vill representera mer komplicerad data, till exempel data som måste fungera som en anpassad ListModel eller på något sätt måste delegeras från en Repeater.

Vi kan skapa våra egna modeller från C++ sidan, och deklarera hur data från modellen ska representeras i QML-gränssnittet.

Förbereda klassen

I den här handledningen skapar vi en klass som innehåller en QMap, där en QString används som nyckel och ett QStringList-objekt används som värde. Gränssnittet kan läsa och visa nycklar och värden och vara enkelt att använda precis som ett endimensionellt fält. Det liknar en ListModel i QML.

För att göra det måste vi deklarera en klass som ärver från QAbstractListModel. Låt oss också lägga till lite data i vår QMap. Deklarationerna finnas i 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"}}
    };
};

Naturligtvis kan vi inte bara visa klassen som den är. Vi måste också tala om för QML hur man representerar data i klassen. Vi kan göra det genom att överskrida tre virtuella funktioner som är viktiga för att göra det, vilka alla har sina egna uppgifter.

  • rowCount(): Se den här funktionen som ett sätt att tala om för QML hur många objekt som ska representeras i modellen.
  • roleNames(): Du kan tänka på rollnamn som egenskapsnamn kopplade till data i QML. Funktionen låter dig skapa dessa roller.
  • data(): Funktionen anropas när man vill hämta data som motsvarar rollnamnen från modellen.

Överskrida och implementera rowCount()

Låt oss överskrida funktionen i deklarationsfilen. rowCount() har sin egen parameter, men användas inte i det här exemplet och exkluderas.

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

Låt oss därefter deklarera hur många rader som finns i modellen i model.cpp.

#include "model.h"

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

Överskrida och implementera roleNames()

Innan vi överskrider roleNames() måste vi deklarera vad rollerna är på C++ sidan med hjälp av en öppen enum. Anledningen till det är att värdena från variabeln enum skickas till data() varje gång QML hämtar en motsvarande roll, och på så sätt kan vi få data() att returnera vad vi vill.

Låt oss börja med att skapa variabeln enum för roller, där varje värde är en roll för C++ sidan.

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

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

När vi väl har löst det kan vi äntligen skapa vad rollerna är på QML-sidan med hjälp av QHash där nycklarna är uppräkningsvärdena parade med [QByteArrays](docs:qtcore;qbytearray .html). Texten i QByteArray är vad som används i den faktiska QML-koden.

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

I vår exempelmodell kan rollen "species" användas för att hämta QString-nyckeln "Feline", "Fox", "Goat", var och en i en separat delegat. Detsamma kan göras med QStringList-värdena i teckennamnlistan.

Överskrida och implementera data()

Det finns två parametrar som skickas med data(): index och role. "index" är platsen där data finns vid delegering. Som tidigare nämnts används role av QML för att få specifik data returnerad när en roll används.

I data() kan vi använda switch för att returnera lämplig data och datatyp beroende på rollen, vilket är möjligt eftersom data() returnerar QVariant. Vi måste dock se till att vi får rätt plats för data. I exemplet nedan visas att en ny iterationsvariabel deklareras, vilken initieras från början av listan plus indexets rad, och data som variabeln pekar på är det som returneras.

Vi kan dock inte bara returnera vilken data vi vill. Vi kanske försöker koppla data till en egenskap med en inkompatibel datatyp, till exempel en QStringList till en QString. Man kan behöva göra datakonvertering för att data ska visas korrekt.

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

Tillåt att klassen kan deklareras i QML

Låt oss inte glömma bort att göra klassen användbar i QML.

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

Klassanvändning i QML

QML-filen som används innehåller bara tre Kirigami.AbstractCard komponenter, där nyckeln är rubriken och värdet är innehållet. Korten skapas genom att delegera ett AbstractCard med hjälp av en Repeater, där den anpassade modellen vi skapade fungerar som modell. Data nås med hjälp av ordet model, följt av de roller vi deklarerade i 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
                    }
                }
            }
        }
    }
}

Skärmbild av programmet

Datamodifikation

Redigera genom att använda dataChanged() och setData()

Man kan stöta på en situation där man vill ändra data i modellen och få ändringarna att reflekteras på gränssnittssidan. Varje gång vi ändrar data i modellen måste vi skicka signalen dataChanged() som verkställer ändringarna på gränssnittssidan för de specifika cellerna som anges i dess argument. I den här handledningen kan vi bara använda argumentet index i setData().

setData() är en virtuell funktion som går att överskrida så att försök att modifiera data från gränssnittssidan automatiskt återspeglar ändringarna på bakgrundssidan. Det kräver tre parametrar:

  • index: Platsen för data.
  • value: Det nya datainnehållet.
  • role: I det här sammanhang används rollen för att tala om för vyer hur de ska hantera data. Rollen ska vara Qt::EditRole här.

Parametern role används i detta fall för att säkerställa att setData() kan redigeras via användarinmatning (Qt::EditRole). Genom att använda index, kan vi bestämma platsen där data ska redigeras med innehållet i 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;
}

Låt oss uppdatera QML-koden så att vi kan öppna en prompt som låter oss redigera modellen med hjälp av en Controls.Button kopplad till korten.

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, när värdena för modellen än ändras i gränssnittet, ska ändringarna automatiskt uppdateras i bakgrundsprogrammet.

app_screenshot_1.png
app_screenshot_2.png

Lägga till rader

Vi har lagt till ett sätt att modifiera data i befintliga nycklar i QMap, och i gränssnittet återspeglas det som att modifiera innehållet inne i AbstractCards. Men vad händer om vi behöver lägga till en ny nyckelpost i QMap och få den att återspeglas på QML-sidan? Låt oss göra det genom att skapa en ny metod som kan anropas på QML-sidan för att utföra uppgiften.

För att göra metoden synlig i QML måste vi använda makrot Q_OBJECT i klassen och börja metoddeklarationen med makrot Q_INVOKABLE. Metoden inkluderar också en strängparameter, som är avsedd att vara den nya nyckeln i QMap.

class Model : public QAbstractListModel {
Q_OBJECT;

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

Inne i metoden måste vi tala om för Qt att vi vill skapa flera rader i modellen. Det görs genom att anropa beginInsertRows() för att påbörja vår operation för att lägga till rader, följt av att infoga det vi behöver, och sedan använda endInsertRows() för att avsluta operationen. Vi behöver dock fortfarande skicka dataChanged() i slutet. Den här gången uppdaterar vi alla rader, från den första raden till den sista eftersom QMap kan ordna om sig själv alfabetiskt, och vi måste hantera det för alla rader.

När vi anropar beginInsertRows() måste vi först skicka in en QModelIndex-klass för att ange platsen där de nya raderna ska läggas till, följt av vad de nya första och sista radnumren blir. I handledning är det första argumentet bara att QModelIndex() eftersom det inte finns något behov av att använda parametern här. Vi kan bara använda den aktuella radstorleken för första och sista radnumret, eftersom vi bara lägger till en rad i slutet av modellen.

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

Låt oss uppdatera QML-koden så att vi får möjlighet att lägga till en ny nyckel till 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 = ""; // Rensa TextField varje gång det är klart
                addPrompt.close();
            }
        }
    }

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

Nu bör vi få en ny åtgärd längst upp i programmet som ger en prompt som gör det möjligt att lägga till ett nytt element till modellen, med våra egna anpassade data.

app_screenshot_add_1.png
app_screenshot_add_2.png

Ta bort rader

Sättet att ta bort rader liknar att lägga till rader. Låt oss skapa en annan metod som vi anropar från QML. Den här gången använder vi en extra parameter, och det är ett heltal som anger radnumret. Artnamnet används för att radera nyckeln från QMap, medan radnumret används för att radera raden i gränssnittet.

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

Låt oss nu uppdatera programmet så att knappen "Ta bort" visas bredvid redigeringsknappen och koppla upp den till vår borttagningsmetod.

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

Hela koden

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

Mer information

För mer information, se Using C++ Models with Qt Quick Views och Model/View Programming.