Ansluta C++ modeller till QML-användargränssnittet
Data 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.
Det rekommenderas starkt att du läser handledningen Listvyer innan den här.
globalDrawer:Kirigami.GlobalDrawer{isMenu:trueactions:[Kirigami.Action{text:i18n("Exposing to QML")icon.name:"kde"onTriggered:pageStack.push(Qt.createComponent("org.kde.tutorial.components","ExposePage"))},Kirigami.Action{text:i18n("C++ models in QML")icon.name:"kde"onTriggered:pageStack.push(Qt.createComponent("org.kde.tutorial.components","ModelsPage"))},Kirigami.Action{text:i18n("Quit")icon.name:"application-exit-symbolic"shortcut:StandardKey.QuitonTriggered:Qt.quit()}]}
Skapa sedan en ny src/components/ModelsPage.qmlmed följande innehåll:
1
2
3
4
5
6
7
8
9
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"// ...
}
Och till sist lägg till det i src/components/CMakeLists.txt:
Det fungerar som arbetsyta för den här handledningssidan.
Använda obehandlade strängar
För att göra det lättare att förstå hur modellen är ifylld i den här handledningen inaktiverar vi en funktion som KDE-program som använder extra-cmake-modules (ECM) normalt använder, vilken optimerar strängkod. Det gör att vi slipper skriva QStringLiteral() varje gång en sträng introduceras i vår C++-kod, vilket är användbart för koden i den kommande deklarationsfilen.
Att inaktivera CMake-flaggan görs bara i förklaringssyfte. Produktionskod bör istället använda QStringLiteral() eller Qt string literals namnrymd där det är möjligt.
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 skapa 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.
Skapa två nya filer, src/components/model.h och src/components/model.cpp.
Lägg till de två nya filerna i src/components/CMakeLists.txt:
Lägg till följande som det initiala innehållet i src/components/model.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic: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 väsentliga virtuella funktioner:
rowCount(): Se den här funktionen som ett sätt att tala om för QML hur många objekt som modellen ska representera.
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 av roleNames() ä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 överskrida rowCount() 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. src/components/model.h. Funktionen rowCount() har sin egen parameter, men den användas inte i det här exemplet och behöver inte nämnas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:introwCount(constQModelIndex&)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
Låt oss därefter deklarera hur många rader som finns i modellen i src/components/model.cpp:
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 uppräkningsvärdena 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 uppräkningsvärden för roller src/components/model.h, där varje värde är en roll för C++ sidan.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};QHash<int,QByteArray>roleNames()constoverride;introwCount(constQModelIndex&)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};};
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). Det ska finnas i src/components/model.cpp. Texten i QByteArray är vad som används i den faktiska QML-koden.
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 till data(): index och role. "index" är positionen där data finns i modellen. 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. För det skapar vi en ny privat, statisk funktion vid namn formatList().
Det resulterar i följande kod i src/components/model.cpp:
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
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().
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{header:Kirigami.Heading{text:model.specieslevel: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 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.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;boolsetData(constQModelIndex&index,constQVariant&value,introle)override;private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
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.
Lägg till följande Kirigami.PromptDialog i src/components/ModelsPage.qml, tillsammans med en ny redigeringsknapp:
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{Layout.fillHeight:trueheader:Kirigami.Heading{text:model.specieslevel:2}contentItem:Item{implicitWidth:delegateLayout.implicitWidthimplicitHeight:delegateLayout.implicitHeightColumnLayout{id: delegateLayoutControls.Label{text:model.characters}Controls.Button{text:"Edit"onClicked:{editPrompt.text=model.characters;editPrompt.model=model;editPrompt.open();}}}}}}}Kirigami.PromptDialog{id: editPromptpropertyvarmodelpropertyaliastext:editPromptText.texttitle:"Edit Characters"standardButtons:Kirigami.Dialog.Ok|Kirigami.Dialog.CancelonAccepted:{constmodel=editPrompt.model;model.characters=editPromptText.text;editPrompt.close();}Controls.TextField{id: editPromptTextonAccepted:editPrompt.accept()}}}
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 börja metoddeklarationen med makrot Q_INVOKABLE. Metoden inkluderar också en strängparameter, som är avsedd att vara den nya nyckeln i QMap.
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;Q_INVOKABLEvoidaddSpecies(constQString&species);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
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.
Funktionen dataChanged() 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.
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"actions:[Kirigami.Action{icon.name:"list-add-symbolic"text:"Add New Species"onTriggered:{addPrompt.open();}}]Model{id: customModel}ColumnLayout{// ...
}Kirigami.PromptDialog{id: addPrompttitle:"Add New Species"standardButtons:Kirigami.Dialog.OkonAccepted:{customModel.addSpecies(addPromptText.text);addPromptText.text="";// Clear TextField every time it's done
addPrompt.close();}Controls.TextField{id: addPromptTextLayout.fillWidth:trueonAccepted:addPrompt.accept()}}Kirigami.PromptDialog{id: editPrompt// ...
}}
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.
Lägg till en ny Q_INVOKABLE funktion kallad deleteSpecies() i src/components/model.h:
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;Q_INVOKABLEvoidaddSpecies(constQString&species);Q_INVOKABLEvoiddeleteSpecies(constQString&speciesName,constint&rowIndex);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};
Med en matchande implementering i src/components/model.cpp:
Låt oss nu uppdatera programmet så att knappen "Ta bort" visas i en RowLayout bredvid redigeringsknappen inne i vårt AbstractCard, och koppla upp den till vår borttagningsmetod.
importorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.Page{title:"Exposing to QML Tutorial"Kirigami.Heading{anchors.centerIn:parenttext:Backend.introductionText}}
src/components/backend.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once
#include<QObject>#include<qqmlintegration.h>classBackend:publicQObject{Q_OBJECTQML_ELEMENTQML_SINGLETONQ_PROPERTY(QStringintroductionTextREADintroductionTextWRITEsetIntroductionTextNOTIFYintroductionTextChanged)public:explicitBackend(QObject*parent=nullptr);QStringintroductionText()const;voidsetIntroductionText(constQString&introductionText);Q_SIGNALvoidintroductionTextChanged();private:QStringm_introductionText=QStringLiteral("Hello World!");};
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiimportorg.kde.tutorial.componentsKirigami.ScrollablePage{title:"C++ models in QML"actions:[Kirigami.Action{icon.name:"list-add-symbolic"text:"Add New Species"onTriggered:{addPrompt.open();}}]Model{id: customModel}ColumnLayout{anchors.left:parent.leftanchors.right:parent.rightRepeater{model:customModeldelegate:Kirigami.AbstractCard{Layout.fillHeight:trueheader:Kirigami.Heading{text:model.specieslevel:2}contentItem:Item{implicitWidth:delegateLayout.implicitWidthimplicitHeight:delegateLayout.implicitHeightColumnLayout{id: delegateLayoutControls.Label{text:model.characters}RowLayout{Layout.fillWidth:trueControls.Button{text:"Edit"onClicked:{editPrompt.text=model.characters;editPrompt.model=model;editPrompt.open();}}Controls.Button{text:"Delete"onClicked:{customModel.deleteSpecies(model.species,index);}}}}}}}}Kirigami.PromptDialog{id: addPrompttitle:"Add New Species"standardButtons:Kirigami.Dialog.OkonAccepted:{customModel.addSpecies(addPromptText.text);addPromptText.text="";// Clear TextField every time it's done
addPrompt.close();}Controls.TextField{id: addPromptTextLayout.fillWidth:trueonAccepted:addPrompt.accept()}}Kirigami.PromptDialog{id: editPromptpropertyvarmodelpropertyaliastext:editPromptText.texttitle:"Edit Characters"standardButtons:Kirigami.Dialog.Ok|Kirigami.Dialog.CancelonAccepted:{constmodel=editPrompt.model;model.characters=editPromptText.text;editPrompt.close();}Controls.TextField{id: editPromptTextonAccepted:editPrompt.accept()}}}
#pragma once
#include<QAbstractListModel>#include<qqmlintegration.h>classModel:publicQAbstractListModel{Q_OBJECTQML_ELEMENTpublic:enumRoles{SpeciesRole=Qt::UserRole,CharactersRole};introwCount(constQModelIndex&)constoverride;QHash<int,QByteArray>roleNames()constoverride;QVariantdata(constQModelIndex&index,introle)constoverride;boolsetData(constQModelIndex&index,constQVariant&value,introle)override;Q_INVOKABLEvoidaddSpecies(constQString&species);Q_INVOKABLEvoiddeleteSpecies(constQString&speciesName,constint&rowIndex);private:QMap<QString,QStringList>m_list={{"Feline",{"Tigress","Waai Fuu"}},{"Fox",{"Carmelita","Diane","Krystal"}},{"Goat",{"Sybil","Toriel"}}};staticQStringformatList(constQStringList&list);};