Accueil > Développement > Utilisation de la Classe NSOutlineView

Utilisation de la Classe NSOutlineView

Par Renaud Préat, le 06/06/2003

Il est une classe que beaucoup d’apprentis programmeurs veulent utiliser dans une de leurs applications, mais dont l’implémentation n’est pas aisée : NSOutlineView. Cette classe permet d’afficher des données dans une liste, le tout organisé de façon hiérarchique, comme par exemple la vue en mode liste du Finder, les contacts organisés par groupe dans des logiciels de messagerie instantannée, etc. L’objectif de ce tutoriel sera d’expliquer les bases nécessaires pour l’utilisation de cette classe.

Deux exemples seront développés dans ce tutoriel : le premier sera une application que nous créerons pour expliquer les bases de l’utilisation de cette classe, et dans le second, je commenterai un des exemples traitant de NSOutlineView fournis avec les outils de développement.

Nous recommandons la lecture et la compréhension du tutoriel de Mike Beam expliquant le fonctionnement des tables avant d’aborder cet article, ainsi que l’article traitant de la gestion de la mémoire, et particulièrement le point traitant des collections.

Organisation des données

Le principal problème rencontré pour l’utilisation de cette classe est l’organisation des données : dans NSTableViewDataSource un tableau contenant par exemple des dictionnaires suffisait. Chaque élément était dès lors identifié par sa position dans le tableau : l’index. Les choses sont donc simples.

Par opposition, dans NSOutlineViewDataSource, les choses sont radicalement différentes, puisque l’index dans la liste peut changer en fonction des actions effectuées par l’utilisateur. Si l’utilisateur choisit par exemple de développer le premier élément de la vue hiérarchique, il y a changement de l’index de tous les éléments qui se trouvent plus « bas » dans la liste. L’index seul ne peut donc servir d’indicateur pour identifier un élément.

La référence pour l’organisation des données dans le NSOutlineViewDataSource (ou plus simplement OVDataSource) est l’élément. Il s’agit d’un objet doit pouvoir fournir au minimum quatre informations au OVDataSource :

  • L’élément peut être développé ou non;
  • Le nombre d’éléments qui lui sont subordonnés;
  • Les éléments qui lui sont subordonnés;
  • Ce qui sera affiché dans la vue.

Pour créer un tel objet, plusieurs possibilités existent : on peut soit créer une nouvelle classe qui inclut des méthodes renvoyant les informations nécessaires à l’OVDataSource, ou bien utiliser une classe existante, comme NSDictionary.

Il existe un élément particulier : l’élément racine. C’est en quelque sorte l’élément fondamental, à partir duquel tout est construit, tous les éléments visibles dans la liste sont subordonnés à lui. Il n’apparaît par contre jamais dans la liste.

Pour déduire l’organisation des données, le OVDataSource commence par examiner l’élément racine en utilisant les méthodes qui renverront les éléments qui lui sont subordonnés (aussi appelés enfants), leur nombre. Une fois que les éléments ont été obtenus, il recommence ensuite sur ces éléments pour savoir si eux-mêmes n’ont pas d’enfants, et ainsi de suite.

Création d’une application

On commencera par la routine pour la création de projets : ouvrir Project Builder, créez une nouvelle application Cocoa. Éditez ensuite le fichier MainMenu.nib : ajoutez dans la fenêtre principale une NSOutlineView. Par défaut, vous remarquerez qu’elle contient déjà des infos. Le but de cet exemple est de créer une application qui contiendra la même chose que ce que vous pouvez voir dans IB, mis à part que les colonnes contenant les abréviations et les noms seront inversés. Éditez l’identifiant de la première colonne et nommez-le Name. Pour la deuxième colonne, attribuez la valeur Abreviation à l’identifiant. Créez ensuite un contrôleur (nommé Controller), créez les fichiers et instanciez-le. Le dataSource de la NSOutlineView doit être le contrôleur, cette attribution se fait de la même manière que pour une NSTableView. Aucune connexion ne doit être créée pour le NSOutlineView. Voilà, c’est tout pour Interface Builder, maintenant ouvrez Property List Editor (qui se trouve dans /Developer/Applications). Entrez les données dans un nouveau fichier en vous inspirant de la structure suivante, à partir de la NSOutlineView montrées dans Interface Builder (l’ensemble des données n’a pas été développé) :

Enregistrez sous le nom Data.plist, et ajouter le fichier à votre projet en glissant l’icône du fichier dans la liste de fichiers de Project Builder.

Il s’agit donc d’un tableau qui contient des dictionnaires. Au sein de ces dictionnaires, nous avons trois types d’entrées : Childs, qui est un NSArray, Name et Abreviation de type NSString. Childs contient lui-même des dictionnaires, qui sont en fait la description des éléments subordonnés, sous forme de NSDictionary.
Rajoutez ensuite la ligne suivante parmi les variables d’instance dans Controller.h :

NSMutableArray *theData;

Dans Controller.m, rajoutez les méthodes suivantes :

- (id)init {
    [super init];
    theData = [[NSMutableArray arrayWithContentsOfFile:
    [[NSBundle mainBundle] pathForResource:@"Data" ofType:@"plist"]] retain];
    return self;
}

- (void)dealloc {
    [theData release];
    [super dealloc];
}

Implémentation de OVDataSource

Quatre méthodes sont obligatoires pour qu’un OVDataSource soit valide. Ces quatre méthodes sont :

  • outlineView:child:ofItem:
  • outlineView:isItemExpandable:
  • outlineView:numberOfChildrenOfItem:
  • outlineView:objectValueForTableColumn:byItem:

Elles seront expliquées une par une dans les lignes qui suivent.

Quel est l’objet subordonné ?

- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item

Cette méthode est sans doute la plus importante parmi les quatre. Si vous avez compris son fonctionnement, l’ajout d’une NSOutlineView dans votre application ne vous causera pas plus de problème qu’une NSTableView. Elle doit renvoyer un élément, mais pas n’importe lequel : celui qui est subordonné à item, à la indexième position parmi les subordonnés.

Bon, je sens que ça ne vous avance pas plus, alors rien de tel qu’un petit exemple : supposons que item soit l’élément racine, les éléments qui lui seront subordonnés seront ceux qui seront affichés au premier niveau hiérarchique dans la liste. Dans le cas notre application, il y a trois éléments : un élément portant le nom Color, un autre portant le nom Size, et un troisième portant le nom Weight. Le nom n’est ici qu’une des propriétés de l’élément, il peut en effet également en avoir d’autres (comme dans notre cas l’abréviation ou les enfants), l’objet qui devra être renvoyé par cette méthode est l’élément entier, et non uniquement son nom. C’est sur l’objet renvoyé par cette méthode que toutes les méthodes du OVDataSource se baseront, y compris celle-ci lorsqu’il faudra déterminer les enfants de cet objet.

Maintenant que le fonctionnement de cette méthode a été exposé, analysons un petit peu les différents arguments de cette méthode :

  • item : item est l’élément pour lequel il faut renvoyer le subordonné. item vaut nil pour l’élément racine.
  • index : index donne la position de l’élément, parmi les parmi les subordonnées de item, qu’il faut renvoyer.
  • outlineView : cet argument est un pointeur vers l’objet NSOutlineView dans lequel il est nécessaire de calculer l’élément à renvoyer.

Ces arguments se trouveront dans les autres méthodes, mis à part index.

Une question qu’on peut maintenant légitiment se poser est : comment obtenir la liste des subordonnés ? Dans ce premier exemple, item est toujours un dictionnaire, et s’il comporte une entrée de type NSArray à la clé @”Childs” on a la liste des éléments. Il suffit donc de renvoyer l’élément se trouvant à la indexième position dans le tableau se trouvant à la clé @”Childs”.

Pour l’élément racine, il faudra se référer à une source extérieure à item, dans notre cas, il s’agit simplement du tableau theData. À moins que vous aimez vous compliquer la vie inutilement, c’est uniquement pour l’élément racine qu’on recherchera les données directement dans theData, pour le reste il est beaucoup plus simple de passer par l’argument item.

Notre code donnera donc :

-(id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
    if (nil == item) //item vaut nil pour l’élément racine
    return [theData objectAtIndex:index];
 else
    return [[(NSDictionary*)item objectForKey:@"Childs"] objectAtIndex:index];
}

Peut-on développer l’élément ?

 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item

Cette méthode renvoie un booléen qui indique si item peut être développé ou non. Dans le cas de notre exemple, item étant toujours de type NSDictionrary, on fera ce test en regardant s’il existe un élément à la clé @”Childs”. Il y aura également une exception pour l’élément racine. Le code est donc :

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    if ((item == nil) || [item objectForKey:@"Childs"])
        return YES;
    else
        return NO;
}

Il a été nécessaire ici d’introduire un opérateur logique, puisque l’élément peut être développé dans deux cas : pour l’élément racine et dans le cas où des éléments sont subordonnés à item.

Combien d’éléments sont subordonnés ?

- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item

Cette méthode doit renvoyer le nombre d’éléments qui seront subordonnés à item. Il suffit simplement de renvoyer le count du tableau indexé à la clé @”Childs” ou pour l’élément racine le count de theData. Cette méthode n’est exécutée que si l’élément est développable, il n’est donc pas nécessaire de vérifier l’existence de @”Childs”, étant donné que ça a été fait avant. Le code est :

- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    if (nil == item)
        return [theData count];
    else
        return [[item objectForKey:@"Childs"] count];
}

Quelle valeur sera affichée dans la vue ?

- (id)outlineView:(NSOutlineView *)outlineView
    objectValueForTableColumn:(NSTableColumn*)
    tableColumn byItem:(id)item

Cette dernière méthode renvoie la valeur à afficher dans la colonne tableColumn, pour l’élément item. Son fonctionnement est similaire à celui de tableView:objectValueForTableColumn:row: de NSTableView, à une exception près : pour identifier un élément, on se sert plus du numéro de la ligne, mais de lui-même directement. Dans notre cas, item est toujours un dictionnaire comportant une entrée @”Name”, et parfois une entrée @”Abreviation”. Les identifiants des colonnes correspondant aux clés, on peut donc s’en servir directement pour savoir quelle valeur renvoyer. Le code est dès lors :

- (id)outlineView:(NSOutlineView *)outlineView
    objectValueForTableColumn:(NSTableColumn *)tableColumn
    byItem:(id)item {
    return [item objectForKey:[tableColumn identifier]];
}

Édition de valeur

Comme pour les NSTableView, il existe une méthode du dataSource qui permet de modifier le contenu d’une cellule.

- (void)outlineView:(NSOutlineView *)outlineView
    setObjectValue:(id)object
    forTableColumn:(NSTableColumn *)tableColumn
    byItem:(id)item

Comme pour toutes les autres méthodes, le plus simple est de « s’adresser » directement à item pour les modifications (pour peu que item soit modifiable). Si item est modifié, le « contenu du tableau » le sera également. Ce point a été expliqué dans l’article traitant de la gestion de la mémoire. Le code à implémenter est :

- (void)outlineView:(NSOutlineView *)outlineView
    setObjectValue:(id)object
    forTableColumn:(NSTableColumn *)tableColumn
    byItem:(id)item {
    [item setObject:object forKey:[tableColumn identifier]];
    [outlineView reloadItem:item];
}

Vous remarquerez dans ce cas précis qu’il n’a pas été nécessaire de créer une connexion entre le contrôleur et la NSOutlineView. Simplement parce qu’il s’agit du premier argument de la méthode, et qu’on peut de ce fait l’utiliser dans la méthode.

Quelques méthodes intéressantes

Avoir un NSOutlineView dans son application peut s’avérer intéressant, mais ce n’est pas une fin en soi, il est intéressant de pouvoir « extraire » les informations contenues par les éléments, en fonction de la ligne sélectionnée par exemple.

Pour obtenir l’élément renvoyé à la ligne sélectionnée, il suffit d’envoyer le message selectedRow au NSOutlineView, qui renvoie l’index de l’élément sélectionné. Pour obtenir l’élément, il faut alors envoyer le message itemAtRow avec comme argument l’index de la ligne sélectionnée, ce qui donnerait (en admettant qu’on ait tiré une connexion nommée outlineView du contrôleur vers la vue) :

NSDictionary *anItem = [outlineView itemAtRow:[outlineView selectedRow];

Si vous modifiez un élément, il vous faudra également mettre à jour le NSOutlineView. Pour ce faire, il existe trois méthodes :

  • - (void)reloadData : cette méthode est héritée de NSTableView, elle recharge toute la table.
  • - (void) reloadItem:(id)item : cette méthode met à jour uniquement l’élément désigné par item.
  • - (void) reloadItem:(id)item reloadChildren:(BOOL)reloadChildren : si on attribue YES à reloadChildren, l’élément désigné par item ainsi que ses enfants, ses petits-enfants, ses petits-petits-enfants,… seront mis à jour. Si on lui attribue NO, cela revient au même que d’utiliser reloadItem, mis à part qu’il faut taper quelques mots en plus.

OutlineView

OutlineView est des exemples traitant des NSOutlineView fournis avec la documentation Cocoa. Si vous avez installé les exemples, vous le trouverez à l’adresse /Developer/Examples/AppKit/OutlineView. Il s’agit d’un petit explorateur de fichier.

Cet exemple présente un intérêt particulier par rapport à ce qui a été vu ici. Plutôt que d’utiliser un dictionnaire pour les éléments, une nouvelle classe a été créée (FileSystemItem). Cette classe contient une série de méthodes qui seront utilisées dans le OVDataSource. Une autre différence fondamentale est que les objets sont créés à la volée, suivant les actions de l’utilisateur.

FileSystemItem

FileSystemItem est la classe créée pour « renseigner » le OVDataSource. Elle contient cinq méthodes qui seront utilisées dans le OVDataSource.

  • + (FileSystemItem *)rootItem; crée simplement l’élément root, de façon à pouvoir remplir le premier niveau hiérarchique de la liste.
  • - (int)numberOfChildren; tout est dans le nom… Si –1 est renvoyé, cela signifie qu’on aura affaire à un fichier. On ne peut en fait pas renvoyer 0 pour ce genre de cas, puisqu’on eut avoir des dossiers qui ne contiennent rien.
  • - (FileSystemItem *)childAtIndex:(int)n; renvoie un élément qui représente le nième fichier présent dans un dossier.
  • - (NSString *)fullPath; cette méthode renvoie le chemin d’accès complet du fichier, elle n’est utile que pour initialiser un nouvel élément.
  • - (NSString *)relativePath; cette méthode donne le nom du fichier ou du dossier. C’est ce nom qui sera affiché dans le NSOutlineView.

(BOOL)?instruction1:instruction2;

Si vous n’avez jamais fait de C précédemment, et que vous avez appris l’Objective-C via les tutoriaux de Mike Beam, il y a sans doute une syntaxe dans le fichier DataSource.m qui doit vous être totalement étrangère : return (item == nil) ? 1 : [item numberOfChildren]; par exemple. Il sera nécessaire de l’expliquer pour comprendre la suite.

Cette syntaxe équivaut à une sorte de test if…then…else…. Si on a (unBooléen)?instruction1:instruction2, instruction1 sera exécuté lorsque unBooléen vaut YES ; et instruction2 le sera lorsque unBooléen vaut NO. La principale différence avec le if…then…else… est que ce code peut être placé directement au sein d’une instruction. De ce fait, on l’utilisera lorsque le choix d’un objet ou d’un message doit être fait en fonction du résultat d’un test. Si on prend le code donné dans le paragraphe précédent, on choisit de renvoyer 1 lorsque item vaut nil, et [item numberOfChildren] lorsque item est différent de nil. Fin de la digression.

Implémentation du DataSource

Maintenant que la syntaxe …?…:… a été expliquée, la compréhension du DataSource.m ne devrait plus causer de problème.

- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    return (item == nil) ? 1 : [item numberOfChildren];
}

Dans ce programme, le développeur a voulu que l’élément se trouvant au premier niveau dans la hiérarchie soit le répertoire racine de l’ordinateur lui-même, c’est pourquoi il ne peut y avoir qu’un seul élément à ce niveau, et que 1 est renvoyé lorsqu’il faut calculer le nombre d’enfants pour l’élément racine. Pour les autres, on utilisera la méthode numberOfChildren de FileSystemItem qui renvoie directement le nombre d’éléments pour item.

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    return (item == nil) ? YES : ([item numberOfChildren] != -1);
}

L’objectif de cette méthode est de déterminer si item est développable, ce qui doit forcément être le cas de l’élément racine. Sinon, on envoie un message numberOfChildren à item, si –1 est renvoyé c’est qu’il s’agit d’un fichier, et donc qu’il ne peut être développé, si c’est une autre valeur, c’est qu’on a affaire à un dossier, qui doit alors présenter un petit triangle, même s’il est vide. C’est donc le résultat du test ([item numberOfChildren] != -1) qui est renvoyé.

- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
   return (item == nil) ? [FileSystemItem rootItem] : [item childAtIndex:index];
}

Ici, pas de surprises, si item vaut nil, il faut renvoyer un élément correspondant au répertoire racine. On peut se permettre de renvoyer directement cet élément sans tenir compte de l’index, puisqu’on a spécifié dans une autre méthode qu’il n’y aurait qu’un seul enfant pour l’élément racine. Sinon il faut renvoyer l’élément à la indexième position. On utilise la méthode childAtIndex: de FileSystemItem pour cela, méthode qui renvoie une instance de FileSystemItem.

- (id)outlineView:(NSOutlineView *)outlineView
   objectValueForTableColumn:(NSTableColumn *)tableColumn
   byItem:(id)item {
   return (item == nil) ? @"/" : (id)[item relativePath];
}

L’existence du test est en fait inutile ici… il doit visiblement s’agir d’une « faute » de distraction de la part du développeur qui a écrit l’exemple. On peut très bien se contenter ici de return [item relativePath];. Explications : comme je l’ai dit précédemment, l’élément racine n’est jamais affiché, faire une exception pour lui dès lors est inutile, cette méthode n’étant jamais invoquée pour lui. La valeur affichée pour l’élément correspondant au répertoire racine (qui n’est pas l’élément racine, mais l’élément qui lui est subordonné) correspond en fait à la valeur renvoyée par [item relativePath].

Retour à notre application

Maintenant que la forme condensée du if…then…else… vous a été présentée, nous pouvons l’appliquer à notre exemple. Vous verrez, le code sera réduit de façon conséquente. Cependant, le code sera parfois moins compréhensible.

- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
    return [(item == nil)?theData:[(NSDictionary*)item objectForKey:@"Childs"]
    objectAtIndex:index];
}
- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    return [(item == nil)?theData:[item objectForKey:@"Childs"] count];
}

Pour ces deux méthodes, nous utilisons la forme condensée pour choisir le destinataire du message. Dans la première, si item vaut nil, c’est theData qui sera le destinataire du message, et si ce n’est pas le cas, ce sera [[(NSDictionary*)item objectForKey:@"Childs"].

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    return (item==nil) ? YES:[item objectForKey:@"Childs"] ? YES : NO;
}

Mais où est le booléen dans le deuxième test? C’est là un des subtilités du C : dans un test de type if…then…else… ou …?…:…, la condition est vérifiée pour toute valeur non nulle, et ne l’est pas pour toute valeur nulle. Dans notre morceau de code, [item objectForKey:@"Childs"] vaut nil s’il n’y a aucune entrée à la clé @”Childs”, il s’agit donc d’une valeur nulle et la condition n’est donc pas remplie, ce qui fait NO est renvoyé. S’il existe une clé à cette entrée, YES sera renvoyé, puisque l’objet [item objectForKey:@"Childs"] existe.
Je dois cependant reconnaître qu’il y a une lacune dans ce code (comme dans la forme non condensée d’ailleurs…), si cette entrée contient une instance d’une autre classe que NSArray (comme NSString), YES sera aussi renvoyé, alors que ce type de valeur ne convient pas, ce qui causera un problème lorsque le message count sera envoyé pour déterminer le nombre d’éléments subordonnés. Ceci dit, si vous tenez à mettre en place des « garde-fous », je vous le laisse comme exercice. Vous remarquerez également que c’est là un des avantages de créer une nouvelle classe pour les éléments : on a la garantie que le bon type d’objet est renvoyé.

La quatrième méthode (outlineView:objectValueForTableColumn:byItem:) ne change pas.

Résumé

Pour remplir un NSOutlineView, quatre méthodes sont nécessaires dans le dataSource:

  • outlineView:child:ofItem: cette méthode doit renvoyer l’élément qui sera à la indexième position parmi les subordonnés de item ;
  • outlineView:isItemExpandable: cette méthode renvoie un booléen, qui indique si item pour être développé ou non ;
  • outlineView:numberOfChildrenOfItem: cette méthode renvoie le nombre d’éléments subordonnés à item ;
  • outlineView:objectValueForTableColumn:byItem: cette méthode renvoie les valeurs à afficher dans la NSOutlineView, en fonction de la colonne.

L’unité de référence pour le dataSource est l’élément, il doit contenir des méthodes qui renvoient les informations nécessaires au dataSource.

Pour modifier un élément, il faut utiliser la méthode :

  • outlineView:setObjectValue:forTableColumn:byItem:

Lorsqu’un élément est modifié, il existe trois méthodes pour mettre à jour la table :

  • - (void)reloadData : met à jour toute la table.
  • - (void)reloadItem:(id)item : ne met à jour que l’élément désigné par item.
  • - (void)reloadItem:(id)item reloadChildren:(BOOL)reloadChildren : ne met à jour que l’élément désigné par item, ainsi que ces enfants si reloadChildren vaut YES.

Conclusion

Vos connaissances doivent maintenant être suffisantes pour ajouter une NSOutlineView dans votre application. En développant un peu les notions présentées dans ce tutoriel, vous devriez être capables de rajouter des éléments, d’en supprimer (mais le drag’n’drop c’est encore une autre histoire ;)).

Bon code !

PS: Si vous désirez obtenir le code source pour cette article, vous pouvez le faire en cliquant ici.

© Juillet 2003 Renaud pour Project:Omega

renaud Développement ,

  1. Pas encore de commentaire
  1. Pas encore de trackbacks
Vous devez être identifié pour poster un commentaire