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
}
}
}
}
}
}
Modificação de dados
Editando dados usando dataChanged() e setData()
Você pode se deparar com uma situação em que deseja modificar dados no modelo e ter as alterações refletidas no frontend. Toda vez que alteramos dados no modelo, devemos emitir o sinal dataChanged(), que aplicará essas alterações no frontend nas células específicas especificadas em seus argumentos. Neste tutorial, podemos usar apenas o argumento index de ``setData()`.
setData() é uma função virtual que você pode sobrescrever para que a tentativa de modificar os dados no frontend reflita automaticamente essas alterações no backend. Ela requer três parâmetros:
index- A localização dos dados.value- O conteúdo dos novos dados.role- Neste contexto, a função aqui é usada para informar às visualizações como elas devem lidar com os dados. A função aqui deve serQt::EditRole.
O parâmetro role neste caso é usado para garantir que setData() possa ser editado por meio de entrada do usuário (Qt::EditRole). Usando index, podemos usá-lo para determinar o local onde os dados devem ser editados com o conteúdo de 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() não emite dataChanged() automaticamente e isso ainda precisa ser feito manualmente.Vamos atualizar o código QML para que possamos abrir um prompt que nos permita editar o modelo usando um Controls.Button anexado aos cartões.
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();
}
}
}
}
}
}
}
}
}Agora, sempre que os valores do modelo forem alterados no frontend, as alterações devem ser atualizadas automaticamente no backend.


Adicionando linhas
Adicionamos uma maneira de modificar os dados nas chaves existentes do QMap e, no front-end, isso se reflete na modificação do conteúdo dentro dos AbstractCards. Mas e se precisarmos adicionar uma nova entrada de chave no QMap e refletir isso no lado QML? Vamos fazer isso criando um novo método que pode ser chamado no lado QML para executar essa tarefa.
Para tornar o método visível em QML, precisamos usar a macro Q_OBJECT na classe e iniciar a declaração do método com a macro Q_INVOKABLE. Este método também incluirá um parâmetro string, que deve ser a nova chave no QMap.
class Model : public QAbstractListModel {
Q_OBJECT;
...
public:
...
Q_INVOKABLE void addSpecies(const QString &species);
};Dentro deste método, precisamos informar ao Qt que queremos criar mais linhas no modelo. Isso é feito chamando beginInsertRows() para iniciar nossa operação de adição de linhas, seguida pela inserção do que for necessário e, em seguida, usando endInsertRows() para finalizar a operação. No entanto, ainda precisamos emitir dataChanged() no final. Desta vez, vamos atualizar todas as linhas, da primeira à última, pois o QMap pode se reorganizar em ordem alfabética, e precisamos capturar isso em todas as linhas.
Ao chamar beginInsertRows(), precisamos primeiro passar uma classe QModelIndex para especificar o local onde as novas linhas devem ser adicionadas, seguido de quais serão os novos números da primeira e da última linha. Neste tutorial, o primeiro argumento será apenas QModelIndex(), pois não há necessidade de usar o parâmetro aqui. Podemos usar apenas o tamanho da linha atual para o número da primeira e da última linha, pois adicionaremos apenas uma linha no final do modelo.
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
A funçãodataChanged() usa QModelIndex como tipo de dado para seus parâmetros. No entanto, podemos converter inteiros em tipos de dados QModelIndex usando a função index().Vamos atualizar o código QML para que possamos adicionar uma nova chave ao 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 = ""; // Limpe TextField sempre que terminar
addPrompt.close();
}
}
}
pageStack.initialPage: Kirigami.ScrollablePage {
actions: [
Kirigami.Action {
icon.name: "add"
text: "Add New Species"
onTriggered: {
addPrompt.open();
}
}
]
...
}
}Agora, devemos receber uma nova ação na parte superior do aplicativo que abre um prompt que permite adicionar um novo elemento ao modelo, com nossos próprios dados personalizados.


Removendo linhas
A maneira de remover linhas é semelhante à de adicionar linhas. Vamos criar outro método que chamaremos em QML. Desta vez, usaremos um parâmetro adicional, que é um inteiro que representa o número da linha. O nome da espécie é usado para excluir a chave do QMap, enquanto o número da linha será usado para excluir a linha no 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));
}Agora, vamos atualizar o aplicativo para que um botão "Excluir" apareça ao lado do botão de edição e conectá-lo ao nosso método delete.
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);
}
}
}
}
}
}
}
}

Código completo
Main.qml
| |
model.h
| |
model.cpp
| |
Mais informações
Para mais informações, consulte Usando Modelos C++ com Qt Quick Views e Programação de Modelos/Visualizações.