III. Récréation : EasyTurtle (logiciel de dessin)▲
Avant d'aborder les outils de programmation et de rentrer dans le cœur de l'interpréteur, une récréation s'impose avec la création d'un petit logiciel de dessin baptisé EasyTurtle.
III-A. Le projet EasyTurtle▲
Il s'agit de mettre en œuvre l'unité GVTurtles en permettant à un enfant de dessiner avec la tortue, de rejouer l'ensemble des ordres qu'il lui aura donnés, ou encore de charger et de sauvegarder ses réalisations.
Quant au programmeur, il pourra utiliser quelques outils particulièrement utiles, en particulier les listes d'actions.
III-B. B - Mode d'emploi rapide▲
III-B-1. L'écran d'accueil▲
L'écran d'accueil se présente comme ceci :
Sur l'essentiel de la fenêtre se situe la zone d'affichage de la tortue : c'est ici que seront réalisés les dessins. Afin de faciliter sa manipulation, la zone de dessin est en mode « clos », ce qui signifie que la tortue ne peut pas être perdue de vue, car tout ordre tendant à la faire disparaître conduira à la faire buter contre le bord de son champ comme s'il y avait une barrière.
À droite de cette zone d'affichage, on aperçoit toute une série de boutons : ce sont les actions mises à disposition de l'utilisateur.
III-B-2. La tortue▲
On distingue ainsi les actions concernant la tortue :
La tortue peut donc avancer, reculer, tourner à gauche et à droite. Chacune de ces opérations se fait selon une valeur par défaut modifiable grâce à une fenêtre de réglage des préférences.
Si elle est de forme triangulaire, la tortue peut aussi grossir et rapetisser. Dans le cas contraire, ces boutons sont grisés, et par conséquent inactifs.
L'utilisateur peut effacer l'écran, renvoyer la tortue à son origine, l'autoriser ou lui interdire d'écrire, la rendre visible ou invisible.
Un bouton indique toujours l'action à réaliser, et non l'état de la tortue. Ainsi, si le bouton comporte le message « N'écris plus », c'est que la tortue laisse actuellement une trace et qu'une pression sur le bouton lui demandera de ne plus écrire.
III-B-3. Couleurs et formes▲
Le panneau suivant s'occupe des couleurs et des formes :
Le premier bouton permute l'apparence de la tortue, entre le triangle et le dessin au format png. Le second ouvre une fenêtre de choix de la couleur du crayon, tout comme le suivant pour la couleur de fond. Les deux derniers dessinent respectivement un carré et un cercle à l'emplacement de la tortue.
III-B-4. Ordres généraux▲
Le panneau suivant regroupe les ordres généraux concernant le travail effectué par la tortue :
EasyTurtle propose un enregistrement des ordres donnés à la tortue. C'est à partir de cet enregistrement qu'opéreront les boutons, « Charge », « Sauve », « RAZ » et « Défais ».
L'utilisateur peut sauvegarder et charger son travail. Il peut ouvrir des fenêtres spécialisées : « Outils », « À Propos » et « Aide ». Il peut aussi rejouer une séquence enregistrée et interrompre cette répétition : ce sont les deux boutons sans légende qui apparaissent en bas à gauche de la copie d'écran. Il peut encore annuler le dernier ordre donné à la tortue (bouton « Défais ») ou remettre à zéro toute la séquence (bouton « RAZ »). Enfin, il peut quitter le logiciel.
Le fait de charger une suite d'ordres l'exécute immédiatement après le chargement.
Suivant l'état du logiciel, certains boutons seront désactivés : par exemple, les boutons « Défais », « Sauve » et « Rejoue » ne seront activés que si des ordres ont été enregistrés. Le bouton « Stop » ne sera activé que si le bouton « Rejoue » a été pressé et seulement le temps de la répétition. Lorsque le bouton « Rejoue » a été pressé, tous les boutons, sauf « Quitter » et « Stop », sont désactivés.
Pour réinitialiser la séquence d'ordres, plusieurs possibilités sont offertes : chargement d'un nouveau fichier d'ordres, pression sur le bouton « RAZ » et modification du fond de l'écran. Contrairement au bouton « Efface l'écran », le bouton « RAZ » conserve la couleur d'écriture et celle du fond de l'écran.
III-B-5. L'aide▲
III-B-6. Boîte « À propos »▲
III-B-7. Boîte des préférences▲
Enfin, une boîte d'outils permet de modifier les valeurs par défaut de certains ordres concernant le dessin effectué par la tortue :
Les modifications apportées grâce à la boîte des préférences sont enregistrées avec le fichier des ordres. Les valeurs sont données en pixels et en degrés.
III-B-8. Autres éléments▲
Tous les boutons décrits ci-dessus ont leur double dans la barre d'outils située en haut de la fenêtre principale :
Les pictogrammes sont évidemment les mêmes, afin de renforcer la cohérence du logiciel. De plus, la plupart des ordres peuvent être donnés par une combinaison de touches indiquée en aide ponctuelle près du bouton avant de le presser et dans la barre de statut.
Pour terminer cette présentation rapide, il faut noter des indicateurs fournis par la barre de statut et par deux disques situés en bas à droite de la fenêtre principale :
On prend ainsi connaissance d'une aide succincte concernant le bouton survolé par la souris, des principales valeurs associées à la tortue et des valeurs attribuées à la couleur du crayon de la tortue (premier disque) et au fond de l'écran (second disque), ainsi que l'état du logiciel : « enregistrement en cours », « sauvegarde en cours », « chargement en cours » et « répétition des ordres ».
III-C. La programmation▲
La suite de cette partie décrit le fonctionnement d'EasyTurtle. Contrairement aux autres logiciels d'exemples, celui-ci fonctionne à partir de plusieurs fiches :
L'unité principale s'appelle Main.pas. GVAbout.pas contient la boîte « À propos », Help.pas l'aide et GVTools.pas la boîte des préférences.
L'appel d'une fiche externe se fait selon un modèle courant depuis la fiche « Main ». Voici, par exemple, l'appel de la boîte « À propos » :
procedure
TMainForm.ActionAboutExecute(Sender: TObject);
// *** boîte à propos ***
begin
GVAbout.AboutForm := TAboutForm.Create(Self
); // on crée la fiche
try
GVAbout.AboutForm.ShowModal; // on l'affiche
finally
GVAbout.AboutForm.Free; // on la libère
end
;
end
;
III-C-1. La fiche principale▲
La fiche « Main » contient toutes les méthodes nécessaires au fonctionnement d'EasyTurtle. Les éléments les plus complexes sont ceux relatifs à la mémorisation des ordres donnés à la tortue : le logiciel utilise à cette fin un tableau ouvert géré par une méthode nommée Memorize :
procedure
TMainForm.Memorize(const
Value: Integer
);
// *** mémorisation d'une action ***
begin
SetLength(MemoInt, Length(MemoInt) + 1
); // on augmente la taille du tableau
MemoInt[Length(MemoInt) - 1
] := Value; // on enregistre
pSaved := False
; // séquence non enregistrée
end
;
Cette méthode ajuste la taille du tableau avant d'enregistrer la nouvelle donnée. Elle indique aussi que la séquence a été modifiée en vue d'un futur enregistrement.
La plupart des ordres sont gérés de manière identique. Voici par exemple l'ordre ActionForwardExecute qui fait avancer la tortue :
procedure
TMainForm.ActionForwardExecute(Sender: TObject);
// *** la tortue avance ***
begin
GVTurtle.Move(pForward); // la tortue bouge
Memorize(CT_Forward); // mémorisation
end
;
On exécute l'ordre et on l'enregistre, rien de plus facile ! Simplement, afin de permettre aux boutons, à la barre d'outils et aux combinaisons de touches de fonctionner de la même manière sans dupliquer le code, on utilise un composant TActionList qui centralise les actions.
En plus de l'exécution, on a prévu une mise à jour (disponibilité, visibilité, affichage) de chaque fonction suivant l'état du logiciel :
procedure
TMainForm.ActionForwardUpdate(Sender: TObject);
// actions actives/inactives
begin
// seulement si enregistrement
(Sender as
TAction).Enabled := (pState = stRecording);
end
;
Ici, l'objet appelant (qui doit être une action) n'est activé que si le mode est celui de l'enregistrement. En effet, il ne faut pas continuer à dessiner au cours de la sauvegarde, du chargement ou si l'on est en train de rejouer toute la séquence.
On aurait pu indiquer directement ActionForward dans la méthode, mais le transtypage (Sender as TAction) permet de partager le même gestionnaire avec d'autres actions au comportement identique (ActionBackward, par exemple).
Rejouer la séquence exige de dispatcher les ordres en fonction de leur enregistrement :
procedure
TMainForm.Replay;
// *** rejoue les actions ***
begin
GVTurtle.ReInit; // réinitialisation de la tortue
GVTurtle.Speed := TbSpeed.Position; // vitesse selon barre
GVTurtle.Screen := teGate; // écran clos
fCmd := C_MinCmds; // on pointe sur le premier élément du tableau hors en-tête
pForward := MemoInt[1
]; // on récupère les données de l'en-tête
pBackward := MemoInt[2
];
pLeft := MemoInt[3
];
pRight := MemoInt[4
];
pLength := MemoInt[5
];
GVTurtle.ScreenColor := MemoInt[6
];
// on boucle tant qu'il y a des ordres et qu'un arrêt n'a pas été demandé
while
(fCmd < Length(MemoInt)) and
(pState = stPlaying) do
// on balaie le tableau
begin
//images adaptées pour les roues
tbReplay.ImageIndex := (fCmd mod
31
) + 15
;
ImageListBigWait.GetBitmap(fCmd mod
31
,ImgRound.Picture.Bitmap);
// on permet aux messages d'être traités
Application.ProcessMessages;
// on répartit le travail suivant les ordres enregistrés
case
MemoInt[fCmd] of
CT_Forward : GVturtle.Move(pForward); // avance
CT_Backward : GVturtle.Move(pBackward); // recule
CT_Left : GVturtle.Turn(pLeft); // Ã gauche
CT_Right : GVturtle.Turn(pRight); // Ã droite
CT_Bigger : GVturtle.Size := GVTurtle.Size + 2
; // taille + 2
CT_Minus : GVturtle.Size := GVTurtle.Size - 2
; // taille - 2
CT_Home : begin
GVTurtle.Home; // maison
Inc(fCmd, 3
); // ordre suivant
end
;
CT_UpDown : GVTurtle.PenDown := not
GVTurtle.PenDown; // crayon baissé
// visibilité
CT_SeeSaw : GVTurtle.TurtleVisible := not
GVTurtle.TurtleVisible;
CT_Kind : if
GVTurtle.Kind <> tkTriangle then
// type
GVTurtle.Kind := tkTriangle
else
GVTurtle.Kind := tkPng;
CT_Pen : begin
// couleur crayon
Inc(fCmd,2
); // ordre suivant
GVTurtle.PenColor := MemoInt[fCmd];
end
;
CT_Square: GVTurtle.Square(pLength); // carré
CT_Circle: GVTurtle.Circle(pLength); // cercle
end
;
Inc(fCmd); // ordre suivant
end
;
Refresh; // remet à jour les voyants
pState := stRecording; // on repasse en mode enregistrement
end
;
Le champ privé fCmd est le pointeur utilisé sur l'ordre en cours. Avant de rejouer les ordres, on s'occupe de l'en-tête qui contient les valeurs modifiables depuis la fenêtre des paramètres. Ces valeurs sont enregistrées avec l'éventuel fichier de sauvegarde.
On remarquera la présence de Application.ProcessMessages qui permet à l'application de réagir aux événements tels que l'appui sur le bouton « Stop ». On profite aussi de cette boucle pour animer certains boutons : ainsi, deux roues seront animées. Certaines données ne sont utiles qu'en cas d'action de correction, en remontant dans le temps : pour rejouer la séquence, on les ignore simplement (voir le traitement de CT_Home, par exemple).
La partie la plus complexe de cette unité est celle relative à la fonction « Défaire » :
procedure
TMainForm.ActionUndoExecute(Sender: TObject);
// *** défaire la dernière action ***
begin
if
(Length(MemoInt) > (C_MinCmds + 4
)) and
// ordre le plus long = HOME
(MemoInt[Length(MemoInt) - 4
] = CT_Home) then
begin
GVTurtle.PenRubber := True
; // on efface
GVTurtle.SetPos(Round(MemoInt[Length(MemoInt) - 2
])
, Round(MemoInt[Length(MemoInt) - 1
])); // on se repositionne
GVTurtle.Heading := MemoInt[Length(MemoInt) - 3
]; // direction de la tortue
GVTurtle.PenRubber := False
; // on écrit normalement
SetLength(MemoInt, Length(MemoInt) - 4
); // on réajuste la mémorisation
end
else
if
(Length(MemoInt) > (C_MinCmds + 3
)) and
(MemoInt[Length(MemoInt) - 3
]
= CT_Pen) then
// couleur de crayon
begin
GVTurtle.PenColor := MemoInt[Length(MemoInt) - 2
]; // couleur récupérée
SetLength(MemoInt, Length(MemoInt) - 3
); // on ajuste
end
else
begin
case
MemoInt[Length(MemoInt) - 1
] of
CT_Forward: begin
// on a avancé
GVTurtle.PenRubber := True
; // on efface
GVTurtle.Move(-pForward); // donc on recule
GVTurtle.PenRubber := False
; // on écrit normalement
end
;
CT_Backward: begin
// on a reculé
GVTurtle.PenRubber := True
; // on efface
GVTurtle.Move(-pBackward); // donc on avance
GVTurtle.PenRubber := False
; // on écrit normalement
end
;
CT_Left: GVTurtle.Turn(-pLeft); // gauche devient droite
CT_Right: GVTurtle.Turn(-pRight); // droite devient gauche
CT_SeeSaw: GVTurtle.TurtleVisible := not
GVTurtle.TurtleVisible; // visibilité
CT_UpDown: GVTurtle.PenDown:= not
GVTurtle.PenDown; // écriture ou non
CT_Kind: if
GVTurtle.Kind = tkTriangle then
// type de tortue
GVTurtle.Kind := tkPNG
else
GVTurtle.Kind := tkTriangle;
CT_Bigger: GVTurtle.Size := GVTurtle.Size - 2
; // on a grossi
CT_Minus: GVTurtle.Size := GVTurtle.Size + 2
; // on a rapetissé
CT_Circle: begin
// un cercle
GVTurtle.PenRubber:= True
;
GVTurtle.Circle(pLength+1
);
GVTurtle.PenRubber:= False
;
end
;
CT_Square: begin
// un carré
GVTurtle.PenRubber:= True
;
GVTurtle.Square(pLength+1
);
GVTurtle.PenRubber:= False
;
end
;
end
;
SetLength(MemoInt, Length(MemoInt) - 1
); // on ajuste la mémorisation
end
;
pSaved := not
(Length(MemoInt) > C_MinCmds); // drapeau d'enregistrement
end
;
En effet, pour être annulées, certaines fonctions exigent que l'état de la tortue soit enregistré au préalable : ainsi, pour annuler un retour à l'origine, il faut se souvenir de l'ancien emplacement de la tortue, mais aussi de son orientation. En amont, il faut donc que chaque ordre mémorise ce qu'attendra une éventuelle correction.
La solution adoptée ici est de repasser sur le dernier trait en l'effaçant. Cette méthode est très rapide, mais elle peut effacer un trait qui devrait être conservé parce qu'il est recouvrant.
La méthode ActionSaveExecute d'enregistrement de la séquence doit tenir compte des remarques précédentes :
procedure
TMainForm.ActionSaveExecute(Sender: TObject);
// *** sauvegarde des ordres de la tortue ***
var
F: TextFile;
OK: Boolean
;
I: Integer
;
begin
OK := False
; // abandon par défaut
repeat
Ok := SaveDialog.Execute; // dialogue de sauvegarde
if
Ok then
begin
// confirmation si le fichier existe
if
FileExists(SaveDialog.FileName) then
// boîte de dialogue si existe
// demande de remplacement si le fichier existe
case
MessageDlg(Format(ME_Replace,[ExtractFileName(SaveDialog.FileName)]),
mtConfirmation, mbYesNoCancel,0
) of
mrYes: Ok := True
; // on écrase l'ancien fichier
mrNo: Ok := False
; // on recommence
mrCancel: Exit; // abandon si le fichier existe
end
;
end
else
Exit; // abandon dès la boîte de dialogue
until
Ok;
try
AssignFile(F,SaveDialog.FileName); // fichier assigné
pState := stSaving; // mode sauvegarde indiqué
try
Rewrite(F); // on remet à zéro le fichier
for
I := 0
to
Length(MemoInt) - 1
do
// on balaie les ordres enregistrés
begin
// images qui tournent pendant le chargement
tbReplay.ImageIndex := (I mod
31
) + 15
;
ImageListBigWait.GetBitmap(I mod
31
,ImgRound.Picture.Bitmap);
Application.ProcessMessages; // on traite les messages
writeln(F, MemoInt[I]); // on écrit dans le fichier
end
;
pSaved := True
; // sauvegarde effectuée
except
// erreur de sauvegarde
MessageDlg(Format(ME_SaveError,[ExtractFileName(SaveDialog.FileName)]),
mtError, [mbOk],0
);
end
;
finally
CloseFile(F); // fermeture du fichier
pState := stRecording;
Refresh; // remet à jour les voyants
end
;
end
;
Cette méthode vérifie l'existence du fichier avant d'éventuellement l'écraser puis enregistre les données dans l'ordre du tableau des commandes. Elle anime aussi deux roues en permettant à l'application de gérer les événements grâce à la méthode Application.ProcessMessages.
Le chargement d'un fichier est un brin plus complexe :
procedure
TMainForm.ActionLoadExecute(Sender: TObject);
// ouverture d'un fichier de commandes
var
F: TextFile;
Num, I: Integer
;
begin
if
OpenDialog.Execute then
// boîte de dialogue d'ouverture de fichier
begin
try
AssignFile(F,OpenDialog.FileName); // fichier assigné
pState := stLoading; // statut signifié
try
Reset(F); // fichier réinitialisé
readln(F, Num);
if
Num <> CT_Version then
begin
MessageDlg(Format(ME_VersionError,[ExtractFileName(SaveDialog.FileName)]),
mtError, [mbOk], 0
); // signale une erreur de version
Exit; // sortie
end
;
readln(F, Num); // récupère les données de l'en-tête
pForward := Num;
readln(F, Num);
pBackward := Num;
readln(F, Num);
pLeft := Num;
readln(F, Num);
pRight := Num;
readln(F, Num);
pLength := Num;
readln(F, Num);
GVTurtle.ScreenColor := Num;
ActionRAZExecute(Sender); // initialisation
I := -1
; // pointeur de travail pour l'affichage
repeat
Inc(I); // élément suivant
// images des roues mises à jour
tbReplay.ImageIndex := (I mod
31
) + 15
;
ImageListBigWait.GetBitmap(I mod
31
,ImgRound.Picture.Bitmap);
Application.ProcessMessages; // on permet les messages
readln(F, Num); // on lit une donnée
Memorize(Num); // qu'on enregistre
until
EOF(F) ; // jusqu'Ã la fin du fichier
except
MessageDlg(Format(ME_LoadError,[ExtractFileName(SaveDialog.FileName)]),
mtError, [mbOk], 0
); // signale une erreur de lecture
ActionEraseExecute(Sender); // on remet à zéro
end
;
finally
CloseFile(F); // ferme le fichier
pState := stRecording; // on enregistre de nouveau
Refresh; // remet à jour les voyants
end
;
ActionReplayExecute(Sender); // rejoue immédiatement la séquence
pSaved := True
; // enregistrement déjà fait
end
;
end
;
Le premier élément du fichier doit contenir le numéro de version actuelle, soit 100. Il est suivi de l'en-tête puis des données proprement dites. Au fur et à mesure de la lecture, on fait tourner les roues habituelles et on enregistre les ordres grâce à la méthode Memorize. À la fin de la méthode, on lance l'exécution de la séquence afin d'avoir un écran à jour : sans ce dernier point, la fonction « Défaire » serait erronée puisqu'elle corrigerait un écran non dessiné !
On peut enfin mentionner la méthode CloseQuery :
procedure
TMainForm.FormCloseQuery(Sender: TObject; var
CanClose: Boolean
);
// *** demande de fermeture ***
begin
// fermeture à confirmer
if
not
pSaved then
// si séquence non enregistrée
begin
// demande d'enregistrement
case
MessageDlg(ME_NotSaved, mtConfirmation, mbYesNoCancel,0
) of
mrYes: begin
// oui
ActionSaveExecute(Sender); // sauvegarde
CanClose := pSaved; // on sort si c'est fait
end
;
mrNo: CanClose := True
; // on sort
mrCancel: CanClose := False
; // on ne sort pas
end
;
end
else
// cas où il n'y a rien à enregistrer
CanClose := (MessageDlg(ME_Close, mtConfirmation, mbYesNo,0
) = mrYes);
// on arrête le dessin si nécessaire
if
CanClose then
pState := stRecording;
end
;
Lors d'une demande de fermeture du logiciel, on vérifie préalablement si la séquence en cours, lorsqu'elle a été modifiée, doit être enregistrée. Les messages diffèrent suivant les cas.
Le test de CanClose qui modifie si nécessaire pState s'explique par le fait que l'utilisateur peut avoir demandé la fermeture du logiciel alors qu'on rejoue une séquence. On interrompt alors cette répétition et l'on ferme le logiciel.
III-C-2. Les autres fiches▲
La boîte des préférences comprise dans l'unité GVTools est d'une simplicité enfantine : elle modifie ou non les données qu'elle doit gérer suivant le bouton pressé.
L'unité Help permet d'introduire quelques animations très simples : une image suit le déplacement de la souris en modifiant son champ Left, les composants TLabel ont leur texte en gras ou non suivant l'entrée/la sortie de la souris de leur surface…
L'unité GVAbout comprend aussi une animation aléatoire de la tortue.
III-C-3. Un logiciel tout en français…▲
Un programmeur non averti peut être surpris par le comportement de Lazarus dès qu'il s'agit de quitter les sentiers de l'anglais.
En effet, un programme aussi simple que celui-ci pose problème :
- créer une nouvelle application ;
- ajouter un TBitBtn à la fiche ;
- modifier sa propriété Kind pour qu'elle soit sur bkClose ;
-
lancer le programme…
Alors que le bouton affichait correctement « Fermer » dans l'EDI, il affiche sa version anglaise « Close » dans notre programme ! Qui plus est, ce comportement qui se répète pour tous les éléments qui ont besoin d'une traduction est indétectable lors de la réalisation de l'interface.
Une solution partielle consiste à forcer la réécriture des éléments à traduire en les modifiant dans l'inspecteur d'objets. Malheureusement, cette solution ne règle pas les messages d'erreur lors des exceptions qui apparaîtront en anglais.
En fait, afin d'obtenir une traduction française de tous les messages venant de la bibliothèque principale LCL, il est absolument nécessaire de modifier le fichier de projet. Pour cela :
-
choisir dans le menu « projet », l'élément « inspecteur de projet » afin de faire apparaître la fenêtre du projet ; cliquer sur le fichier <nom_du_projet>.lpr qui contient le code source du projet ;
-
repérer le chemin qui mène vers le fichier lclstrconsts.fr.po (normalement, pour Windows : <emplacement_de_Lazarus>\lcl\languages\) ;
- modifier le projet en utilisant le listing qui suit(19)Â !
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Interfaces, // this includes the LCL widgetset
Forms, bgrabitmappack, Main, GVAbout, Help, GVTools, GVConsts, GVTurtles,
SysUtils, GetText, Translations; // traduction française de la LCL
{$R *.res}
procedure
TranslateLCL;
// *** traduction ***
var
Lang, DefLang: string
;
begin
Lang := EmptyStr;
Deflang := EmptyStr;
GetLanguageIDs({%H-}
Lang, {%H-}
DefLang);
// utilisation du fichier corrigé
TranslateUnitResourceStrings('LCLStrConsts'
,
'..\..\3rdparty\lclstrconsts.fr.po'
, Lang, DefLang);
end
;
begin
RequireDerivedFormResource := True
;
TranslateLCL; // traduction
Application.Initialize;
Application.CreateForm(TMainForm, MainForm);
Application.Run;
end
.