Connetti i modelli C++ alla tua interfaccia utente QML
Dati dal backend C++ al frontend QML
As shown in the previous tutorial, you can connect C++ code to QML by creating a class that will be treated as just another component in QML. Tuttavia, potresti voler rappresentare dati più complicati, come i dati che devono agire come un ListModel personalizzato o che in qualche modo devono essere delegati da un Repeater.
Possiamo creare i nostri modelli dal lato C++ e dichiarare come i dati di quel modello dovrebbero essere rappresentati sul frontend QML.
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()}]}
Quindi, crea un nuovo src/components/ModelsPage.qml con il seguente contenuto:
1
2
3
4
5
6
7
8
9
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"// ...
}
E infine aggiungilo a src/components/CMakeLists.txt:
Questo servirà come tela per questa pagina del tutorial.
Utilizzo di stringhe grezze
Per rendere questo tutorial più semplice per comprendere come viene popolato il modello, disabiliteremo una funzionalità utilizzata per impostazione predefinita dalle applicazioni KDE che utilizzano moduli extra-cmake (ECM) che ottimizza il codice stringa. Questo ci consente di evitare di dover scrivere QStringLiteral() ogni volta che viene introdotta una stringa nel nostro codice C++, il che sarà utile per il codice nel prossimo file di intestazione.
Nel file root «CMakeLists.txt», aggiungi quanto segue:
La disabilitazione di questo flag CMake viene eseguita solo per scopi didattici. Il codice di produzione dovrebbe invece utilizzare QStringLiteral() o lo spazio dei nomi dei valori letterali delle stringhe Qt, ove possibile.
Preparazione della classe
Creeremo una classe che contiene un QMap, dove un QString viene utilizzato come chiave e gli oggetti QStringList vengono utilizzati come valori. Il frontend sarà in grado di leggere e visualizzare chiavi e valori e sarà semplice da usare proprio come un array unidimensionale. Dovrebbe essere simile a un ListModel QML.
Per fare ciò, dobbiamo creare una classe che erediti da QAbstractListModel. Aggiungiamo anche alcuni dati alla QMap. Queste dichiarazioni si troveranno in "model.h".
Crea due nuovi file, src/components/model.h e src/components/model.cpp.
Aggiungi questi due nuovi file a src/components/CMakeLists.txt:
Aggiungi quanto segue come contenuto iniziale 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"}}};};
Ovviamente non possiamo semplicemente visualizzare questa classe così com'è. Dobbiamo anche dire a QML come rappresentare questi dati nella classe. Possiamo farlo sovrascrivendo tre funzioni virtuali essenziali:
rowCount() - Pensa a questa funzione come un modo per indicare a QML quanti elementi deve presentare il modello.
roleNames() - Puoi pensare ai nomi dei ruoli come nomi di proprietà allegati ai dati in QML. Questa funzione consente di creare tali ruoli.
data() - Questa funzione viene chiamata quando si desidera recuperare i dati che corrispondono ai nomi dei ruoli dal modello.
Nota
I nomi dei ruoli personalizzati creati da roleNames() sono utilizzabili solo quando un modello viene delegato e non sono utilizzabili al di fuori di esso. Vedi Modelli e viste.
Nota
Tecnicamente, i modelli in Qt sono rappresentati come tabelle, con righe e colonne. Quindi, ciò che fa l'override di rowCount() è dire a Qt quante righe ci sono in un modello. Dato che in questo tutorial abbiamo a che fare solo con un array unidimensionale, puoi semplicemente pensare alle "righe" come al "numero di elementi".
Sostituire e implementare rowCount()
Sovrascriviamo la funzione nel file header src/components/model.h. La funzione rowCount() viene fornita con il proprio parametro, ma non verrà utilizzata in questo esempio e quindi non è necessario nominarla.
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"}}};};
Quindi, dichiariamo quante righe ci sono in questo modello in src/components/model.cpp:
Prima di sovrascrivere roleNames(), dobbiamo dichiarare quali sono i ruoli nel lato C++ utilizzando un'enumerazione pubblica. La ragione di ciò è che questi valori enum vengono passati a data() ogni volta che QML accede a un ruolo corrispondente e come tale possiamo fare in modo che data() restituisca ciò che vogliamo.
Iniziamo con la creazione dell'enumerazione per i ruoli in src/components/model.h, dove ogni valore è un ruolo per il lato 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 volta risolto questo problema, possiamo finalmente creare quali siano questi ruoli nel lato QML utilizzando un QHash dove le chiavi sono i valori enumerati abbinati a QByteArrays. Questo dovrebbe andare su "src/components/model.cpp". Il testo nel QByteArray è ciò che viene utilizzato nel codice QML effettivo.
Nel nostro modello di esempio, il ruolo "specie" può essere utilizzato per recuperare la chiave QString "Felino", "Volpe", "Capra", ciascuna in un delegato separato. Lo stesso può essere fatto con i valori QStringList per l'elenco dei nomi dei caratteri.
Sostituire e implementare data()
Ci sono due parametri che vengono passati a data(): index e role. L'indice è la posizione dei dati nel modello. Come affermato in precedenza, "role" viene utilizzato da QML per ottenere dati specifici restituiti quando accede a un ruolo.
In data(), possiamo utilizzare un'istruzione switch per restituire i dati e il tipo di dati appropriati a seconda del ruolo, il che è possibile poiché data() restituisce un QVariant. Dobbiamo comunque assicurarci di ottenere la posizione appropriata dei dati. Nell'esempio seguente, puoi vedere che viene dichiarata una nuova variabile iteratore, che viene impostata dall'inizio dell'elenco più la riga dell'indice, e i dati a cui punta l'iteratore sono ciò che viene restituito.
Tuttavia, non possiamo semplicemente restituire i dati che vogliamo. Potremmo provare ad associare i dati a una proprietà con un tipo di dati incompatibile, ad esempio una QStringList a una QString. Potrebbe essere necessario eseguire la conversione dei dati affinché i dati vengano visualizzati correttamente. Per questo creiamo una nuova funzione privata e statica denominata formatList().
Il risultato è il seguente codice in 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);};
Utilizzo delle classi in QML
Il file QML utilizzato conterrà solo tre componenti Kirigami.AbstractCard, dove la chiave è l'intestazione e il valore è il contenuto. Queste schede vengono create delegando una AbstractCard utilizzando un Repeater, dove il modello personalizzato che abbiamo creato funge da modello. Si accede ai dati utilizzando la parola "model", seguita dai ruoli dichiarati in "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}}}}}
Modifica dei dati
Modifica dei dati utilizzando dataChanged() e setData()
Potresti riscontrare una situazione in cui desideri modificare i dati nel modello e visualizzare le modifiche sul lato frontend. Ogni volta che modifichiamo i dati nel modello, dobbiamo emettere il segnale dataChanged() che applicherà tali modifiche sul lato frontend alle celle specifiche specificate nei suoi argomenti. In questo tutorial, possiamo semplicemente utilizzare l'argomento index di setData().
setData() è una funzione virtuale che puoi sovrascrivere in modo che la modifica dei dati dal lato frontend rifletta automaticamente tali modifiche sul lato backend. Richiede tre parametri:
index - la posizione dei dati.
value - Il contenuto dei nuovi dati.
"ruolo" - In questo contesto, il ruolo viene utilizzato per indicare alle visualizzazioni come dovrebbero gestire i dati. Il ruolo qui dovrebbe essere Qt::EditRole.
Il parametro role in questo caso viene utilizzato per garantire che setData() possa essere modificato tramite input dell'utente (Qt::EditRole). Utilizzando "index", possiamo utilizzarlo per determinare la posizione in cui i dati devono essere modificati con il contenuto di "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);};
Aggiorniamo il codice QML in modo da poter aprire un prompt che ci consenta di modificare il modello utilizzando un Controls.Button allegato alle carte.
Aggiungi il seguente Kirigami.PromptDialog a src/components/ModelsPage.qml, insieme a un nuovo pulsante di modifica:
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()}}}
Ora, ogni volta che i valori del modello cambiano nel frontend, le modifiche dovrebbero aggiornarsi automaticamente nel backend.
Aggiungere righe
Abbiamo aggiunto un modo per modificare i dati nelle chiavi esistenti di QMap e, nel front-end, ciò si riflette nella modifica dei contenuti all'interno di AbstractCards. Ma cosa succede se dobbiamo aggiungere una nuova voce chiave nella QMap e rifletterla sul lato QML? Facciamolo creando un nuovo metodo richiamabile dal lato QML per eseguire questa attività.
Per rendere il metodo visibile in QML, dobbiamo iniziare la dichiarazione del metodo con la macro Q_INVOKABLE. Questo metodo includerà anche un parametro stringa, destinato a essere la nuova chiave nella 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);};
All'interno di questo metodo, dobbiamo dire a Qt che vogliamo creare più righe nel modello. Questo viene fatto chiamando beginInsertRows() per iniziare l'operazione di aggiunta di righe, seguito dall'inserimento di tutto ciò di cui abbiamo bisogno, quindi utilizzare endInsertRows() per terminare l'operazione. Tuttavia, dobbiamo ancora emettere dataChanged() alla fine. Questa volta aggiorneremo tutte le righe, dalla prima all'ultima poiché la QMap potrebbe riorganizzarsi in ordine alfabetico e dobbiamo catturarlo su tutte le righe.
Quando chiamiamo beginInsertRows(), dobbiamo prima passare una classe QModelIndex per specificare la posizione in cui dovrebbero essere aggiunte le nuove righe, seguita da quali saranno i numeri della prima e dell'ultima riga. In questo tutorial, il primo argomento sarà semplicemente QModelIndex() poiché non è necessario utilizzare il parametro qui. Possiamo semplicemente utilizzare la dimensione della riga corrente per il numero della prima e dell'ultima riga, poiché aggiungeremo semplicemente una riga alla fine del modello.
La funzione dataChanged() utilizza QModelIndex come tipo di dati per i suoi parametri. Tuttavia, possiamo convertire numeri interi nei tipi di dati QModelIndex utilizzando la funzione index().
Aggiorniamo il codice QML in modo da avere la possibilità di aggiungere una nuova chiave alla 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// ...
}}
Ora, dovremmo ricevere una nuova azione nella parte superiore dell'applicazione che fa apparire un prompt che consente di aggiungere un nuovo elemento al modello, con i nostri dati personalizzati.
Rimozione di righe
Il modo in cui rimuovi le righe è simile all'aggiunta di righe. Creiamo un altro metodo che chiameremo in QML. Questa volta utilizzeremo un parametro aggiuntivo, ovvero un numero intero che rappresenta il numero di riga. Il nome della specie viene utilizzato per eliminare la chiave dalla QMap, mentre il numero di riga verrà utilizzato per eliminare la riga sul front-end.
Aggiungi una nuova funzione Q_INVOKABLE denominata deleteSpecies() in 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);};
Con un'implementazione corrispondente in src/components/model.cpp:
Ora aggiorniamo l'applicazione in modo che venga visualizzato un pulsante "Elimina" in un RowLayout accanto al pulsante di modifica all'interno della nostra AbstractCard e colleghiamolo al nostro metodo di eliminazione.
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);};