Ansluta modeller till QML-användargrä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
.
Anmärkning
Om du följer med, kom ihåg att uppdatera filenCMakeLists.txt
.#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.
Anmärkning
De anpassade rollnamnen som skapats avroleNames()
är endast användbara när en modell delegeras och är inte användbara utanför den. Se Modeller och vyer.Anmärkning
Tekniskt sett representeras modeller i Qt som tabeller, med rader och kolumner. Så vad att överskridarowCount()
gör är att tala om för Qt hur många rader som finns i en modell. Eftersom vi bara har att göra med ett endimensionellt fält i handledning, kan man bara föreställa sig "rader" som "antal element".Ö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
}
}
}
}
}
}
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 varaQt::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;
}
Anmärkning
setData()
skickar inte automatiskt dataChanged()
så det måste fortfarande göras manuellt.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.
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));
}
Anmärkning
FunktionendataChanged()
använder QModelIndex som datatyp för sina parametrar. Dock kan vi konvertera heltal i QModelIndex datatyper med användning av funktionen index()
.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.
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);
}
}
}
}
}
}
}
}
Hela koden
Main.qml
|
|
model.h
|
|
model.cpp
|
|
Mer information
För mer information, se Using C++ Models with Qt Quick Views och Model/View Programming.