Accueil > Programmation Cocoa > Ajouter une fenêtre de gestion des préférences à votre application

Ajouter une fenêtre de gestion des préférences à votre application

Par Mike Beam, le 17/09/2001

traduit par Thierry, le 2/07/2002

Jusqu’à présent, nos applications n’étaient que des applications de type mono-fenêtre (SimpleTextEditor était cependant une application multi-fenêtre du fait de son type : « application de gestion de documents ». Dans ce cas, la mécanique intrinsèque à ce genre d’application prend soin de gérer les fenêtres multiples).

Le but de cet article sera d’ajouter une fenêtre de gestion des Préfèrences à l’application Carnet d’Adresses, gestion de plus en plus nécessaire à toute application. Cette fenêtre va permettre à l’utilisateur du Carnet d’Adresses de sélectionner la colonne qu’il souhaite afficher dans le tableau.

Tout ce que nous allons faire ici sera basé sur le projet Carnet d’Adresses clos à la fin du précédent article. Vous pouvez le télécharger ici.

Nous avons beaucoup à faire dans cet article, donc, sans plus tarder, allons-y.

Interface Builder ! Encore une fois !

Avant que les choses ne deviennent trop compliquées, nous allons effectuer la tâche essentielle suivante : construire l’interface de la fenêtre de préfèrences. Ayant déjà bâti pas mal d’interfaces dans cette série d’articles, je vais vous donner un minimum de directives pour l’édification du panneau des Préférences, en me concentrant sur les choses nouvelles ou différentes.

La fenêtre des préférences n’est rien d’autre qu’une autre fenêtre. IB propose une palette contenant un large de choix de types de fenêtres différentes. Cette palette presente quatre objets : une fenêtre standard, une fenêtre de type panneau, une fenêtre associée à un tiroir, puis un tiroir, qui peut être ajouté à une fenêtre.


La palette des fenêtres dans Interface Builder

Concentrons-nous une minute sur la fenêtre standard et sur celle de type panneau. La fenêtre standard n’est rien d’autre qu’une fenêtre : donc, rien de spécial à dire ici. Les panneaux, par contre, appartiennent à un type particulier de fenêtre. La classe de définition des comportements des panneaux, NSPanel, est en fait une sous-classe de NSWindow.

NSPanel modifie le comportement des fenêtres standard de différentes manières. Quelques unes devraient vous être familières de part votre interaction quotidienne avec Mac OS X, je n’entrerai donc pas dans des tonnes de détails. Par exemple, quand une application s’exécute avec un panneau ouvert et que vous basculez vers une autre application, le panneau disparaît, alors que les fenêtres restent elles visibles. Cela permet de réduire l’occupation de l’écran. Vous pouvez vous en rendre compte dans IB, quand vous cliquez sur le Finder, le panneau « Palettes » et le panneau « Get Info » disparaissent. Pour plus d’informations sur les panneaux, consultez la documentation de référence de la classe NSPanel.

La plupart des applications utilisent NSWindow comme fenêtre de préférences et nous allons en faire de même. Tout ce que vous avez à faire pour ajouter une nouvelle fenêtre à votre application se résume à la glisser hors de la pallette n’importe où sur l’écran. Cela ajoutera l’instance de la fenêtre à votre fichier nib sous l’onglet « Instances », et affichera la fenêtre ouverte à l’écran. Sous l’onglet « Instances », il est possible de changer le nom des icônes de la fenêtre en ce que vous voudrez. Pour nous aider à distinguer nos deux fenêtres, renommer « Window » (celle avec la table) en mainWindow, et « Window1 » (celle que nous avons ajoutée) en prefsWindow. Un double-clic sur le nom permet de faire ceci.

Nous allons placer six boites à cocher dans notre fenêtre de préférences, où chaque boite sera associée à une colonne potentielle de la table. Ces boites sont localisées dans la palette de IB où se trouvent les champs textes et les boutons, et portent le nom de « Switch », glissez en six sur la fenêtre de préférences. Donnez à ces boites les noms qui suivent et qui sont identiques aux noms et aux identifiants de colonnes (assurez vous que ces noms correspondent effectivement aux noms et identifiants de colonnes) :

  • First Name
  • Last Name
  • Email
  • Home Phone
  • Work Phone
  • Mobile Phone

Et oui, je réalise que nous n’avons pas encore mis en place les données « Work Phone » et « Mobile Phone ». L’activation de ces champs additionnels devrait être triviale suite à l’article consâcré aux vues de tables. Vous devriez n’avoir qu’à ajouter les éléments appropriés d’interface et modifier la méthode createRecord. Je laisse cet ajout de champs et d’éléments d’interface à l’état d’exercice que vous effectuerez quand vous aurez le temps ou l’inclination. Ne créez par contre pas les colonnes correspondantes. Laissez les telles qu’elles sont.

Le dernier élément d’interface (« Control ») à ajouter est un bouton « Close ». Glisser un bouton commun sur la fenêtre des préférences et changer son nom en « Close ».

Voici une image de ma fenêtre de préférences telle que je l’ai conçue :

Pour donner à notre code un accès à ces nouveaux éléments, nous devons ajouter une prise (« outlet ») pour chacune des boites à cocher. Ajoutez à la classe Controller les six prises suivantes qui correspondent aux six boites à cocher, et effectuez les connexions appropriées :

  • firstNameCB
  • lastNameCB
  • emailCB
  • homePhoneCB
  • workPhoneCB
  • mobilePhoneCB

Nous devons aussi ajouter une méthode d’action au Controller qui sera invoquée à chaque fois que l’utilisateur changera l’état de chaque boite à cocher. Nous appellerons cette méthode setColumn car elle contiendra le code qui ajoute ou enlève des colonnes à la vue de table en fonction de la sélection ou de la désélection, par l’utilisateur, de chaque boite à cocher.

Nous voulons que nos six boites invoquent cette méthode à chaque changement d’état, nous allons donc connecter les six boites à notre méthode unique d’action du Controller. Tirez un cable, et effectuez la connexion, à partir de chaque boite, vers le Contrôleur. Il est parfaitement acceptable d’avoir plusieurs éléments de contrôle d’interface qui invoquent la même méthode d’action. Quand viendra le temps de coder setColumn, nous utiliserons l’argument émetteur qui est fourni dans toute méthode d’action pour déterminer quelle boite à cocher a émis un message.

La procédure standard veut qu’une fenêtre de préférences soit ouverte quand l’utilisateur sélectionne la commande « Préférences » du menu qui porte le nom de l’application. Pour autoriser ceci, nous allons établir une connexion entre l’article de menu « Préférences » et une méthode d’action prédéfinie de prefsWindow appelée makeKeyAndOrderFront:. Le cablage s’effectue comme celui de n’importe quel autre élément de contrôle d’interface vers une action.

Tirez un cable à partir de « Preferences », situé sous « NewApplication » dans la barre de menu, vers l’icône de la fenêtre de préférences, prefsWindow. Dans la fenêtre « Info », sélectionnez makeKeyAndOrderFront: dans la colonne des actions et cliquez sur « Connect » (vous pouvez aussi effectuer la connexion en double-cliquant sur le nom de l’action. Mon compagnon de chambrée m’a montré cette façon de faire et c’est assez commode). Notez qu’en fonction de la taille de votre fenêtre d’information, le nom de l’action makeKeyAndOrderFront: pourra être tronqué.

Connexion de l’article de menu « préférences » à l’action de la fenêtre de préférences makeKeyAndOrderFront:

Finalement, nous devons brancher notre bouton « Close » à une action dont le but est de fermer la fenêtre. Cela s’effectue en tirant un cable à partir du bouton « Close » vers prefsWindow : dans la liste des Actions, choisissez performClose:. Cette action simule un clic sur le bouton de fermeture de la fenêtre.

Connexion du bouton de fermeture à l’action performClose: de la fenêtre des préférences

Quand vous aurez fini les réglages de l’interface, ne créez pas de fichiers pour le Contrôleur, nous en avons déjà un et cela aurait pour conséquence d’écraser notre précédent travail. Retournez plutôt dans Project Builder et ajoutez manuellement les nouvelles prises et la nouvelle action au fichier Controller.h (nous vérifierons Controller.m plus tard). Donc, dans Controller.h, ajoutez les six lignes suivantes au bloc de déclaration des variables d’instance :

IBOutlet id firstNameCB;
IBOutlet id lastNameCB;
IBOutlet id emailCB;
IBOutlet id homePhoneCB;
IBOutlet id workPhoneCB;
IBOutlet id mobilePhoneCB;

Et ajoutez ceci aux déclarations de méthode :

- (void)setColumn:(id)sender;

et, ça devrait le faire :-).

Passons maintenant au coding.

Le coding

Pour lancer la discussion relative au coding, voyons ce qui va exactement se passer avec les préférences de colonne et comment elles vont affecter la vue de la table, ainsi que notre stratégie pour implémenter ceci.

Notre stratégie

Nous avons vu que la fenêtre des préférences consistait en six interrupteurs qui agissent sur la présence des colonnes dans la table, et qui permettent à l’utilisateur d’ajouter ou de supprimer des colonnes de la table. Six choix différents impliquent qu’il y a deux colonnes supplémentaires par rapport à ce que nous avions précédemment mis en place. Au lieu de créer une sixième et une cinquième colonne dans la vue de la table sous IB, nous allons les coder. Tout ce que nous allons faire à présent se fera par le biais du code.

Au niveau de l’utilisateur, les boites à cocher vont fonctionner de la manière suivante : quand un utilisateur sélectionne une boite à cocher qui était dans un état inactif, la colonne indiquée est immédiatement ajoutée à la vue de la table. A l’inverse, la colonne est immédiatement retirée.

Discutons maintenant des différentes parties dont nous aurons besoin dans le code pour faire marcher tout ça : en clair, notre stratégie. Premièrement, nous devons avoir une suite d’objets NSTableColumn préfabriqués (oui, vous pouvez avoir une colonne de table qui ne fait pas partie de la table et qui n’est pas montrée dans l’interface) qui est créée au moment du lancement de l’application.

Deuxièmement, après que la suite d’objets préfabriqués ait été créée, l’application va explorer la base des valeurs par défaut de l’utilisateur de manière à y détecter la liste des colonnes qui auraient été éventuellement sélectionnées précédemment pour les inclure dans la vue. Si cette liste existe, l’application va reconfigurer la table de façon à ne contenir que ces colonnes. Sinon, l’application n’aura rien à faire d’autre que d’utiliser la configuration par défaut de la vue de table établie dans IB.

A ce point, il est nécessaire de régler les états initiaux des boites à cocher de manière à ce qu’elles reflètent les colonnes affichées dans la vue tableView, nous ferons donc cela aussi. Finalement, nous avons à mettre en place le code qui ajoutera ou retirera les colonnes de la table en réponse aux actions de l’utilisateur.

D’abord, une suite de colonnes préfabriquées

Nous avons statué dans notre stratégie sur la nécessité de créer un ensemble d’instances préfabriquées de la classe NSTableColumn. Donc, comment nous y prenons nous et, de manière plus importante, comment allons nous stocker cette suite de colonnes ? La manière idéale serait d’accéder aux colonnes préfabriquées par leur identifiant, donc je pense qu’un NSDictionary nous ira bien.

En pratique, quand l’application a besoin d’une colonne de table, elle passe au dictionnaire des colonnes de table une clé qui sera l’identifiant de la colonne requise, ainsi la suite retournera alors l’objet NSTableColumn correspondant. Bon, essayons nous à la création de nouvelles colonnes et au remplissage de dictionnaire avec plusieurs d’entre elles au moment où l’application est lancée (et cela doit être la première chose à faire du fait que la configuration initiale de la table dépendra de sa capacité à accéder les objets colonne de table).

Avant de commencer, déclarons dans Controller.h la variable d’instance tableColumns de la classe NSMutableDictionary :

NSMutableDictionary *tableColumns;

Et initialisons cette variable d’instance dans awakeFromNib de façon standard :

tableColumns = [[NSMutableDictionary alloc] init];

Maintenant, ce qui suit est le code de création d’une nouvelle instance de NSTableColumn, de sa configuration, et de son ajout à tableColumns; nous allons donc créé l’objet « First Name ».

NSTableColumn *newColumn;
newColumn = [[[NSTableColumn alloc] initWithIdentifier:@"First Name"] autorelease];
[[newColumn headerCell] setStringValue:@"First Name"];
[[newColumn headerCell] setAlignment:NSCenterTextAlignment];
 [newColumn setEditable:YES];
 [tableColumns setObject:newColumn forKey:@"First Name"];

D’abord, nous avons déclaré une variable pour stocker temporairement notre objet newColumn de la classe NSTableColumn. Dans la ligne suivante, nous avons simplement créé une nouvelle instance de NSTableColumn en utilisant alloc, puis nous l’avons initialisée en utilisant la méthode d’initialisation initWithIdentifier: de la classe NSTableColumn. L’argument à passer à cette méthode est bien sûr l’identifiant que nous voulons que notre colonne possède, et que nous avons auparavant réglé dans IB. En lui appliquant un « autorelease », nous indiquons que nous ne voulons pas être responsable de ce nouvel objet au delà de notre besoin : nous laisserons le dictionnaire tableColumns être responsable de toutes les colonnes que nous ajouterons à la suite (rappelez vous, les suites émettent des messages « retain » aux nouveaux objets membres récemment ajoutés, et ce faisant, se rendent propriétaire de l’objet membre).

La ligne suivante met en place le nom de la colonne qui comme vous pouvez le voir est le même que celui de l’identifiant. Notez la façon d’écrire cette ligne. Nous émettons d’abord un message headerCell vers newColumn, ce qui retourne l’objet qui représente l’en-tête de la colonne. Nous ne pouvons indiquer à tableColumn d’en faire son titre de cette façon car ce n’est pas le rôle de NSTableColumn de le faire. NSTableColumn compte sur les instances de NSTableHeaderCell pour s’occuper de ce genre de truc. La méthode qui est ici utilisée, setStringValue: est déclarée dans la classe NSCell, de laquelle NSTableHeaderCell hérite.

Après cette ligne, nous mettons en place l’alignement du titre dans la cellule d’en-tête de la colonne. Il y a cinq possibilités d’alignement de texte dans une cellule chacune représentée par une constante : dont vous en voyez une dans le code au-dessus. Ces cinq possibilités et leurs constantes correspondantes sont les suivantes :

Alignement

Constante

Droite NSRightTextAlignment
Gauche NSLeftTextAlignment
Centre NSCenterTextAlignment
Justifié NSJustifiedTextAlignment
Par défaut NSNaturalTextAlignment

Comme vous pouvez le voir, j’ai décidé d’aligner mes titres de colonne de manière centrée.

Dans la ligne d’après, nous retournons à la modification de la colonne en indiquant son caractère modifiable ce qui permettra de modifier ou pas le contenu de la cellule. Finalement, nous ajoutons notre colonne configurée au dictionnaire tableColumns, avec l’identifiant de la colonne comme clé. Maintenant, à chaque fois que nous émettons un message objectForKey:@”First Name” à tableColumns, cette colonne, que nous avons créée, sera retournée.

Si vous souhaitez configurer encore plus vos colonnes, vous pouvez ajouter plus de code de configuration entre la ligne d’initialisation et celle d’ajout au dictionnaire. Pour se rendre compte de toutes les possibilités, comme toujours, veuillez consulter la documentation des classes NSTableColumn et NSTableHeaderCell (ce qui vous ménera plus haut dans la hiéarchie des classes parentes).

Maintenant, tout ce que nous avons à faire est de copier et coller ce code pour chacune des cinq colonnes restantes, et changer la chaîne de l’identifiant pour chacune. Bah ! Ce serait trop long à faire et ce serait trop rigide à notre goût, nous allons donc continuer à paufinner ce bloc de code.

Une modification possible serait de placer ce code dans une nouvelle méthode qui prendrait l’identifiant de la colonne comme argument. Donc, plutôt que de copier et coller ce large morceau de code, nous pourrions simplement invoquer cette nouvelle méthode six fois de suite, en changeant l’argument à chaque fois. Cette méthode est appelée addInitialColumnForIdentifier: (”add…” dans le sens où l’on va ajouter une nouvelle colonne au dictionnaire tableColumns) , et ressemblerait à ceci :

- (void)addInitialColumnForIdentifier:(NSString *)identifier {
  NSTableColumn *newColumn;
  newColumn = [[[NSTableColumn alloc] initWithIdentifier:identifier] autorelease];
  [[newColumn headerCell] setStringValue:identifier];
  [[newColumn headerCell] setAlignment:NSCenterTextAlignment];
  [newColumn setEditable:YES];
  [tableColumns setObject:newColumn forKey:identifier];
}

Tout ce que j’ai fait ici a été de copier le code original dans la définition de la méthode et de changer les chaînes d’identifiant du style @”First Name” par une variable identifier. Maintenant, dans awakeFromNib, pour remplir tableColumns avec nos six colonnes, nous n’avons qu’à invoquer cette méthode six fois en changeant d’identifiant à chaque fois :

[self addInitialColumnForIdentifier:@"First Name"];
[self addInitialColumnForIdentifier:@"Last Name"];
[self addInitialColumnForIdentifier:@"Email"];
[self addInitialColumnForIdentifier:@"Home Phone"];
[self addInitialColumnForIdentifier:@"Work Phone"];
[self addInitialColumnForIdentifier:@"Mobile Phone"];

Maintenant, notre code est un peu plus plaisant à l’intellecte (de mon point de vue) et un peu plus flexible, mais je soutiens que nous pourrions faire encore mieux. Pourquoi devrions nous coder en dur six appels à cette méthode alors que nous pourrions nous servir de l’automatisme des énumérateurs ? Oui mes amis, les énumérateurs.

Considérons qu’au lieu de faire six appels à addInitialColumnForIdentifier, nous pourrions créer une rangée de chaînes d’identifiants, et appeler cette rangée identifiers. Puis nous pourrions créer un énumérateur pour cette rangée, l’utiliser dans une boucle while, et à l’intérieur de cette boucle émettre un message addInitialColumnForIdentifier vers self. Voyons ce que deviendrait donc notre code :

NSArray *identifiers = [NSArray arrayWithObjects:
@"First Name",
@"Last Name",
@"Email",
@"Home Phone",
@"Work Phone",
@"Mobile Phone", nil];
id identifier;
NSEnumerator *e = [identifiers objectEnumerator];
while ( (identifier = [e nextObject]) ) {
    [self addInitialColumnForIdentifier:identifier];
}

Si l’on poursuit notre tendance à créer de nouvelles méthodes pour encapsuler de nouvelles logiques, faisons une méthode similaire à addInitialColumnForIdentifier: appelée addInitialColumnsForIdentifiers: dont l’argument serait une rangée de chaînes identifiantes :

- (void)addInitialColumnsForIdentifiers:(NSArray *)identifiers {
  id identifier;
  NSEnumerator *e = [identifiers objectEnumerator];
  while ( (identifier = [e nextObject]) ) {
     [self addInitialColumnForIdentifier:identifier];
  }
}

puis, dans awakeForNib nous n’aurions qu’à écrire :

NSArray *identifiers; // Au début de awakeFromNib
identifiers = [NSArray arrayWithObjects:
   @"First Name",
   @"Last Name",
   @"Email",
   @"Home Phone",
   @"Work Phone",
   @"Mobile Phone", nil];
[self addInitialColumnsForIdentifiers:identifiers];

Là, nous n’avons pas pris la décision de savoir quelle colonne sera incluse dans le code dont le but sera de remplir tableColumns, et comme je l’ai dit, cela rend les choses plus flexibles et les possibilités plus intéressantes.

Par exemple, personne n’a dit que nous devions créer de manière statique la rangée d’identifiants comme nous l’avons fait au-dessus. Nous pourrions par contre initialiser ces identifiants à partir d’un fichier. Oui … cela rendrait notre application un peu plus flexible encore. Réfléchisez-y. Si nous implémentions notre code de cette manière, nous ne verrions nulle part dans notre code à quoi ressemblent les identifiants de colonne : à la place, ils seraient définis dans un fichier. La décision de créer telle ou telle colonne vient de ce fait au moment où l’utilisateur lance l’application, pas au moment où elle est compilée.

Le développeur ingénieux pourrait étendre ce concept un peu plus loin pour créer dynamiquement la forme d’alimentation des données et les boites à cocher relatives aux identifiants de colonnes contenus dans ce fichier externe. Ainsi, l’utilisateur adroit pourrait tailler à sa guise cette application de façon à organiser et afficher n’importe quels champs de données (car en fait tout est lié aux noms que nous attribuons aux champs de données et aux colonnes, hein ?) en bidouillant le fichier qui contient les déclarations des identifiants.

Changeons ce code une fois de plus de manière à charger la rangée d’identifiants à partir d’un fichier, en se servant de la classe NSBundle.

Les « Bundles »

Dans le dernier article, nous avons appris comment nous pourrions initialiser des rangées à partir de fichiers arbitraires. Maintenant, je vais étendre ce principe et vous indiquer comment nous pouvons initialiser des rangées à partir de fichiers considérés comme des ressources à l’intérieur d’un package applicatif : « bundle ». Les packages sont une des merveilleuses nouvelles choses de Mac OS X. En résumé, un package est un répertoire qui contient toutes les ressources dont a besoin une application pour s’exécuter, dont font partie les images spéciales, les sons, les fichiers nib , le fichier exécutable, et un certain nombre de fichiers de configuration. C’est l’extension .app d’un package applicatif que le « finder » reconnaît de façon à ne pas en afficher le contenu à l’utilsateur. Cela permet au système d’être plus propre et plus organisé, et cela réduit le risque de suppression de fichiers critiques qui pourrait être commise par inadvertance par un utilisateur peu précautioneux.

Cocoa fournit une interface située dans la classe NSBundle du Kit Applicatif pour accéder aux ressources contenues à l’intérieure d’un package applicatif. La rangée identifiers sera initialisée en se servant de initWithContentsOfFile: de la manière que nous avons apprise auparavant. Au lieu de déclarer un chemin statique, utilisons la méthode pathForResource:ofType:, de la classe NSBundle, qui localise la ressource indiquée dans le package applicatif et retourne le chemin absolu. pathForResource:ofType: attend deux arguments : le nom de la ressource, et l’extension de fichier ou le type de la ressource. Ce fichier sera nommé Identifiers.plist.

Nous connaissons le nom de la ressource que nous voulons accéder : Identifiers : tout autant que le type de fichier de cette ressource : plist. Nous avons besoin d’une instance de NSBundle pour recevoir les messages, instance qui sera créée en utilisant la méthode de classe mainBundle. Voyons à quoi cela ressemble dans le code :

NSBundle *bundle;  // Ces trois premières lignes vont au début de awakeFromNib
NSString *path;
NSArray *identifiers;
bundle = [NSBundle mainBundle];
path = [bundle pathForResource:@"Identifiers" ofType:@"plist"];
identifiers = [[NSArray alloc]  initWithContentsOfFile:path];
[self addInitialColumnsForIdentifiers:identifiers];

Voilà comment nous pouvons accéder à des ressources contenues dans un package applicatif. Evidemment, nous pouvons faire beaucoup plus avec NSBundle, mais je voulais juste vous montrer brièvement une des possibilités. Pour plus d’informations, référez vous à la documentation de la classe NSBundle.

Une dernière chose importante à faire pour compléter l’implémentation du package relatif au premier pas de notre stratégie est de créer la ressource à partir de laquelle la rangée d’identifiants sera initialisée. Cela se fait facilement en créant un nouveau fichier vide dans votre projet, nommez ce fichier Identifiers.plist; il sera automatiquement placé dans le groupe « Resources », et dans le package applicatif au moment de la compilation.

Le contenu de ce fichier sera comme suit :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM  "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
 <array>
  <string>First Name</string>
  <string>Last Name</string>
  <string>Email</string>
  <string>Home Phone</string>
  <string>Work Phone</string>
  <string>Mobile Phone</string>
 </array>
</plist>

Ce n’est qu’une représentation standard XML d’une liste de propriété relative à une rangée de chaînes, qui est fonctionnellement équivallente à la ligne d’initialisation de la rangée statique que nous avions avec initWithObjects:.

Maintenant, awakeFromNib devrait ressembler au code suivant :

- (void)awakeFromNib {
NSBundle *bundle;
NSString *path;
NSArray *identifiers;
prefs = [[NSUserDefaults standardUserDefaults] retain];
tableColumns = [[NSMutableDictionary alloc] init];
recordsFile = [NSString stringWithString:@"~/Library/Preferences/AddressBookData.plist"];
recordsFile = [[recordsFile stringByExpandingTildeInPath] retain];
if ( [prefs arrayForKey:@"Addresses"] != nil )
    records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
else
if ( [[NSFileManager defaultManager] fileExistsAtPath:recordsFile] == YES )
    records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
else
    records = [[NSMutableArray alloc] init]; // On charge le tableau des identifiants
                                             // à partir des ressources du bundle
bundle = [NSBundle mainBundle];
path = [bundle pathForResource:@"Identifiers" ofType:@"plist"];
identifiers = [[NSArray alloc] initWithContentsOfFile:path];
[self addInitialColumnsForIdentifiers:identifiers]; }

Ceci complète la première partie de notre stratégie de coding, passons à la deuxième.

Deuxième partie, les préférences par défaut

Cette section se préoccupe de l’utilisation de la fondation, que nous venons de construire dans le but d’initialiser la table et en afficher les colonnes choisies par l’utilisateur, en se basant sur les préférences enregistrées dans la base de données des préférences utilisateur. L’état dans lequel nous avons laissé notre application à la fin de l’article précédent comporte déjà les bases d’accès aux paramètres utilisateurs grace à l’instance prefs de la classe NSUserDefaults.

Une manière simple de garder en mémoire les colonnes choisies par un utilisateur est de se servir d’une rangée d’identifiants de colonne. Ainsi, nous aurions une clé « User Columns » dans la base de données des préférences utilisateur et cette clé correspondrait à un objet préférence qui serait une rangée de chaînes représentant les identifiants des colonnes de la table de l’utilisateur. Mettons en place la variable qui pointera vers cette rangée : déclarons la dans le fichier interface avec les autres variables d’instance (vous verrez dans peu de temps pourquoi nous faisons de userColumns un objet transformable) :

NSMutableArray *userColumns;

Le code de awakeFromNib qui initialise userColumns ressemble à ça :

if ( [prefs arrayForKey:@"User Columns"] != nil ) {
    userColumns = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"User Columns"]];
    [self initializeTableWithColumns:userColumns];
} else {
    userColumns = [[NSMutableArray alloc] init];
}

La chose importante à noter ici est l’instruction if. Nous testons d’abord la présence d’un objet correspondant à la clé @”User Columns”. Si elle existe, nous initialisons userColumns avec le tableau stocké dans les paramètres utilisateur par défaut, et invoquons une nouvelle méthode initializeTableWithColumns: car nous présumons que les colonnes préférées d’un utilisateur ne sont pas forcément celles que nous avons mises en place dans IB. Sinon, nous initialisons userColumns sous forme de tableau transformable vide, prêt à l’emploi. Dit autrement, la table que nous avons mise en place dans IB est la configuration par défaut, elle n’est changée que s’il existe des préférences utilisateurs différentes.

La seconde ligne appelle une nouvelle méthode appelée initializeTableWithColumns: que nous devons définir, et qui prend comme argument la rangée d’identifiants issue des préférences d’affichage des colonnes. La consistance de cette étape est contenue dans cette méthode, voyons donc ce que nous pouvons en faire.

La classe NSTableView définit plusieurs méthodes qui nous permettent d’ajouter, supprimer, et de manipuler autrement des colonnes dans une table. Par exemple, nous nous servirons de la méthode addTableColumn: pour ajouter une colonne à une table, et de removeTableColumn pour enlever une colonne. Chacune de ces ces méthodes prend un NSTableColumn: comme argument.

La chose la plus directe à faire serait d’enlever toutes les colonnes définies dans IB et de les remplacer par celles indiquées dans les préférences utilisateurs. Nous allons le faire en nous servant de deux énumérateurs différents, comme suit :

- (void)initializeTableWithColumns:(NSArray *)identifiers {
    NSEnumerator *e;
    id column, identifier; // efface les colonnes existantes dans tableView
    e = [[tableView tableColumns] objectEnumerator];
    while ( (column = [e nextObject]) ) {
        [tableView removeTableColumn:column]; } // ajoute des colonnes à partir de l'argument
                                                // de type array
    e = [identifiers objectEnumerator];
    while ( (identifier = [e nextObject]) ) {
        column = [tableColumns objectForKey:identifier];
        [tableView addTableColumn:column];
    }
}

Vous pouvez voir comment cette méthode utilise deux énumérateurs. Le premier concerne le tableau des colonnes en cours d’affichage dans tableView. Ce tableau est obtenu en émettant un message tableColumns vers tableView, qui retourne le tableau de tous les objets NSTableColumn contenus dans la table. Nous créons immédiatement un énumérateur en émettant un message objectEnumerator vers ce tableau, et nous engageons notre première boucle while. Cette boucle parcours simplement le tableau du NSTableColumns existant, et enlève les colonnes une par une via la ligne [tableView removeTableColumn:column].

Avec ça nous avons une table dévouée aux colonnes. Ensuite, nous créons un nouvel énumérateur, cette fois pour parcourir le tableau, passé en argument, qui contient les identifiants des colonnes à ajouter dans la vue de la table. Dans la boucle while, nous obtenons de tableColumns l’objet actuel NSTableColumn dont la clé correspond à l’identifiant, et ajoutons chaque NSTableColumn à tableView. Et c’est tout ! Nous avons configuré la vue de table de manière à ne contenir que les colonnes préférées de l’utilisateur.

Un raccourci

Si vous étiez dans le business de développement d’applications gérant plusieurs vues de table, vous pourriez modifier initializeTableWithColumns: pour qu’elle fonctionne avec des tables multiples. Vous auriez eu à changer le nom de la méthode en initializeTable:withColumns: de façon à ajouter un autre argument de type “vue de table”. L’implémentation réelle changerait un peu :

- (void)initializeTable:(NSTableView *)aTableView
   withColumns:(NSArray *)identifiers {
   NSEnumerator *e;
   id column, identifier; // efface les colonnes existantes dans tableView
   e = [[aTableView tableColumns] objectEnumerator];
   while ( (column = [e nextObject]) ) {
       [aTableView removeTableColumn:column]; } // ajoute des colonnes à partir
                                                // de l'argument de type array
    e = [identifiers objectEnumerator];
    while ( (identifier = [e nextObject]) ) {
        column = [tableColumns objectForKey:identifier];
        [aTableView addTableColumn:column];
    }
}

Notez que ce que nous avons a juste cosnsiter à remplacer toutes les instances de la classe tableView, notre prise, avec le nom de la variable passée en argument : aTableView. Dans awakeFromNib là où nous faisons appel à ceci, nous aurions la ligne suivante :

[self initializeTable:tableView withColumns:userColumns];

Vous pouvez vous servir de cette version si vous voulez pour cette application, mais cela n’a pas vraiment d’importance compte tenu que nous n’avons qu’une seule table.

Sauvegarde des préférences de colonne

La prochaine étape consiste à implémenter une méthode de sauvegarde de la configuration de la table sous forme de préférences utilisateurs. Nous appellerons cette méthode saveTableColumnPrefs. Cette méthode est analogue dans sa fonction à la méthode saveData que nous avons implémentée deux articles plus haut. L’implémentation montrée ici sera très rapide, seules les colonnes présentes dans la table et l’ordre dans lequel elles sont affichées seront sauvegardés. Plus tard, nous discuterons des plus amples possibilités de sauvegarde d’informations qui sont en rapport avec la configuration de la table.

Dans cette méthode nous allons créer un énumérateur de colonnes de table comme nous l’avons fait auparavant, effacer le tableau userTableColumns, puis repeupler userTableColumns avec les identifiants de colonne réellement contenus dans la table.

Voyons ce que cela donne dans le code :

- (void)saveTableColumnPrefs {
    id column;
    NSEnumerator *e = [[tableView tableColumns] objectEnumerator];
    [userColumns removeAllObjects];
    while ( (column = [e nextObject]) ) {
        [userColumns addObject:[column identifier]]; }
    [prefs setObject:userColumns forKey:@"User Columns"];
}

Voici ce que nous avons : d’abord nous créons l’énumérateur à partir du tableau de colonnes retourné par le message tableColumns émis vers tableView. La ligne suivante nettoie la rangée userColumns en utilisant removeAllObjects. Puis nous entrons dans la boucle while qui se sert de l’énumérateur pour obtenir la prochaine colonne du tableau. A l’intérieur de la boucle nous ajoutons à userColumns la chaîne retournée par un message de demande d’identification émis vers chaque colonne.

A la fin de la boucle, nous obtenons un tableau d’identifiants qui préserve l’ordre des colonnes de la table, mais avant de quitter la méthode, dans la dernière ligne, nous indiquons à prefs de faire en sorte que userColumns soit l’objet associé à la clé @”User Columns”. Cette méthode sera utilisée dans un moment par la méthode d’action setColumn.

Réglage de l’état initial des boites à cocher

Une autre chose que nous devons faire est de régler l’état initial des boites à cocher pour qu’elles correspondent aux colonnes installées dans la table par initializeTableWithColumns. Nous pourrions faire ceci simplement dans la méthode initializeTableWithColumns:, dans la seconde énumération, de la manière suivante :

- (void)initializeTableWithColumns:(NSArray *)identifiers {
    NSEnumerator *e;
    id column, identifier;
    NSTableColumn *column; // efface les colonnes existantes de tableView
    e = [[tableView tableColumns] objectEnumerator];
    while ( (column = [e nextObject]) ) {
        [tableView removeTableColumn:column]; } // ajoute des colonnes à partir de l'argument array
    e = [identifiers objectEnumerator];
    while ( (identifier = [e nextObject]) ) {
        column = [tableColumns objectForKey:identifier];
        [tableView addTableColumn:column];
        if ( [identifier isEqualToString:@"First Name"] ) [firstNameCB setState:NSOnState];
        if ( [identifier isEqualToString:@"Last Name"] ) [lastNameCB setState:NSOnState];
        // And so on with the other four check boxes.
    }
}

A chaque passage de la boucle while, nous vérifions que la colonne que nous ajoutons avec la méthode NSString vérifie l’égalité isEqualToString:, puis nous mettons en place la boite à cocher appropriée avec la valeur NSOnState.

Comme toujours, cependant, nous pouvons largement améliorer la manière de gérer ceci. Nous pourrions créer un dictionnaire à partir d’un ensemble de clés qui représenteraient les identifiants de colonne, et à partir des valeurs correspondantes qui représenteraient les six prises de boites à cocher. La table suivante montre ce à quoi ressemblerait le dictionnaire :

Clé

Valeur

@”First Name” firstNameCB
@”Last Name” lastNameCB
@”Email” emailCB
@”Home Phone” homePhoneCB
@”Work Phone” workPhoneCB
@”Mobile Phone” mobilePhoneCB

Nous appèlerons ce dictionnaire checkBoxes, et le déclarerons dans Controller.h:

NSDictionary *checkBoxes;

Puis nous l’utiliserons dans initializeTableWithColumns: au niveau de la seconde énumération comme suit :

// ajoute des colonnes à partir de l'argument de type array
e = [identifiers objectEnumerator];
while ( (identifier = [e nextObject]) ) {
    column = [tableColumns objectForKey:identifier];
    [tableView addTableColumn:column];
    [[checkBoxes objectForKey:identifier] setState:NSOnState];
}

Et vous voyez comment la série d’instructions if a été remplacée par une seule ligne. Toutefois, avant de pouvoir s’en servir, nous devons initialiser checkBoxes dans awakeFromNib. Nous allons le faire en utilisant la méthode dictionaryWithObjects:forKeys: de la classe NSDictionary. Cette méthode prend deux arguments de type NSArray, ils doivent avoir tous les deux le même nombre d’éléments. Le premier argument sera un tableau de checkBoxes, et le second sera le tableau des identifiants que nous avons obtenus auparavant à partir des ressources du package. Donc cette initialisation nécessite que nous créions d’abord un tableau constitué des outlets relatives aux boites à cocher, puisque nous l’utilisions en coordination avec les identifiants de tableaux de manière à créer un dictionnaire. Là l’ordre est important : l’ordre des boites à cocher dans le tableau doit être le même que celui des chaînes dans les identifiants. Voici le code :

NSArray *checkBoxesArray;  // Au début de awakeFromNib, avec le reste
checkBoxesArray = [NSArray arrayWithObjects:firstNameCB,
                                            lastNameCB,
                                            emailCB,
                                            homePhoneCB,
                                            workPhoneCB,
                                            mobilePhoneCB,
                                            nil];
checkBoxes = [[NSDictionary alloc] initWithObjects:checkBoxesArray forKeys:identifiers];

Nous faisons en quatre lignes de code ce qui en nécessitait douze auparavant, et cela permettra d’inclure des boites à cocher supplémentaires. Donc, maintenant, nous pouvons accéder les boites à cocher par l’identifiant de colonne.

Nous n’en avons cependant pas fini avec cette section. Nous n’avons toujours pas envisagé la possibilité que les boites à cocher pourraient être réglées en dehors de initializeTableWithColumns:, surtout dans le cas où il n’y aurait pas de préférences pour la clé @”User Columns”. Rappelez-vous du code de awakeFromNib quand nous testions ceci :

if ( [prefs arrayForKey:@"User Columns"] != nil ) {
    userColumns = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"User Columns"]];
    [self initializeTableWithColumns:userColumns];
} else {
    userColumns = [[NSMutableArray alloc] init];
}

Rappelez vous que quand cette préférence particulière n’est pas présente dans la base de données des préférences par défaut, cette instruction if sera évaluée à faux, et ainsi nous initialiserons simplement userColumns en tant que dictionnaire vide.

Maintenant que nous traitons des états initiaux des boites à cocher, nous avons une autre chose dont nous devons nous soucier dans le bloc else : les états par défaut des boutons, pour être en accord avec la configuration de la table. Dans notre cas, les colonnes par défaut sont « First Name », « Last Name », « Email », et « Home Phone », nous devons donc régler l’état de ces boites à cocher à NSOnState :

if ( [prefs arrayForKey:@"User Columns"] != nil ) {
    userColumns = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"User Columns"]];
    [self initializeTableWithColumns:userColumns];
} else {
    userColumns = [[NSMutableArray alloc] init];
    [firstNameCB setState:NSOnState];
    [lastNameCB setState:NSOnState];
    [emailCB setState:NSOnState];
    [homePhoneCB setState:NSOnState];
}

Une autre possibilité aurait été de laisser ce code comme il était et de construire simplement l’interface de manière à ce que ces quatre boutons soient en position « on » par défaut. A votre choix.

A ce point awakeFromNib ressemble à ceci :

- (void)awakeFromNib
{
  NSBundle *bundle;
  NSString *path;
  NSArray *identifiers;
  NSArray *checkBoxesArray;

  prefs = [[NSUserDefaults standardUserDefaults] retain];
  tableColumns = [[NSMutableDictionary alloc] init];

  recordsFile = [NSString stringWithString:@"~/Library/Preferences/AddressBookData.plist"];
  recordsFile = [[recordsFile stringByExpandingTildeInPath] retain];

  if ( [prefs arrayForKey:@"Addresses"] != nil )
    records = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"Addresses"]];
  else if ( [[NSFileManager defaultManager] fileExistsAtPath:recordsFile] == YES )
    records = [[NSMutableArray alloc] initWithContentsOfFile:recordsFile];
  else
    records = [[NSMutableArray alloc] init];

  // Chargement du tableau d'identifiants à partir des ressources du bundle
  bundle = [NSBundle mainBundle];
  path = [bundle pathForResource:@"Identifiers" ofType:@"plist"];
  identifiers = [[NSArray alloc] initWithContentsOfFile:path];

  // Création du dictionnaire de boites à cocher
  // pour référencer facilement les contrôles de la fenêtre de préférences
  checkBoxesArray = [NSArray arrayWithObjects:firstNameCB, lastNameCB,
    emailCB, homePhoneCB, workPhoneCB, mobilePhoneCB, nil];
  checkBoxes = [NSDictionary dictionaryWithObjects:checkBoxesArray forKeys:identifiers];

  [self addInitialColumnsForIdentifiers:identifiers];

  if ( [prefs arrayForKey:@"User Columns"] != nil ) {
    userColumns = [[NSMutableArray alloc] initWithArray:[prefs arrayForKey:@"User Columns"]];
    [self initializeTableWithColumns:userColumns];
  } else {
    userColumns = [[NSMutableArray alloc] init];
    [firstNameCB setState:NSOnState];
    [lastNameCB setState:NSOnState];
    [emailCB setState:NSOnState];
    [homePhoneCB setState:NSOnState];
  }
}

Finallement, implémentons ’setColumn’

Avec les deux premières parties de notre stratégie en place, nous pouvons maintenant facilement ajouter et enlever des colonnes en fonction des préférences de l’utilisateur, et préserver ces changements dans les préférences utilisateurs. Tout ceci est fait dans la méthode setColumn. Sachant que nous n’avons pas créé de nouveaux fichiers « Controller » dans IB, nous devons ajouter setColumn à la main. Regardons d’un peu plus près cette méthode pour avoir une idée claire de la façon dont nous allons l’implémenter.

La définition de la méthode devrait être ajoutée dans Controller.m:

- (IBAction)setColumn:(id)sender
{
}

Rappelez vous comment nous avions connecté six boutons différents à ce message d’action unique. L’idée est qu’à chaque fois que l’utilisateur change une des boites à cocher, setColumn sera invoquée, et qu’ainsi nous serons capables d’identifier quelle boite à cocher a été modifiée en fonction de l’information passée comme argument, et par conséquent, déterminer quelle colonne à ajouter ou à enlever.
La caractéristique qui permet de distinguer l’objet émetteur est le titre du bouton que nous avons adroitement intitulé de la même manière
que les identifiants de colonne, qui ont eux aussi été intitulés de la même manière que les clés du dictionnaire tableColumns ! Nous pouvons obtenir la chaîne du titre en émettant un message title à l’émetteur, puis nous pouvons stocker cette chaîne dans une variable locale, identifier :

NSString *identifier = [sender title];

Une fois que nous avons le nom de la colonne avec laquelle nous allons oeuvrer, nous devons déterminer si nous devons ajouter ou enlever la colonne de la vue, ceci en rapport avec l’état du bouton après qu’il ait été cliqué. Les boites à cocher ont deux états possibles par défaut : NSOnState et NSOffState. Nous pouvons obtenir l’état de la boite à cocher émettrice en lui envoyant un message d’état dont la valeur de retour sera comparée aux constantes NSOnState et NSOffState pour déterminer les actions à engager.

Après avoir mis toutes ces pièces ensemble, voici ce que donne l’implémentation de setColumn:

- (IBAction)setColumn:(id)sender
{
  NSString *identifier = [sender title];
  NSTableColumn *column = [tableColumns objectForKey:identifier];
  if ( [sender state] == NSOnState ) {
    [tableView addTableColumn:column];
    [self saveTableColumnPrefs];
  } else if ( [sender state] == NSOffState ) {
    [tableView removeTableColumn:column];
    [self saveTableColumnPrefs];
  }
}

Dans la première ligne de cette méthode nous prenons le titre du bouton émetteur et le stoquons dans la variable NSString « identifier ». Dans la ligne suivante, nous déclarons une variable NSTableColumn temporaire, « column », que nous assignons à l’objet retourné par tableColumns qui contient l’identifiant correspondant à la clé.

Finalement, nous testons l’état de la boite à cocher après que l’utilisateur l’ait cliquée. S’il est à NSOnState, nous ajoutons la colonne à tableView, s’il est à NSOffState, nous enlevons la colonne. La deuxième instruction if est actuellement un peu redondante : elle aurait pu n’être qu’une instruction else car si l’état n’est pas à NSOnState, il est alors à NSOffState. Cependant, je préfère la version plus explicite : juste une préférence personnelle.

Pensées finales

Ca y est, les bases de l’ajout et de la suppression de colonnes intervenant à la fois lors de la phase d’initialisation et de celle de l’exécution sont jetées. Lors de l’initialisation, nous nous référons aux préférences sauvegardées et, lors de l’exécution, à celles modifiées. Pour finir, nous avons ajouté cinq méthodes à notre application, et nous avons modifié intensivement awakeFromNib.
Ces cinq méthodes sont :

  • addColumnForIdentifer:
  • addColumnsForIdentifiers:
  • initializeTableWithColumns:
  • setColumn
  • saveTableColumnPrefs

Le dossier du projet final de cet article peut être téléchargé ici.

Un dernier petit ménage que vous pourriez faire serait d’aller dans la méthode dealloc et d’y ajouter les suppressions nécessaires d’instances que nous avons créées et utilisées.

Ce code ajoutera et supprimera des colonnes presque à notre guise. Il y a plusieurs choses dans cette implémentation qui le rende incomplet dans le sens où nous n’allons pas obtenir en résultat une belle apparence de la table. D’abord parce que le code ne prend pas en compte les largeurs des colonnes. Si nous réglons ce point alors notre mise au point sera assez bonne.

Une première amélioration évidente de notre code serait de stocker dans les préférences utilisateurs plus d’informations qu’une simple liste de colonnes. Nous pourrions stocker un tableau de dictionnaires à la place, où chaque dictionnaire aurait la faculté de décrire en détail comment sont configurées les colonnes.

Par exemple, ce dictionnaire pourrait avoir une clé nommé @”Identifier” qui retournerait la chaîne identifiant de la colonne (information contenue dans notre liste originale). Une deuxième clé qui améliorerait le look de notre table serait @”Width”, qui nous permettrait de stocker et restaurer la largeur des colonnes. D’autres possibilités pourraient être envisagées. Nous pourrions avoir un couple clé-valeur pour stocker la manière dont serait justifié le titre des colonnes, ou un autre qui par le biais d’un booléen indiquerait le caractère éditable du contenu de la colonne. Je laisse l’ajout de ces fonctions au lecteur. Le code source du prochain article fera état de mes propres solutions.

En parlant du prochain article, nous allons y apprendre comment améliorer notre interface en utilisant cette innovation géniale apportée par Mac OS X : les feuilles (sheets). A la prochaine.

Textes originaux en anglais sur O’Reilly : http://www.macdevcenter.com/pub/au/159

Thierry Programmation Cocoa , , ,

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