Un exemple de développement d'un logiciel graphique. On
utilise pour cela les primitives Unix, la bibliothèque graphique
Cocoa et le langage Objective-C.
Motivation : un problème récurrent
Vous est-il déjà arrivé de vouloir installer un
logiciel sur votre ordinateur, et de vous rendre compte qu'il n'y a pas
assez de place sur le disque ? Pourtant, celui-ci fait bien 120 GB !
Alors il faut faire le ménage. Mais c'est toujours difficile de
voir quels répertoires prennent réellement une place
excessive. Sous Mac OS X, on peut utiliser le Finder qui affiche
l'espace utilisé par le contenu d'un répertoire en
utilisant le menu contextuel « Lire les informations ». Un
utilisateur de Unix ferait quelque chose du genre
du -k | awk '{if
( $1 > 5000 ) {print $0 ; } } '. Cependant, aucune des deux
solutions ne donne vraiment une vue d'ensemble de l'espace
occuppé sur le disque par l'arborescence.
Ici s'impose donc d'écrire soi-même une petite application
pour accomplir cette tâche. Elle devra afficher la structure des
répertoires à partir d'un répertoire racine
indiqué par l'utilisateur, comme sur la figure 1. Elle servira
donc à visualiser en priorité les gros consommateurs de
place (comme
test.eps dans l'exemple), et elle tournera sous
Mac OS X, sans grand espoir de portabilité.
Pour faire vous-même les pas décrits dans la suite de
l'article, il vous faut :
- un mac
- Mac OS X
- le « developer package » de Apple
Nous supposons aussi que vous avez quelque connaissance de la
programmation en C, du développement d'applications graphiques et
des langages orientés-objet.
CocoaDU
Commençons par créer un nouveau projet sous Project
Builder. C'est un projet de type « Cocoa Application » que
nous appelons « CocoaDU ». L'application par défaut
affiche juste une fenêtre vide et une barre de menus fournie, mais
qui ne fait rien.
Project Builder est un Environnement de Développement
Intégré classique. Par défaut, elle présente
(figure 2) :
- à droite une fenêtre qui permet de naviguer entre
les fichiers, les classes, et les paramètres de compilation ;
- à gauche en haut : les messages de compilation, la
fenêtre du débugger et les sorties stdout et stderr ;
- à gauche en bas : l'éditeur.
Une application Cocoa est constiuée d'un
ensemble de d'objets (graphiques ou non) qui communiquent entre eux par
des messages. Théoriquement, si on a tout bien
préparé sur le papier, la création d'une
application peut se faire en deux étapes :
- Avec Interface Builder : on crée les classes, on les
instantie, et on relie les instances entre elles pour qu'elles puissent
s'envoyer des messages.
- Avec Project Builder : on ajoute les implantations des
méthodes, on compile et on débugge
Dans notre cas, avant de créer l'application graphique, nous
allons commencer par écrire le code qui lit le contenu des
répertoires.
Exploration de l'arborescence des fichiers
Il faut une fonction qui explore une arborescence à partir d'un
répertoire racine donné et qui la renvoie sous forme
d'une structure d'arbre. Chaque nœud correspond à un fichier ou
un répertoire et contient l'information dont on a besoin pour
l'afficher (son nom, sa taille, son type, etc.)
Pour cela, nous créons un module en C, composé de deux
fichiers :
fileops.h et
fileops.c. Un nœud
est défini par la structure C suivante :
struct FileNode {
int flags; /* error ? répertoire ? fichier spécial
(pipe ou symlink) ? */
unsigned long size;
/* en blocs de 512 octets, inclut la taille des sous-fichiers
pour
* les répertoires */
unsigned long d_size; /* taille des données, en octets */
unsigned long r_size; /* taille des ressources, en
octets */
int depth; /*
profondeur du répertoire, 0 pour un fichier */
/* chaînage */
struct FileNode *next;
struct FileNode *children;
char name[1];
/* le nom du fichier/répertoire est collé
derrière la structure,
* comme ça on n'utilise qu'un malloc par nœud */
};
La fonction d'exploration a donc le prototype suivant :
struct FileNode
*explore_tree(char *root_name);
L'implantation est en C, avec des appels système Unix standards.
Apple propose aussi une classe qui manipule des fichiers (
NSFileManager,
dans le framework
Foundation),
mais elle est d'une lourdeur injustifiée dans notre cas. Pour
chaque fichier exploré, on utilise la fonction
lstat pour
récupérer ses propriétés. Si c'est un
répertoire, on l'explore récursivement avec
readdir.
La taille occupée par le fichier est 512 fois le champ
st_blocks
du
struct stat renvoyé par
lstat. Les fichiers
Macintosh ont la particularité d'être composés de
deux
forks :
- le data fork contient
les données du fichier, comme sous Windows ou Unix ;
- le ressource fork
contient des méta-informations sur le fichier, comme
l'application qui l'a crée, son icône. Parfois le ressource
fork est indispensable pour ouvrir le fichier (par exemple les
exécutables Classic). Les primitives Unix peuvent accéder
aux ressources du fichier toto.txt
sous le nom toto.txt/..namedfork/rsrc, ce qui permet de
remplir le champ r_size de notre structure de nœud.
Il faut jongler avec la taille des données dans la structure,
parce que le type
long long de 64 bits n'est pas bien
supporté par Objective-C. Cette structure de données est
à l'aise avec des fichiers de moins de 4 GB et des
répertroires de moins de 2 TB.
Ce module prêt et débuggé, nous pouvons commencer
à construire l'interface.
Le constructeur d'interfaces
Le système de création d'interfaces graphiques Cocoa,
hérité de NextStep, est d'une efficacité
déconcertante.
En cliquant sur le fichier
MainMenu.nib dans le paneau de
droite de l'IDE, nous lançons Interface Builder. Celui-ci
affiche quatre fenêtres (figure 3) :
- une fenêtre principale qui récapitule le contenu du
fichier MainMenu.nib : classes, fenêtres, et objets
non-graphiques ;
- la barre de menus de l'application, qui contient les options par
défaut : File, Edit, Window, Help ;
- la fenêtre principale de CocoaDU, vide pour le moment ;
- une palette de Widgets (WInDow gadGETs, composants graphiques)
prêts à l'emploi.
D'abord, il nous faut un bouton qui permet de choisir un
répertoire. Pour cela, on fait un glisser-déposer d'un
bouton de la palette vers la fenêtre principale. Redimensionnons
le bouton et affectons lui un titre : « open » (figure 4).
On affche les propriétés du bouton (et de toutes les
autres instances et classes) avec shift-option-I. Dans l'onglet size, on
obtient un schéma du genre de la figure 5. Les ressorts
déterminent dans quelle direction le widget bouge si la
fenêtre englobante est redimensionnée. Dans cet exemple, le
widget peut changer de hauteur, mais pas de largeur, et restera
toujours à même distance du bord gauche de la
fenêtre. Ce système, ainsi que les règles
magnétiques, permettent de positionner précisément
et sans douleur les widgets.
Ajoutons de la même manière un label (
NSTextField) qui affiche le nom
du répertoire qu'on explore.
Un nouveau type de widget
Faisons ensuite le widget qui contient et affiche la structure
FileNode.
Il faut pour cela créer un nouveau type de widget, donc une
nouvelle classe (appellée
DUView), et ensuite
l'instancier en le mettant sur la fenêtre.
Pour créer la classe, on clique sur l'onglet «
Classes » de la fenêtre principale. Ceci affiche la
hiérarchie de classes. Tous les objets de Cocoa descendent de
NSObject,
et les widgets sont des objets descendant de
NSView . Un
control-clic sur cette classe affiche un menu contextuel dans lequel on
choisit « subclass NSView » pour créér notre
classe
DUView (figure 6).
Pour ajouter un widget de classe
DUView, on
glisse-dépose un widget « Cutstom View »
à la fenêtre, et on précise dans ses attributs que
c'est un
DUView.
Maintenant, nous avons trois widgets : le bouton (
NSButton),
le label (
NSTextField), et le widget central (
DUView).
Si l'utilisateur appuie sur le bouton, il faut que que celui-ci dise au
DUView d'ouvrir un
répertoire, et il faut que le
DUView affiche le nom de ce
répertoire dans le label. C'est ici qu'interviennent les messages.
Le modèle de messages (actions)
Les messages sont des appels de méthode (il n'y a pas de
distinction entre les deux termes en Objective-C). Interface Buider
gère un type particuler de messages : les
actions. Ce
sont des méthodes qui prennent en paramètre un pointeur
sur l'objet émetteur.
Seuls les widgets descendant de
NSControl
(comme
NSButton) peuvent
être connectés à une action. Ces widgets
contrôlent une valeur, comme une chaîne de
caractères, un nombre, un booléen ou un indice de
radio button. Quand cette valeur
change, le widget envoie un message à un
target, qui est
une action d'un autre objet. On connecte les targets aux actions
correspondantes depuis Interface Builder.
Nous rajoutons donc une action
open à la classe
DUView dans le paneau
attributes de sa fenêtre de
propriétés. On connecte ensuite le target du bouton
à l'action
open: de
l'instance de
DUView, en
faisant un glisser-déposer pendant lequel on maintient
appuyée la touche control (figure 7). Maintenant, quand
l'utilisateur clique sur le bouton open, ça appelle
automatiquement la méthode
open:
de l'objet
DUView.
Références entre widgets (outlets)
La méthode
open:
que nous allons implanter va afficher le nom du répertoire dans
le label. Pour cela, il faut que le
DUView puisse faire
référence au label (widget
NSTextField).
Interface Builder permet de définir un type particuler
d'attributs pour les objets : les
outlets. Ce sont des pointeurs
initialisés vers un autre objet géré par
Interface Builer. Dans notre cas, ce pointeur permet en
d'accéder au label depuis le code du
DUView.
Nous créons donc le outlet
directoryNameDisplayer
à peu près de la même manière qu'une action,
on peut (ou non) contraindre son type (figure 8). Nous
établissons le lien avec un ctrl-glisser-déposer du
DUView vers le
NSTextField.
Finalement
La hiérarchie de fichiers que nous allons afficher va être
assez grande, et va souvent dépasser la surface d'affichage
disponible. La solution est de mettre des barres de défilement
autour du widget. Cela se fait en encapsulant le
DUView dans un
NSScrolView
(menu Layout > Make subviews of > Scroll View) . La liaison entre
les deux widgets est automatique, nous ne nous en occupons plus.
Toutes les informations que nous avons définies dans Interface
Builder (création de classe, instanciation et liens) sont
stockées dans le fichier
MainMenu.nib.
Celui-ci est chargé automatiquement lors du lancement de
l'application, ce qui permet d'initialiser les objets que nous
utilisons. Tout ceci peut aussi se faire dans le programme, mais c'est
beaucoup plus lourd...
Maintenant, il faut écrire le code pour DUView.
Commençons par choisir le langage : Objective-C ou Java. Nous
prenons le premier parce qu'il est plus facile à interfacer avec
le module fileops (il n'y a rien de spécial à faire pour
appeller du C), plus rapide, et plus original :-). On
génère les fichiers de code de notre classe DUView depuis
son menu contextuel. Ils apparaissent dans Project Builder
(en-tête :
DUView.h,
implantation :
DUView.m).
Objective-C
Objective-C est une extension orientée-objet de C. C'est un
langage plus simple que C++, et il fait à l'exécution
beaucoup de choses que C++ fait à la compilation : toutes les
méthodes sont virtuelles, et la réflexion est constamment
utilisée. En fait, le typage à la compilation est
optionnel, on peut donner à tous les objets le type
id
(pointeur sur objet).
Les classes de Cocoa, notamment les widgets sont regroupés dans
un
framework appellé
Application Kit. Un framework regroupe les en-têtes, les
librairies dynamiques et la documentation d'une bibliothèque dans
une arborescence standardisée qui inclut aussi le numéro
de version. C'est la solution de Apple aux éternels
problèmes de chemins d'accès pendant la compilation et
l'exécution.
Nous allons donner les éléments les plus utiles de la
syntaxe du langage. Voici l'en-tête d'une classe :
// MonFramework n'est pas un répertoire, mais un framework
// import est comme include, mais il protège automatiquement de
la
// double inclusion
#import <MonFramework/Pere.h>
// Fils hérite de Pere
@interface Fils: Pere
{
// un attribut entier
int champ;
}
// constructeur, pas de paramètre, renvoie un id
- (id) init;
// une méthode de classe (+), renvoie un pointeur sur Fils
+ (Fils *)unFilsSpecial;
// une méthode qui prend deux paramètres.
// Les ':' font partie du nom de la méthoude,
// qui est donc "ajouterChampA:et:"
- (int)ajouterChampA:(int)val1 et:(int)val2;
@end
Et voici son implantation :
@implementation Fils
// par convention, un constructeur s'écrit :
- (id) init {
[super init]; // constructeur du père
champ = 10;
return self; // il se retourne lui-même
}
+ (Fils *) unFilsSpecial {
id a; // id : objet quelconque
// allocation d'un Fils, tous les champs sont
// forcés à 0
a = [ Fils alloc ];
[a init]; // appel du constructeur
// on écrit souvent : a=[[Fils alloc] init];
[a ajouterChampA:5 et:10];
return a;
}
- (int)ajouterChampA:(int)val1 et:(int)val2 {
champ = val1 + val2;
return champ;
}
@end
La classe racine de Application Kit,
NSObject, inclut un compteur de
références qui fait que dans la plupart des cas, il n'est
pas nécessaire de déallouer explicitement les objets.
Quand on a un peu trop abusé de cette possibilité,
l'application Malloc Debug permet de traquer les fuites de
mémoire...
Implantation
Rajoutons un champ contenant le répertoire exploré dans
la classe
DUView, et la
méthode permettant de dessiner le widget (le texte en gras a
été généré par Interface Builder, le
reste a été rajouté) :
#import <Cocoa/Cocoa.h>
@interface DUView : NSView
{
IBOutlet NSTextField
*directoryNameDisplayer; // un outlet
char *root_name;
struct FileNode *root_node;
}
- (IBAction)open:(id)sender;
// une action
- (void)drawRect:(NSRect)rect;
// méthode appellée par le système graphique pour
afficher le widget
@end
L'implantation de la méthode
open: consiste à :
- afficher une boîte de dialogue (NSOpenPanel) pour
que l'utilisateur choisisse un répertoire ;
- appeller la fonction explore_tree
du module fileops qui
transforme ce répertoire en FileNode ;
- afficher le nom du répertoire avec [directoryNameDisplayer
setStringValue:root_name] ;
- forcer un rafraîchissement du widget pour afficher
l'arborescence (avec [self setNeedsDisplay: YES]).
La seule difficulté qui apparaît à ce stade est le
codage des chaînes de caractères. Toutes les
méthodes de Application Kit manipulent des
NSStrings (classe qui encapsule
une chaîne en unicode), alors que le module en C n'utilise que des
char *. Comme les noms de
fichiers sont codés en UTF8, on peut passer du
char * au
NSString par le constructeur
stringWithUTF8String:, et
l'inverse en employant la méthode
UTF8String.
Graphismes
Maintenant, nous devons afficher l'arborescence en implantant la
méthode
drawRect:. Le
rect passé en
paramètre correspond à la partie visible du widget,
déterminée par le
NSScrollView.
Le moteur d'affichage
Le moteur d'affichage de Mac OS X, Aqua, est très
sophistiqué. Les concepts de base viennent des langages de
description de page PostScript et PDF.
Le système de coordonnées de PostScript, en flottants,
peut être translaté, mis à l'échelle, ou
tourné par rapport au canevas (au wiget englobant dans le cas de
Cocoa). Les contextes graphiques sont dans une pile, qu'on peut
manipuler avec la classe
NSGraphicsContext.
La primitive de dessin est le
path. C'est une liste de points
qui sont soit des extrémités, soit des points de
contrôle de lignes. Le type de données correspondant en
Cocoa est
NSBezierPath. Pour construire un path, on peut :
- ajouter un point (commande PostScript moveto,
méthode moveToPoint: de NSBezierPath)
- ajouter une ligne (PS: lineto, NSBezierPath: LineToPoint:)
- ajouter un arc (arc, appendBezierPathWithArcFromPoint:toPoint:radius:
)
- ajouter un spline de Bézier (curveto, curveToPoint:controlPoint1:controlPoint2:
)
- ajouter un glyph = forme d'un lettre dans une fonte
donnée (charpath, appendBezierPathWithGlyph:inFont:)
Après avoir construit un path, on peut :
- le dessiner (stroke, stroke) ;
- le remplir (fill, fill) ;
- le définir comme clipping zone pour les opérations
de dessin suivantes (clip, setClip) ;
- le manipuler : accéder aux points, tester si un point est
à l'intérieur, etc.
Quelques fonctions pratiques
Cette architecture, alliée à une gestion fine de la
transparence, permet d'afficher avec une grande précision tout ce
qu'on veut. Ça peut cependant devenir un peu lent. Pour
accélérer l'affichage de formes simples, Cocoa propose des
fonctions de plus bas niveau :
NSRectStroke,
NSRectFill
(pour les rectangles). L'affichage d'images bitmap ou vectorielles
(obtenues à partir d'un fichier) se fait avec la
méthode
drawAtPoint:fromRect:operation:fraction:
de
NSImage.
Pour afficher du texte, nous utilisons une fonction de plus haut niveau
: NSString a une méthode (
drawInRect:withAttributes:)
pour afficher le texte dans un rectangle, qui coupe le texte aux espaces
pour passer à la ligne au mieux.
Le menu « print » de l'application fonctionne
automatiquement : il positionne un contexte graphique spécifique
; ensuite, il appelle le
drawRect: du widget et les
instructions graphiques sont envoyées vers un fichier PDF ou une
imprimante au lieu d'être exécutées par le moteur
d'affichage.
Performances du système graphique
Il faut bien avouer que le widget qu'on obtient en procédant
ainsi n'est pas assez rapide. Les première optimisations que nous
avons implantées sont :
- ne pas dessiner les rectangles qui ne sont pas visibles,
- ne pas descendre dans les répertoires de moins d'un pixel
de haut, mais dessiner une ligne de longueur proportionnelle à
leur profondeur,
- ne pas dessiner de texte dans les rectangles de moins de 5 pixels
de haut,
- utiliser une fonte suffisament petite pour ne pas être
antialiasée (taille <= 10 pixels).
La vitesse d'affichage n'est toujours pas fameuse (parfois le temps
d'affichage atteint 1.5 s), mais c'est utilisable. Les facteurs de
ralentissment proviennent principalement du fait que :
- le tracé de lignes est anti-aliasé
- tout est double-bufferisé
- les widgets peuvent se superposer et être transparents,
donc Cocoa dessine tous les widgets, même s'il sait que certains
sont cachés.
On peut bricoler ces trois paramètres, mais ce serait au
détriment de la qualité d'affichage. La figure 9
présente l'aspect de la sortie qu'on obtient.
Extensions
À ce stade, CocoaDU est une bonne petite application, qui permet
de voir rapidement qui occupe indûment le disque. Mais ce serait
plus simple si on pouvait faire un drag'n'drop d'un répertoire de
CocoaDU vers le Finder ou un terminal, pour le manipuler de là.
Le chargement est trop lent pour les gros répertoires, il
faudrait une barre de progression qui indique l'avencement de la
progression. Et puis il faudrait que CocoaDU soit un paquet facilement
installable, et localisé en français.
Dans la deuxième partie de cette série d'articles (et la
deuxième verision de CocoaDU), nous allons nous occupper de ces
problèmes : les messages entre applications, le multitâche,
le packaging et la localisation.
Contact :
Matthijs Douze prépare une thèse dans une école
d'ingénieurs à Toulouse (France). Il a fait des graphismes
avec QBasic, BGI (Borland) et GRX (DJGPP). Ensuite, il a
découvert les environnements fenêtrés avec Delphi, a
beaucoup utilisé X, AWT (Java), et TCL/Tk (et Tkinter), et Qt,
pour passer à Cocoa et QuickDraw quand il a eu son mac.
mailto:douze@enseeiht.fr
Références :
Source de CocoaDU :
http://www.enseeiht.fr/~douze/CocoaDU/
Developer Package de Apple (il faut s'inscire) :
http://www.apple.com/developer/
Programmation sous Cocoa (doc de référence presque
complète) : /Developer/Documentation/Cocoa/CocoaTopics.html (dans
le developer package de Apple)
Tutorial Cocoa (très détaillé) :
/Developer/Documentation/Cocoa/ObjCTutorial/CurrencyConverterTutorial.pdf
(idem)
Appels système Unix : taper
man lstat ou
man readdir dans une console.
Les langages Postscript et PDF :
http://partners.adobe.com/asn/developer/pdfs/tn/psrefman.pdf
http://partners.adobe.com/asn/tech/pdf/specifications.jsp