Conecte modelos C++ à sua interface de usuário QML
Conforme demonstrado no tutorial anterior, você pode conectar código C++ ao QML criando uma classe que será tratada como apenas mais um componente em QML. No entanto, você pode querer representar dados mais complexos, como dados que precisam atuar como um ListModel personalizado ou que, de alguma forma, precisam ser delegados de um Repetidor.
Podemos criar nossos próprios modelos do lado C++ e declarar como os dados desse modelo devem ser representados no frontend QML.
Preparando a Classe
Neste tutorial, criaremos uma classe que contém um QMap, onde uma QString é usada como chave e objetos QStringList são usados como valores. O frontend será capaz de ler e exibir as chaves e valores e será simples de usar, como um array unidimensional. Deve ser semelhante a um ListModel QML.
Para fazer isso, precisamos declarar uma classe que herda de QAbstractListModel. Vamos também adicionar alguns dados ao QMap. Essas declarações estarão localizadas em model.h
.
Nota
Se você estiver acompanhando, lembre-se de atualizar seu arquivoCMakeLists.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"}}
};
};
Claro, não podemos simplesmente exibir esta classe como está. Também precisamos informar ao QML como representar esses dados na classe. Podemos fazer isso sobrescrevendo três funções virtuais essenciais para isso, cada uma delas realizando suas próprias tarefas.
rowCount()
- Pense nesta função como uma maneira de informar ao QML quantos itens estão no modelo a serem representados.roleNames()
- Você pode pensar em nomes de funções como nomes de propriedades anexados a dados em QML. Esta função permite criar essas funções.data()
- Esta função é chamada quando você deseja recuperar os dados que correspondem aos nomes das funções do modelo.
Nota
Os nomes de funções personalizados criados porroleNames()
só podem ser usados quando um modelo está sendo delegado e não podem ser usados fora dele. Consulte Modelos e Visualizações.Nota
Tecnicamente, os modelos no Qt são representados como tabelas, com linhas e colunas. Portanto, o que sobrescreverrowCount()
faz é informar ao Qt quantas linhas existem em um modelo. Como estamos lidando apenas com um array unidimensional neste tutorial, você pode pensar em "linhas" como "número de elementos".Substituindo e implementando rowCount()
Vamos substituir a função no arquivo de cabeçalho. A função rowCount()
vem com seu próprio parâmetro, mas não será usada neste exemplo e foi excluída.
class Model : public QAbstractListModel {
...
public:
int rowCount(const QModelIndex &) const override;
};
Então, vamos declarar quantas linhas existem neste modelo em model.cpp
.
#include "model.h"
int Model::rowCount(const QModelIndex &) const {
return m_list.count();
}
Substituindo e implementando roleNames()
Antes de substituir roleNames()
, precisamos declarar quais são as funções no lado C++ usando uma variável pública enum
. O motivo para isso é que esses valores da variável enum
são passados para data()
toda vez que o QML acessa uma função correspondente e, assim, podemos fazer com que data()
retorne o que queremos.
Vamos começar criando a variável enum
para funções, onde cada valor é uma função para o lado C++.
class Model : public QAbstractListModel {
...
public:
enum Roles {
SpeciesRole = Qt::UserRole,
CharactersRole
};
...
QHash<int, QByteArray> roleNames() const override;
};
Uma vez que tenhamos isso definido, podemos finalmente criar quais são essas funções no lado QML usando um QHash onde as chaves são os valores enumerados pareados com QByteArrays. O texto no QByteArray é o que é usado no código QML real.
QHash<int, QByteArray> Model::roleNames() const {
return {
{SpeciesRole, "species"},
{CharactersRole, "characters"}
};
}
Em nosso modelo de exemplo, a função "species" pode ser usada para recuperar a chave QString "Feline", "Fox", "Goat", cada uma em um delegado separado. O mesmo pode ser feito com os valores de QStringList para a lista de nomes de personagens.
Substituindo e implementando data()
Existem dois parâmetros passados em data()
: index
e role
. index
é o local onde os dados estão quando delegados. Como afirmado anteriormente, role
é usado pelo QML para obter dados específicos retornados ao acessar uma função.
Em data()
, podemos usar uma instrução switch
para retornar os dados apropriados e o tipo de dados, dependendo da função, o que é possível, pois data()
retorna um QVariant. Ainda precisamos garantir que obtemos a localização apropriada dos dados. Neste exemplo abaixo, você pode ver que uma nova variável iteradora está sendo declarada, que é definida a partir do início da lista mais a linha do índice e os dados para os quais o iterador está apontando são o que está sendo retornado.
No entanto, não podemos simplesmente retornar os dados que quisermos. Podemos estar tentando vincular dados a uma propriedade com um tipo de dado incompatível, como uma QStringList a uma QString. Talvez seja necessário converter os dados para que eles sejam exibidos corretamente.
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;
}
Permitir que a classe seja declarada em QML
Não vamos esquecer de tornar nossa classe utilizável em QML.
int main(int argc, char *argv[]) {
...
qmlRegisterType<Model>("CustomModel", 1, 0, "CustomModel");
...
}
Uso de Classe no QML
O arquivo QML utilizado conterá apenas três componentes Kirigami.AbstractCard, onde a chave é o cabeçalho e o valor é o conteúdo. Esses cards são criados delegando um AbstractCard usando um repetidor, onde o modelo personalizado que criamos atua como modelo. Os dados são acessados usando a palavra model
, seguida pelas funções que declaramos em 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
}
}
}
}
}
}
Data Modification
Editing Data Using dataChanged()
and setData()
You may encounter a situation where you want to modify data in the model, and have the changes reflected on the frontend side. Every time we change data in the model, we must emit the dataChanged()
signal which will apply those changes on the frontend side at the specific cells specified in its arguments. In this tutorial, we can just use the index
argument of setData()
.
setData()
is a virtual function you can override so that attempting to modify the data from the frontend side automatically reflects those changes on the backend side. It requires three parameters:
index
- The location of the data.value
- The contents of the new data.role
- In this context, the role here is used to tell views how they should handle data. The role here should beQt::EditRole
.
The role
parameter in this case is used to ensure setData()
can be edited via user input (Qt::EditRole). Using index
, we can use that to determine the location of where the data should be edited with the contents of 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;
}
Nota
setData()
does not automatically emit dataChanged()
and that still has to be done manually.Let's update the QML code so that we can open up a prompt that allows us to edit the model using a Controls.Button attached to the cards.
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();
}
}
}
}
}
}
}
}
}
Now, whenever the values of the model change in the frontend, the changes should automatically update on the backend.


Adding Rows
We added a way to modify the data in existing keys of the QMap, and in the front end, this is reflected as modifying the contents inside the AbstractCards. But what if we need to add a new key entry in the QMap and have that reflected on the QML side? Let's do this by creating a new method that is callable on the QML side to perform this task.
To make the method visible in QML, we must use the Q_OBJECT macro in the class, and begin the method declaration with the Q_INVOKABLE macro. This method will also include a string parameter, which is intended to be the new key in the QMap.
class Model : public QAbstractListModel {
Q_OBJECT;
...
public:
...
Q_INVOKABLE void addSpecies(const QString &species);
};
Inside of this method, we need to tell Qt that we want to create more rows in the model. This is done by calling beginInsertRows()
to begin our row adding operation, followed by inserting whatever we need, then use endInsertRows()
to end the operation. We still need to emit dataChanged()
at the end, however. This time, we are going to update all rows, from the first row to the last one as the QMap may alphabetically reorganize itself, and we need to catch that across all rows.
When calling beginInsertRows()
, we need to first pass in a QModelIndex class to specify the location of where the new rows should be added, followed by what the new first and last row numbers are going to be. In this tutorial, the first argument will just be QModelIndex()
as there is no need to use the parameter here. We can just use the current row size for the first and last row number, as we'll just be adding one row at the end of the model.
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));
}
Nota
ThedataChanged()
function uses QModelIndex as the data type for its parameters. However, we can convert integers in QModelIndex data types using the index()
function.Let's update the QML code so we are given the ability to add a new key to the 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 = ""; // Clear TextField every time it's done
addPrompt.close();
}
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
actions: [
Kirigami.Action {
icon.name: "add"
text: "Add New Species"
onTriggered: {
addPrompt.open();
}
}
]
...
}
}
Now, we should be given a new action at the top of the app that brings up a prompt that allows to add a new element to the model, with our own custom data.


Removing Rows
The way remove rows is similar to adding rows. Let's create another method that we'll call in QML. This time, we will use an additional parameter, and that is an integer that is the row number. The species name is used to delete the key from the QMap, while the row number will be used to delete the row on the front end.
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));
}
Now, let's update the application so a "Delete" button appears alongside the edit button, and hook it up to our delete method.
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);
}
}
}
}
}
}
}
}


Full Code
Main.qml
|
|
model.h
|
|
model.cpp
|
|
More Information
For more information, see Using C++ Models with Qt Quick Views and Model/View Programming.