Utiliser un modèle de liste C++ en QML

| Christophe Dumez | traducteur : Thibaut Cuvelier |

La plupart des exemples QML actuels sont exclusivement écrits en QML et JavaScript, probablement pour les garder simples. Cependant, dans une application réelle, pour des raisons de performances, il est recommandé d'écrire autant de code que possible en C++ et de n'utiliser QML que pour l'interface utilisateur. Au final, il est important de savoir comment utiliser un modèle C++ en QML.

QML ne fournit de support direct que pour les modèles de liste pour le moment (comme QAbstractListModel, à cause des vues actuellement supportées (ListView, GridView...).

Le but de cet article est de montrer une manière facile d'utiliser un modèle de liste en C++ en QML au lieu d'un modèle QML comme ListModel.

Commentez

- Titre : Utiliser un modèle de liste C++ en QML

- Auteurs : Christophe Dumez, traducteur : Thibaut Cuvelier

- Parution : 4 février 2012

- Mise à jour : 16 février 2012

- Licence : Copyright ® 2012 Christophe Dumez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Première étape : écrire un modèle de liste en C++ qui dérive de QAbstractListModel

- III. Deuxième étape : créer une classe d'item pour le modèle (ListItem)

- IV. Exposer le modèle C++ à QML

- V. Créer une vue QML qui utilise le modèle C++

- VI. Remerciements

I - L'article original

Cet article est une adaptation en langue française de How to use a C++ list model in QML.

II - Première étape : écrire un modèle de liste en C++ qui dérive de QAbstractListModel

Pour l'exemple, on a écrit une classe ListModel qui hérite de QAbstractListModel et fournit les méthodes les plus couramment utilisées. Elle utilise un conteneur QList pour stocker les items et définit une classe abstraite ListItem pour aider à définir des classes d'items qui conviennent.

listmodel.h

class ListItem: public QObject {
Q_OBJECT

public:
ListItem(QObject* parent = 0) : QObject(parent) {}
virtual ~ListItem() {}
virtual QString id() const = 0;
virtual QVariant data(int role) const = 0;
virtual QHash<int, QByteArray> roleNames() const = 0;

signals:
void dataChanged();
};

class ListModel : public QAbstractListModel
{
Q_OBJECT

public:
explicit ListModel(ListItem* prototype, QObject* parent = 0);
~ListModel();
int rowCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
void appendRow(ListItem* item);
void appendRows(const QList<ListItem*> &items);
void insertRow(int row, ListItem* item);
bool removeRow(int row, const QModelIndex &parent = QModelIndex());
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
ListItem* takeRow(int row);
ListItem* find(const QString &id);
QModelIndex indexFromItem( const ListItem* item) const;
void clear();

private slots:
void handleItemChange();

private:
ListItem* m_prototype;
QList<ListItem*> m_list;
};

listmodel.cpp

/*
* Author: Christophe Dumez <dchris@gmail.com>
* License: Public domain (No attribution required)
* Website: http://cdumez.blogspot.com/
* Version: 1.0
*/

#include "listmodel.h"

ListModel::ListModel(ListItem* prototype, QObject *parent) :
QAbstractListModel(parent), m_prototype(prototype)
{
setRoleNames(m_prototype->roleNames());
}

int ListModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_list.size();
}

QVariant ListModel::data(const QModelIndex &index, int role) const
{
if(index.row() < 0 || index.row() >= m_list.size())
return QVariant();
return m_list.at(index.row())->data(role);
}

ListModel::~ListModel() {
delete m_prototype;
clear();
}

void ListModel::appendRow(ListItem *item)
{
appendRows(QList<ListItem*>() << item);
}

void ListModel::appendRows(const QList<ListItem *> &items)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount()+items.size()-1);
foreach(ListItem *item, items) {
connect(item, SIGNAL(dataChanged()), SLOT(handleItemChange()));
m_list.append(item);
}
endInsertRows();
}

void ListModel::insertRow(int row, ListItem *item)
{
beginInsertRows(QModelIndex(), row, row);
connect(item, SIGNAL(dataChanged()), SLOT(handleItemChange()));
m_list.insert(row, item);
endInsertRows();
}

void ListModel::handleItemChange()
{
ListItem* item = static_cast<ListItem*>(sender());
QModelIndex index = indexFromItem(item);
if(index.isValid())
emit dataChanged(index, index);
}

ListItem * ListModel::find(const QString &id) const
{
foreach(ListItem* item, m_list) {
if(item->id() == id) return item;
}
return 0;
}

QModelIndex ListModel::indexFromItem(const ListItem *item) const
{
Q_ASSERT(item);
for(int row=0; row<m_list.size(); ++row) {
if(m_list.at(row) == item) return index(row);
}
return QModelIndex();
}

void ListModel::clear()
{
qDeleteAll(m_list);
m_list.clear();
}

bool ListModel::removeRow(int row, const QModelIndex &parent)
{
Q_UNUSED(parent);
if(row < 0 || row >= m_list.size()) return false;
beginRemoveRows(QModelIndex(), row, row);
delete m_list.takeAt(row);
endRemoveRows();
return true;
}

bool ListModel::removeRows(int row, int count, const QModelIndex &parent)
{
Q_UNUSED(parent);
if(row < 0 || (row+count) >= m_list.size()) return false;
beginRemoveRows(QModelIndex(), row, row+count-1);
for(int i=0; i<count; ++i) {
delete m_list.takeAt(row);
}
endRemoveRows();
return true;
}

ListItem * ListModel::takeRow(int row)
{
beginRemoveRows(QModelIndex(), row, row);
ListItem* item = m_list.takeAt(row);
endRemoveRows();
return item;
}

On peut réutiliser facilement ces fichiers pour créer son propre modèle.

III - Deuxième étape : créer une classe d'item pour le modèle (ListItem)

On montre ici comment hériter de la classe ListItem pour créer des items. On considère l'exemple d'un fruit ayant les propriétés de nom, taille (petit, moyen, grand) et prix.

fruititem.h

class FruitItem : public ListItem
{
Q_OBJECT

public:
enum Roles {
NameRole = Qt::UserRole+1,
SizeRole,
PriceRole
};

public:
FruitItem(QObject *parent = 0): ListItem(parent) {}
explicit FruitItem(const QString &name, const QString &size, QObject *parent = 0);
QVariant data(int role) const;
QHash<int, QByteArray> roleNames() const;
void setPrice(qreal price);
inline QString id() const { return m_name; }
inline QString name() const { return m_name; }
inline QString size() const { return m_size; }
inline qreal price() const { return m_price; }

private:
QString m_name;
QString m_size;
qreal m_price;
};

fruititem.cpp

FruitItem::FruitItem(const QString &name, const QString &size, QObject *parent) :
ListItem(parent), m_name(name), m_size(size), m_price(-1)
{
}

void FruitItem::setPrice(qreal price)
{
if(m_price != price) {
m_price = price;
emit dataChanged();
}
}

QHash<int, QByteArray> FruitItem::roleNames() const
{
QHash<int, QByteArray> names;
names[NameRole] = "name";
names[SizeRole] = "size";
names[PriceRole] = "price";
return names;
}

QVariant FruitItem::data(int role) const
{
switch(role) {
case NameRole:
return name();
case SizeRole:
return size();
case PriceRole:
return price();
default:
return QVariant();
}
}

Il est important de comprendre la fonction roleNames(). Elle sera utilisée par le ListModel pour retourner ses noms de rôle (voir QAbstractItemModel::roleNames()). Ainsi, le délégué QML aura accès aux différentes propriétés de l'item en utilisant les noms de rôle assignés.

De même, le prix peut être changé en utilisant la méthode setPrice(). Pour cette raison, on s'assure que setPrice() émet le signal dataChanged() pour que le modèle transmette l'information à la vue, qui sera alors mise à jour.

IV - Exposer le modèle C++ à QML

Une manière facile d'intégrer C++ et QML est d'utiliser la classe QDeclarativeView. Elle fournit un widget pour afficher une interface graphique déclarative. On peut alors utiliser QDeclarativeView::rootContext()::setContextProperty(QString name, QObject* value) pour exposer une instance du modèle à QML et lui assigner un nom de variable utilisable depuis QML.

Voici un exemple :

ListModel* createModel() {
ListModel *model = new ListModel(new FruitItem, qApp);
model->appendRow(new FruitItem("Apple", "medium", model));
model->appendRow(new FruitItem("PineApple", "big", model));
model->appendRow(new FruitItem("Grape", "small", model));
return model;
}

int main(int argc, char *argv[])
{
QApplication app(argc, argv);

QDeclarativeView view;
view.rootContext()->setContextProperty("fruitsModel",  createModel());
view.setSource(QUrl::fromLocalFile("qml/QMLCPP/main.qml"));
view.show();

return app.exec();
}

La fonction createModel() crée une instance de ListModel en utilisant la classe FruitItem, que l'on vient de définir, comme prototype. Elle remplit aussi le modèle avec quelques items avant de le retourner à l'appelant.

V - Créer une vue QML qui utilise le modèle C++

Utiliser le modèle C++ en QML est maintenant aussi facile que lui assigner son nom de variable (défini lors de l'exposition du modèle avec setContextPropery()) à la propriété de modèle de la vue.

L'exemple suivant utilise une vue ListView de QML pour afficher les données du modèle de fruits :

main.qml

import QtQuick 1.0

Rectangle {
width: 360
height: 360
ListView {
id: fruitsView
anchors.fill: parent
model: fruitsModel
delegate: DummyDelegate{}
}
}

Voici le code du délégué qui accède aux propriétés des items du modèle en utilisant les noms de rôle assignés (ici, name et size) :

DummyDelegate.qml

import QtQuick 1.0

Item {
id: delegate
height: 30
width: delegate.ListView.view.width

Text {
anchors.fill: parent;
text: name +" ("+size+")"
}
}

VI - Remerciements

Merci à Guillaume Belz et Claude Leloup pour leur relecture attentive !