Connectar models C++ a la vostra interfície d'usuari en QML
Dades des del dorsal C++ a un frontal QML
Com es mostra a la guia d'aprenentatge anterior, podeu connectar codi C++ al QML creant una classe que es tractarà com un altre component en QML. No obstant això, és possible que vulgueu representar dades més complicades, com ara les dades que han d'actuar com un ListModel personalitzat o, d'alguna manera, han de ser delegades des d'un Repeater.
Podem crear els nostres propis models des de la banda del C++, i declarar com s'han de representar les dades d'aquest model en el frontal QML.
És molt recomanable que llegiu la guia d'aprenentatge Vistes de llista abans d'aquest.
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()}]}
Després, creeu un src/components/ModelsPage.qml nou amb el contingut següent:
1
2
3
4
5
6
7
8
9
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"// ...
}
I finalment, afegiu-lo a src/components/CMakeLists.txt:
Això servirà com a llenç per a aquesta pàgina de la guia d'aprenentatge.
Ús de cadenes en brut
Per a fer que aquesta guia d'aprenentatge sigui més fàcil d'entendre com s'omple el model, desactivarem una característica que les aplicacions KDE que utilitzen els extra-cmake-modules (ECM) fan servir de manera predeterminada d'optimització del codi de cadenes. Això ens permet evitar haver d'escriure QStringLiteral() cada vegada que s'introdueixi una cadena en el nostre codi C++, el qual serà útil per al codi en el fitxer de capçalera proper.
Al fitxer arrel CMakeLists.txt, afegiu el següent:
La desactivació d'aquest indicador del CMake només es fa amb finalitats didàctiques. El codi de producció ha d'utilitzar QStringLiteral() o l'espai de noms de literals de cadenes de les Qt quan sigui possible.
Preparant la classe
Crearem una classe que conté un QMap, on s'utilitza un QString com a clau i objectes QStringList com a valors. El frontal podrà llegir i mostrar les claus i els valors i serà senzill d'utilitzar com una matriu unidimensional. Hauria de ser semblant a un ListModel en QML.
Per a fer-ho, necessitem crear una classe que hereti de QAbstractListModel. Afegim també algunes dades addicionals al QMap. Aquestes declaracions s'ubicaran a model.h.
Creeu dos fitxers nous, src/components/model.h i src/components/model.cpp.
Afegiu aquests dos fitxers a src/components/CMakeLists.txt:
Afegiu el següent com a contingut inicial a 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"}}};};
Per descomptat, no podem simplement mostrar aquesta classe com és. També hem de dir-li al QML com representar aquestes dades a la classe. Podem fer-ho sobreescrivint tres funcions virtuals essencials:
rowCount() - Penseu en aquesta funció com una manera de dir al QML quants elements ha de representar el model.
roleNames() - Podeu pensar en els noms dels rols com a noms de propietats adjunts a les dades en QML. Aquesta funció permet crear aquests rols.
data() - Aquesta funció es crida quan voleu recuperar les dades que corresponen als noms dels rols a partir del model.
Nota
Els noms de rol personalitzats creats per roleNames() només es poden utilitzar quan s'està delegant un model, i no es poden utilitzar fora d'ell. Vegeu Models i vistes.
Nota
Tècnicament, els models en Qt es representen com a taules, amb files i columnes. Per tant, el que fa rowCount() és dir a les Qt quantes files hi ha en un model. Com que només estem tractant amb una matriu unidimensional en aquesta guia d'aprenentatge, només podeu pensar en «files» com a «nombre d'elements».
Sobreescriure i implementar rowCount()
Sobreescrivim la funció al fitxer de capçalera src/components/model.h. El rowCount() ve amb el seu paràmetre propi, però no s'utilitzarà en aquest exemple i, per tant, no cal anomenar-lo.
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"}}};};
Llavors, declarem quantes files hi ha en aquest model a src/components/model.cpp:
Abans de substituir roleNames(), necessitem declarar quins són els rols a la banda del C++ utilitzant una enumeració pública. El motiu d'això és perquè aquests valors de la variable d'enumeració es passen a data() cada vegada que el QML accedeix a un rol corresponent, i com a tal podem fer que data() retorni el que volem.
Comencem per crear la variable d'enumeració per als rols a src/components/model.h, a on cada valor és un rol de la banda del C++.
#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"}}};};
Una vegada que hàgim resolt això, finalment podrem crear quins són aquests rols en la banda del QML utilitzant un QHash on les claus són els valors enumerats aparellats amb QByteArrays. Això hauria d'anar a src/components/model.cpp. El text del QByteArray és el que s'utilitza en el codi QML real.
En el nostre model d'exemple, el rol «espècies» es pot utilitzar per a recuperar la clau del QString «Feline», «Fox», «Goat», cadascuna en un delegat separat. El mateix es pot fer amb els valors de QStringList per a la llista de noms de caràcters.
Sobreescriure i implementar data()
Hi ha dos paràmetres que es passen a data(): index i role. L'index és la posició de les dades en el model. Com s'ha indicat anteriorment, el QML utilitza el role per a obtenir dades específiques que es retornen quan s'està accedint a un rol.
A data(), podem utilitzar una declaració de commutació per a retornar les dades i el tipus de dades adequats depenent del rol, el qual és possible ja que data() retorna una QVariant. No obstant això, encara hem d'assegurar-nos d'aconseguir la ubicació adequada de les dades. En aquest exemple a continuació, podeu veure que s'està declarant una variable nova de l'iterador, que s'estableix des del principi de la llista més la fila de l'índex i les dades a les quals apunta l'iterador que és el que s'està retornant.
Però no podem retornar les dades que vulguem. És possible que estiguem intentant vincular dades a una propietat amb un tipus de dades incompatible, com ara una QStringList a una QString. És possible que hàgiu de fer la conversió de dades perquè les dades es mostrin correctament. Per a això, creem una funció estàtica privada nova anomenada formatList().
Això dona com a resultat el codi següent a 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);};
Ús de la classe en el QML
El fitxer QML que s'utilitza només contindrà tres components Kirigami.AbstractCard, on la clau és la capçalera i el valor és el contingut. Aquestes targetes es creen delegant una AbstractCard utilitzant un repetidor, on el model personalitzat que hem creat actua com a model. S'accedeix a les dades utilitzant la paraula model, seguida dels rols que hem declarat a 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}}}}}
Modificació de dades
Edició de dades utilitzant dataChanged() i setData()
Podeu trobar-vos amb una situació en la qual vulgueu modificar les dades del model, i tenir els canvis reflectits a la banda del frontal. Cada vegada que canviem dades en el model, hem d'emetre el senyal dataChanged() que aplicarà aquests canvis a la banda del frontal a les cel·les específiques especificades en els seus arguments. En aquesta guia d'aprenentatge, només podem utilitzar l'argument index de setData().
setData() és una funció virtual que podeu sobreescriure de manera que modificar les dades des de la banda del frontal reflecteix automàticament aquests canvis a la banda del dorsal. Requereix tres paràmetres:
index - La ubicació de les dades.
value - El contingut de les dades noves.
role - En aquest context, el rol aquí s'utilitza per a dir a les vistes com haurien de gestionar les dades. El rol aquí hauria de ser Qt::EditRole.
El paràmetre role en aquest cas s'utilitza per a assegurar que setData() es pot editar mitjançant l'entrada de l'usuari (Qt::EditRole). Utilitzant index, podem utilitzar això per a determinar la ubicació d'on s'han d'editar les dades amb el contingut de 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);};
Actualitzem el codi QML de manera que puguem obrir un indicador que ens permeti editar el model utilitzant un Controls.Button adjunt a les targetes.
Afegim el Kirigami.PromptDialog següent al src/components/ModelsPage.qml, junt amb el botó nou d'edició:
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()}}}
Ara, sempre que els valors del model canviïn al frontal, els canvis s'hauran d'actualitzar automàticament al dorsal.
Afegir files
S'ha afegit una manera de modificar les dades a les claus existents del QMap, i al frontal, això es reflecteix com modificar el contingut dins de les AbstractCards. Però, i si necessitem afegir una entrada nova de clau al QMap i tenir això reflectit a la banda del QML? Fem-ho creant un mètode nou que es pot cridar a la banda del QML per a realitzar aquesta tasca.
Per a fer visible el mètode en el QML, cal començar la declaració del mètode amb la macro Q_INVOKABLE. Aquest mètode també inclourà un paràmetre de cadena, que es pretén que sigui la clau nova en el 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);};
Dins d'aquest mètode, hem de dir a les Qt que volem crear més files en el model. Això es fa cridant a beginInsertRows() per a començar la nostra operació d'addició de files, seguida d'inserir el que necessitem, i després utilitzeu endInsertRows() per a acabar l'operació. No obstant això, encara cal emetre dataChanged() al final. Aquesta vegada, actualitzarem totes les files, des de la primera fila fins a l'última, ja que el QMap pot reorganitzar-se alfabèticament, i hem d'agafar-ho a totes les files.
En cridar beginInsertRows(), primer hem de passar en una classe QModelIndex per a especificar la ubicació d'on s'han d'afegir les files noves, seguit de quins seran els números nous de primera i última fila. En aquesta guia d'aprenentatge, el primer argument serà QModelIndex(), ja que no cal utilitzar el paràmetre aquí. Només podem utilitzar la mida de fila actual per al primer i últim número de fila, ja que només afegirem una fila al final del model.
La funció dataChanged() utilitza QModelIndex com a tipus de dades per als seus paràmetres. No obstant això, podem convertir enters en tipus de dades QModelIndex utilitzant la funció index().
Actualitzem el codi en QML, de manera que se'ns dona la possibilitat d'afegir una clau nova al 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// ...
}}
Ara, se'ns ha de donar una acció nova a la part superior de l'aplicació que obri un indicador que permeti afegir un element nou al model, amb les nostres pròpies dades personalitzades.
Eliminar files
La manera d'eliminar files és similar a afegir files. Creem un altre mètode que cridarem en el QML. Aquesta vegada, utilitzarem un paràmetre addicional, i és un enter que és el número de fila. El nom de l'espècie s'utilitza per a suprimir la clau del QMap, mentre que el número de fila s'utilitzarà per a suprimir la fila en el frontal.
Afegiu una funció Q_INVOKABLE nova anomenada deleteSpecies() a 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);};
Amb una implementació coincident a src/components/model.cpp:
Ara, actualitzem l'aplicació de manera que apareixerà un botó «Elimina» en un RowLayout al costat del botó d'edició dins la nostra AbstractCard, i enganxeu-lo al nostre mètode d'eliminació.
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);};