Le modèle OpenGL : implémentation pour la lecture

| Witold Wysota | Traducteur : Thibaut Cuvelier |

Ici, on expliquera quelles étapes doivent être effectuées pour obtenir un modèle OpenGL qui fonctionne et est prêt à fournir des données aux vues. Cet article est divisé en deux parties : ce qu'il faut implémenter pour un modèle en général puis une application au modèle OpenGL.

Commentez

- Titre : Le modèle OpenGL : implémentation pour la lecture

- Auteurs : Witold Wysota, Traducteur : Thibaut Cuvelier

- Parution : 6 avril 2011

- Mise à jour : 2 mai 2011

- Licence : Copyright ® 2006 Witold Wysota. 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. Implémenter un modèle en général

- II-A. Géométrie du modèle

- II-B. Traversée du modèle

- II-C. Récupérer des données

- III. Un modèle OpenGL lisible

- IV. Remerciements

I - L'article original

Cet article est la traduction de l'article Implement for reading de Witold Wysota.

II - Implémenter un modèle en général

Pour obtenir un modèle fonctionnel, il faut implémenter quelques méthodes de l'interface QAbstractItemModel. En regardant la documentation de QAbstractItemModel, on remarque quelques méthodes définies comme abstraites pures :

virtual int columnCount ( const QModelIndex & parent = QModelIndex() ) const = 0
virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const = 0
virtual QModelIndex index ( int row, int column, const QModelIndex & parent = QModelIndex() ) const = 0
virtual QModelIndex parent ( const QModelIndex & index ) const = 0
virtual int rowCount ( const QModelIndex & parent = QModelIndex() ) const = 0

Elles doivent être réimplémentées pour créer des instances de la classe de modèle, cela suggère donc que ces méthodes sont responsables de l'unicité du modèle.

II-A - Géométrie du modèle

int columnCount ( const QModelIndex & parent = QModelIndex() ) const
int rowCount ( const QModelIndex & parent = QModelIndex() ) const

Ces deux méthodes sont responsables de la définition de la géométrie du modèle. Les deux prennent un objet QModelIndex (NULL par défaut), qui l'informe de l'item pour lequel on doit fournir des données. Comme le nom de la méthode le suggère, elles retournent les nombres de lignes et de colonnes d'un tableau contenant les enfants de l'item spécifié par parent. Si l'argument est invalide (comme la valeur par défaut), ces méthodes doivent retourner des valeurs pour l'élément de plus haut niveau du modèle (on dit souvent que cette valeur du paramètre correspond à une racine du modèle).

Par exemple, pour un modèle de tableau contenant des données pour un joueur de Battleships, on pourrait réimplémenter ces méthodes comme suit :

int BattleshipsModel::columnCount(const QModelIndex &parent) const {
if(parent.isValid())
return 0;  // items don't have children
return 10;  // "root item" has children in 10 columns
}
int BattleshipsModel::rowCount(const QModelIndex &parent) const {
if(parent.isValid())
return 0;  // items don't have children
return 10;  // "root item" has children in 10 rows
}

La vue va d'abord demander au modèle le nombre de lignes et de colonnes pour l'item racine (index parent invalide), puis demander le nombre de lignes et de colonnes pour chacun des enfants de l'item racine et agir récursivement de la sorte jusqu'à atteindre un nœud sans enfant (zéro colonne ou zéro ligne).

II-B - Traversée du modèle

QModelIndex parent ( const QModelIndex & index ) const
QModelIndex index ( int row, int column, const QModelIndex & parent = QModelIndex() ) const

Très souvent, on doit traverser le modèle et récupérer ce que ces méthodes sont censées retourner. La première renvoie un index d'un item parent de l'item pointé par index, on peut donc traverser le modèle de bas en haut. La deuxième méthode retourne un index d'un item qui est dans la ligne row et dans la colonne column et qui est enfant de parent. Ceci permet de traverser le modèle de haut en bas et d'atteindre la fratrie d'un item (traversée au même niveau).

La méthode index() recherche l'item spécifié par les coordonnées données et utilise une méthode protégée du modèle abstrait d'item createIndex(), qui crée un index pour les coordonnées données et permet de stocker un pointeur additionnel ou un numéro (identifiant) dans l'index. On peut utiliser cette valeur supplémentaire pour tout ce qui semble nécessaire - elle est majoritairement utilisée pour garder un pointeur sur un item dans les structures de données internes, qui détiennent l'information sur cette cellule particulière du modèle (par exemple, on peut garder un pointeur sur un élément d'un objet QList), ce qui rend vraiment facile la récupération d'informations sur l'objet dans d'autres méthodes en utilisant QModelIndex::internalPointer() ou QModelIndex::internalId(). Évidemment, s'il n'y a pas de besoin pour une telle valeur interne, on n'est pas obligé de l'utiliser et, dans un tel cas, on peut créer un objet QModelIndex sans utiliser createIndex().

Voici une implémentation d'exemple de la méthode pour un modèle arborescent conservant ses données dans une structure Node (gardée dans le membre m_root du modèle), avec un membre QList<Node> m_children pour garder une trace des enfants.

QModelIndex ListModel::index(int row, int column, const QModelIndex &parent) const {
if(column!=0)
return QModelIndex(); // the model only has a single column
if(row < 0 || row > rowCount(parent))
return QModelIndex(); // make sure the row number is valid
Node *node = m_root;
if(parent.isValid())
node = parent.internalPointer(); // fetch the internal pointer from the parent
return createIndex(row, 0, node->m_children.at(row)); // create and return an index
}

Utiliser un pointeur interne a permis d'échapper à la recherche du parent dans les structures de données internes.

Avec un modèle plat, parent() renverra toujours un index invalide (ce qui signifie que les items n'ont pas de parent, ce qui est une autre manière de dire que l'item racine est le parent de tous les items). Pour des modèles hiérarchiques, cette méthode recherche généralement le parent d'un item dans les structures de données internes et renvoie un index.

Une implémentation d'exemple pour un modèle contenant des items QGraphicsScene :

QModelIndex GraphicsScene::parent ( const QModelIndex & index ) const {
if(!index.isValid())
return QModelIndex(); // invalid items don't have parents

QGraphicsItem *gitem = dynamic_cast<QGraphicsItem*>(index.internalPointer());
if(!gitem)
return QModelIndex(); // invalid internal pointer

QGraphicsItem *gparent = gitem->parentItem(); // fetch the parent item
if(!gparent)
return QModelIndex(); // item is top level (doesn't have a parent)

bool parentIsTop = gparent->parentItem() !=0; // does parent have a parent?

// fetch the list of all children of the grandparent item (if exists)
// or the scene (for parent being a top level item)
QList<QGraphicsItem*> items = parentIsTop ? gparent->scene()->items()
: gparent->parentItem()->children();

// "items" is now used to determine the row of the parent
int row = items.indexOf(gparent); // get row of the parent in its parent's child list

return createIndex(row, 0, gparent); // create and return an index
}

II-C - Récupérer des données

QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const

La dernière chose à faire est de fournir un accès en lecture aux données du modèle. La méthode data() en est responsable. Dans cette méthode, on doit rechercher les données de la source de données (interne ou externe) et les retourner à l'appelant. On devrait fournir des données pour tous les rôles disponibles et les index de modèles. Si une donnée pour un index particulier ou pour un rôle particulier n'est pas disponible, il suffit de retourner un QVariant vide.

Voici un exemple qui retourne des données d'un modèle conservant des items comme des pointeurs sur des objets QGraphicsItem. On prend pour acquis que l'on veut afficher les noms des items (conservés dans QGraphicsItem::data() avec une clé 0) et que l'on veut afficher en rouge unnamed si l'item n'a pas de nom. On les veut aussi alignés à droite si le nom est un entier :

QVariant GraphicsScene::data ( const QModelIndex & index, int role = Qt::DisplayRole ) const {
if(!index.isValid())
return QVariant(); // return empty values for invalid items
QGraphicsItem *item = dynamic_cast<QGraphicsItem*>(index.internalPointer());
if(!item)
return QVariant(); // return empty values for invalid internal pointers
QVariant name = item->data(0); // fetch the name
switch(role){
case Qt::DisplayRole:
if(name.toString().isEmpty())
return "unnamed";            // return "unnamed" for empty names
else return name;              // return the name otherwise
case Qt::TextAlignmentRole:
if(name.canConvert(QVariant::Int))
return Qt::AlignRight;       // return right alignment if variant is int
else return Qt::AlignLeft;     // return left alignment otherwise
case Qt::ForegroundRole:
if(name.toString().isEmpty())
return Qt::red;              // return red colour for unnamed items
else return QVariant();        // return default colour otherwise
default:
return QVariant();             // return empty (default) values for other roles
}
}

Il y a aussi une méthode supplémentaire que l'on pourrait vouloir ré-implémenter (elle n'est pas abstraite) :

QVariant headerData ( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const

Elle est très semblable à data(), mais elle est liée aux en-têtes du modèle. L'implémentation est assez rapide, on ne la montrera donc pas en exemple.

III - Un modèle OpenGL lisible

L'implémentation du modèle OpenGL est très simple ; il suffit d'implémenter toutes les méthodes susmentionnées. Tout d'abord, l'en-tête de la classe :

class QwwGLModel : public QAbstractItemModel {
public:
enum { XRole = Qt::UserRole, YRole, ZRole, MaterialRole,
TextureRole, FlagsRole, TypeRole, XNormalRole,
YNormalRole, ZNormalRole, NameRole, SizeXRole,
SizeYRole, SizeZRole, Attr1Role, Attr2Role };

QwwGLModel(QObject *parent = 0);
~QwwGLModel();
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;
QModelIndex index ( int row, int column, const QModelIndex & parent = QModelIndex() ) const;
QModelIndex parent ( const QModelIndex & index ) const;
int rowCount ( const QModelIndex & parent = QModelIndex() ) const;
private:
QwwGLModelNode *_rootItem;
};

Maintenant, tentons d'implémenter ces méthodes. Les deux premières sont très simples :

QwwGLModel::QwwGLModel(QObject *parent) : QAbstractItemModel(parent){
_rootItem = new QwwGLModelNode;
}

QwwGLModel::~QwwGLModel(){
delete _rootItem;
}

La seule chose qui a besoin d'un mot d'explication est le membre rootItem. C'est une manière simple et pratique de stocker des données pour le modèle. Grâce à ce rootItem, on pourra opérer facilement sur les parents des items, comme on le verra dans un instant.

La géométrie du modèle est aussi simple et n'a pas besoin de plus de commentaires. Notons que rowCount() exploite le fait que chaque item valide a un parent QwwGLModelNode valide.

int QwwGLModel::columnCount( const QModelIndex &parent) const {
return 1;
}

int QwwGLModel::rowCount( const QModelIndex &parent) const {
QwwGLModelNode *parentNode = _rootItem;
if(parent.isValid())
parentNode = static_cast<QwwGLModelNode*>(parent.internalPointer());
return parentNode->children().count();
}

Pour la traversée :

QModelIndex QwwGLModel::index ( int row, int column, const QModelIndex & parent ) const {
if(column!=0)
return QModelIndex();
QwwGLModelNode *parentNode = _rootItem;
if(parent.isValid())
parentNode = static_cast<QwwGLModelNode*>(parent.internalPointer());
QList<QwwGLModelNode*> children = parentNode->children();
if(row>=children.count() || row <0)
return QModelIndex();
return createIndex(row, column, children.at(row));
}

Tout d'abord, on vérifie que la colonne et valide, ensuite on tente de récupérer un pointeur sur l'item parent. Si l'index du parent n'est pas valide, cela signifie que le nœud parent est bien _rootItem, sinon on le récupère du pointeur interne sur l'item parent. Tout ce qui reste est de récupérer le pointeur sur l'item spécifié en utilisant la liste des enfants du parent.

QModelIndex QwwGLModel::parent ( const QModelIndex & index ) const {
if(!index.isValid())
return QModelIndex();
QwwGLModelNode *parentNode = static_cast<QwwGLModelNode*>(index.internalPointer())->parent();
if(parentNode==_rootItem)
return QModelIndex();
int row = parentNode->parent()->children().indexOf(parentNode);
return createIndex(row, 0, parentNode);
}

Cette méthode est très similaire à la précédente. Tout d'abord, on vérifie que l'index donné est valide (ce qui implique qu'il a un parent valide, soit _rootItem, soit un item réel). Si index pointe sur un item de plus haut niveau, on retourne un index invalide pour le parent. Sinon, on détermine le numéro de ligne du parent dans la liste des enfants du parent et on retourne un index propre pour cet item.

Pour retourner les données :

QVariant QwwGLModel::headerData ( int section, Qt::Orientation orientation, int role ) const {
if(orientation!=Qt::Horizontal || section!=0 || role!=Qt::DisplayRole)
return QVariant();
return tr("GL Scene");
}

L'en-tête est très simple : si la requête concerne l'affichage de la première section de l'en-tête horizontal, on retourne GL Scene. Sinon, on retourne une valeur vide.

QVariant QwwGLModel::data ( const QModelIndex & index, int role = Qt::DisplayRole ) const {
static char *__roleAttrs[] = { "X", "Y", "Z", "Material", "Texture", "Flags", "Type", "XN",
"YN", "ZN", "Name", "XS", "YS", "ZS", "Attr1", "Attr2" };
if(!index.isValid())
return QVariant();
QwwGLModelNode *node = static_cast<QwwGLModelNode*>(index.internalPointer());
switch(role){
case Qt::NameRole:
case Qt::DisplayRole: // return the name
return node->attribute("Name");
case Qt::DecorationRole: // return an icon for each type
return QIcon(QPixmap(QString(":/types/%1.png").arg((int)node->type())));
case Qt::ToolTipRole:  // return the coordinate set
return QString("X: %1; Y: %2; Z: %3").arg(node->attribute("X"))
.arg(node->attribute("Y"))
.arg(node->attribute("Z"));
case TypeRole:
return (int)node->type();
default:
if(role>Attr2Role || role <0 || role-XRole>15)
return QVariant();
return node->attribute(__roleAttrs[role-XRole]);
}
}

On ne fournit qu'une implémentation très simple de la méthode data(). On peut l'étendre pour se débarrasser de QwwGLModelNode::atribute() ou pour fournir d'autres données.

Le modèle est maintenant lisible, mais on ne peut pas encore y écrire. On doit donc l'initialiser avec des données afin de le tester.

void QwwGLModel::init(){
QwwGLModelNode *t1 = new QwwGLModelNode(_rootItem);
t1->setType(0); // point
t1->setAttribute("Name", "Point");
t1->setAttribute("X", 10.0);
t1->setAttribute("Y", 10.0);
t1->setAttribute("Z", -5.0);
QwwGLModelNode *t2 = new QwwGLModelNode(_rootItem);
t2->setType(1); // line
t2->setAttribute("Name", "Line");
QwwGLModelNode *p1 = new QwwGLModelNode(t2);
p1->setType(0); // point
p1->setAttribute("X", 0);
p1->setAttribute("Y", 0);
p1->setAttribute("Z", 0);
p1->setAttribute("Name", "Point 1");
QwwGLModelNode *p2 = new QwwGLModelNode(t2);
p2->setType(0); // point
p2->setAttribute("X", 5.0);
p2->setAttribute("Y", 5.0);
p2->setAttribute("Z", 15.5);
p2->setAttribute("Name", "Point 2");
}

Et pour voir le résultat, avec les en-têtes qui sont nécessaires :

int main(int argc, char **argv){
QApplication app(argc, argv);
QwwGLModel model;
model.init();
QTreeView tv;
tv.setModel(&model);
tv.show();
return app.exec();
}

Les sources sont disponibles.

IV - Remerciements

Merci à Louis du Verdier et à Claude Leloup pour leur relecture !