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();
}
IV. Remerciements▲
Merci à Louis du Verdier et à Claude Leloup pour leur relecture !