Turbo Vision : Programmation avancée

Mathieu DALBIN

Apprenez à programmer des interfaces grâce à Turbo Vision en Turbo Pascal, grâce à un exemple d'application fonctionnelle. ♪

- Titre : Turbo Vision : Programmation avancée

- Auteur : Mathieu DALBIN

- Parution : 4 septembre 2006

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2006 Mathieu Dalbin. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Avant-propos

- I. Structure du programme

- I-A. Quelle application veut-on développer ?

- I-B. Comment la concevoir : fonctionnalités de base requises

- I-B-1. L'interface graphique

- I-B-2. Module de gestion de la disquette

- II. L'interface graphique

- II-A. Éléments de base

- II-B. Composants visuels utilisés

- II-C. Fonctionnalités supplémentaires

- II-D. Résultat espéré

- III. Programmation de l'interface graphique

- III-A. Création de l'application

- III-B. Création des composants

- III-B-1. Un bureau tel que nous en rêvions

- III-B-1-a. Le bureau

- III-B-1-b. Les barres de menus

- III-B-1-b-i. La fausse barre de menu

- III-B-1-b-ii. La barre d'état

- III-B-1-b-iii. La palette de couleurs

- III-B-2. Les composants de la fenêtre principale

- III-B-2-a. Le champ de commande

- III-B-2-b. La gestion des listes déroulantes

- III-B-2-c. La liste des commandes

- III-B-2-d. La liste des résultats

- III-C. Création des fenêtres

- III-C-1. Fenêtre de logs

- III-C-2. Fenêtre « mémoire »

- III-C-3. Fenêtre « About »

- III-C-4. Fenêtre de choix

- IV. Programmation du Gestionnaire de Disquette

- IV-A. Structure d'une disquette

- IV-B. Implémentation des primitives

- IV-B-1. Lecture

- IV-B-2. Écriture

- IV-B-3. Formatage

- IV-C. Gestion des erreurs

- V. Intégration du gestionnaire et de l'interface

- V-A. Pré requis et mise en œuvre

- V-B. Pour améliorer le tout…

- V-B-1. La barre de progression

- V-B-2. La fenêtre de progression

- V-B-3. Liaison avec le gestionnaire de disquette

- VI. Conclusion

Avant-propos

Suite au précédent tutoriel sur Turbo Vision dont le but était de présenter brièvement les possibilités de cette bibliothèque, nous allons à présent aborder une programmation plus avancée, en créant un programme permettant d'accéder aux secteurs d'une disquette. Afin de disposer d'un certain confort pour l'utilisateur, l'interface graphique de ce programme sera donc basée sur Turbo Vision : nous allons ici analyser rapidement la programmation des composants les plus courants et les plus utilisés lors de la création d'interfaces. Nous verrons également un exemple d'intégration de cette interface et du système de gestion des secteurs de la disquette au sein d'un même programme. Pour ce tutoriel, nous utiliserons le compilateur Turbo Pascal, afin de pouvoir accéder au lecteur de disquette via les interruptions en langage assembleur.

I - Structure du programme

I-A - Quelle application veut-on développer ?

Dans ce tutoriel, notre but sera donc de créer une application illustrant les principes de la programmation de Turbo Vision et l'intégration des éléments de l'interface graphique au sein du programme. Pour cela, nous allons concevoir notre programme en le séparant en deux parties, sachant que notre but est de créer une application permettant de lire, écrire et formater les secteurs d'une disquette.

I-B - Comment la concevoir : fonctionnalités de base requises

I-B-1 - L'interface graphique

Les exigences, au niveau de l'interface graphique, se portent principalement sur la simplicité d'utilisation. Notre objectif est de présenter à l'utilisateur toutes les informations dont il a besoin en un seul regard. Ainsi, il doit pouvoir disposer à tout moment de la liste des instructions disponibles et pouvant être exécutées, et du résultat de la dernière commande qu'il a tapée. On veut également connaître la liste des instructions qui ont déjà été exécutées et leur résultat. Afin de gagner en simplicité d'utilisation, et dans l'objectif d'utiliser pleinement tout le potentiel de la librairie Turbo Vision, notre interface graphique doit se baser sur une programmation permettant la prise en compte de tous les types d'évènements que Turbo Vision peut gérer.

I-B-2 - Module de gestion de la disquette

Nous avons cependant quelques exigences, tant au niveau de l'interface que des fonctionnalités que notre logiciel doit posséder. En effet, comme notre programme est censé manipuler les secteurs d'une disquette, il est évident que nous devons nécessairement posséder les primitives de base pour accéder aux informations sur le périphérique : nous devons donc avoir une routine de lecture et une routine d'écriture. De plus, les périphériques de stockage possèdent la capacité de pouvoir être totalement et «irrémédiablement» vidés : ceci est le formatage. En résumé, chaque secteur peut donc être lu et écrit, et la disquette peut être formatée, c'est-à-dire effacée. Notre module devra donc permettre l'exécution de ces trois primitives.

II - L'interface graphique

Nous nous concentrerons dans un premier temps sur la conception de l'interface graphique. Notre but ici est de visualiser l'interface de notre future application, c'est-à-dire les composants utilisés, leur emplacement et les différentes fonctionnalités que l'interface doit offrir à l'utilisateur.

II-A - Éléments de base

Commençons tout simplement par imaginer ce que l'utilisateur doit voir lorsqu'il démarre l'application : on peut imaginer tout simplement un bureau (au sens que l'entend Turbo Vision), une barre d'état au bas de l'écran qui affiche une liste de raccourcis disponibles par la souris ou une combinaison de touches au clavier, et, pour la convivialité, une barre en haut de l'écran qui affiche l'heure par exemple.

II-B - Composants visuels utilisés

Pour construire le véritable cœur de notre interface, nous devons considérer à nouveau les exigences que nous devons respecter pour le module de gestion de disquette. Le but de cette application est de voir le contenu des secteurs d'une disquette, mais aussi d'écrire ou même formater des secteurs. Afin de respecter ces spécifications, il semble évident que l'élément de l'interface qui doit être destiné à l'affichage des données de la disquette doit être le plus grand possible, pour garder un confort lors de la visualisation de ces données. Cette large zone doit pouvoir afficher les données sous forme de colonnes, et il est probable que le nombre de lignes à afficher soit très grand : cette présentation peut être effectuée facilement à l'aide d'une liste, dont la méthode d'affichage sera légèrement modifiée pour pouvoir former des colonnes.

Afin d'offrir un certain confort à l'utilisateur, on peut prévoir d'afficher la liste des commandes disponibles. Un champ permettrait de taper la commande, et la liste des commandes disponibles se réduirait automatiquement. Ceci peut être fait à l'aide d'un simple champ texte et d'une liste déroulante.

II-C - Fonctionnalités supplémentaires

L'application peut offrir des fonctionnalités pour augmenter encore le confort de l'utilisateur. Outre le champ texte et la liste des commandes cités précédemment, on peut aussi penser à rajouter une fenêtre permettant de consulter les logs de l'application ou la quantité de mémoire disponible, ou encore une boîte de dialogue affichant des informations sur l'application.

II-D - Résultat espéré

Voici le résultat que l'on peut espérer : on peut entrevoir donc en arrière-plan le bureau, et les deux barres qui l'enserrent. Au centre de l'écran, on reconnaît la large zone où seront affichées les données des secteurs. Au-dessus, un champ de texte permet de saisir les commandes, résumées dans la liste directement dessous. La barre de menu du bas contient un « lien » pour quitter directement l'application, ainsi que des « liens » pour afficher les fenêtres de logs, de mémoire et d'information.

III - Programmation de l'interface graphique

Nous allons voir à présent comment définir ses propres composants et ses propres fenêtres, afin d'obtenir le meilleur résultat possible. Ces opérations de modifications vont ainsi nous permettre de construire l'interface telle que nous l'avons pensée, en ajoutant les fonctionnalités que nous désirions implémenter.

III-A - Création de l'application

C'est par là que nous allons commencer. Il faut en effet créer une application. Turbo Vision dispose d'un objet déjà implémenté définissant une application. Cet objet TApplication de l'unité App, permet de créer un cadre pour l'exécution de procédures : c'est dans ce sens qu'il faut comprendre le mot application dans Turbo Vision. Ce cadre est défini à l'aide de trois méthodes :

- le constructeur Init, qui permet d'initialiser les différents éléments de l'application qui ont été ajoutés ou modifiés par le programmeur ;

- le destructeur Done, qui permet de terminer l'application et de libérer la mémoire utilisée ;

- la méthode Run, qui permet de véritablement lancer l'application.

On peut comparer cette méthode Run à une boucle « infinie » de gestion des évènements. L'application est lancée et devient autonome seulement après l'appel à cette méthode particulière.

Comme nous voulons profiter des avantages de l'objet TApplication, mais que nous voulons spécifier nos propres composants pour cette application, nous allons créer un type dérivant du type TApplication. Nous appellerons notre type TDisk8Reader :

type TDisk8Reader = object(TApplication)
public
constructor Init;
destructor Done; virtual;
end;

Pour définir et créer nos propres objets et composants lors de l'instanciation de l'application, nous allons modifier le constructeur Init. La destruction de ces éléments supplémentaires se fera dans le destructeur Done de l'application. La méthode Run n'a pas besoin d'être surchargée, car son comportement est le même pour toutes les applications.

Dans le cas d'une surcharge de la méthode Run, veillez à appeler la méthode ancêtre Run de l'objet TApplication parent, car sinon votre application ne « tournera » pas !

III-B - Création des composants

III-B-1 - Un bureau tel que nous en rêvions

III-B-1-a - Le bureau

Votre première mission en tant que programmeur Turbo Vision sera de redéfinir le bureau et les barres, situées en haut et en bas de l'écran. Un rapide coup d'œil dans la documentation de Turbo Vision permet de voir qu'un objet a déjà été défini pour représenter le bureau : TDesktop, de l'unité App. Ce bureau « de base » nous convient parfaitement : il affiche une mosaïque de caractères afin de former un fond uniforme. Il est possible de spécifier d'utiliser un autre caractère pour créer ce fond, le caractère utilisé par défaut étant le caractère ASCII 177.

Nous aurions pu éviter de le redéfinir, mais nous allons quand même voir comment faire pour définir proprement un bureau. L'objet TApplication possède deux éléments intéressants concernant le bureau : une variable globale de type PDesktop (c'est-à-dire un pointeur sur un type TDesktop) et une méthode appelée automatiquement par le constructeur Init : InitDesktop (il n'est donc pas nécessaire d'appeler cette méthode directement). C'est dans cette méthode que nous pourrions construire un bureau sur mesure, mais nous utiliserons ici un bureau standard dont le type sera TDesktop. Nous allons surcharger la méthode InitDesktop pour créer notre bureau :

procedure TDisk8Reader.InitDesktop;
var Bounds: TRect;
begin
Bounds.Assign(0, 0, 80, 49);
Desktop := New(PDesktop, Init(Bounds));
end;

III-B-1-b - Les barres de menus

III-B-1-b-i - La fausse barre de menu

Nous allons redéfinir à présent les barres qui entourent l'écran, mais nous allons utiliser une petite astuce. Comme je vous l'ai dit précédemment, nous souhaitons que la barre du haut contienne une horloge. Or la barre du haut est normalement réservée pour l'affichage de la barre de menu, nous allons donc éviter d'afficher une barre de menu pour la remplacer par un champ statique de texte qui prendra toute la largeur de l'écran pour créer l'illusion !

Nous allons tout d'abord définir notre objet qui servira d'horloge. Pour ce faire, nous pouvons utiliser l'objet TStaticText qui permet d'afficher du texte de façon statique : cet objet ne possède donc pas de gestionnaire d'évènements. Ce texte ne peut cependant pas être changé, car il n'existe pas de méthode dans l'objet TStaticText pour le changer, mais la documentation nous indique qu'il existe un champ Text de type PString (c'est-à-dire un pointeur sur une chaîne de caractères). Turbo pascal possède deux procédures pour créer en mémoire (dans le tas) et supprimer des chaînes de caractères : NewStr et DisposeStr. Ce sont ces méthodes que nous utiliserons pour mettre à jour l'affichage de l'horloge. Notre objet possèdera donc trois méthodes :

- le constructeur Init surchargé qui permettra d'initialiser nos propres champs (pour la mise à jour de l'heure) ;

- une méthode SetText qui permettra de changer le texte affiché, en faisant bien attention de bien maîtriser les pointeurs que nous manipulons ;

- une méthode Update dont le but sera de rafraîchir l'affichage de la fausse barre.

Voici la définition de notre type, que nous appellerons TClockStaticText :

type PClockStaticText = ^TClockStaticText;
TClockStaticText = object(TStaticText)
private
Second, HundredthSecond: Word;
public
constructor Init(var Bounds: TRect; AText: String);
procedure SetText(AText: String);
procedure Update;
end;

Et l'implémentation :

constructor TClockStaticText.Init(var Bounds: TRect; AText: String);
begin
if not inherited Init(Bounds, AText) then Fail;
Second := 0;
HundredthSecond := 0;
end;

procedure TClockStaticText.SetText(AText: String);
begin
if Text <> Nil then DisposeStr(Text);
Text := NewStr(AText);
end;

procedure TClockStaticText.Update;
var H, M, S, Sec: Word;
SH, SM, SS, SSec: String;
begin
GetTime(H, M, S, Sec);
If (S * 100 + Sec) - (Second * 100 + HundredthSecond) >= 100 then
begin
Second := S;
HundredthSecond := Sec;
Str(H, SH); if H < 10 then SH := '0' + SH;
Str(M, SM); if M < 10 then SM := '0' + SM;
Str(S, SS); if S < 10 then SS := '0' + SS;
Str(Sec, SSec);
SetText(SH + ':' + SM + ':' + SS);
DrawView;
end;
end;

Nous voici donc en possession de notre fausse barre ! Il ne nous reste plus qu'à la positionner sur notre bureau, tout en faisant en sorte de ne pas créer de vraie barre de menus. Comme pour la création du bureau, l'objet TApplication possède une variable globale MenuBar de type PMenuView et une méthode InitMenuBar appelée par le constructeur Init, qui est censée créer une barre de menu standard. Nous allons donc redéfinir cette méthode pour ne pas qu'elle crée de barre de menu :

procedure TDisk8Reader.InitMenuBar;
begin
MenuBar := Nil;
end;

Nous allons à présent modifier la déclaration de notre type application TDisk8Reader, son constructeur Init et son destructeur Done pour gérer la création et la destruction de la fausse barre. Pour plus de clarté, nous créerons une procédure InitClock permettant d'initialiser l'horloge :

type TDisk8Reader = object(TApplication)
private
ClockStaticText: PClockStaticText;
public
constructor Init;
procedure InitMenuBar; virtual;
procedure InitStatusLine; virtual;
procedure InitDesktop; virtual;
procedure InitClock;
procedure Idle; virtual;
destructor Done; virtual;
end;

constructor TDisk8Reader.Init;
begin
if not inherited Init then Fail;
SetScreenMode(smCO80 + smFont8x8);
Redraw;
InitClock;
end;

procedure TDisk8Reader.InitClock;
var Bounds: TRect;
begin
Bounds.Assign(0, 0, 80, 1);
ClockStaticText := New(PClockStaticText, Init(Bounds, ''));
Desktop^.Insert(ClockStaticText);
end;

destructor TDisk8Reader.Done;
begin
Dispose(ClockStaticText, Done);
inherited Done;
end;

Notre barre d'horloge est maintenant mise en place, mais elle n'affiche aucun texte… Il faut utiliser une dernière astuce pour la rendre vivante ! Nous allons en effet mettre à jour l'horloge grâce à la méthode Idle du type TApplication. Cette méthode spéciale est appelée par le gestionnaire d'évènements lorsqu'il n'a pas d'évènement à traiter, ce qui arrive généralement lorsque l'utilisateur ne fait aucune action. Nous allons utiliser cette méthode pour mettre à jour notre horloge :

procedure TDisk8Reader.Idle;
begin
inherited Idle;
ClockStaticText^.Update;
end;

Si vous utilisez la méthode Idle, veillez à toujours appeler la méthode ancêtre Idle de l'objet TApplication parent sinon votre application risque de ne plus se comporter normalement et d'être bloquée pour la gestion des évènements. Veillez également à ne pas faire trop d'opérations dans cette méthode, car, dans ce cas, la gestion d'évènements peut être bloquée.

Dans le constructeur de notre application, nous utilisons la commande SetScreenMode. Cette commande, avec le paramètre smCO80 + smFont8x8 permet de passer le mode vidéo utilisé en couleur et 80 colonnes (paramètre smCO80) et en 50 lignes (paramètre smFont8x8). La méthode Redraw appelée immédiatement après permet de redessiner l'interface en prenant en compte les nouvelles dimensions de l'écran.

III-B-1-b-ii - La barre d'état

Nous allons à présent créer notre barre d'état : ici nous n'utiliserons pas d'astuce, nous allons créer une vraie barre d'état ! Comme pour le bureau et la barre de menu, l'objet TApplication dispose d'une variable globale StatusLine de type PStatusLine et d'une méthode InitStatusLine appelée par le constructeur. Comme nous voulons ajouter des « liens » à notre barre en plus des « liens » normaux, nous devons redéfinir un objet PStatusLine avec les bons paramètres. Nous voulons donc que notre barre contienne un premier lien permettant de quitter l'application et trois autres liens permettant d'afficher diverses fenêtres (nous verrons plus loin quelles fenêtres seront affichées et comment elles seront construites). Nous obtenons le code suivant :

procedure TDisk8Reader.InitStatusLine;
var Bounds: TRect;
begin
Bounds.Assign(0, 49, 80 ,50);
StatusLine := New(PStatusLine, Init(Bounds, NewStatusDef(0, 0,
NewStatusKey('~Alt+X~ Exit', kbAltX, cmQuit,
NewStatusKey('~Alt+L~ Log', kbAltL, cmShowLogDialog,
NewStatusKey('~Alt+M~ Memory', kbAltM, cmShowMemoryDialog,
NewStatusKey('~Alt+A~ About', kbAltA, cmShowAboutDialog, Nil)))), Nil)));
end;

La construction des barres d'état, tout comme celle des barres de menus, est intéressante à analyser : en effet, il faut définir les éléments à utiliser de façon imbriquée. Dans notre cas, nous définissons tout d'abord un objet TStatusLine : le constructeur de TStatusLine possède un paramètre qui est un pointeur vers un objet TStatusDef (qui représente une ligne dans la barre d'état). Pour créer les différents éléments, nous utilisons ensuite les fonctions NewStatusDef et NewStatusKey, qui prennent chacune en paramètre un pointeur vers l'élément suivant. Si ce paramètre est Nil, alors l'élément créé est le dernier de la liste.

III-B-1-b-iii - La palette de couleurs

Nous allons utiliser ici une dernière astuce, qui peut être très amusante… En effet, l'objet TApplication possède une méthode GetPalette qui retourne un pointeur de type PPalette vers la palette utilisée par l'application. Or ce type PPalette est en réalité un pointeur vers le type TPalette, qui est lui-même un type équivalent au type String. La documentation n'est pas très explicite pour ce type TPalette, mais comme il existe en tout 63 couleurs, on en déduit donc que la palette de l'application est une chaîne de caractères dont la taille est de 63 caractères. Chacune de ces 63 valeurs représente une couleur correspondant à un élément précis des interfaces : il y a par exemple une couleur définissant la couleur du bureau, une pour la couleur des menus, une autre pour le fond des fenêtres, etc. Il est possible de presque tout changer ! La liste des couleurs utilisées dans les interfaces Turbo Vision est néanmoins documentée dans la section « Turbo_Vision_palettes » de l'aide, on peut également connaître le détail des couleurs en cherchant le mot-clé CColor. Nous allons voir comment modifier la palette courante. L'opération consiste à redéfinir la fonction GetPalette de notre objet TApplication, en ayant au préalable défini notre propre palette :

const CAppPalette = #$78#$70#$78#$74#$20#$74#$78#$73#$7F#$7A +
#$31#$31#$1E#$71#$00#$2F#$3F#$3A#$13#$13 +
#$3E#$21#$00#$70#$7F#$13#$78#$74#$70#$7F +
#$00#$70#$7F#$7A#$13#$13#$70#$70#$7F#$7E +
#$20#$2B#$2F#$78#$2E#$70#$30#$3F#$3E#$1F +
#$2F#$1A#$20#$72#$31#$31#$30#$2F#$3E#$31 +
#$13#$00#$00;

function TDisk8Reader.GetPalette: PPalette;
const P: String[Length(CAppPalette)] = CAppPalette;
begin
GetPalette := @P;
end;

La méthode GetPalette permet donc de charger notre propre palette et est appelée à chaque fois que l'application a besoin de dessiner un élément de l'interface. Pour cette application, j'ai, entre autres, changé les couleurs des éléments lorsqu'ils sont sélectionnés et la couleur de fond du champ de saisie des commandes.

Voici le résultat obtenu à la fin de ces différents ajouts :

III-B-2 - Les composants de la fenêtre principale

Nous allons détailler ici les principaux composants de la fenêtre principale et leur programmation :

- tout d'abord un champ où l'utilisateur pourra entrer la commande qu'il souhaite utiliser ;

- une liste qui affichera les différentes commandes possibles pour l'utilisateur ;

- enfin une liste déroulante qui permettra d'afficher les données relatives aux secteurs de la disquette.

III-B-2-a - Le champ de commande

La création d'un objet spécifique pour l'utilisation d'un champ d'entrée n'est pas un réel problème : comme le composant existant, c'est-à-dire TInputLine, possède un comportement similaire à celui que l'on veut obtenir, nous nous baserons sur cet objet pour définir le nôtre. Typiquement, il suffit de modifier la méthode HandleEvent afin que le composant réagisse de la façon dont nous le souhaitons.

Dans notre cas, nous souhaitons qu'il y ait un lien entre ce que l'utilisateur entre dans ce champ et la liste des commandes disponibles, qui est affichée séparément dans la liste déroulante juste dessous. À chaque évènement correspondant à un ajout ou une suppression d'une lettre dans le champ texte, la liste doit être actualisée en affichant alors les commandes disponibles. Le comportement de cette liste est le suivant.

Considérons les commandes typiques « read », « write » et « formatAll » (que nous implémenterons plus loin), la liste déroulante affiche alors les trois commandes, car le champ texte est vide. Si l'utilisateur tape la lettre « r », seule la commande « read » doit s'afficher. S’il tape « ra », aucune commande ne sera affichée.

Donc à chaque changement dans le champ texte, la liste des commandes affichées doit être reconstruite et affichée à nouveau, et cet enchaînement peut être implémenté avec l'aide des évènements. En effet, au lieu d'appeler une méthode pour reconstruire la liste, nous allons générer un évènement qui sera traité par le gestionnaire d'évènements, qui se chargera d'exécuter la bonne procédure. Cette méthodologie a plusieurs avantages :

- premièrement, ce n'est pas le programmeur qui appelle implicitement la méthode, il y a donc potentiellement moins de risques de se tromper, en particulier s’il y a plusieurs programmeurs qui ne codent pas de la même façon. La responsabilité est laissée au gestionnaire d'évènements ;

- ensuite, le code est plus flexible : ainsi, lors de modifications, il n'est pas nécessaire de réécrire le code qui envoie le message, mais seulement la partie qui traite le nouveau message envoyé ;

- enfin, l'exécution de la méthode appelée est gérée par le gestionnaire d'évènements, qui l'exécutera seulement lorsque l'évènement sera traité. Ainsi on ne sait pas quand la méthode ainsi invoquée sera effectivement exécutée, mais ce temps est généralement très rapide vu la faible utilisation du gestionnaire d'évènements. Cela peut être un inconvénient si le gestionnaire d'évènements est fortement mis à contribution par l'application, ainsi il se peut que l'exécution de la méthode soit légèrement différée dans le temps.

Ce qu'il faut garder à l'esprit avec la programmation évènementielle est qu'elle permet de faire communiquer plusieurs éléments entre eux de façon simple : dans notre cas, nous faisons ainsi « dialoguer » un champ d'entrée et une liste déroulante. Le schéma est reproductible pour tous les autres types de composants (boîte de dialogue, fenêtre, bouton, etc.).

Analysons à présent le code de la méthode HandleEvent de l'objet TCommandInputLine, qui hérite de l'objet TInputLine :

procedure TCommandInputLine.HandleEvent(var Event: TEvent);
var Command: String;
begin
if ((Event.What = evKeyDown) and (Event.CharCode in CorrectChars)) then
begin
case Event.CharCode of
#13 : begin
Event.What := evCommand;
Event.Command := cmExecuteCommand;
end;
#8  : begin
GetData(Command);
Command := Copy(Command, 0, Length(Command) - 1);
SetData(Command);
SelectAll(False);
Event.What := evCommand;
Event.Command := cmBuildCommandListBox;
end
else begin
GetData(Command);
Command := Command + Event.CharCode;
SetData(Command);
SelectAll(False);
Event.What := evCommand;
Event.Command := cmBuildCommandListBox;
end;
end;
Event.InfoPtr := Nil;
PutEvent(Event);
end;
if ((Event.What = evMouseDown) and (Event.Buttons = mbLeftButton) and (Event.Double)) then
begin
Event.What := evCommand;
Event.Command := cmExecuteCommand;
end;
inherited HandleEvent(Event);
end;

Dans un premier temps, nous récupérons l'évènement qui a été généré par l'utilisateur : ici nous nous occupons seulement de l'appui des touches du clavier, d'où le choix evKeyDown. La seconde étape consiste à vérifier que la touche tapée correspond bien à un caractère autorisé pour la ligne de commande. La liste des caractères autorisés a été définie dans la variable CorrectChars qui est un tableau de caractères. Si le caractère entré est admis comme correct, un autre choix est fait : si la touche Entrée a été tapée (caractère #13), la commande équivalente à la commande tapée est exécutée. Si la touche Retour (#8) a été tapée, le dernier caractère de la ligne de commande est supprimé et l'évènement de reconstruction de la liste des commandes est lancé. Si un autre caractère correct et différent de #13 et #8 est entré, il est ajouté à la fin de la commande et l'évènement pour la reconstruction de la liste est lancé.

Un autre test est ensuite effectué : si l'utilisateur a double-cliqué sur le champ Input, la commande qui est inscrite est exécutée. Dans les deux cas de validation (appui de la touche Entrée ou double-clic), si la commande entrée n'existe pas, rien ne s'exécutera : ceci est géré dans la procédure HandleEvent de l'objet application, qui se charge d'exécuter les commandes.

Notez que ces différentes actions entraînent toutes la création d'un nouvel évènement, ou plutôt la réutilisation de l'évènement déjà présent qui était reçu en paramètre de la méthode. En modifiant les valeurs des champs de cet objet Event, nous pouvons définir un nouveau type d'évènement : ici l'objet Event représentera un évènement « commande », pouvant avoir deux valeurs possibles selon les cas, cmExecuteCommand et cmBuildCommandListBox. Ces deux évènements que nous avons définis nous-mêmes se verront traités dans le gestionnaire d'évènement de l'objet le moins spécifique, c'est-à-dire l'objet TApplication. Nous verrons plus tard comment sont gérées les commandes dans ce gestionnaire d'évènements.

III-B-2-b - La gestion des listes déroulantes

Nous avons donc vu comment définir notre propre composant héritant de TInputLine, et nous allons à présent analyser comment générer nos propres listes déroulantes, à partir de l'objet TListBox. Mais nous devons tout d'abord faire une parenthèse sur le comportement général des listes déroulantes : donc, pour ceux qui ne le savent pas encore, les listes déroulantes servent à afficher de façon séquentielle un certain nombre d'informations, généralement des chaînes de caractères. À ce stade, tout est normal, mais si l'on se penche plus précisément sur les mécanismes de gestion de ces informations, on peut s'attendre à quelques surprises. En effet, l'on se rend compte que l'objet TListBox ne possède pas d'attribut permettant, à proprement parler, de stocker les informations. Le seul attribut présent dans la déclaration de cet objet est un champ List de type PCollection. Deux objets héritent de TCollection :

- tout d'abord TSortedCollection, qui représente des collections triées ;

- et TStringCollection, qui hérite de TSortedCollection et permet de stocker des chaînes de caractères.

Ces objets ont un comportement particulier : en effet, ces listes d'éléments sont en réalité des listes de pointeurs, ce qui induit que les collections ne possèdent pas véritablement les données, mais des références vers les données. Il n'est ainsi pas possible d'ajouter des variables locales, par exemple si l'on construit la liste au sein d'une méthode, car ces variables locales sont détruites à la fin de l'exécution de la méthode. Dans ce cas-ci, l'astuce consiste à créer la chaîne de caractère dans le tas grâce à la procédure NewStr et de passer le pointeur vers cette String à la collection de la liste déroulante. Dans d'autres cas, il faudra utiliser des listes intermédiaires qui stockeront les données, et prévoir un mécanisme de copie de ces listes vers les collections.

III-B-2-c - La liste des commandes

Nous allons à présent nous intéresser à la liste des commandes, reconstruite à chaque changement. Pour présenter ces commandes, nous allons nous baser sur l'objet TListBox qui possède un comportement similaire à celui que nous souhaitons avoir pour notre liste. Comme précédemment, il suffira de redéfinir la méthode HandleEvent afin de rajouter une subtilité : l'utilisateur doit pouvoir choisir la commande à partir de la liste, en la sélectionnant puis en appuyant sur la touche Entrée ou Espace, ou en double-cliquant dessus. Analysons le code de cette méthode :

procedure TCommandListBox.HandleEvent(var Event: TEvent);
begin
if ((Event.What = evKeyDown) and ((Event.CharCode = #13) or (Event.CharCode = #32))) then
begin
Event.What := evCommand;
Event.Command := cmChangeCommandFromListBox;
Event.InfoPtr := Nil;
PutEvent(Event);
end;
if ((Event.What = evMouseDown) and (Event.Buttons = mbLeftButton) and (Event.Double)) then
begin
Event.What := evCommand;
Event.Command := cmChangeCommandFromListBox;
Event.InfoPtr := Nil;
PutEvent(Event);
end;
inherited HandleEvent(Event);
end;

Dans ce cas-ci, nous gérons les évènements « touche pressée », qui peuvent être les touches Entrée (caractère #13) ou Espace (#32), et les évènements « bouton pressé » si le bouton gauche a été double-cliqué. À la détection de ces évènements par le gestionnaire de la liste, un nouvel évènement est envoyé, ayant pour commande cmChangeCommandFromListBox. Nous verrons plus tard comment cet évènement est géré.

III-B-2-d - La liste des résultats

Pour définir ce composant, nous allons également nous baser sur l'objet TListBox, mais nous allons modifier quelque peu son comportement. Le but est en effet d'afficher les informations récupérées lors de la lecture des secteurs de la disquette : ces données sont récupérées dans un objet de type TBuffer, qui est en réalité un tableau de 512 octets contenant des entiers courts non signés (Byte). Il nous faut donc un mécanisme permettant d'afficher les données contenues dans les tampons obtenus, de façon claire et précise.

Intéressons-nous à présent à l'implémentation de l'objet TBufferDump qui se base sur TListBox. Rappelons tout d'abord que l'objet TListBox possède un attribut List de type PCollection, pointant sur la collection d'objets que la liste doit afficher (typiquement des chaînes de caractères, l'objet utilisé est alors PStringCollection au lieu de PCollection). C'est cet objet List que nous devrons modifier pour mettre à jour la liste et y incorporer nos propres valeurs. Ici nous admettrons que l'initialisation du champ List et de l'objet liste chaînée est effectuée dans le constructeur de l'objet TBufferDump. Voyons l'implémentation de la procédure LoadFromBuffer qui convertit les données du tampon obtenu en paramètre en chaînes de caractères :

procedure TBufferDump.LoadFromBuffer(Buffer: TBuffer);
var I, J: Word;
S: String;
begin
List^.FreeAll;
for I := 0 to 31 do
begin
S := Hexa16(I * 16) + 'h   ';
for J := 0 to 15 do S := S + Hexa8(Buffer[I * 16 + J]) + ' ';
S := S + '  ';
for J := 0 to 15 do
begin
if Chr(Buffer[I * 16 + J]) in CorrectChars then
S := S + Chr(Buffer[I * 16 + J])
else
S := S + '.';
end;
List^.Insert(NewStr(S));
end;
SetRange(List^.Count);
DrawView;
end;

Notre première action est de vider de ses contenus la liste chaînée et la collection de Strings. Vient ensuite la boucle permettant de mettre en forme les données du tampon, qui sont traitées selon notre gré (converties en hexadécimal et/ou affichées). Lorsque la conversion est terminée pour toutes les données du buffer, la collection List est remplie avec le contenu de la liste chaînée. SetRange permet de définir le nombre d'éléments visibles à l'écran et l'appel à la procédure héritée DrawView permet de redessiner le composant à l'écran, pour que les modifications soient visibles par l'utilisateur.

Nous pouvons voir ici un exemple du résultat obtenu dans ce composant, après l'exécution de la commande « Read » sur un secteur de la disquette :

III-C - Création des fenêtres

III-C-1 - Fenêtre de logs

Commençons par la fenêtre des logs : cette fenêtre doit permettre d'afficher la liste des opérations qui ont été effectuées par l'utilisateur, avec l'heure, et les erreurs probables. La liste des messages devra donc être stockée en mémoire, nous utiliserons l'objet TStringLinkedList, que nous avons créé, pour faire ceci simplement.

Les méthodes de l'objet TStringLinkedList sont les suivantes :

- procedure Add(Value: String) ;

- function GetValueAt(Index: Integer): Pstring ;

- procedure Clear ;

- function GetCount : Integer.

La procédure Add permet donc d'ajouter la chaîne passée en paramètre à la liste, la fonction GetValueAt permet de récupérer successivement les chaînes de la liste selon leur index (ou leur position). La procédure Clear vide la liste chaînée, et enfin la fonction GetCount renvoie le nombre d'éléments dans la liste. Les éléments de la liste sont numérotés à partir de 1 jusqu'à Count.

Voyons ensuite la déclaration du type TLogDialog :

type PLogDialog = ^TLogDialog;
TLogDialog = object(TDialog)
private
LogListBox: PListBox;
LogScrollBar: PScrollBar;
OKButton, ClearButton: PButton;
Log: PStringLinkedList;
public
constructor Init(var Bounds: TRect; ATitle: TTitleStr; LogToShow: PStringLinkedList);
procedure HandleEvent(var Event: TEvent); virtual;
destructor Done; virtual;
end;

Comme on peut le voir, notre fenêtre héritera de TDialog, qui est l'objet typique pour ce genre de présentations. TDialog possède plusieurs avantages, par rapport à TWindow : les boîtes de dialogues ne sont pas redimensionnables, mais peuvent être déplacées et fermées, et elles sont obligatoirement modales, c'est-à-dire qu'elles doivent être fermées pour que l'application puisse continuer à s'exécuter normalement.

Pour notre objet TLogDialog, nous définissons :

- une liste déroulante qui affichera les messages de logs avec sa barre de défilement ;

- deux boutons, un pour effacer le log, et un autre pour fermer la boîte de dialogue ;

- un pointeur sur une liste chaînée de chaînes de caractères, ce pointeur pointera vers la variable globale Log, qui stockera les messages.

Regardons brièvement le constructeur de cet objet :

constructor TLogDialog.Init(var Bounds: TRect; ATitle: TTitleStr; LogToShow: PStringLinkedList);
var I: Integer;
begin
if not inherited Init(Bounds, ATitle) then Fail;
Bounds.Assign(28, 14, 41, 16);
OKButton := New(PButton, Init(Bounds, 'OK', cmCancel, bfDefault));
Insert(OKButton);
Bounds.Assign(13, 14, 26, 16);
ClearButton := New(PButton, Init(Bounds, 'Clear Log', cmClearLogCommand, bfNormal));
Insert(ClearButton);
Bounds.Assign(53, 2, 54, 12);
LogScrollBar := New(PScrollBar, Init(Bounds));
Insert(LogScrollBar);
Bounds.Assign(2, 2, 53, 12);
LogListBox := New(PListBox, Init(Bounds, 1, LogScrollBar));
Insert(LoglistBox);
LogListBox^.List := New(PStringCollection, Init(10, 1));
for I := 1 to LogToShow^.GetCount do LogListBox^.List^.Insert(LogToShow^.GetValueAt(I));
Log := LogToShow;
LogListBox^.SetRange(LogListBox^.List^.Count);
LogListBox^.DrawView;
end;

Nous voyons tout d'abord la création des éléments de l'interface : liste et boutons. Enfin, une boucle For permet de copier les messages du log vers la collection de la liste déroulante. Afin de gérer le bouton pour effacer le log, nous devons redéfinir le gestionnaire d'évènements pour cette fenêtre, ceci est fait en redéfinissant la procédure HandleEvent :

procedure TLogDialog.HandleEvent(var Event: TEvent);
var I: Integer;
begin
if Event.Command = cmClearLogCommand then
begin
Log^.Clear;
LogListBox^.List^.DeleteAll;
LogListBox^.SetRange(LogListBox^.List^.Count);
LogListBox^.DrawView;
end
else inherited HandleEvent(Event);
end;

Si l'évènement est une commande cmClearLogCommand (commande envoyée lors du clic sur le bouton ClearButton), alors la liste chaînée pointée par l'attribut Log est vidée, et la collection de la liste est aussi vidée, et la liste est redessinée.

Il faut également penser à redéfinir le destructeur dans le cas où l'on crée des objets supplémentaires : boutons, listes, etc. En effet, il faut aussi libérer ces objets lors de la destruction de la boîte de dialogue.

Voici le résultat obtenu pour cette boîte de dialogue :

III-C-2 - Fenêtre « mémoire »

Attaquons-nous maintenant à la fenêtre « mémoire » dont le but est de donner la quantité de mémoire restante sur le tas. Pour cela, nous allons définir un objet TMemoryDialog dérivant de TDialog. Nous placerons au milieu de la fenêtre un simple texte qui affichera la quantité de mémoire disponible. Voici la déclaration du type TMemoryDialog :

type PMemoryDialog = ^TMemoryDialog;
TMemoryDialog = object(TDialog)
private
MemoryStaticText: PStaticText;
OKButton: PButton;
public
constructor Init(var Bounds: TRect; ATitle: TTitleStr);
destructor Done; virtual;
end;

La redéfinition du constructeur permettra de définir les deux objets MemoryStaticText et OKButton. Voyons le constructeur :

constructor TMemoryDialog.Init(var Bounds: TRect; ATitle: TTitleStr);
var SMemAvail: String;
begin
if not inherited Init(Bounds, ATitle) then Fail;
Str(MemAvail:6, SMemAvail);
Bounds.Assign(2, 2, 34, 3);
MemoryStaticText := New(PStaticText, Init(Bounds, 'Available Memory : ' + SMemAvail + ' Bytes'));
Insert(MemoryStaticText);
Bounds.Assign(12, 5, 22, 7);
OKButton := New(PButton, Init(Bounds, 'OK', cmCancel, bfDefault));
Insert(OKButton);
end;

Dans ce cas-ci, nous récupérons la quantité de mémoire disponible lors de l'appel au constructeur : comme la fenêtre est recréée à chaque fois lorsqu'on veut l'afficher, nous obtiendrons toujours la bonne quantité. Nous n'avons pas besoin de redéfinir le gestionnaire d'évènements de cette boîte de dialogue, car nous avons défini le bouton OKButton de telle façon qu'il envoie la commande cmCancel à la boîte de dialogue le contenant : ainsi le gestionnaire d'évènement standard de l'objet TDialog (et donc des objets héritant de TDialog) sait traiter naturellement la commande cmCancel, qui provoque la fermeture de la boîte de dialogue.

III-C-3 - Fenêtre « About »

Le principe est le même pour la fenêtre « About » :

- nous créons un objet TAboutDialog dérivant de TDialog pour bénéficier des avantages des boîtes de dialogues ;

- nous redéfinissons le constructeur afin qu'il effectue les opérations que nous souhaitons.

III-C-4 - Fenêtre de choix

Cette fenêtre de choix est utilisée lors des appels aux primitives de lecture et d'écriture des secteurs de la disquette : ce dialogue permet à l'utilisateur de choisir le secteur auquel accéder par l'intermédiaire du triplet (Tête, Cylindre, Secteur). Nous avons donc pour cette fenêtre trois champs d'entrée qui ne doivent accepter que des bytes, compris entre deux valeurs différentes à chaque fois :

- 0 ou 1 pour la tête ;

- 0 à 79 pour la piste ;

- 1 à 18 pour le secteur.

Pour obtenir cela, nous allons définir deux nouveaux objets : tout d'abord l'objet TByteInputLine dérivant de TInputLine et servant à la saisie des nombres, et ensuite TByteValidator dérivant de TFilterValidator qui permettra de valider ou non que les nombres entrés par l'utilisateur soient corrects, c'est-à-dire formés de chiffres et compris entre les valeurs énoncées plus haut.

Nous pouvons à présent définir l'objet TChooseDialog :

type PChooseDialog = ^TChooseDialog;
TChooseDialog = object(TDialog)
private
HeadStaticText: PStaticText;
HeadInputLine: PByteInputLine;
CylinderStaticText: PStaticText;
CylinderInputLine: PByteInputLine;
TrackStaticText: PStaticText;
TrackInputLine: PByteInputLine;
OKButton : PButton;
CancelButton: PButton;
public
constructor Init(var Bounds: TRect; ATitle: TTitleStr);
destructor Done; virtual;
end;

L'appel à cette boîte de dialogue se fera néanmoins de façon particulière. En effet, nous avons besoin de connaître les valeurs que l'utilisateur a entrées après qu'il ait validé la boîte de dialogue : ces valeurs doivent être donc stockées quelque part. La fonction ExecuteDialog de l'objet TApplication prévoit ce genre de mécanisme : il est possible de passer à la boîte de dialogue un pointeur vers une zone tampon où des données d'échanges sont stockées. Lors de la construction de la boîte de dialogue, cette zone de mémoire est lue séquentiellement et les champs de la boîte sont remplis avec les valeurs lues. Lors de la destruction, le processus inverse se déroule, les données des différents champs sont écrites dans cette zone mémoire, et peuvent ensuite être récupérées. Nous utiliserons le type TDataBuffer que nous avons défini, pour déterminer cette zone d'échange. Le principal problème de ce mécanisme est qu'il faut convertir les données en caractères d'abord, car seuls les flux de caractères sont gérés par cette méthode.

Afin de simplifier ces opérations de conversion, nous avons défini une méthode dans notre objet dérivant de TApplication, qui rassemble toutes les opérations nécessaires :

function TDisk8Reader.ExecuteChooseDialog(ATitle: TTitleStr; var Head, Cylinder, Track: Byte): Word;
var Bounds: TRect;
DataBuffer: PDataBuffer;
SHead, SCylinder, STrack: String;
Code: Integer;
begin
DataBuffer := New(PDataBuffer);
FillChar(DataBuffer^, 9, 0);
Str(Head, SHead);
Str(Cylinder, SCylinder);
Str(Track, STrack);
Move(SHead, DataBuffer^[0], Length(SHead) + 1);
Move(SCylinder, DataBuffer^[3], Length(SCylinder) + 1);
Move(STrack, DataBuffer^[6], Length(STrack) + 1);
Bounds.Assign(27, 19, 52, 31);
ExecuteChooseDialog := ExecuteDialog(New(PChooseDialog, Init(Bounds, ATitle)), DataBuffer);
Move(DataBuffer^[0], SHead, DataBuffer^[0] + 1);
Move(DataBuffer^[3], SCylinder, DataBuffer^[3] + 1);
Move(DataBuffer^[6], STrack, DataBuffer^[6] + 1);
Val(SHead, Head, Code);
Val(SCylinder, Cylinder, Code);
Val(STrack, Track, Code);
Dispose(DataBuffer);
end;

À la fin de l'exécution de cette fonction, les variables Head, Cylinder et Track passées en paramètre par adresse auront les valeurs que l'utilisateur a entrées.

Afin de vérifier la validité des données entrées par l'utilisateur, nous allons définir un objet TByteValidator permettant d'effectuer les vérifications nécessaires et héritant de l'objet TFilterValidator. TFilterValidator possède deux méthodes intéressantes :

- IsValid, qui détermine si la chaîne de caractères passée en paramètre est correcte, c'est-à-dire si elle ne comporte pas de caractère non autorisé ;

- Error, qui est appelée lorsque le validateur détecte un caractère non autorisé dans la chaîne qu'il analyse.

La liste des caractères valides pour ce validateur est renseignée dans le constructeur de TFilterValidator. Pour notre objet, la liste des caractères autorisés est uniquement constituée des chiffres, car notre validateur ne doit fonctionner que pour des nombres entiers de type byte. Nous devrons passer cependant une borne minimale et une borne maximale au constructeur de TByteValidator afin qu'il vérifie bien que le nombre entré par l'utilisateur est dans le bon intervalle borné.

type PByteValidator = ^TByteValidator;
TByteValidator = object(TFilterValidator)
private
Name: String;
Min, Max: Byte;
public
constructor Init(AName: String; AMin, AMax: Byte);
procedure Error; virtual;
function IsValid(const S: String): Boolean; virtual;
end;

constructor TByteValidator.Init(AName: String; AMin, AMax: Byte);
var Chars: TCharSet;
begin
Chars := ['0'..'9'];
if not inherited Init(Chars) then Fail;
Name := AName;
Min := AMin;
Max := AMax;
end;

procedure TByteValidator.Error;
begin
MessageBox('Incorrect ' + Name + ' number...', Nil, mfWarning + mfOKButton);
end;

function TByteValidator.IsValid(const S: String): Boolean;
var Value: Byte;
Code: Integer;
begin
if Length(S) <> 0 then
begin
Val(S, Value, Code);
IsValid := ((inherited IsValid(S)) and (Value >= Min) and (Value <= Max));
end
else IsValid := False;
end;

Ici, notre méthode Error se contente juste d'afficher une boîte de dialogue contenant un message d'erreur. La méthode IsValid se comporte comme prévu : elle vérifie tout d'abord que la chaîne est valide (c'est-à-dire qu'elle ne contient pas de caractère non autorisé), et ensuite que le nombre entré par l'utilisateur est bien compris entre la borne inférieure et la borne supérieure de l'intervalle autorisé.

IV - Programmation du Gestionnaire de Disquette

IV-A - Structure d'une disquette

Rappelons dans un premier temps ce qu'est une disquette et de quelle manière elle est structurée. Une disquette est donc un dispositif de stockage créé par IBM à la fin des années 60. Dans un premier temps au format 8 pouces puis au format 5 pouces ¼, la disquette s'est véritablement imposée lors du développement des premiers ordinateurs personnels, au milieu des années 80. Avec l'apparition du format 3 pouces ½ à la fin des années 80, la disquette s'installa dans tous les foyers possédant un ordinateur.

Analysons la structure des disques amovibles :

Une disquette 3 pouces ½ est « découpée » de la manière suivante : tout d'abord, le disque est partagé en pistes ou cylindres (en jaune sur le schéma). Les cylindres sont au nombre de 80, et sont numérotés de 0 à 79. Un autre découpage est ensuite effectué au niveau des pistes, en utilisant les secteurs. Un secteur est une zone pouvant contenir 512 octets (ici en orange). Chaque piste est ainsi partagée en 18 secteurs, qui sont numérotés de 1 à 18. Petit calcul : il y a 80 cylindres, 18 secteurs et 2 faces, le nombre total de secteurs est donc de 80 * 18 * 2, soit 2880. Chaque secteur contenant 512 octets, la capacité totale d'une disquette est donc de 512 * 2880 octets, soit 1 474 560 octets, soit environ 1,4 Mo.

IV-B - Implémentation des primitives

Dans l'optique d'une modélisation par objets, nous avons décidé de créer un nouvel objet, qui servira pour la gestion de toutes les fonctions de base soient la lecture, l'écriture et le formatage. Pour que notre objet soit relativement complet et puisse offrir un minimum de confort pour le programmeur souhaitant le réutiliser, nous y avons inclus un mécanisme de gestion d'erreur sommaire, que nous détaillerons plus tard.

Comme on peut s'en douter, la lecture et l'écriture de données utilisent des zones de mémoire tampon où sont stockées les données à traiter. Ainsi à la phase de lecture, le tampon sera rempli par les données lues à partir du secteur donné, et à la phase d'écriture, les données présentes dans le tampon seront écrites sur le secteur donné. Pour faciliter cette représentation, nous avons défini le type TBuffer, qui est un simple tableau de 512 octets. C'est également ce type TBuffer qui est utilisé dans la méthode LoadFromBuffer de TBufferDump.

Évidemment, ce gestionnaire ne gère que les disquettes dont les secteurs possèdent 512 octets, les autres structures de secteurs ne seront pas supportées. Cela dit, les disquettes possédant des secteurs de 512 octets sont les plus répandues.

Pour les méthodes de lecture et d'écriture, nous passerons un numéro de secteur, représenté par le triplet (Tête, Cylindre, Secteur). Nous effectuerons tout d'abord un appel permettant de connaître l'état du lecteur de disquette, et ainsi déterminer s’il est prêt à exécuter nos commandes (sous-fonction 00h de l'interruption 13h). Pour le formatage, seuls les numéros de tête et de cylindre seront nécessaires.

Toutes ces méthodes sont codées en assembleur, des connaissances en ce langage sont requises si vous souhaitez bien saisir le fonctionnement global.

IV-B-1 - Lecture

Pour la lecture d'un secteur, nous utiliserons la sous-fonction 02h de l'interruption 13h. Cette interruption prend en paramètre AL, le nombre de secteurs à lire (ici 1), CH le numéro de cylindre, CL le numéro de secteur, DH le numéro de tête et DL le numéro du lecteur disquette (ici 0, qui désigne le premier lecteur de disquette reconnu). ES:BX représente un pointeur vers une zone mémoire où seront stockées les données lues. Voici le code de cette méthode :

procedure TFloppyHandler.ReadFloppy(Head, Cylinder, Sector: Byte; Buffer: TBuffer); assembler;
asm
PUSHA;
MOV AH, 00h;
MOV DL, 00h;
INT 13h;
MOV [FloppyLastError], AH;
CMP AH, 0;
JE @@OK;
JMP @@Fin;
@@OK : MOV AH, 02h;
MOV AL, 01h;
MOV CH, Cylinder;
MOV CL, Sector;
MOV DH, Head;
MOV DL, 00h;
LES BX, Buffer;
INT 13h;
MOV [FloppyLastError], AH;
@@Fin : POPA;
end;

Après chaque opération, l'attribut FloppyLastError est mis à jour avec la dernière valeur renvoyée par le contrôleur de disquette.

IV-B-2 - Écriture

Pour l'écriture, nous utiliserons la sous-fonction 03h, les paramètres étant les mêmes que pour la fonction de lecture :

procedure TFloppyHandler.WriteFloppy(Head, Cylinder, Sector: Byte; Buffer: TBuffer); assembler;
asm
PUSHA;
MOV AH, 00h;
MOV DL, 00h;
INT 13h;
MOV [FloppyLastError], AH;
CMP AH, 0;
JE @@OK;
JMP @@Fin;
@@OK :  MOV AH, 03h;
MOV AL, 01h;
MOV CH, Cylinder;
MOV CL, Sector;
MOV DH, Head;
MOV DL, 00h;
LES BX, Buffer;
INT 13h;
MOV [FloppyLastError], AH;
@@Fin : POPA;
end;

IV-B-3 - Formatage

Dans ce cas-ci, nous utiliserons la sous-fonction 05h, qui permet de formater un cylindre entier. L'opération de formatage permet de reformer correctement un cylindre et les secteurs le constituant, en leur attribuer les bons numéros de tête, de cylindre et de secteur et en leur « redonnant » la bonne taille (dans notre cas 512 octets). De cette façon, le contenu des secteurs est effacé définitivement. Cette opération peut créer des dommages importants, si les valeurs que vous entrez ne sont pas conformes. De par ce fait, l'auteur ne pourra être tenu responsable de tout dommage, si vous n'utilisez pas le bon matériel (disquette double face 1.44 Mo) ou les bonnes valeurs.

Voici le code de cette méthode :

procedure TFloppyHandler.FormatFloppy(Head, Cylinder: Byte);
var Buffer: array[0..71] of Byte;
I: Integer;
PBuffer: Pointer;
begin
for I := 0 to 17 do
begin
Buffer[(I * 4) + 0] := Cylinder;
Buffer[(I * 4) + 1] := Head;
Buffer[(I * 4) + 2] := I + 1;
Buffer[(I * 4) + 3] := 2;
end;
PBuffer := @Buffer;
asm
PUSHA;
MOV AH, 05h;
MOV AL, 18;
MOV CH, Cylinder;
MOV DH, Head;
MOV DL, 00h;
LES BX, PBuffer;
INT 13h;
MOV [FloppyLastError], AH;
POPA;
end;
end;

Ici le paramètre Buffer doit contenir une table regroupant le numéro de cylindre, le numéro de tête, le numéro de secteur et la taille du secteur (0 = 128 octets, 1 = 256 octets, 2 = 512 octets) pour tous les secteurs du cylindre. Puisque nos disquettes 3 pouces ½ 1.44 Mo possèdent 18 secteurs par cylindre, la taille du tableau Buffer est donc de 18 * 4 octets, soit 72 octets.

IV-C - Gestion des erreurs

Pour la gestion des erreurs, nous avons vu qu'après chaque opération, le résultat renvoyé par le contrôleur de disquette était stocké dans l'attribut FloppyLastError : ceci va permettre d'obtenir des messages d'erreur plus sophistiqués. Pour cela, il suffit de récupérer la liste des messages d'erreurs correspondants aux codes renvoyés. Pour exemple, le code 0 signifie qu'il n'y a eu aucune erreur, le code 80h signifie que le lecteur n'était pas prêt. Ainsi nous avons ajouté deux méthodes dans notre objet TFloppyReader :

- GetFloppyLastError, qui retourne le dernier code d'erreur renvoyé ;

- GetFloppyLastErrorMessage, qui retourne un message d'erreur complet (en anglais) en fonction du dernier code d'erreur reçu.

La liste des messages d'erreurs est disponible, en variable globale, dans le programme, sous la forme d'un tableau de chaînes de caractères.

V - Intégration du gestionnaire et de l'interface

V-A - Pré requis et mise en œuvre

Tout d'abord, faisons un point sur la situation :

- nous avons d'un côté une interface graphique qui est prête à fonctionner correctement ;

- nous avons ensuite un objet gestionnaire de disquettes qui possède les commandes de base que notre application propose, c'est-à-dire la lecture, l'écriture et le formatage.

Il nous reste donc une seule chose à faire : assembler les deux parties pour créer un programme complet ! Concrètement cela revient à associer un évènement « Turbo Vision » à une commande du gestionnaire. Voyons les dernières étapes à accomplir pour atteindre notre but final.

Pour intégrer notre nouvel objet TFloppyHandler à notre application, nous devons tout d'abord le déclarer : nous allons ainsi ajouter un attribut FloppyHandler dans notre objet TDisk8Reader. De la même façon, nous penserons à modifier le constructeur et le destructeur de TDisk8Reader pour prendre en compte ce nouvel attribut.

Nous pouvons maintenant nous attaquer à la gestion des évènements. Comme dit précédemment, il nous suffira d'associer à chaque évènement une action du gestionnaire : il faudra donc modifier la méthode de gestion des évènements HandleEvent de notre objet TDisk8Reader. Comme nous l'avons vu précédemment pour le champ d'entrée principal, il existe la commande cmExecuteCommand que nous avons définie et qui va nous servir à ordonner à l'application d'exécuter la commande entrée par l'utilisateur : lorsque cette commande est rencontrée par le gestionnaire d'évènements de TDisk8Reader, l'application va lancer une action du gestionnaire de disquette en fonction de la valeur du champ d'entrée.

Voici un exemple pour la commande de lecture d'un secteur :

procedure TDisk8Reader.HandleEvent(var Event: TEvent);
var Command: String;
I: Integer;
Bounds: TRect;
Head, Cylinder, Sector: Byte;
FloppyBuffer: TBuffer;
STime, SHead, SCylinder, SSector, FileName, LogLine: String;
FormatAllWindow: PFormatAllWindow;
begin
inherited HandleEvent(Event);
case Event.What of
evCommand : begin
if Event.Command = cmExecuteCommand then
begin
CommandInput^.GetData(Command);
Command := UpcaseStr(Command);
Head := 0;
Cylinder := 0;
Sector := 1;
if ((Command = 'READ') and (ExecuteChooseDialog('Read', Head, Cylinder, Sector) = cmOK)) then
begin
Str(Head, SHead);
Str(Cylinder, SCylinder);
Str(Sector, SSector);
FloppyHandler^.ReadFloppy(Head, Cylinder, Sector, FloppyBuffer);

CommandInput est le champ où l'utilisateur inscrit la commande qu'il veut lancer : la première chose que fait ce gestionnaire lorsqu'il reçoit un évènement cmExecuteCommand est de récupérer la valeur du champ, il lance ensuite la bonne action. Le choix se fait avec une imbrication classique de if, et ainsi, si aucune commande équivalente à ce que l'utilisateur a entré n'est trouvée, rien ne sera exécuté.

V-B - Pour améliorer le tout…

Afin d'améliorer encore un peu le confort d'utilisation, nous voulons ajouter une barre de défilement pour l'opération de formatage complet (correspondant à la commande « FormatAll »). Nous allons pour cela définir deux objets : tout d'abord, la barre de progression en elle-même, et ensuite un objet héritant de TWindow qui servira à l'affichage de la barre de progression.

V-B-1 - La barre de progression

Comme il n'existe pas d'objet de type barre de progression dans la bibliothèque Turbo Vision, nous allons en créer un de toutes pièces. Il faut tout d'abord savoir que tous les composants doivent hériter de l'objet TView, qui est l'objet ancêtre de tous les composants de l'interface. Pour définir notre composant, nous devons donc redéfinir les méthodes de TView à l'intérieur de notre nouvel objet. La méthode héritée de TView la plus intéressante est certainement Draw : c'est cette méthode qui permet d'afficher le composant à l'écran. Nous allons également redéfinir la palette de ce composant afin qu'il possède les couleurs que nous désirons. Voici le code qui lui est associé :

type PProgressBar = ^TProgressBar;
TProgressBar = object(TView)
private
Min, Max, Position: Integer;
public
constructor Init(var Bounds: TRect; AMin: Integer; AMax: Integer);
procedure Draw; virtual;
procedure Progress(Value: Integer);
function GetPalette: PPalette; virtual;
end;

constructor TProgressBar.Init(var Bounds: TRect; AMin: Integer; AMax: Integer);
begin
if not inherited Init(Bounds) then Fail;
Min := AMin;
Max := AMax;
Position := Min;
end;

procedure TProgressBar.Progress(Value: Integer);
begin
Position := Position + Value;
if Position > Max then Position := Max;
end;

procedure TProgressBar.Draw;
var I, J: Integer;
SPosition: String;
begin
inherited Draw;
for I := 0 to Size.X - 1 do
for J := 0 to Size.Y - 1 do
begin
if I <= (Position / Max * Size.X) then
WriteStr(I, J, #177, 1)
else
WriteStr(I, J, ' ', 1);
end;
str(Position / Max * 100:3:0, SPosition);
SPosition := SPosition + '%';
WriteStr(Size.X div 2 - 2, Size.Y div 2 , SPosition, 1);
end;

function TProgressBar.GetPalette: PPalette;
const P: String[Length(CAppPalette)] = CProgressBar;
begin
GetPalette := @P;
end;

Le fonctionnement de ce composant est simple : en fonction de l'avancement de la tâche, représenté par la valeur de Position dans l'intervalle formé par Min et Max, il dessine le caractère ASCII 177 ou un espace, et ceci jusqu'à la fin de la barre. La barre se remplit donc de gauche à droite et affiche le pourcentage équivalent au centre. En ce qui concerne l'affichage à proprement parler, il faut impérativement utiliser les fonctions WriteStr, WriteChar, WriteBuf ou WriteLine. Concernant les modifications pour la palette, il suffit de redéfinir la méthode GetPalette de TView. Le fonctionnement de la palette pour les composants est cependant totalement différent de celui des palettes utilisées pour l'application. En effet, un composant doit utiliser la palette de la fenêtre ou de la boîte de dialogue qui le contient : ainsi, la palette d'un composant ne correspond plus à une liste de couleurs (comme pour la palette application), mais à un index dans la palette de son conteneur. Dans notre cas, la constante CProgressBar vaut #6, ce qui signifie que notre barre de progression devra utiliser la sixième couleur de la palette de son conteneur. Nous avons choisi d'utiliser une fenêtre comme conteneur, la couleur sera donc $1E, ce qui correspond à la combinaison d'un fond bleu foncé (les 4 bits de poids fort) et d'une police jaune (les 4 bits de poids faible). Pour connaître les palettes exactes des autres composants de Turbo Vision, vous pouvez vous reporter au mot-clé CColor de l'aide de Turbo Pascal.

V-B-2 - La fenêtre de progression

Tout d'abord, pourquoi le choix de la fenêtre ? Cette question est légitime, car l'utilisation d'une boîte de dialogue était également possible. Pour ce cas-ci, nous avons décidé de laisser le traitement du formatage dans le programme principal, et de n'utiliser la fenêtre que pour afficher la progression : ceci n'est possible que parce que les fenêtres ne sont pas modales, contrairement aux boîtes de dialogues (vous verrez par la suite comment sont effectués l'affichage et la mise à jour de la fenêtre). Voyons tout de suite le code associé à notre fenêtre :

type PFormatAllWindow = ^TFormatAllWindow;
TFormatAllWindow = object(TWindow)
private
ProgressBar: PProgressBar;
public
constructor Init(var Bounds: TRect; ATitle: TTitleStr; ANumber: Integer; AMin: Integer; AMax: Integer);
procedure Progress(Value: Integer);
destructor Done; virtual;
end;

constructor TFormatAllWindow.Init(var Bounds: TRect; ATitle: TTitleStr; ANumber: Integer; AMin: Integer; AMax: Integer);
const Commands: TCommandSet = [cmClose];
begin
if not inherited Init(Bounds, ATitle, ANumber) then Fail;
DisableCommands(Commands);
Flags := $0000;
Bounds.Assign(2, 2, Bounds.B.X - Bounds.A.X - 2, 5);
ProgressBar := New(PProgressBar, Init(Bounds, AMin, AMax));
Insert(ProgressBar);
end;

procedure TFormatAllWindow.Progress(Value: Integer);
begin
ProgressBar^.Progress(Value);
end;

destructor TFormatAllWindow.Done;
begin
dispose(ProgressBar, Done);
inherited Done;
end;

Le seul point intéressant ici est dans le constructeur, où l'attribut Flags est modifié : cet attribut permet de modifier l'apparence de la fenêtre. En y attribuant la valeur 0, la fenêtre ne peut être ni déplacée ni fermée, ni redimensionnée ni « dépliée ». Enfin, la commande DisableCommand permet de désactiver un ensemble de commandes qui ne seront plus traitées par la fenêtre : ici, nous désactivons la commande de fermeture, l'utilisateur n'aura donc aucun moyen de fermer cette fenêtre.

V-B-3 - Liaison avec le gestionnaire de disquette

Il est temps à présent de mettre en place notre fenêtre pour la fonction de formatage complet. Comme vu précédemment, nous allons modifier la procédure HandleEvent de notre objet Application. Pour information, le formatage complet s'effectue à l'aide de deux boucles imbriquées, chacune faisant varier un élément du couple (Tête, Cylindre) : ceci garantit le formatage total de la disquette. Voyons à présent les instructions permettant d'afficher et de mettre à jour automatiquement la fenêtre :

if ((Command = 'FORMATALL') and (MessageBox('Format All ?', Nil, mfWarning + mfYesButton + mfCancelButton) = cmYes)) then
begin
Bounds.Assign(13, 21, 68, 29);
FormatAllWindow := New(PFormatAllWindow, Init(Bounds, 'Format All', 0, 0 , 80 * 2));
InsertWindow(FormatAllWindow);
Cylinder := 0;
while ((Cylinder <= 79)) and (FloppyHandler^.GetFloppyLastError = 0) do
begin
Head := 0;
while ((Head <= 1)) and (FloppyHandler^.GetFloppyLastError = 0) do
begin
FloppyHandler^.FormatFloppy(Head, Cylinder);
Head := Head + 1;
FormatAllWindow^.Progress(1);
FormatAllWindow^.Redraw;
end;
Cylinder := Cylinder + 1;
end;
FormatAllWindow^.Close;
if FloppyHandler^.GetFloppyLastError = 0 then
begin
ClockStaticText^.GetText(STime);
Log^.Add(STime + '  FormatAll Success');
MessageBox('FormatAll Success !',  Nil, mfInformation + mfOKButton);
end
else
begin
ClockStaticText^.GetText(STime);
Log^.Add(STime + '  FormatAll Failure');
MessageBox('FormatAll Failure...' + #13#10 + 'Error : ' + FloppyHandler^.GetFloppyLastErrorMessage, Nil, mfInformation + mfOKButton);
end;
end;

L'affichage se fait grâce à l'appel de la méthode héritée de TApplication, InsertWindow. La mise à jour de l'affichage est effectuée à l'intérieur de la boucle : l'utilisation de la méthode Progress permet de mettre à jour la barre de progression, et la méthode Redraw permet de réafficher immédiatement la fenêtre.

Il existe cependant un désavantage à utiliser de telles boucles : lors de l'exécution, il peut alors arriver que la charge de travail soit trop importante, et ainsi le gestionnaire d'évènements ne peut plus traiter tous les évènements qu'il reçoit. Il en résulte une certaine congestion au niveau de l'exécution des évènements : ceci peut être une source de gêne pour certains utilisateurs. Il n'était malheureusement pas possible dans ce cas de mettre en place un mécanisme différent de façon simple. Lorsque vous programmez dans des environnements possédant la gestion des évènements, veillez à ne jamais négliger l'aspect de congestion, pour le confort des utilisateurs.

VI - Conclusion

Après avoir mis en place ces améliorations, vous obtenez une interface graphique capable de lancer les différentes commandes du gestionnaire de disquette. L'application est donc terminée, du moins dans la forme telle que nous la souhaitions : on peut penser que les utilisateurs de notre programme disposent d'un certain confort, même s’il est évident que différents ajouts peuvent encore être envisagés.

Lors de ce tutoriel, nous avons pu découvrir de nouveaux aspects de la programmation d'interfaces sous Turbo Vision : vous avez à présent la possibilité de redéfinir tout ou une partie d'un composant afin qu'il se comporte tel que vous le souhaitez, et vous savez également créer un composant de toute pièce. Plus rien ne vous empêche à présent de créer vos propres applications grâce à cette bibliothèque.

Ressources disponibles :

Tutoriel au format HTML : Lien FTPLien HTTP

Tutoriel au format PDF : Lien FTPLien HTTP

Sources du programme Disk8Reader : Lien FTPLien HTTP