Modellen verbinden naar uw QML gebruikersinterface
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
.
Notitie
Als u dit volgt, onthoud om uw bestandCMakeLists.txt
bij te werken!#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.
Notitie
De aangepaste rolnamen aangemaakt metroleNames()
zijn alleen te gebruiken wanneer een model wordt gedelegeerd en zijn daar buiten niet bruikbaar. Zie Modellen en weergaven.Notitie
Technisch worden modellen in Qt gerepresenteerd als tabellen, met rijen en kolommen. Dus, wat overschrijven vanrowCount()
doet aan Qt vertellen hoeveel rijen er in een model zitten. Omdat we van doen hebben met met een een-dimensionaal array in deze inleiding, kunt u gewoon denken aan "rijen" als "aantal elementen."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
}
}
}
}
}
}
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 zijnQt::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;
}
Notitie
setData()
stuurt niet automatisch dataChanged()
uit en dat moet handmatig gedaan worden.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.
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));
}
Notitie
De functiedataChanged()
gebruikt QModelIndex als het type gegeven voor zijn parameters. We kunnen echter gehele getallen in QModelIndex typengegevens converteren met de functie index()
.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.
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);
}
}
}
}
}
}
}
}
Volledige code
Main.qml
|
|
model.h
|
|
model.cpp
|
|
Meer informatie
Voor meer informatie, zie Using C++ Models with Qt Quick Views en Model/Weergave programmeren.