Gérer une liste hiérarchique (OutlineView)

A l'issue de cet exposé, le programme affichera ceci :

outlineview

Commençons par définir l'interface.

Dans la fenêtre du programme, ajoutez un objet de type "NSOutlineView". Double-cliquez sur le contrôle pour activer l'objet NSOutlineView. Double-cliquez sur la première colonne et affichez la fenêtre des informations pour renseigner le titre de la colonne et le champ "tag". Nommez le tag de la première colonne "Node" et celui de la seconde "Name" :

outlinecolumn

Double-cliquez sur le Contrôleur de votre application (le cube bleu) et ajouter un "outlet" que vous appellerez "outline" de type "NSOutlineView" :

outlineoutlet

Nous allons maintenant raccorder l'objet "NSOutlineView" au Contrôleur.

Tout en maintenant la touche [Ctrl], cliquez sur le contrôleur et faites glisser la souris jusqu'au contrôle "outline". Relâchez la souris. Sélectionnez l'objet "outline" dans l'onglet "Outlets" et cliquez sur "Connect' :

outlineconnect_1

Double-cliquez sur le contrôle "outline" pour activer l'objet "NSOutlineView" qui est en fait imbriqué dans un objet de type "NSScrollView". Cliquez sur l'objet et tout en maintenant la touche [Ctrl], faites glisser la souris vers le contrôleur. Relâchez la souris. Dans la fenêtre d'information, sélectionnez "dataSource" et cliquez sur "Connect"; sélectionnez ensuite "delegate" et cliquez également sur "Connect".

outlinedelegate

Enregistrez et quitter le programme "Interface Builder".

Nous allons terminer l'exemple dans xCode car il va bien falloir programmer un peu.

Tout d'abord, dans le header (le fichier avec l'extension .h), vous devez ajouter la déclaration de l'outlet si ce n'est pas déjà fait à partir de Interface Builder :

    IBOutlet NSOutlineView *outline;


Ajoutez également la déclaration des méthodes auxquelles votre contrôleur doit répondre en tant que "delegate" :

- (BOOL)outlineView: (NSOutlineView *)outlineView shouldEditTableColumn
    : (NSTableColumn *)tableColumn item: (id)item;
- (BOOL)outlineView: (NSOutlineView *)outlineView shouldSelectItem: (id)item;
- (int)outlineView: (NSOutlineView *)outlineView numberOfChildrenOfItem: (id)item;
- (BOOL)outlineView: (NSOutlineView *)outlineView isItemExpandable: (id)item;
- (id)outlineView: (NSOutlineView *)outlineView child: (int)index ofItem: (id)item;
- (id)outlineView: (NSOutlineView *)outlineView objectValueForTableColumn
    : (NSTableColumn *)tableColumn byItem: (id)item;


Passons maintenant au fichier du programme (avec l'extension .m). Ajoutez les instructions suivantes :

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


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


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


- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
    if (item == nil)
        return @"Racine";
    else {
        if ([[tableColumn identifier] isEqualToString : @"Node"])
            return [item returnNodeValue];
        else
            return [item returnNameValue];
    }
}


- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
    return NO;
}


- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item
{
       return NO;
}


Vous avez noté des méthodes dans les objets génériques "item". Il nous faut maintenant définir une classe dont les instances représenteront les éléments du contrôle. Le contrôle "Outline" est en fait semblable à une grille (NSOutlineView est effectivement une classe dérivée de NSTableView). Chaque ligne représente un élément d'un tableau de la classe que nous allons définir. Chaque colonne est une propriété des objets instanciés dans cette classe. La différence par rapport à une simple table est que nous avons ici une relation père-fils entre chaque élément et un élément précédent. A l'origine du tout, nous avons donc un élément "racine" appelé "rootItem" dans le programme. Chacun des noeuds peut donc contenir d'autres éléments de la même classe

Le fichier de déclarations de cette classe est constitué comme suit :

@interface Node : NSObject {
    NSMutableArray *childNodes;        // tableau des éléments du contrôle
    NSMutableString *nodeString;    // élément de la première colonne
    NSMutableString *nameString;    // élément de la deuxième colonne
}

+ (Node *)rootItem;
-(id)init;
-(void)dealloc;
-(void)addChildObject:(id)childObject;
-(int)numberOfChildren;
-(id)childAtIndex:(int)index;
-(void)setNameString:(NSString *)newString;
-(void)setNodeString:(NSString *)newString;
-(id)returnNameValue;
-(id)returnNodeValue;

@end


Le fichier du programme sera comme suit :

#import "Node.h"

@implementation Node

/*
Création d'un noeud
*/
static Node *rootItem = nil;
+ (Node *)rootItem
{
    if (rootItem == nil)
    {
        rootItem = [[Node alloc] init];
    }
    
    return rootItem;      
}

/*
Initialisation du noeud
*/
-(id)init
{
    if (self == [super init])
    {
        /* Initialisation des éléments des colonnes */
        nameString = [[NSMutableString alloc] initWithFormat:@"Racine de l'arbre"];
        nodeString = [[NSMutableString alloc] initWithFormat:@"Racine"];
        /* Initialisation du tableau des éléments  */
        childNodes = [[NSMutableArray alloc] init];
    }
    return self;
}


/*
Désallocation du noeud
*/
-(void)dealloc
{
    [childNodes release];
    [nameString release];
    [nodeString release];
    if (rootItem != nil)
    {
        [rootItem release];
    }
    [super dealloc];
}

/*
Elément à l'index donné.
*/
-(id)childAtIndex:(int)index
{
    return [[[childNodes objectAtIndex:index] retain] autorelease];
}

/*
Nombre d'élements dans un noeud.
*/
-(int)numberOfChildren
{
    return [childNodes count];
}

/*
Ajout d'un objet
*/
-(void)addChildObject:(id)childObject
{
    [childNodes addObject:childObject];
}

/*
contenu des colonnes
*/
-(id)returnNodeValue
{
    return [[nodeString retain] autorelease];
}
-(id)returnNameValue
{
    return [[nameString retain] autorelease];
}

/*
Initialisation des valeurs dans les colonnes
*/
-(void)setNodeString:(NSString *)newString
{
    [nodeString setString:newString];
}
-(void)setNameString:(NSString *)newString
{
    [nameString setString:newString];
}

@end


Tout est maintenant en place. Il ne reste plus qu'à écrire le bout de code qui remplira le tableau. Vous pouvez le mettre où vous voulez pour l'exemple, par exemple dans la méthode awakeFromNib :

    Node* child1 = [[Node alloc] init];
    Node* child2 = [[Node alloc] init];
    Node* child3 = [[Node alloc] init];
    [Root setNodeString : @"0"];
    [Root setNameString : @"Racine de l'arbre"];
    [child1 setNodeString: @"1"];
    [child1 setNameString: @"Niveau 1"];
    [child2 setNodeString: @"2"];
    [child2 setNameString: @"Niveau 2"];
    [child1 addChildObject: child2];
    [Root addChildObject: child1];
    [child3 setNodeString: @"1"];
    [child3 setNameString: @"Niveau 3"];
    [Root addChildObject: child3];
    [outline reloadData];


C'est terminé. Compilez et lancez le programme.

Bien sûr, cet exemple est simpliste. Vous pourriez ajouter la gestion de la modification des éléments, mettre des icônes, gérer le glisser-déposer, etc.

Vous pouvez télécharger le projet complet sur ma
page de téléchargement (DemoOutline.zip)