TD-Prog Programmer avec la bibliothèque LiliMath

Contenu du document

Présentation générale

INTRODUCTION

On peut décrire un programme de la façon générale suivante : une image est affichée à l'écran, elle suggère certaines actions de l'utilisateur par l'intermédiaire du clavier ou de la souris, elle se transforme en fonction des actions subies. Pour le programmeur, cela signifie écrire une procédure d'affichage, puis une procédure de réception des actions de l'utilisateur et enfin une procédure de réaction à ces actions. Evidemment cela suppose un certain nombre de structures de données (ce qu'on affiche, l'endroit où on l'affiche,...) et de méthodes (réception et traitement des actions de l'utilisateur, routines d'affichage,...).

La bibliothèque proposée fournit des outils généraux facilitant cette partie de la tâche du programmeur. Ces outils regroupant structures de données et méthodes, ils ont tout naturellement la forme d'objets. Il sera donc ici question de programmation orientée objets.

D'autre part la volonté de prendre en compte les actions de l'utilisateur sous au moins deux formes (clavier et souris) nous amène à définir une structure particulière, la structure d'évènement, qui unifiera la gestion des ces actions. Il sera donc aussi question de programmation évènementielle.

LES EVENEMENTS

Les évènements pris en compte dans les programmes seront de 2 types :

  1. évènements provoqués par l'utilisateur
  2. évènements provoqués par le programme lui-même.

Evènements provoqués par l'utilisateur

Ces évènements sont aussi de deux types selon l'instrument utilisé pour les générer: le clavier ou la souris.

Evènements clavier

Un évènement clavier est généré lorsque l'utilisateur appuie sur une touche. Une réaction appropriée à ce type d'évènement nécessite la reconnaissance de la touche pressée, celle-ci est donc représentée par un code numérique qui sera contenu dans la structure d'évènement. Ce code est simplement le code ASCII pour les touches qui en possèdent un différent de 0 (caractères alpha-numériques), ou le 'ScanCode' augmenté de 256 pour les autres touches (par exemple les flèches, les touches de fonctions Fn,...). Des constantes mnémoniques permettent une utilisation aisée de ces codes.

Evènements souris

Un évènement souris est généré lorsque l'utilisateur utilise la souris. Il est caractérisé par deux renseignements fondamentaux : le type d'évènement (appui sur un bouton, relachement d'un bouton, déplacement avec ou sans appui sur un bouton,...) et la position de la souris au moment du déclenchement. Ces deux données feront donc partie de la structure d'évènement.

Evènements provoqués par le programme

Le programme lui-même doit être en mesure de générer des évènements, par exemple pour permettre le passage d'une phase à une autre ou pour unifier la gestion de deux actions différentes de l'utilisateur (appui sur une touche et clic sur une certaine partie de l'écran). Ces évènements seront des évènements commande caractérisés par un code numérique que le programmeur devra définir lui-même en fonction de ses besoins. Ils pourront en outre véhiculer une information supplémentaire sous la forme d'un nombre entier ou d'un pointeur.

Le type TEvent

L'analyse précédente de la notion d'évènements mène à la définition de la structure de données conditionnelle représentée par le type TEvent. Un premier champ, nommé What et commun à tous les évènements, permet de déterminer le type d'évènement étudié, puis un certain nombre de champs complémentaires dont la forme dépend du champ What fournissent les renseignements nécessaires à l'exploitation de l'évènement: code d'une touche, ou position de la souris, ou code d'une commande avec champ d'information supplémentaire.

Le type TEvent est finalement défini de la façon suivante :

   TEvent = record
    What : Word;             { Nature de l'évènement }
    case Word of
     evNothing: ();          { Evènement vide }
     evMouse: (              { Evènement souris } 
      LButton : Boolean;     { Etat bouton de gauche }
      RButton : Boolean;     { Etat bouton de droite }
      Where : Tpoint);       { Position de la souris }
     evKeyDown: (            { Evènement clavier }
      KeyCode : Word);       { Code de la touche }
     evCommand: (            { Evènement commande }
      Command : Word;        { Numéro de la commande }
      case Word of           { champ info }
       0:(InfoByte:Byte);    { Info de type Byte }
       1:(InfoInt:Integer);  { Info de type Integer }
       2:(InfoPtr:Pointer)); { Info de type Pointer }
    End;

Utilisation d'une variable de type TEvent

Lecture du champ What

L'analyse d'une variable de type TEvent commence toujours par la lecture du champ What qui fournit le type d'évènement considéré. Ce type est déterminé par les constantes suivantes :

     evNothing    = $0000; { pas d'évènement }
     evMouseLDown = $0001; { bouton gauche appuyé }
     evMouseLUp   = $0002; { bouton gauche relaché }
     evMouseRDown = $0004; { bouton droit appuyé }
     evMouseRUp   = $0008; { bouton droit relaché }
     evDoubleClic = $0010; { double clic bouton gauche }
     evMouseAct   = { l'un des évènements liés à une action sur les boutons } 
            evMouseLDown+evMouseLUp+evMouseRDown+evMouseRUp+evDoubleClic;
     evMouseMove  = $0020; { mouvement de la souris avec bouton gauche relaché }
     evMouseAuto  = $0040; { bouton gauche appuyé sans mouvement }
     evMouseTrack = $0080; { mouvement de la souris avec bouton gauche appuyé }
     evMouse      = { l'un des évènements liés à la souris } 
            evMouseAct+evMouseMove+evMouseAuto+evMouseTrack;
     evKeyDown    = $0100; { appui sur une touche }
     evCommand    = $0200; { commande logicielle }

Lecture des champs complémentaires

Le type d'évènement étant déterminé, il est possible de lire le contenu des champs complémentaires associés. Pour les évènements clavier (evKeyDown), le champ KeyCode contient le code de la touche pressée. Pour les évènements souris (evMouseLDown, evMouseLUp, etc...), le champ Where de type TPoint contient la position de la souris. Pour les évènements commande (evCommand), le champ Command contient le code de la commande et l'un des champs InfoInt ou InfoPtr contient d'éventuelles données supplémentaires.

L'unité UDrivers

Le type TEvent et l'ensemble des constantes, variables, procédures et fonctions associées sont définis dans l'unité UDrivers. Celle-ci est totalement indépendante de toute autre unité (sauf l'unité DOS fournie avec Turbo Pascal). Elle est un élément essentiel de tout programme écrit avec la bibliothèque.

Un programme utilisant l'unité UDrivers devra évidemment la déclarer dans la clause Uses. Il devra ensuite appeler la procédure DriversInit, en particulier pour initialiser le driver souris. La procédure ReadEvent permettra de lire les évènements. La procédure SetEvent permettra à un programme d'en créer.

L'unité UDrivers contient aussi de nombreuses procédures et fonctions permettant de gérer la souris en mode graphique, en particulier lors des opérations d'affichage.

LES OBJETS GRAPHIQUES

Un programme doit afficher un écran. Celui-ci peut être divisé en zones rectangulaires ayant chacune une fonction spécifique. Chacune de ces zones sera représentée par un objet graphique qui sera un descendant du type générique TGenView et qui devra donc être en mesure de s'afficher et de réagir à des évènements.

Le type TGenView

Tous les objets graphiques dérivent du type TGenView. Ils ont donc en commun un certain nombre de champs et de méthodes permettant leur fonctionnement et leur intégration dans le programme. Ils pourront être simples ou composés, selon qu'ils contiennent ou non d'autres objets du type TGenView. Lors de la création d'un objet descendant du type TGenView le programmeur devra réécrire ou plus souvent compléter un certain nombre de méthodes.

Le constructeur Init

Le constructeur Init hérité du type TGenView attend comme paramètres 4 entiers qui définissent la zone occupée par l'objet: les coordonnées du coin supérieur gauche, la largeur et la hauteur. Ces valeurs sont placées dans les champs Origin et Size de type TPoint. Le constructeur d'un nouvel objet dérivé du type TGenView devra commencer appeler cette méthode Init héritée. Il devra ensuite initialiser les nouveaux champs dont il a besoin et éventuellement incorporer d'autres objets.

Le destructeur Done

Le destructeur Done hérité du type TGenView appelle les destructeurs des objets incorporés puis détache l'objet de son propriétaire. Le r“le du destructeur est essentiellement de libérer la mémoire utilisée par l'objet.

La méthode virtuelle Draw

L'affichage de l'objet se fait par l'intermédiaire de la méthode virtuelle Draw. La méthode Draw héritée du type TGenView appelle, lors du premier affichage, la méthode virtuelle BackGround qui doit dessiner le fond fixe de l'objet, puis elle met à jour le champ Etat pour indiquer que l'objet est affiché, et enfin elle appelle les méthodes Draw des objets incorporés. Le programmeur pourra selon les cas compléter cette méthode ou redéfinir la méthode BackGround.

Pour gérer le problème de la souris et celui des coordonnées pendant les opérations d'affichage, les méthodes DrawBegin et DrawEnd permettent d'une part de cacher puis de rétablir le pointeur souris lorsque cela est nécessaire, et d'autre part de réduire l'écran à la zone couverte par l'objet pour permettre l'emploi de coordonnées locales.

La méthode virtuelle HandleEvent

La façon dont l'objet réagit aux évènements est définie dans la méthode virtuelle HandleEvent déclarée par :

     Procedure HandleEvent(Var Event:TEvent); virtual;

La méthode héritée du type TGenView traite l'évènement commande cmQuit qui met fin au programme, le mouvement de la souris avec éventuelle mise à jour du pointeur de la souris et la propagation de l'évènement Event dans les objets incorporés.

Les objets graphiques simples

Les objets graphiques simples ne contiennent pas d'autres objets de type TGenView. Ils forment les entités minimales affichées à l'écran. Divers objets de ce type sont fournis par la bibliothèque: boutons, lignes d'édition, listes de sélection, afficheur de textes, zones de dessin géométrique, .... Ces objets ont en général une fonction simple et précise; ils auront à réagir à certains évènements ce qui se traduira par l'écriture de leur méthode HandleEvent. Ils sont en général destinés à être incorporés dans des objets composés.

Les objets graphiques composés

Les objets graphiques composés contiennent un ou plusieurs objets de type TGenView. L'exemple classique d'un tel objet est une fenêtre de dialogue contenant une ligne d'édition et deux boutons pour la validation et l'annulation. Pour incorporer les objets 'fils' dans un objet composé on utilisera une nouvelle méthode.

La méthode Insert

Cette méthode permet d'établir un lien entre l'objet composé et les objets fils qu'il contient. Par exemple, la méthode Draw d'un objet composé peut se charger automatiquement de l'affichage des objets incorporés. De même, la méthode HandleEvent se charge automatiquement de la diffusion des évènements aux objets incorporés.

Le travail de programmation d'un objet composé se réduit donc souvent à une description de son contenu en termes d'objets. Ceci se fait dans le constructeur en utilisant la méthode Insert.

L'objet Application

Le programme lui-même sera représenté par un objet spécial dérivé du type TGenView par l'intermédiaire du type TGenApp. Il s'agira évidemment d'un objet composé qui occupera tout l'écran. Le rôle de cet objet est d'initialiser le programme, par exemple en installant le mode graphique, puis de lire et de diffuser les évènements aux objets qu'il contient jusqu'à réception de la commande cmQuit qui met fin au programme.

Un certain nombre d'objets sont incorporés dans l'application dès son départ, on utilise alors la méthode Insert, déjà présentée, dans le constructeur du programme. Il est cependant souvent nécessaire d'incorporer de nouveaux objets pendant l'exécution du programme. Cela peut se réaliser de deux façons.

La procédure InsertView(V:PGenView)

Cette procédure se charge d'insérer l'objet V dans l'application grâce à la méthode Insert, puis de l'afficher grâce à la méthode Draw. L'objet V ainsi inséré apparait ainsi immédiatement à l'écran et est en mesure de recevoir les évènements et de les traiter grâce à sa méthode HandleEvent. L'objet V fonctionne ainsi comme les autres objets déjà présents. On peut le retirer de l'application en utilisant son destructeur Done.

La fonction ExecView(V:PGenView; data:Pointer):Byte;

Cette fonction insère l'objet V dans l'application et l'affiche comme le fait la procédure InsertView. La différence vient du fait qu'elle va focaliser tous les évènements sur V jusqu'à modification d'un champ de type Byte nommé ExitCode. L'objet V est alors détruit. Le résultat renvoyé par la fonction est l'ExitCode qui a mis fin à l'existence de V. Cette façon de procéder est réservée aux boŒtes de dialogues.

Le pointeur data transmis à la fonction permet, lorsqu'il est différent de Nil, d'effectuer des échanges entre l'objet V et l'application.

Les unités à utiliser

Les notions présentées ci-dessus sont mises en oeuvre dans les unités OGenView et ODialog. Elles constituent deux nouveaux éléments essentiels pour l'utilisation de la bibliothèque. Enfin, l'unité UConst définit un certain nombre de constantes et de structures utilisées dans les autres unités.

L'unité OGenView

L'unité OGenView constitue avec l'unité UDrivers, le second élément essentiel à tout programme utilisant la bibliothèque. Elle définit les objets de base de type TGenView et TGenApp.

L'unité ODialog

L'unité ODialog fournit des procédures et fonctions utilitaires auxquelles tout programme pourra faire appel comme l'affichage d'un message, la sélection d'un fichier, etc... Ces fonctions utilisent des objets dérivés du type TGenView qui sont prêts à l'emploi.

L'unité UConst

L'unité UConst définit les constantes et structures utilisées par les objets dérivés du type TGenView.



Un exemple

Rien ne vaut un exemple bien commenté pour apprendre à se servir des unités UDrivers, OGenView, ODialog et UConst. Nous allons donc construire pas à pas un programme utilisant ces unités. Ceci se fera en plusieurs étapes : partant d'un programme vide, nous incorporerons progressivement plusieurs objets. Nous verrons qu'en fait tout se passe comme avec un jeu de construction où on emboite des éléments et où on les met en relation.

CAHIER DES CHARGES DU PROGRAMME

Le programme que nous allons écrire a pour but d'afficher des icônes Windows. Il fonctionnera en mode graphique VGA. Il présentera un listeur de fichiers qui permettra de naviguer sur le disque dur à la recherche des fichiers *.ICO. Dès qu'un tel fichier sera trouvé, il sera affiché.

Pour finir, nous ajouterons un menu qui permettra d'effectuer des opérations simples sur les fichiers : effacer, copier, déplacer, renommer.

Nous utiliserons l'unité IcoUtils pour afficher les icônes et effectuer les opérations concernant les fichiers. Cette unité ne sera pas commentée, elle ne contient que des procédures et fonctions ne faisant pas intervenir les objets graphiques.

Le travail sera divisé en plusieurs étapes:

ETAPE 1

Comme pour tout programme utilisant l'unité OGenView, la 1ère étape consiste à créer un objet application dérivé du type TGenApp. Ceci se fera en déclarant le nouveau type TIcoApp et le type associé PIcoApp qui est un pointeur sur les variables de type TIcoApp.

Pour cette première étape l'application de type TIcoApp se contentera de faire apparaître un bouton "Quitter" permettant de mettre fin au programme. Ce bouton va être incorporé au moment de la construction de l'application, on déclarera donc aussi le constructeur Init des objets TIcoApp.

Le résultat se trouve dans le fichier Ico01.Pas.

Program Ico01;
{ Projet Afficheur d'Icônes Windows }
{ K.B. avril 1996 }
{ Etape 1 :
  Création de l'objet application avec le bouton Quitter }

{ déclaration des unités utilisées }
Uses Graph,
     IcoUtils,
     UConst, UDrivers,
     OGenView, ODialog;

{ déclaration du nouveau type d'application }
Type
 { la nouvelle application descend du type TGenApp }
 PIcoApp = ^TIcoApp;
 TIcoApp = object(TGenApp)
  Constructor Init;
  End;

{ code associé à l'objet TIcoApp }
Constructor TIcoApp.Init;
Begin
 { constructeur hérité }
 TGenApp.Init(VGA);
 { ajout d'un bouton Quitter qui provoque la commande cmQuit et
   qui réagit à la touche Echap }
 Insert(New(PGrButton,Init(540,100, cmQuit, ' QUITTER  ',Echap)));
 { 540 et 100 indiquent la position d'affichage du bouton,
   cmQuit est la commande associée au bouton, c'est la commande standard
   qui met fin à toute application dérivée de TGenApp,
   ' QUITTER  ' est la chaîne qui s'inscrira sur le bouton,
   Echap est le code de la touche qui permet d'activer le bouton sans la
   souris }
End;

{+++++++++++++++++ corps du programme qui ne changera plus +++++++++++++++++}

{ déclaration d'une variable qui représentera l'application } 
Var IcoApp : PIcoApp;

BEGIN
 { initialisation par appel au constructeur }
 IcoApp:=New(PIcoApp,Init);
 { exécution du programme }
 IcoApp^.Exec;
 { destruction et fin }
 dispose(IcoApp,Done);
END.

Nous avons obtenu une application qui sait uniquement se terminer. C'est peu, mais déjà complexe lorsqu'on pense à tout ce qui a déjà été effectué : installation du mode VGA, activation de la souris, réaction du bouton "Quitter" à la souris et à la touche Echap.

ETAPE 2

Nous allons enrichir notre application de type TIcoApp en lui ajoutant un bouton "A Propos" permettant d'obtenir un message.

Cela nous amène à déclarer pour le type TIcoApp la méthode virtuelle HandleEvent pour permettre la réaction à la commande lancée par le bouton "A Propos".

La déclaration du type TIcoApp doit donc être complétée pour devenir :

TIcoApp = object(TGenApp)
 Constructor Init;
 { initialisation et insertion des boutons }
 Procedure HandleEvent(Var Event:TEvent);
 { gestion des commandes envoyées par les boutons }
 End;

La déclaration étant faite, il reste à écrire le texte de la nouvelle méthode, mais aussi à compléter le constructeur de l'application pour incorporer le nouveau bouton. On obtient donc le listing suivant définissant notre application de type TIcoApp.

{ objet TIcoApp }

Constructor TIcoApp.Init;
Begin
 { constructeur hérité }
 TGrApp.Init(VGA);
 { ajout d'un bouton Quitter qui provoque la commande cmQuit et qui réagit à 
la touche Echap }
 Insert(New(PGrButton,Init(540,100, cmQuit, ' QUITTER  ',Echap)));
 { ajout d'un bouton A Propos qui provoque la commande cmAbout et qui réagit 
à la touche F1 }
 Insert(New(PGrButton,Init(540,120, cmAbout,' A PROPOS ',F1)));
End;

Procedure TIcoApp.HandleEvent(Var Event:TEvent);
Begin
 { appel de la méthode héritée : elle transmet les évènements aux objets
   incorporés dans l'application, pour l'instant 2 boutons }
 TGenApp.HandleEvent(Event);

 { évènements traités par l'application }
 case Event.What of
  evCommand :   { réagir à la commande cmAbout }
   case Event.Command of
    cmAbout :   Message('  Afficheur d''icônes   '+chr(13)
                      + '    K.B. avril 1996     ');
    end;
  end;
 { la fonction Message permet d'afficher à l'écran une boîte de dialogue
   contenant un message et un bouton OK; le symbole chr(13) indique un
   passage à la ligne }
End;

Ces ajouts se trouvent dans le fichier Ico02.Pas. Le bouton "A Propos" activable avec la souris ou avec la touche F1 permet de faire apparaître une boîte de message qu'on peut déplacer avec la souris en cliquant sur la barre titre. Les touches Entrée et Echap permettent de fermer la boîte. Un clic sur le bouton droit de la souris aussi.

ETAPE 3

L'étape 3 va consister à donner une couleur au fond et à dessiner des cadres en relief pour les objets que nous ajouterons par la suite. Ces opérations qui n'ont lieu qu'une fois dans le programme seront écrites dans la méthode virtuelle BackGround de l'application. Il nous faudra donc la déclarer, puis l'écrire. Pour localiser les cadres à dessiner nous utiliserons des constantes de type TPoint.

Constantes déclarées

Const
 { positions des éléments }
 PosTitre:TPoint = (X:8;   Y:8);
 PosListe:TPoint = (X:10;  Y:40);
 PosIcone:TPoint = (X:560; Y:40);
 PosMenu :TPoint = (X:100; Y:300);

Déclaration de TIcoApp

Type
 { la nouvelle application descend du type TGrApp }
 PIcoApp = ^TIcoApp;
 TIcoApp = object(TGenApp)
  Constructor Init;
  Procedure BackGround; virtual;
  Procedure HandleEvent(Var Event:TEvent); virtual;
  End;

Code de la méthode BackGround de TIcoApp

Procedure TIcoApp.BackGround;
Const TitStr = 'Afficheur d''icônes  (K.B. avril 1996)';
Begin
 { préparer l'affichage }
 DrawBegin;
 { fond bleu clair }
 SetFillStyle(SolidFill,BleuClair);
 Bar(0,0,Size.X-1,Size.Y-1);
 { affichage et encadrement du titre }
 SetFillStyle(SolidFill,GrisClair);
 Bar(PosTitre.X,PosTitre.Y,8+length(TitStr)*8+8,8+16);
 Encadrer(PosTitre.X,PosTitre.Y,length(TitStr)*8+8,16,noir,gris,grisclair,blanc);
 Ecrire(TitStr,PosTitre.X+4,PosTitre.Y+2,noir,grisclair);
 { encadrer la liste de fichiers }
 Encadrer(PosListe.X-2,PosListe.Y-2,8*20*3+16+4,15*12+4,noir,gris,grisclair,blanc);
 { encadrer l'icône }
 Encadrer(PosIcone.X-2,PosIcone.Y-2,32+4,32+4,noir,gris,grisclair,blanc);
 { terminer l'affichage }
 DrawEnd;
End;

Ces 3 éléments ajoutés au fichier Ico02.Pas nous fournissent le fichier Ico03.Pas. L'écran se colore et prend du relief.

ETAPE 4

L'étape 4 va consister à installer la liste des fichiers. Pour cela nous définirons des objets de type TIcoList qui seront des descendants immédiats du type TDirView défini dans l'unité ODialog. Les objets de type TDirView affichent la liste des fichiers d'un répertoire et permettent de naviguer d'un répertoire à l'autre sur tout le disque dur. Nos descendants de type TIcoList fixeront le masque d'affichage des fichiers à *.ICO, choisiront un affichage sur 3 colonnes et 15 lignes et choisiront de nouvelles couleurs d'affichage en utilisant les champs cTexte, cFond, iTexte et iFond. Nous aurons donc à faire la déclaration suivante :

PIcoList = ^TIcoList;
TIcoList = object(TDirView)
 Constructor Init(x,y:Integer);
 { x et y sont les coordonnées du coin supérieur gauche }
 End;

puis à écrire le code correspondant.

{ objet TIcoList }

Constructor TIcoList.Init(x,y:Integer);
Begin
 { appel du constructeur hérité en fixant 3 colonnes de fichiers et 15 
   fichiers par colonnes, avec sélection des fichiers *.ico }
 TDirView.Init(3,15,'*.ICO');
 { choix de l'origine : coin supérieur gauche }
 Origin.X:=x;
 Origin.Y:=y;
 { pointeur souris associé }
 MouseAspect:=@PointeurStylo;
 { couleurs utilisées }
 cFond:=BleuClair;
 cTexte:=Blanc;
 iFond:=Jaune;
 iTexte:=Noir;
End;

Pour que ce nouvel objet apparaisse dans l'application, il faut l'insérer, ceci se fera en ajoutant une ligne dans le constructeur du type TIcoApp. Enfin, pour rendre la liste de fichiers immédiatement opérationnelle, il faut la sélectionner. On obtient alors le nouveau constructeur du type TIcoApp.

{ objet TIcoApp }

Constructor TIcoApp.Init;
Var V : PIcoList;
Begin
 { constructeur hérité }
 TGrApp.Init(VGA);
 { ajout d'un bouton Quitter qui provoque la commande cmQuit et qui réagit à 
   la touche Echap }
 Insert(New(PGrButton,Init(540,100, cmQuit, ' QUITTER  ',Echap)));
 { ajout d'un bouton A Propos qui provoque la commande cmAbout et qui réagit 
   à la touche F1 }
 Insert(New(PGrButton,Init(540,120, cmAbout,' A PROPOS ',F1)));
 { création de la liste des fichiers }
 V:=New(PIcoList,Init(10,40));
 { ajout de cette liste à l'application }
 Insert(V);
 { sélection de la liste }
 V^.Select;
End;

Ces nouveaux changements sont inscrits dans le fichier Ico04.Pas. Notre nouvelle application affiche bien une liste de fichiers déjà fonctionnelle : elle répond aux flèches du clavier et à la souris; la touche Entrée ou un double clic permet de changer de répertoire lorsqu'un répertoire est sélectionné; la combinaison Alt L permet de changer de disque.

ETAPE 5

Après la liste des fichiers, il nous faut créer l'objet qui affichera les icônes. Pour cela nous déclarons le type TIcoView directement hérité de TGenView. Il aura un champ supplémentaire IcoName de type PathStr qui contiendra le nom du fichier à afficher. La méthode Draw règlera le problème de l'affichage : l'icône si IcoName n'est pas une chaîne vide et un carré blanc autrement.

On obtient alors la déclaration suivante :

PIcoView = ^TIcoView;
TIcoView = object(TGenView)
 IcoName : PathStr;
 Constructor Init(x,y:Integer);
 Procedure Draw; virtual;
 End;

Puis le code correspondant.

{ objet TIcoView }

Constructor TIcoView.Init(x,y:Integer);
Begin
 { appel du constructeur hérité; les dimensions d'une icône sont 32x32; on 
   ajoute 2 pixels de chaque côté pour dessiner le cadre }
 TGenView.Init(x,y,36,36);
 { pas de fichier à afficher pour l'instant }
 IcoName:='';
End;

Procedure TIcoView.Draw;
Begin
 { préparer l'affichage }
 DrawBegin;
 { effacer le dessin courant en donnant un fond blanc et encadrer }
 SetFillStyle(SolidFill,Blanc);
 Bar(0,0,Size.X-1,Size.Y-1);
 { afficher la nouvelle icône s'il y a lieu }
 if IcoName<>''  { si pas de nom, alors pas d'icône }
  then AffIco(0,0,IcoName);   
 { fonction de l'unité IcoUtils; on utilise des coordonnées locales car
   cette fonction utilise la procédure PutPixel de l'unité Graph }
 { terminer l'affichage }
 DrawEnd;
End;

Comme pour la liste des fichiers il faut insérer cet objet dans l'application. Il suffit pour cela d'ajouter la ligne

 Insert(New(PIcoView,Init(560,40)));

dans le constructeur du type TIcoApp.

Tout ceci nous donne le fichier Ico05.Pas. L'application obtenue fait apparaître l'objet devant afficher les icônes mais malheureusement aucune icône ne s'affiche. Il faut maintenant donner à cet objet le moyen d'obtenir un nom de fichier à afficher.

Etape 6

La liste de fichiers et l'afficheur d'icônes sont mis en place. Il ne reste plus qu'à établir une liaison entre ces deux objets. Nous utiliserons pour cela une commande cmChange qui sera lancée par le listeur à chaque changement de fichier sélectionné. Lorsque l'afficheur d'icônes recevra cette commande, il demandera le nom du fichier sélectionné dans le listeur (en se servant du champ InfoPtr de l'évènement commande) et affichera enfin l'icône. Pour mettre ceci en oeuvre, il nous faut tout d'abord déclarer la constante associée à la commande.

 cmChange = 10000;

Il nous faut ensuite compléter les objets de type TIcoList et TIcoView en surchargeant leur méthode HandleEvent. Evidemment cette surcharge doit être annoncée au moment de la déclaration.

Pour les objets de type TIcoList nous ferons aussi apparaître un champ supplémentaire CurFile qui contiendra le numéro du dernier fichier sélectionné; cela nous permettra de repérer les changements.

Finalement on obtient les ajouts suivants :

TIcoList = object(TDirView)
 CurFile : Integer; 
 { dernier fichier choisi, permet de détecter les changements }
 Constructor Init(x,y:Integer);
 Procedure HandleEvent(Var Event:TEvent); virtual;
 End;

Procedure TIcoList.HandleEvent(Var Event:TEvent);
Begin
 { appel de la méthode héritée }
 TDirView.HandleEvent(Event);
 { prise en compte des changements de fichiers }
 if Choix<>CurFile 
 { Choix contient le numéro du fichier actuellement sélectionné }
  then begin
        { mettre à jour le numéro de fichier choisi }
        CurFile:=Choix;
        { envoi de la commande cmChange; InfoPtr pointe sur l'objet }
        SetCommand(cmChange);
       end;
End;

Pour les objets de type TIcoView cela nous donne :

TIcoView = object(TGenView)
 IcoName : PathStr;
 Constructor Init(x,y:Integer);
 Procedure Draw; virtual;
 Procedure HandleEvent(Var Event:TEvent); virtual;
 End;

Procedure TIcoView.HandleEvent(Var Event:TEvent);
Var
 FName : PathStr;
Begin
 { appel de la méthode héritée }
 TGenView.HandleEvent(Event);
 { traitement spécifique }
 case Event.What of
  evCommand :
   case Event.Command of
    cmChange :
     begin
      { on récupère le nom du fichier en utilisant InfoPtr et en le 
      transtypant; InfoPtr pointe en effet sur la liste de fichiers }
      FName:= PIcoList(Event.InfoPtr)^.FichierChoisi;
      { on vérifie que le fichier ne correspond pas à un répertoire }
      { on utilise pour cela une fonction de l'unité IcoUtils }
      if EstFichier(FName)
       then IcoName:=FName
       else IcoName:='';
      { affichage de l'icône ou effacement }
      Draw;
     end;
    else exit;
    end;
  else exit;
  end;
End;

Le fichier Ico06.Pas est ainsi terminé. Il fournit une application entièrement fonctionnelle qui répond à la première partie du cahier des charges fixé au départ: affichage de l'icône sélectionnée dans la liste de fichiers.

ETAPE 7

Nous allons dans cette étape installer le menu permettant d'effectuer des opérations élémentaires sur le fichier sélectionné. Cela nécessitera plusieurs ajouts au programme.

Commandes associées au menu

Nous commencerons par définir les commandes associées aux différents items du menu : cmCopier, cmDeplacer, cmRenommer, cmEffacer. Ces 4 nouvelles constantes viendront s'ajouter à celles déjà définies dans le programme.

 { commandes du menu }
 cmCopier   = 10001;
 cmEffacer  = 10002;
 cmRenommer = 10003;
 cmDeplacer = 10004;

Créer la procédure d'exécution du menu

Cette procédure n'a pas besoin d'être une méthode d'un objet; elle est définie de fa‡on indépendante, avant le code définissant les objets.

Son principe est simple :

Ceci nous donne le code suivant :

{ procédure d'affichage du menu }
Procedure ExecMenu;
Var WMenu   : PMenuView;
Begin
 { création de l'objet menu }
 WMenu:=New(PMenuView,Init);
 with WMenu^ do
  begin
   MouseAspect:=@PointeurStylo;
   AjouterItem('Copier...         ',0,cmCopier);
   AjouterItem('Renommer...       ',0,cmRenommer);
   AjouterItem('Déplacer...       ',0,cmDeplacer);
   AjouterItem('Effacer...        ',0,cmEffacer);
  end;
 { insertion dans une fenêtre et exécution }
 ExecView(New(PGrMenuWin,Init(PosMenu.X,PosMenu.Y,WMenu)),nil);
End;

Activation du menu

Le menu étant prêt à fonctionner, il faut définir les moyens qui permettront à l'utilisateur de l'activer. Ceci pourra se faire de deux façons.

Activation 1

Un nouveau bouton à insérer dans l'application permettra d'activer le menu en envoyant la commande cmMenu. Il suffira d'ajouter dans le constructeur de l'application la ligne :

 Insert(New(PGrButton,Init(540,140, cmMenu, '   MENU   ',F10)));

Il faut ensuite déterminer quel objet répondra à la commande cmMenu. Ce sera l'objet liste de fichiers, car il pourra aisément vérifier que l'élément de liste qu'il indique est bien un fichier et non un répertoire. Nous devrons donc ajouter dans la méthode HandleEvent de la liste de fichiers une réaction à la commande cmMenu qui permettra d'activer le menu si cela est justifié.

Activation 2

La liste de fichiers hérite du type TDirView la faculté d'envoyer une commande cmOK lorsqu'on appuie sur Entrée ou lorsqu'on double-clique sur un élément qui représente un fichier.

Nous allons la faire réagir elle-même à cette commande en activant le menu. Ceci se traduit par une nouvelle ligne supplémentaire dans sa méthode HandleEvent.

Ces nouveaux ajouts fournissent le fichier Ico07.Pas. Le programme ainsi obtenu permet bien d'afficher le menu promis. Mais il ne réagit pas encore aux commandes du menu.

ETAPE 8

L'étape 8 va nous permettre de faire réagir le programme aux commandes lancées par le menu. L'objet qui devra réagir à ces commandes est évidemment la liste de fichiers : les commandes s'adressent au fichier sélectionné dans la liste, on obtient son nom complet en utilisant la fonction FichierChoisi.

Il nous faut donc compléter encore une fois la méthode HandleEvent de la liste de fichiers en indiquant la conduite à tenir pour chacune des 4 commandes du menu.

Effacer
On demandera une confirmation de l'effacement en utilisant la fonction OuiNon de l'unité ODialog. On fera ensuite appel à la fonction Effacer de l'unité IcoUtils.

Copier, Renommer, Déplacer
On demandera à l'utilisateur d'indiquer la destination de la copie ou du déplacement, ou le nouveau nom à attribuer au fichier. On utilisera pour cela la fonction WinRead de l'unité ODialog et on fera appel aux fonctions Copier, Renommer, Deplacer de l'unité IcoUtils.

Ces transformations effectuées, il reste un dernier problème à régler. Après une suppression, un effacement ou un changement de nom les données contenues dans la liste de fichiers ne sont plus exactes. Il faudra les mettre à jour et il en ira peut être de même pour l'icône affichée dans l'afficheur d'icônes. Nous allons, pour régler ce problème, écrire une nouvelle méthode de l'objet liste de fichiers : la méthode Refresh. Elle obligera le tableau de fichiers utilisé à relire le contenu du répertoire, puis elle réaffichera la liste mise à jour et elle pourra envoyer envoyer la commande cmChange qui obligera l'afficheur d'icônes à se mettre à jour lui aussi. Ceci nous donne :

Procedure TIcoList.Refresh;
Begin
 DirTab^.ReadDir;
 if Choix>NombreItems
  then Choix:=NombreItems;
 Draw;
 SetCommand(cmChange);
End;

Après ces derniers ajouts, le fichier Ico08.Pas est terminé. Il fournit l'application répondant complètement au cahier des charges fixé au départ.



Conclusion

Utiliser la bibliothèque n'est pas immédiat. Cela suppose une certaine connaissance des outils fournis pris individuellement, mais aussi de leurs interconnexions nombreuses dues à la notion d'héritage en programmation orientée objets. L'effort à fournir pour obtenir une maitrise suffisante de cet ensemble me semble cependant rentable lorsqu'on mesure les avantages fournis.

Une interface conviviale

L'utilisation de la programmation évènementielle fournit à l'utilisateur du programme produit une interface conviviale gérant clavier et souris, offrant boutons et listes de sélection, permettant à tout moment l'utilisation d'outils divers comme des pages d'aides ou des calculatrices. Tout ceci correspond simplement pour le programmeur à l'insertion de l'objet adéquat en réponse à une action.

La modularité

L'utilisation d'objets permet de traiter de fa‡on indépendante les différents problèmes posés par un programme. Cela ouvre en outre la possibilité d'un travail en équipe. Enfin cela permet une maintenance plus aisée du programme, qu'il s'agisse de corriger d'inévitables erreurs ou d'ajouter de nouvelles fonctions. Corriger un objet ou en ajouter un ne nécessite pas de bouleversements plus ou moins bien réussis dans la structure du programme.

La réutilisabilité

Les objets créés sont souvent utilisables dans plusieurs programmes, soit de façon directe, soit en les enrichissant de nouveaux champs et de nouvelles méthodes. Cela représente une économie de travail qui permet d'aller rapidement à l'essentiel dans la conception d'un nouveau programme.

Une façon de programmer unifiée

La bibliothèque a été créée pour permettre d'utiliser la même logique, les mêmes fonctions et procédures dans de nombreux programmes.