Konektu modelojn al via uzantinterfaco de QML

Konekti modelojn de C++ backend al QML fasado

Kiel montrite de la antaŭa lernilo, vi povas konekti C++-kodon al QML kreante klason, kiu estos traktata kiel nur alia komponanto en QML. Tamen, vi eble volas reprezenti pli komplikajn datumojn, kiel datumojn, kiuj devas funkcii kiel propra ListModel aŭ iel devas esti delegitaj de Ripetilo.

Ni povas krei niajn proprajn modelojn de la C++-flanko, kaj deklari kiel la datumoj de tiu modelo devus esti reprezentitaj sur la QML-fasado.

Preparante la Klason

En ĉi tiu lernilo, ni kreos klason, kiu enhavas QMap, kie QString estas uzata kiel ŝlosilo kaj QStringList-objektoj estas uzataj kiel valoroj. La fasado povos legi kaj montri la ŝlosilojn kaj valorojn kaj esti simpla uzebla same kiel unudimensia tabelo. Ĝi devus aspekti simila al QML ListModel.

Por fari tion, ni devas deklari klason, kiu heredas de QAbstractListModel. Ni ankaŭ aldonu kelkajn aldonajn datumojn al la QMap. Ĉi tiuj deklaroj troviĝos 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"}}
    };
};

Kompreneble, ni ne povas simple montri ĉi tiun klason kiel estas. Ni ankaŭ devas diri al QML pri kiel reprezenti ĉi tiujn datumojn en la klaso. Ni povas fari tion per superregado de tri virtualaj funkcioj, kiuj estas esencaj por fari ĉi tion, kiuj ĉiuj faras siajn proprajn taskojn.

  • rowCount() - Pensu pri ĉi tiu funkcio kiel maniero diri al QML kiom da eroj estas en la modelo por reprezenti.
  • roleNames() - Vi povas pensi pri rolnomoj kiel proprecnomoj ligitaj al datumoj en QML. Ĉi tiu funkcio permesas krei tiujn rolojn.
  • data() - Ĉi tiu funkcio estas vokita kiam vi volas preni la datumojn kiuj respondas al la rolnomoj de la modelo.

Anstataŭigi kaj Efektivigi rowCount()

Ni superregu la funkcion en la kapdosiero. La rowCount() venas kun sia propra parametro, sed ne estos uzata en ĉi tiu ekzemplo kaj estas ekskludita.

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

Tiam, ni deklaru kiom da vicoj estas en ĉi tiu modelo en model.cpp.

#include "model.h"

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

Anstataŭigi kaj Efektivigi roleNames()

Antaŭ ol ni superregas roleNames(), ni devas deklari kiajn rolojn estas en la C++-flanko uzante publikan enum variablon. La kialo de tio estas ĉar ĉi tiuj valoroj de la variablo enum estas transdonitaj en data() ĉiufoje kiam QML aliras respondan rolon, kaj kiel tia ni povas fari data() redoni kion ni volas.

Ni komencu kreante la variablon enum por roloj, kie ĉiu valoro estas rolo por la C++-flanko.

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

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

Post kiam ni havas tion aranĝita, ni povas finfine krei kiajn ĉi tiujn rolojn estas en la QML-flanko uzante QHash kie la ŝlosiloj estas la listigitaj valoroj parigitaj kun QByteArrays. .html). La teksto en la QByteArray estas tio, kio estas uzata en la reala QML-kodo.

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

En nia ekzempla modelo, la rolo "specio" povas esti uzata por preni la QString-ŝlosilon "Feline", "Vulpo", "Kaprino", ĉiu en aparta delegito. La sama povas esti farita kun la QStringList-valoroj por la listo de signonomoj.

Anstataŭigi kaj Efektivigi data()

Estas du parametroj kiuj estas pasigitaj en data(): indekso kaj rolo. indekso estas la loko de kie la datumoj estas delegitaj. Kiel antaŭe dirite, "rolo" estas uzata de QML por ricevi specifajn datumojn resenditajn kiam ĝi aliras rolon.

En data(), ni povas uzi deklaron switch por redoni la taŭgajn datumojn kaj datumtipon depende de la rolo, kio eblas ĉar data() liveras QVariant. Ni ankoraŭ devas certigi, ke ni ricevas la taŭgan lokon de la datumoj, tamen. En ĉi tiu malsupra ekzemplo, vi povas vidi, ke nova iteratora variablo estas deklarita, kiu estas agordita de la komenco de la listo plus la vico de la indekso kaj la datumoj, kiujn la iteratoro montras, estas kio estas resendita.

Ni ne povas simple redoni kiajn ajn datumojn ni volas tamen. Ni eble provas ligi datumojn al propreco kun nekongrua datumtipo, kiel QStringList al QString. Vi eble devos fari datuman konvertiĝon por ke la datumoj estu montritaj ĝuste.

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

Permesu la Klason esti Deklarita en QML

Ni ne forgesu fari nian klason uzebla en QML.

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

Klasa Uzado en QML

La QML-dosiero uzata nur enhavos tri Kirigami.AbstractCard komponantojn, kie la ŝlosilo estas la kaplinio kaj la valoro estas la enhavo. Ĉi tiuj kartoj estas kreitaj per delegado de Abstrakta Karto uzante Ripetilon, kie la propra modelo, kiun ni kreis, funkcias kiel la modelo. La datumoj estas aliritaj per vorto modelo, sekvata de la roloj, kiujn ni deklaris en 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
                    }
                }
            }
        }
    }
}

Aplika Ekrankopio

Modifo de Datumoj

Redaktante Datumojn Uzante dataChanged() kaj setData()

Vi povas renkonti situacion kie vi volas modifi datumojn en la modelo, kaj havi la ŝanĝojn reflektitaj sur la fasado. Ĉiufoje kiam ni ŝanĝas datumojn en la modelo, ni devas elsendi la signalon dataChanged(), kiu aplikos tiujn ŝanĝojn ĉe la fasado ĉe la specifaj ĉeloj specifitaj en ĝiaj argumentoj. En ĉi tiu lernilo, ni povas simple uzi la argumenton indekso de setData().

setData() estas virtuala funkcio, kiun vi povas superregi, por ke provi modifi la datumojn de la fasa flanko aŭtomate reflektas tiujn ŝanĝojn ĉe la malantaŭa flanko. Ĝi postulas tri parametrojn:

  • index - La loko de la datumoj.
  • valoro - La enhavo de la novaj datumoj.
  • rolo - En ĉi tiu kunteksto, la rolo ĉi tie estas uzata por diri al vidoj kiel ili devas manipuli datumojn. La rolo ĉi tie devus esti Qt::EditRole.

La parametro role ĉi-kaze estas uzata por certigi ke setData() povas esti redaktita per uzanta enigo (Qt::EditRole). Uzante indekso', ni povas uzi tion por determini la lokon de kie la datumoj devas esti redaktitaj kun la enhavo de valoro`.

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

Ni ĝisdatigu la QML-kodon por ke ni povu malfermi promptilon, kiu ebligas al ni redakti la modelon per Controls.Button alkroĉita al la kartoj.

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

Nun, kiam ajn la valoroj de la modelo ŝanĝiĝas en la fasado, la ŝanĝoj aŭtomate devas ĝisdatigi en la backend.

app_screenshot_1.png
app_screenshot_2.png

Aldonante Vicoj

Ni aldonis manieron modifi la datumojn en ekzistantaj ŝlosiloj de la QMap, kaj en la antaŭa fino, ĉi tio estas reflektita kiel modifado de la enhavo ene de la AbstractCards. Sed kio se ni bezonas aldoni novan ŝlosilan eniron en la QMap kaj tion reflektas sur la QML-flanko? Ni faru tion kreante novan metodon, kiu estas vokebla ĉe la QML-flanko por plenumi ĉi tiun taskon.

Por igi la metodon videbla en QML, ni devas uzi la Q_OBJECT-makroon en la klaso, kaj komenci la metododeklaron per la Q_INVOKABLE-makroo. Ĉi tiu metodo ankaŭ inkluzivos ĉenparametron, kiu celas esti la nova ŝlosilo en la QMap.

class Model : public QAbstractListModel {
Q_OBJECT;

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

Ene de ĉi tiu metodo, ni devas diri al Qt, ke ni volas krei pli da vicoj en la modelo. Ĉi tio estas farita per vokado beginInsertRows() por komenci nian vicon aldonante operacion, sekvita per enmeto kion ajn ni bezonas, tiam uzu endInsertRows() por fini la operacion. Ni ankoraŭ bezonas elsendi dataChanged() fine, tamen. Ĉi-foje, ni ĝisdatigos ĉiujn vicojn, de la unua vico ĝis la lasta ĉar la QMap povas alfabete reorganizi sin, kaj ni devas kapti tion tra ĉiuj vicoj.

Kiam vi vokas beginInsertRows(), ni unue devas eniri QModelIndex-klason por specifi la lokon de kie la novaj vicoj devas esti aldonitaj, sekvitaj de kio la novaj unua kaj lasta vico-numeroj estos. En ĉi tiu lernilo, la unua argumento estos nur QModelIndex() ĉar ne necesas uzi la parametron ĉi tie. Ni povas simple uzi la nunan vicon por la unua kaj lasta vico-numero, ĉar ni nur aldonos unu vicon ĉe la fino de la modelo.

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

Ni ĝisdatigu la QML-kodon, por ke ni ricevu la eblon aldoni novan ŝlosilon al la 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 = ""; // Klarigi TextField ĉiufoje kiam ĝi estas farita
                addPrompt.close();
            }
        }
    }

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

Nun, ni devus ricevi novan agon ĉe la supro de la aplikaĵo, kiu aperigas prompton, kiu permesas aldoni novan elementon al la modelo, kun niaj propraj propraj datumoj.

app_screenshot_add_1.png
app_screenshot_add_2.png

Forigante Vicojn

La maniero forigi vicojn similas al aldoni vicojn. Ni kreu alian metodon, kiun ni nomos en QML. Ĉi-foje, ni uzos plian parametron, kaj tio estas entjero, kiu estas la vico-numero. La specionomo estas uzata por forigi la ŝlosilon de la QMap, dum la vico-numero estos uzata por forigi la vicon ĉe la antaŭa fino.

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

Nun ni ĝisdatigu la aplikaĵon, tiel ke butono "Forigi" aperos apud la redaktbutono, kaj ligu ĝin al nia foriga metodo.

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

Plena Kodo

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

Pliaj Informoj

Por pliaj informoj, vidu Uzante C++-Modelojn kun Qt Rapidaj Vidoj kaj Modelo/Vida Programado.