Povežite modele z vašim uporabniškim vmesnikom QML

Povežite modele iz zaledja C++ z začeljem QML

Kot je prikazano v prejšnjem učbeniku, lahko kodo C++ povežete s QML tako, da ustvarite arazred, ki bo obravnavan kot le še ena komponenta v QML. Vendar pa morda želite predstaviti bolj zapletene podatke, kot so podatki, ki morajo delovati kot ListModel ali jih je potrebno nek način je treba delegirati iz Repeater .

Ustvarimo lahko lastne modele s strani C++ in navedite, kako naj bodo podatki iz tega modela predstavljeni na vmesniku QML.

Priprava razreda

V tem učbeniku bomo ustvarili razred, ki vsebuje QMap, kjer se QString uporablja kot ključ, objekti QStringList pa kot vrednosti. Začelje bo lahko prebralo in prikazalo ključe in vrednosti ter bo preprosto za uporabo kot enodimenzionalni niz. Videti mora biti podobno kot QMLListModel.

Da bi to naredili, moramo deklarirati razred, ki deduje od QAbstractListModel . Dodajmo še nekatere dodatne podatke v QMap. Te deklaracije se bodo nahajale v 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"}}
    };
};

Seveda tega razreda ne moremo prikazati takšnega, kot je. Povedati moramo tudi QML o tem, kako te podatke predstaviti v razredu. To lahko storimo s preglasitvijo treh virtualnih funkcij, ki so bistvenega pomena za to, in za vse svoje naloge.

  • rowCount() – Zamislite si to funkcijo kot način, kako QML povedati, koliko elementovje v modelu, ki ga predstavljajo.
  • roleNames() – Imena vlog si lahko predstavljate kot imena lastnosti, priloženapodatkom v QML. Ta funkcija vam omogoča ustvarjanje teh vlog.
  • data() – Ta funkcija se pokliče, ko želite pridobiti podatke, ki ustrezajo imenom vlog iz modela.

Preglasitev in implementacija rowCount()

Preglasimo funkcijo v datoteki glave. Funkciji rowCount() je priloženlasten parameter, vendar v tem primeru ne bo uporabljen in je izključen.

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

Nato deklarirajmo, koliko vrstic je v tem modelu v model.cpp.

#include "model.h"

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

Preglasitev in implementacija roleNames()

Preden preglasimo roleNames(), moramo deklarirati, v čem so vloge strani C++ z uporabo javne spremenljivke enum. Razlog za to je, ker se te vrednosti iz spremenljivke enum vsakič posredujejo v data() kadar QML dostopa do ustrezne vloge in tako lahko vrnemo prek data() kar želimo.

Začnimo z ustvarjanjem spremenljivke enum za vloge, kjer je vsaka vrednost vloga za stran C++.

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

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

Ko bomo to uredili, lahko končno ustvarimo, v čem so te vloge strani QML z uporabo QHash , kjer so ključi oštevilčene vrednosti v paru z QByteArrays . Besedilo v QByteArray je tisto, kar se uporablja v dejanski kodi QML.

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

V našem vzorčnem modelu lahko vlogo "species" uporabite za pridobitevQString ključ "Feline", "Fox", "Goat", v vsakem ločenem delegatu. Enako lahko storite z vrednostmi QStringList za seznam imen znakov.

Preglasitev in implementacija data()

V data() sta posredovana dva parametra: index in role. indeks je lokacija, kjer so podatki, ko so delegirani. Kot kot je bilo že navedeno, QML uporablja role - vlogo za vrnitev določenih podatkov, kodostopa do vloge.

V data() lahko uporabimo stavek switch, da vrnemo ustrezne podatke in podatkovne tipe, odvisno od vloge, kar je mogoče, saj data() vrne QVariant . Še vedno se moramo prepričati, da dobimo primerno lokacijo podatkov. V tem primeru spodaj lahko viditeda se deklarira nova spremenljivka iteratorja, ki je nastavljena iz začetka seznama plus vrstica indeksa in podatki, ki jih iterator, na katerega kaže prav tisto, kar je vrnjeno.

Ne moremo pa kar tako vrniti podatkov, ki jih želimo. Morda se poskušamo zavezatipodatkov v lastnost z nezdružljivim tipom podatkov, kot je QStringList vQString. Morda boste morali izvesti pretvorbo podatkov, da bodo podatki lahkoprikazani pravilno.

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

Dovolite, da se razred deklarira v QML

Ne pozabimo narediti našega razreda uporabnega v QML.

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

Uporaba razreda v QML

Uporabljena datoteka QML bo vsebovala samo tri komponente Kirigami.AbstractCard , kjer je ključ glava in vrednost vsebina. Te kartice so ustvarjene z delegiranjem AbstractCard z uporabo Repeaterja, kjer model po meri, ki smo ga ustvarili, deluje kotmodel. Do podatkov dostopamo z besedo model, ki ji sledijo vloge deklarirane v 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

Spreminjanje podatkov

Urejanje podatkov z uporabo dataChanged() in setData()

Lahko se zgodi, da želite spremeniti podatke v modelu, in naj se spremembe odražajo na začelju. Vsakič, ko spremenimo podatke v modelu, moramo oddati signal dataChanged(), ki bo uveljavil te spremembe na sprednji strani v posebnih celicah, ki so določene v njegovih argumentih. V tem učbeniku lahko samo uporabimo argument index od setData().

setData() je navidezna funkcija, ki jo lahko preglasite, tako da poskus spreminjanja podatkov na začelju samodejno odraža te spremembe na ozadju. Zahteva tri parametre:

  • index – Lokacija podatkov.
  • value – Vsebina novih podatkov.
  • role – V tem kontekstu se role - vloga tukaj uporablja, da pogledom pove, kako morajo ravnati s podatki. Vloga tukaj bi morala biti Qt::EditRole.

Parameter role se v tem primeru uporablja za zagotovitev, da je setData() lahkourejana z uporabniškim vnosom (Qt::EditRole). Z uporabo indexa lahko to uporabimo zadoločanje lokacije, kjer naj bodo podatki urejani z vsebino 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;
}

Posodobimo kodo QML, da bomo lahko odprli poziv, ki nam to omogoča urejanje modela z gumbom Controls.Button, ki je pritrjen na kartice.

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

Zdaj, ko se vrednosti modela spremenijo v sprednjem delu, se morajo spremembe samodejno posodobiti v zadnjem delu.

app_screenshot_1.png
app_screenshot_2.png

Dodajanje vrstic

Dodali smo način za spreminjanje podatkov v obstoječih ključih QMap in v začelju, se to odraža kot spreminjanje vsebine znotraj AbstractCards. Kaj pa, če moramo dodati nov ključni vnos v QMap inse je to odraža na strani QML? Naredimo to tako, da ustvarimo novo metodo, ki jo je mogoče priklicati na strani QML za izvedbo te naloge.

Da bi bila metoda vidna v QML, moramo uporabiti makro Q_OBJECT v razredu in začeti deklaracijo metode z makrom Q_INVOKABLE. Ta metoda bo vključevala tudi parameter niza, ki naj bi bil novi ključ v QMap.

class Model : public QAbstractListModel {
Q_OBJECT;

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

Znotraj te metode moramo Qt-ju povedati, da želimo ustvariti več vrstic v modelu. To naredimo tako, da pokličemo beginInsertRows() za začetek naše vrsticeoperacija dodajanja, ki ji sledi vstavljanje vsega, kar potrebujemo, nato uporabaendInsertRows() za končanje operacije. Še vedno moramo oddajatidataChanged() na koncu pa. Tokrat bomo posodobili vse vrstice, od prve vrstice do zadnje, kot lahko QMap po abecedi se reorganizira in to moramo ujeti v vseh vrsticah.

Ko kličemo beginInsertRows(), moramo najprej posredovati razred QModelIndex, da določite lokacijo, kamor naj bodo dodane nove vrstice, ki ji sledijo nove številke prve in zadnje vrstice. V tem učbeniku, bo prvi argument samo QModelIndex(), ker ne ni potrebe uporabiti parametra. Uporabimo lahko samo trenutno velikost vrstice za številko prve in zadnje vrstice, saj bomo dodali samo eno vrstico na koncumodela.

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

Posodobimo kodo QML, da bomo lahko dodali nov ključ v 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 = ""; // Počisti TextField vsakič, ko je narejeno
                addPrompt.close();
            }
        }
    }

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

Zdaj bi morali dobiti novo dejanje na vrhu aplikacije, ki prikaže poziv, ki omogoča dodajanje novega elementa s podatki v model po lastni meri.

app_screenshot_add_1.png
app_screenshot_add_2.png

Odstranjevanje vrstic

Način odstranjevanja vrstic je podoben dodajanju vrstic. Ustvarimo drugo metodo, ki jo bomo poklicali v QML. Tokrat bomo uporabili dodaten parameter in to je celo število, ki je številka vrstice. Ime vrste se uporablja za brisanje ključa iz QMap, medtem ko bo številka vrstice uporabljena za brisanjevrstice na začelju.

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

Zdaj pa posodobimo aplikacijo, da se gumb »Izbriši« prikaže poleg gumba za urejanje in ga povežimo z našo metodo brisanja.

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

Celotna koda

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

Več informacij

Za več informacij glejte Uporaba modelov C++ s hitrimi pogledi Qt in Model/View Programming .