Prepojte C++ modely s vaším používateľským rozhraním QML
Dáta z C++ backendu do QML frontendu
Ako bolo ukázané v predchádzajúcom tutoriáli, môžete pripojiť kód C++ k QML vytvorením triedy, ktorá bude spracovaná ako ďalší komponent v QML. Avšak možno budete chcieť reprezentovať zložitejšie dáta, ako napríklad dáta, ktoré musia fungovať ako vlastný ListModel alebo nejakým spôsobom musia byť delegované z Repeater.
Môžeme vytvoriť vlastné modely zo strany C++ a deklarovať, ako majú byť dáta z daného modelu reprezentované na frontende 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()}]}
Potom vytvorte nový src/components/ModelsPage.qml s nasledovným obsahom:
1
2
3
4
5
6
7
8
9
importQtQuickimportQtQuick.LayoutsimportQtQuick.ControlsasControlsimportorg.kde.kirigamiasKirigamiKirigami.ScrollablePage{title:"C++ models in QML"// ...
}
Toto bude slúžiť ako plátno pre túto stránku tutoriálu.
Prekrývajúce listy
Aby sme v tomto tutoriáli jednoduchšie pochopili, ako sa model napĺňa, vypneme funkciu, ktorú KDE aplikácie používajúce extra-cmake-modules (ECM) používajú štandardne a ktorá optimalizuje kód reťazcov. To nám umožní obísť nutnosť písať QStringLiteral() zakaždým, keď sa v našom kóde C++ uvedie reťazec, čo bude užitočné pre kód v nadchádzajúcom hlavičkovom súbore.
V koreňovom súbore CMakeLists.txt pridajte nasledovné:
Vypnutie tohto príznaku CMake sa robí iba na didaktické účely. Produkčný kód by mal namiesto toho používať QStringLiteral() alebo menný priestor Qt string literals, kde je to možné.
Príprava triedy
Vytvoríme triedu, ktorá obsahuje QMap, kde sa QString používa ako kľúč a objekty QStringList sa používajú ako hodnoty. Frontend bude schopný čítať a zobrazovať kľúče a hodnoty a bude jednoduchý na používanie rovnako ako jednorozmerné pole. Malo by to vyzerať podobne ako QML ListModel.
Na to musíme vytvoriť triedu, ktorá dedí z QAbstractListModel. Pridajme tiež nejaké dáta do QMap. Tieto deklarácie budú umiestnené v model.h.
Vytvorte dva nové súbory, src/components/model.h a src/components/model.cpp.
Pridajte nasledovné ako počiatočný obsah do 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"}}};};
Samozrejme, nemôžeme jednoducho zobraziť túto triedu ako je. Tiež musíme povedať QML, ako reprezentovať tieto dáta v triede. Môžeme to urobiť prepísaním troch základných virtuálnych funkcií:
rowCount() - Túto funkciu si predstavte ako spôsob, ako povedať QML, koľko položiek má model prezentovať.
roleNames() - Názvy rolí si môžete predstaviť ako názvy vlastností pripojených k dátam v QML. Táto funkcia vám umožňuje vytvoriť tieto roly.
data() - Táto funkcia sa volá, keď chcete získať dáta, ktoré zodpovedajú názvom rolí z modelu.
Poznámka
Vlastné názvy rolí vytvorené funkciou roleNames() sú použiteľné iba keď je model delegovaný a nie sú použiteľné mimo neho. Pozrite si Modely a zobrazenia.
Poznámka
Technicky sú modely v Qt reprezentované ako tabuľky s riadkami a stĺpcami. Takže prepísanie rowCount() povie Qt, koľko riadkov je v modeli. Keďže v tomto tutoriáli pracujeme iba s jednorozmerným poľom, môžete si "riadky" jednoducho predstaviť ako "počet prvkov."
Prekonanie a implementácia rowCount()
Prepíšme funkciu v hlavičkovom súbore src/components/model.h. Funkcia rowCount() prichádza s vlastným parametrom, ale v tomto príklade sa nepoužije, a preto ho netreba pomenovať.
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"}}};};
Potom deklarujme, koľko riadkov je v tomto modeli v src/components/model.cpp:
Predtým, než prepíšeme roleNames(), musíme deklarovať, aké sú roly na strane C++ pomocou verejného enumu. Dôvodom je to, že tieto hodnoty enumu sa odovzdávajú do data() zakaždým, keď QML pristupuje k zodpovedajúcej role, a ako také môžeme prinútiť data() vrátiť to, čo chceme.
Začnime vytvorením enum pre roly v src/components/model.h, kde každá hodnota je rola pre stranu 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"}}};};
Keď to máme vyriešené, môžeme konečne vytvoriť tieto roly na strane QML pomocou QHash, kde kľúče sú enumerované hodnoty spárované s QByteArrays. Toto by malo ísť do src/components/model.cpp. Text v QByteArray je to, čo sa používa v skutočnom kóde QML.
V našom vzorovom modeli možno rolu "species" použiť na získanie kľúča QString "Feline", "Fox", "Goat", každý v samostatnom delegátovi. To isté možno urobiť s hodnotami QStringList pre zoznam mien postáv.
Prekonanie a implementácia data()
Do funkcie data() sa odovzdávajú dva parametre: index a role. index je pozícia dát v modeli. Ako bolo uvedené predtým, role sa používa v QML na získanie konkrétnych dát vrátených pri prístupe k role.
Vo funkcii data() môžeme použiť príkaz switch na vrátenie príslušných dát a dátového typu v závislosti od roly, čo je možné, pretože data() vracia QVariant. Stále však musíme zabezpečiť, aby sme získali správne umiestnenie dát. V nasledujúcom príklade vidíte, že sa deklaruje nová premenná iterátora, ktorá je nastavená od začiatku zoznamu plus riadok indexu, a dáta, na ktoré iterátor ukazuje, sú to, čo sa vracia.
Nemôžeme však vrátiť akékoľvek dáta, ktoré chceme. Môžeme sa pokúšať naviazať dáta na vlastnosť s nekompatibilným dátovým typom, ako napríklad QStringList na QString. Možno budete musieť vykonať konverziu dát, aby sa dáta zobrazovali správne. Na to vytvoríme novú privátnu statickú funkciu s názvom formatList().
Výsledkom je nasledovný kód v 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);};
Použitie triedy v QML
Použitý súbor QML bude obsahovať iba tri komponenty Kirigami.AbstractCard, kde kľúč je hlavička a hodnota je obsah. Tieto karty sa vytvárajú delegovaním AbstractCard pomocou Repeatera, kde vlastný model, ktorý sme vytvorili, funguje ako model. K dátam sa pristupuje pomocou slova model, za ktorým nasledujú roly, ktoré sme deklarovali v 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}}}}}
Modifikácia dát
Úprava dát pomocou dataChanged() a setData()
Môžete sa stretnúť so situáciou, keď chcete upraviť dáta v modeli a nechať zmeny prejaviť na strane frontendu. Zakaždým, keď zmeníme dáta v modeli, musíme emitovať signál dataChanged(), ktorý aplikuje tieto zmeny na strane frontendu v konkrétnych bunkách uvedených v jeho argumentoch. V tomto tutoriáli môžeme jednoducho použiť argument index funkcie setData().
setData() je virtuálna funkcia, ktorú môžete prepísať tak, aby úprava dát zo strany frontendu automaticky odrážala tieto zmeny na strane backendu. Vyžaduje tri parametre:
index - Umiestnenie dát.
value - Obsah nových dát.
role - V tomto kontexte sa rola používa na informovanie zobrazení, ako majú spracovať dáta. Rola by tu mala byť Qt::EditRole.
Parameter role sa v tomto prípade používa na zabezpečenie, že setData() je možné upraviť prostredníctvom vstupu používateľa (Qt::EditRole). Pomocou index môžeme určiť umiestnenie, kde majú byť dáta upravené s obsahom 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);};
Aktualizujme kód QML, aby sme mohli otvoriť dialóg, ktorý nám umožní upraviť model pomocou Controls.Button pripojeného ku kartám.
Pridajte nasledujúci Kirigami.PromptDialog do src/components/ModelsPage.qml spolu s novým tlačidlom na úpravu:
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()}}}
Teraz, kedykoľvek sa zmenia hodnoty modelu na frontende, zmeny by sa mali automaticky aktualizovať na backende.
Akcie
Pridali sme spôsob úpravy dát v existujúcich kľúčoch QMap a na frontende sa to odráža ako úprava obsahu vo vnútri AbstractCards. Ale čo ak potrebujeme pridať nový záznam kľúča do QMap a nechať to prejaviť na strane QML? Urobme to vytvorením novej metódy, ktorá je volateľná na strane QML na vykonanie tejto úlohy.
Aby bola metóda viditeľná v QML, musíme začať deklaráciu metódy makrom Q_INVOKABLE. Táto metóda bude tiež obsahovať parameter reťazca, ktorý má byť novým kľúčom v 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);};
Vo vnútri tejto metódy musíme povedať Qt, že chceme vytvoriť viac riadkov v modeli. To sa robí volaním beginInsertRows() na začatie operácie pridávania riadkov, po ktorom nasleduje vloženie toho, čo potrebujeme, a potom použitie endInsertRows() na ukončenie operácie. Na konci však stále musíme emitovať dataChanged().
Pri volaní beginInsertRows() musíme najprv odovzdať triedu QModelIndex na určenie umiestnenia, kam sa majú pridať nové riadky, nasledované novými číslami prvého a posledného riadku. V tomto tutoriáli bude prvý argument jednoducho QModelIndex(), pretože tu nie je potrebné používať tento parameter. Na číslo prvého a posledného riadku môžeme jednoducho použiť aktuálnu veľkosť riadkov, pretože budeme pridávať iba jeden riadok na koniec modelu.
Funkcia dataChanged() používa QModelIndex ako dátový typ pre svoje parametre. Avšak celé čísla môžeme konvertovať na dátové typy QModelIndex pomocou funkcie index().
Aktualizujme kód QML, aby sme získali možnosť pridať nový kľúč do 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// ...
}}
Teraz by sme mali mať novú akciu v hornej časti aplikácie, ktorá zobrazí dialóg umožňujúci pridať nový prvok do modelu s vlastnými dátami.
Odstraňovanie riadkov
Spôsob odstraňovania riadkov je podobný pridávaniu riadkov. Vytvorme ďalšiu metódu, ktorú zavoláme v QML. Tentoraz použijeme dodatočný parameter, a to celé číslo, ktoré je číslom riadku. Názov druhu sa používa na vymazanie kľúča z QMap, zatiaľ čo číslo riadku sa použije na vymazanie riadku na frontende.
Pridajte novú funkciu Q_INVOKABLE s názvom deleteSpecies() do 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);};
S zodpovedajúcou implementáciou v src/components/model.cpp:
Teraz aktualizujme aplikáciu tak, aby sa tlačidlo "Delete" zobrazilo v RowLayout vedľa tlačidla úpravy vo vnútri nášho AbstractCard a pripojme ho k našej metóde mazania.
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);};