Apostille au MVC : les délégués

| Octal | dourouc05 |

L'architecture modèle vue contrôleur (Model View Controler) est un modèle de conception (design pattern) bien connu et beaucoup utilisé lors de la création d'interfaces graphiques.

Commentez

- Titre : Apostille au MVC : les délégués

- Auteurs : Octal, dourouc05

- Parution : 18 juillet 2011

- Mise à jour : 1 septembre 2011

- Licence :

Sommaire

- I. Quelques piqûres de rappel

- I-A. L'architecture MVC

- I-B. Mise en œuvre avec Qt

- I-B-1. Les modèles

- I-B-2. Les vues

- II. Parlons du contrôleur

- II-A. Présentation du concept

- II-B. Utiliser un délégué existant

- II-C. Jetons un coup d'œil à la mécanique interne

- III. Créer son propre délégué

- III-A. createEditor()

- III-B. setEditorData()

- III-C. setModelData()

- III-D. updateEditorGeometry()

- III-E. paint()

- IV. Passons à la pratique

- IV-A. Le modèle

- IV-A-1. La classe

- IV-A-2. flags()

- IV-A-3. rowCount() et columnCount()

- IV-A-4. data(), headerData() et setData()

- IV-A-5. addElement()

- IV-A-6. Exercice

- IV-B. Le délégué

- IV-B-1. La classe

- IV-B-2. createEditor()

- IV-B-3. setEditorData() et setModelData()

- IV-B-4. updateEditorGeometry()

- IV-B-5. paint()

- V. Conclusion

- VI. Remerciements

I - Quelques piqûres de rappel

I-A - L'architecture MVC

Originaire du langage Smalltalk, ce modèle permet de diviser une interface utilisateur en trois entités :

- le modèle contient les données. Il s'occupe du traitement et de l'interaction des données avec, par exemple, une base de données ;

- la vue est la représentation graphique. Elle correspond à la partie affichage des données, de ce que l'utilisateur peut voir ;

- le contrôleur prend en charge l'interaction avec l'utilisateur, recevant tous les événements déclenchés par l'utilisateur (clic, sélection...) et mettant par la suite à jour les données.

Cette architecture permet donc de séparer les différentes entités d'une application, aboutissant à une architecture flexible, claire et maintenable.

I-B - Mise en œuvre avec Qt

Dans sa version 4, Qt a introduit un ensemble de nouvelles vues mettant en œuvre une architecture de type modèle/vue.

I-B-1 - Les modèles

Tous les modèles fournis par Qt sont basés sur la classe QAbstractItemModel. Cette classe est la plus abstraite et la plus haute dans la hiérarchie des classes des différents modèles. Cette classe fournit une interface que tous les modèles doivent respecter, afin d'être utilisés correctement avec une vue.

De base, Qt fournit un ensemble de modèles pouvant être directement utilisés comme :

- QStringListModel : stockage d'une liste de QString ;

- QFileSystemModel : un ensemble d'informations sur un fichier ou un répertoire du système de fichier local (anciennement QDirModel) ;

- QStandardItemModel : gestion de tout type de structure, complexe ou non ;

- QSqlTableModel, QSqlRelationalTableModel : accès à une base de données (SQLite, MySQL...).

Cependant, il arrive parfois (voire souvent) que ces modèles ne conviennent pas à l'utilisation et que l'on souhaite créer ses propres modèles. Pour cela, rien de réellement complexe. Il suffit en effet de créer une classe dérivant d'une des classes suivantes :

- QAbstractItemModel : comme évoqué plus haut, il s'agit de la classe la plus abstraite ;

- QAbstractListModel : classe abstraite fournissant une interface pour un modèle de type liste (associé à une QListView) ;

- QAbstractTableModel : classe abstraite fournissant une interface pour un modèle de type tableau (associé à une QTableView).

À noter qu'il n'existe pas de classe séparée pour un modèle de type hiérarchique (QTreeView). Il s'agit simplement de QAbstractItemModel.

Vous remarquerez que toutes ces classes utilisent la même racine, à savoir QAbstractXModel.

Une fois que l'on a choisi la classe de base convenant le mieux à notre utilisation, il ne reste plus qu'à redéfinir certaines méthodes virtuelles comme :

- int QAbstractItemModel::rowCount(const QModelIndex & parent = QModelIndex()) const : fonction retournant le nombre de lignes du modèle ;

- QVariant QAbstractItemModel::data(const QModelIndex & index, int role = Qt::DisplayRole) const : fonction renvoyant la donnée associée au rôle role pour l'index index.

Pour des modèles modifiables, il est aussi nécessaire de redéfinir la fonction bool QAbstractItemModel::setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole).

La création de modèles personnalisés n'est cependant pas l'objet de ce tutoriel. Bien que nous l'aborderons brièvement au travers d'un exemple, je vous renvoie à la documentation traitant le sujet en profondeur.

I-B-2 - Les vues

Une fois que l'on dispose d'un modèle (déjà existant ou personnalisé), on dispose de trois types de vue :

- QListView : liste d'éléments ;

- QTableView : tableau d'éléments ;

- QTreeView : représentation d'éléments sous forme hiérarchique (arbre d'éléments).

Une fois la vue choisie, le modèle y est affecté en utilisant la fonction void QAbstractItemView::setModel(QAbstractItemModel * model).

Pour davantage de précisions, libre à vous de vous reporter à la documentation.

II - Parlons du contrôleur

Comme vous l'aurez sans doute remarqué, nous n'avons jusqu'à présent pas évoqué le dernier aspect, à savoir le contrôleur. Voici la vérité : Qt n'utilise pas tout à fait une architecture MVC. Ce n'est plus un C, mais bien un D. Pour ceux qui n'auraient pas fait le rapprochement, il s'agit d'un D pour délégué (delegate). Eh oui, maintenant, vous savez tout : Qt utilise en réalité une architecture modèle-vue entourée d'un délégué.

II-A - Présentation du concept

Mais à quoi diantre ce fameux délégué sert-il ?

Au contraire d'une architecture MVC classique, l'architecture modèle-vue instaurée par Qt ne fournit pas de réel composant permettant de gérer les interactions avec l'utilisateur. De ce fait, ceci est géré par la vue elle-même.

Cependant, pour des raisons de flexibilité, l'interaction et les entrées utilisateurs ne sont non pas prises en compte par un composant totalement séparé, à savoir le contrôleur, mais par un composant interne à la vue : le délégué. Ce composant est responsable de deux choses :

- personnaliser l'édition des éléments au moyen d'un éditeur ;

- personnaliser le rendu des éléments à l'intérieur d'une vue.

Ainsi, grâce à un délégué, il vous est possible de personnaliser la manière dont les entrées utilisateurs seront gérées, ainsi que le rendu des éléments. Par exemple, lorsqu'on utilise une QTableView avec un modèle éditable ou clique sur une cellule de votre table, il est possible de modifier la valeur de la cellule grâce à un QLineEdit. Cela est possible grâce au délégué qui a fourni à la vue un moyen d'éditer les données au travers d'un composant de type QLineEdit.

II-B - Utiliser un délégué existant

Vous aurez sans doute remarqué que, par défaut, même si vous n'avez paramétré aucun délégué, votre vue se charge toute seule de fournir un moyen d'éditer vos données. Cela est dû au fait que les vues sont déjà dotées d'un délégué par défaut : QStyledItemDelegate. En effet, de base, Qt fournit deux délégués :

- QItemDelegate ;

- QStyledItemDelegate.

La seule différence résidant entre ces deux délégués est que QStyledItemDelegate utilise le style courant pour le rendu des données. Ces deux délégués héritent cependant de la même classe, à savoir QAbstractItemDelegate, fournissant une interface de base générique à tous les délégués. Qt fournit en outre QSqlRelationalDelegate, permettant d'éditer et afficher des données d'un QSqlRelationalTableModel.

Cependant, même si les vues sont dotées d'un délégué par défaut, il est bien entendu possible de modifier et paramétrer celui-ci grâce à la fonction void QAbstractItemView::setItemDelegate(QAbstractItemDelegate * delegate). Il est aussi possible de récupérer le délégué courant grâce à la fonction QAbstractItemDelegate * QAbstractItemView::itemDelegate() const.

II-C - Jetons un coup d'œil à la mécanique interne

Nous avons dit que le délégué était en partie chargé de fournir à l'utilisateur un moyen d'éditer les données au sein d'une vue, en fournissant un éditeur, tout simplement un composant de type QWidget. Même si, précédemment, on a évoqué l'exemple d'une QLineEdit, le délégué par défaut ne crée pas que des QLineEdit. En effet, celui-ci crée le composant adapté au type de données de la cellule. Par exemple, si la cellule contient un int, le délégué par défaut créera un composant de type QSpinBox. Pour un composant de type double, il s'agira d'un QDoubleSpinBox.

Regardons le code de %QTDIR%/src/gui/itemviews/qstyleditemdelegate.cpp, notamment la méthode createEditor() :

QVariant::Type t = static_cast<QVariant::Type>(index.data(Qt::EditRole).userType());
return d->editorFactory()->createEditor(t, parent);

Que font ces deux lignes ? Tout d'abord, le type de la donnée contenue dans la cellule est stocké dans la variable t. Souvenez-vous, la fonction data() de QAbstractItemModel renvoyant une variable de type QVariant, celle-ci est capable de gérer de nombreux types de données. La fonction userType() renvoie ce type sous forme d'un int (ce qui justifie l'emploi de static_cast). Si la variable est de type QString, t serait donc égal à QVariant::String,QVariant::Date pour une variable de type QDate, etc.

La deuxième ligne fait appel à la méthode createEditor() de QItemEditorFactory, editorFactory() étant définie comme :

const QItemEditorFactory *editorFactory() const
{
return factory ? factory : QItemEditorFactory::defaultFactory();
}

On y comprend donc que cette méthode retourne la fabrique par défaut si aucune n'a été définie. Mais qu'est-ce donc que cette fameuse fabrique ? QItemEditorFactory est une classe dite de fabrique (factory), permettant entre autres de créer l'éditeur correspondant au type de données que l'on a évoqué plus haut.

Par défaut, voici les composants créés en fonction du type :

Type

Widget

bool

QComboBox

int / unsigned

QSpinBox

double

QDoubleSpinBox

QDate

QDateEdit

QDateTime

QDateTimeEdit

QPixmap

QLabel

QString

QLineEdit

QTime

QTimeEdit

Le délégué par défaut est donc capable de gérer une grande partie des types de données. Cependant, il se peut que nous ayons besoin d'un comportement autre que celui par défaut. Pour cela, il suffit de créer son propre délégué, c'est ce que nous allons voir dans le chapitre suivant.

III - Créer son propre délégué

Comme vu dans la partie précédente, on dispose de trois classes de base déjà fournies par Qt :

- QAbstractItemDelegate ;

- QItemDelegate ;

- QStyledItemDelegate.

Depuis Qt 4.4, il est recommandé d'utiliser QStyledItemDelegate. On l'utilisera donc comme classe de base de nos délégués.

Lorsque l'on souhaite uniquement personnaliser l'édition des éléments dans notre vue et non le rendu, on doit redéfinir quatre méthodes.

III-A - createEditor()

QWidget * QStyledItemDelegate::createEditor ( QWidget * parent, const QStyleOptionViewItem & option,
const QModelIndex & index ) const

Cette fonction retourne le widget (éditeur) pour éditer l'item se trouvant à l'index index. Le paramètre option permet de contrôler comment le widget apparaît.

Cette fonction sera par exemple appelée lorsque l'utilisateur effectuera un double-clic sur la cellule d'une QTableView. L'éditeur retourné par la fonction sera alors présenté à l'utilisateur.

III-B - setEditorData()

void QStyledItemDelegate::setEditorData ( QWidget * editor, const QModelIndex & index ) const

Cette fonction permet de transmettre à l'éditeur editor les données à afficher à partir du modèle se trouvant à l'index index.

Une fois l'éditeur créé et ouvert, cette fonction sera appelée afin de le remplir avec la donnée concernée à partir du modèle. La donnée sera alors extraite du modèle puis affichée.

III-C - setModelData()

void QStyledItemDelegate::setModelData ( QWidget * editor, QAbstractItemModel * model, const QModelIndex & index ) const

Cette fonction est, en quelque sorte, l'inverse de la précédente. Son rôle est de récupérer les données de l'éditeur et de les stocker à l'intérieur du modèle, à l'index identifié par le paramètre index.

III-D - updateEditorGeometry()

void QStyledItemDelegate::updateEditorGeometry ( QWidget * editor, const QStyleOptionViewItem & option,
const QModelIndex & index ) const

Lorsque la taille de la vue change, cette fonction permet de redimensionner l'éditeur à la bonne taille.

De manière générale, l'implémentation contiendra la ligne de code suivante :

editor->setGeometry(option.rect);

option.rect délimite la zone de l'éditeur.

Si l'on souhaite par ailleurs personnaliser le rendu des éléments, il faudra redéfinir une cinquième méthode.

III-E - paint()

void QStyledItemDelegate::paint ( QPainter * painter, const QStyleOptionViewItem & option,
const QModelIndex & index ) const

Comme son nom l'indique, cette fonction effectue le rendu d'un certain élément en utilisant painter. Les éléments sont rendus en utilisant le style courant de la vue.

IV - Passons à la pratique

Maintenant que l'on sait comment créer son propre délégué, on va mettre ce qu'on vient d'apprendre en œuvre au travers d'un exemple. Pour éviter de s'épancher dans un long discours, voici une capture d'écran de l'application à réaliser :

Il s'agit donc d'un tableau (QTableView) comportant deux colonnes, présentant à gauche un élément et à droite une valeur associée (un pourcentage). Lorsque l'on n'est pas en mode édition, on dessine une barre, représentant le pourcentage. En édition, on utilise un QSlider afin de faire varier le pourcentage comme bon nous semble.

IV-A - Le modèle

La première étape va être de créer son propre modèle. On n'utilisera pas ici de QStandardItemModel. La première chose à faire est de réfléchir à quelle classe de base on va hériter. Le modèle devant être présenté en tant que tableau, il devient alors évident que l'on va hériter de la classe QAbstractTableModel.

IV-A-1 - La classe

class TableModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum Columns { Element = 0, Value, Count = Value + 1 };

explicit TableModel(QObject *parent = 0);

Qt::ItemFlags flags(const QModelIndex &index) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const;

QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole);

void addElement(const QString &element, int value);

private:
QList<QPair<QString, int> > mElements;

};

Pour des raisons de lisibilité et flexibilité, on définit une énumération Columns permettant de lister les différentes colonnes. Count est une petite astuce pour obtenir le nombre d'éléments et servira pour la méthode columnCount().

La méthode Qt::ItemFlags flags(const QModelIndex & index) const; sera utile pour décider quelles parties de notre modèle seront éditables ou non (la colonne des éléments sera en lecture seule).

Les éléments seront stockés à l'intérieur d'une liste de QPair. Pour rappel, une paire est un ensemble composé de deux éléments. Le premier élément sera le texte QString et le deuxième la valeur associée (int). La méthode void addElement(const QString & element, int value); permettra d'ajouter un élément à l'intérieur du modèle.

IV-A-2 - flags()

Qt::ItemFlags TableModel::flags(const QModelIndex &index) const
{
if (index.column() == Element)
{
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
else if (index.column() == Value)
{
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}

return QAbstractTableModel::flags(index);
}

À l'intérieur de la fonction, on vérifie la colonne visée grâce à la méthode column() de index. Si la colonne est la colonne des éléments (Element), on retourne les drapeaux Qt::ItemIsEnabled | Qt::ItemIsEnabled. Ceci signifie que notre élément sera activé et sélectionnable mais pas éditable. Si la colonne est la colonne des valeurs (Value), on y ajoute le drapeau Qt::ItemIsEditable, permettant à l'utilisateur d'éditer l'élément.

Si on ne se trouve dans aucune de ces configurations (ce qui ne devrait pas être le cas ici), on laisse la responsabilité à la classe de base de retourner les drapeaux nécessaires.

IV-A-3 - rowCount() et columnCount()

int TableModel::columnCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : Count;
}

Si l'index parent est valide (ce modèle n'est pas hiérarchique), on renvoie 0, sinon Count (énumération précédemment définie).

int TableModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : mElements.count();
}

Comme précédemment, on renvoie 0 si parent est valide. Sinon, le nombre de lignes du modèle correspond au nombre d'éléments de mElements : mElements.count().

IV-A-4 - data(), headerData() et setData()

QVariant TableModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= mElements.count())
{
return QVariant();
}

switch (role)
{
case Qt::DisplayRole:
case Qt::EditRole:
if (index.column() == Element)
{
return mElements[index.row()].first;
}
else if (index.column() == Value)
{
return mElements[index.row()].second;
}
break;
}

return QVariant();
}

On commence par vérifier la validité de l'index. Si tel n'est pas le cas, on renvoie une donnée invalide : QVariant().

On renvoie ensuite, selon le rôle role, la donnée correspondante. Dans ce modèle, on prend en compte les rôles Qt::DisplayRole et Qt::EditRole. S'il s'agit d'un de ces deux rôles, on renvoie le premier élément de la paire correspondant à l'index index si la colonne est Element ou bien le second pour la colonne Value.

Pour tous les rôles restants, on retourne une donnée invalide.

QVariant TableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
switch (section)
{
case Element:
return trUtf8("Elément");
break;
case Value:
return trUtf8("Valeur");
break;
}
}

return QAbstractTableModel::headerData(section, orientation, role);
}

Ce code ne devrait pas avoir besoin de descriptions supplémentaires. Si on est dans l'orientation Horizontal avec le rôle DisplayRole, on renvoie le texte correspondant à la section (colonne).

bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() < 0 || index.row() >= mElements.count())
{
return false;
}

switch (role)
{
case Qt::DisplayRole:
case Qt::EditRole:
if (index.column() == Element)
{
mElements[index.row()].first = value.toString();
}
else if (index.column() == Value)
{
mElements[index.row()].second = value.toInt();
}
emit dataChanged(index, index);
return true;
break;
}

return false;
}

Voir la fonction data(). Il est par ailleurs important de prendre soin d'émettre le signal void QAbstractItemModel::dataChanged(const QModelIndex & topLeft, const QModelIndex & bottomRight), afin de notifier à la vue que les données ont changé.

IV-A-5 - addElement()

void TableModel::addElement(const QString &element, int value)
{
const int count = mElements.count();
if (value < 0)
{
value = 0;
}
else if (value > 100)
{
value = 100;
}
beginInsertRows(QModelIndex(), count, count);
mElements << qMakePair(element, value);
endInsertRows();
}

On commence par récupérer le nombre d'éléments courant puis on s'assure que value est comprise entre 0 et 100. La fonction void QAbstractItemModel::beginInsertRows(const QModelIndex & parent, int first, int last) permet de commencer une opération d'insertion de ligne dans un modèle (il est nécessaire de l'appeler). Après avoir ajouté l'élément à l'intérieur de la liste en utilisant la fonction utilitaire QPair<T1, T2> qMakePair(const T1 & value1, const T2 & value2), on termine l'opération d'insertion avec endInsertRows().

IV-A-6 - Exercice

En vous aidant de la documentation, modifiez le modèle afin que les éléments de la première colonne soient alignés de manière centrée.

Premier indice : la fonction à modifier est data().

Deuxième indice : le rôle à gérer est Qt::TextAligmentRole.

La solution

case Qt::TextAlignmentRole:
if (index.column() == Element)
{
return Qt::AlignCenter;
}
break;

IV-B - Le délégué

Le modèle étant désormais terminé et prêt à l'emploi, on va s'atteler au délégué, qui nous permettra d'éditer les valeurs grâce à un curseur et de dessiner une barre en dehors du mode édition.

IV-B-1 - La classe

class TableDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit TableDelegate(QObject *parent = 0);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
void setEditorData(QWidget *editor, const QModelIndex &index) const;
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const;

void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const;

};

Aucune surprise ici. On crée une classe TableDelegate héritant de QStyledItemDelegate et on redéfinit les méthodes nécessaires (voir précédemment).

IV-B-2 - createEditor()

QWidget *TableDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
if (index.column() == TableModel::Value)
{
QSlider *editor = new QSlider(Qt::Horizontal, parent);
editor->setRange(0, 100);
editor->setAutoFillBackground(true);
return editor;
}

return QStyledItemDelegate::createEditor(parent, option, index);
}

Comme à l'habitude, on agit en fonction de la colonne (column()). Dans le cas de la colonne Value, on crée un QSlider horizontal. On lui affecte un intervalle de valeurs de 0 à 100 et on place la propriété autoFillBackground à true afin d'obtenir un fond opaque. Sinon, on laisse la responsabilité à la classe parente de créer l'éditeur.

IV-B-3 - setEditorData() et setModelData()

void TableDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
if (index.column() == TableModel::Value)
{
QSlider *slider = qobject_cast<QSlider *>(editor);
if (slider)
{
const int value = index.model()->data(index).toInt();
slider->setValue(value);
}
}
else
{
QStyledItemDelegate::setEditorData(editor, index);
}
}

Afin d'initialiser correctement le curseur, on récupère la valeur correspondante dans le modèle à l'index index (que l'on n'oublie pas de convertir en int, la fonction data() renvoyant un QVariant) puis on l'affecte au curseur en appelant la fonction setValue(). Le paramètre editor étant de type QWidget, on n'oublie pas de le convertir en QSlider à l'aide de qobject_cast.

void TableDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
if (index.column() == TableModel::Value)
{
QSlider *slider = qobject_cast<QSlider *>(editor);
if (editor)
{
model->setData(index, slider->value());
}
}
else
{
QStyledItemDelegate::setModelData(editor, model, index);
}
}

On réalise ici l'opération inverse. Lors de la fin de l'édition, on récupère la valeur du curseur puis on l'affecte à notre modèle à l'index index. Ceci permet donc de mettre à jour le modèle une fois l'édition terminée et l'éditeur fermé.

IV-B-4 - updateEditorGeometry()

void TableDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
Q_UNUSED(index);
editor->setGeometry(option.rect);
}

Comme convenu, on met à jour la géométrie (position et taille) de l'éditeur via les informations du paramètre option. La macro Q_UNUSED permet de marquer le paramètre index comme non utilisé, afin de ne pas obtenir d'avertissement lors de la compilation (unused parameter).

IV-B-5 - paint()

void TableDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.column() == TableModel::Value)
{
painter->save();
const int value = index.model()->data(index).toInt();
QRect rect(option.rect);
const int width = (value * rect.width()) / 100;
rect.setWidth(width);
QColor c;
if (value <= 20)
{
c = Qt::red;
}
else if (value <= 50)
{
c = QColor(240, 96, 0);
}
else
{
c = Qt::green;
}

painter->fillRect(rect, c);
QTextOption o;
o.setAlignment(Qt::AlignCenter);
painter->drawText(option.rect, QString("%1 %").arg(value), o);

painter->restore();
}
else
{
QStyledItemDelegate::paint(painter, option, index);
}
}

Pour finir, la partie rendu. Souhaitant uniquement agir sur la colonne des valeurs, on commence par vérifier si on est dans ce cas. Sinon, on laisse la responsabilité à la classe parente d'effectuer le rendu.

On commence par enregistrer (sauvegarder) l'état courant du painter. On récupère la valeur située à l'index correspondant afin de calculer la largeur effective de la barre grâce à un simple produit en croix. On obtient donc un rectangle QRect. On en profite pour affecter une couleur différente selon la valeur correspondante :

- rouge si la valeur est inférieure ou égale à 20 ;

- orange si la valeur est inférieure ou égale à 50 ;

- vert sinon.

Disposant du rectangle et de sa couleur, on utilise la fonction fillRect() pour dessiner et remplir le rectangle de la couleur correspondante. Pour finir, on dessine le texte correspondant à la valeur suivie du pourcentage, de manière centrée, avant de restaurer l'état du painter préalablement sauvegardé.

Ceci conclut donc le programme. Il est laissé au soin du lecteur de le compléter en créant la vue, le modèle puis le délégué avant de remplir le modèle des valeurs souhaitées, afin d'avoir un programme complet et opérationnel.

V - Conclusion

Au terme de ce tutoriel, vous aurez appris :

- ce que sont les délégués : des composants permettant de personnaliser l'édition et le rendu des éléments à l'intérieur d'une vue ;

- comment créer son propre délégué.

D'autre part, on a vu, au travers d'un exemple simple, comment créer de toutes pièces son propre modèle. Pour un autre exemple d'utilisation, un peu plus concret, la documentation fournit par ailleurs un Star Delegate Example.

VI - Remerciements

Merci à Thibaut Cuvelier, Claude Leloup et Maxime Gault pour leur relecture !