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 :

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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

 
Sélectionnez
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 :

 
Sélectionnez
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) :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 !