1. Programmes événementiels avec Delphi
1.1 Pointeur de méthode
1.2 Affecter un pointeur de méthode
1.3 Un événement est un pointeur de méthode
1.4 Quel est le code engendré
2. Exercice-récapitulatif
Notice
méthodologique pour créer un nouvel événement
1. Programmes événementiels
avec Delphi
Delphi comme les autres RAD événementiels permet de construire
du code qui est exécuté en réponse à des
événements. Un événement Delphi est une propriété
d'un type spécial que nous allons examiner plus loin.
Le code de réaction à un événement particulier
est une méthode qui s'appelle un gestionnaire de cet événement.
Un événement est en fait un pointeur vers la méthode
qui est chargée de le gérer. Définissons la notion
de pointeur de méthode qui est utilisée ici, notion largement
utilisée en général dans les langages objets.
1.1 Pointeur de méthode
Un pointeur de méthode est une paire de pointeurs (adresses mémoire), le premier contient l'adresse d'une méthode et le second une référence à l'objet auquel appartient la méthode.
Schéma ci-après d'un objet Obj1 de classe clA contenant
un champ du type pointeur vers la méthode
meth1 de l'objet Obj2 de classe clB :
Pointeur de méthode en Delphi
Pour pointer la méthode d'une instance d'objet en Delphi, nous devons déclarer un nouveau type auquel nous ajoutons à la fin le qualificateur of object .
Exemples de types pointeurs de méthode et de variable de type
pointeur de méthode :
type
ptrMethode1 =
procedure of object ;
ptrMethode2 =
procedure (x : real
) of
object
;
ptrMethode3 =
procedure (x,y : integer
; var z :char )
of object ;
var
Proc1 :
ptrMethode1
; // pointeur vers une méthode sans paramètre
Proc2 : ptrMethode2 ;
// pointeur vers une méthode è un paramètre real
Schéma ci-après d'un objet Obj1 de classe clA contenant
un champ proc1 du type ptrMethode1 qui pointe vers la meth1 de l'objet Obj2
de classe clB. On suppose que l'adresse mémoire de l'objet
est 14785 et l'adresse de la méthode meth1 de l'objet est
14792 :
Il est impératif que la méthode vers laquelle pointe la variable
de pointeur de méthode soit du type prévu par le type pointeur
de méthode, ici la méthode meth1 doit obligatoirement être
une procédure sans paramètre (compatibilité d'en-tête).
Schéma ci-après d'un objet Obj1 de classe clA contenant
un champ proc2 du type ptrMethode2 qui pointe vers la meth2 de l'objet
Obj2 de classe clB. On suppose que l'adresse mémoire
de l'objet est 14785 et l'adresse de la méthode meth2 de l'objet
est 14805 :
La remarque précédente sur l'obligation de compatibilité
de l'en-tête de la méthode et le type pointeur de méthode
implique que la méthode meth2 doit nécessairement être
une procédure à un seul paramètre real.
1.2 Affecter un pointeur de méthode
Nous venons de voir comment déclarer un type pointeur de méthode
et un champ de ce même type, nous avons signalé que ce champ
doit pointer vers une méthode ayant une en-tête compatible
avec le type pointeur de méthode, il nous reste à connaître
le mécanisme qu'utilise Delphi pour lier un champ pointeur et une
méthode à pointer.
Types pointeurs de méthodes
ptrMethode1 = procedure of object;
ptrMethode2 = procedure (x : real) of object;
champs de pointeurs de méthodes
Proc1 : ptrMethode1; // pointeur vers une méthode sans paramètre
Proc2 : ptrMethode2; // pointeur vers une méthode à un paramètre real
Diverses méthodes :
procedure P1;
begin
end;
procedure P3 (x:char);
begin
end;
procedure P2 (x:real);
begin
end;
procedure P4;
begin
end;
Recensons d'abord les compatibilités d'en-tête qui autoriseront
le pointage de la méthode :
Proc1 peut pointer vers P1 , P4 qui sont les deux seules méthodes
compatibles.
Proc2 ne peut pointer que vers P2 qui est la seule méthode à
un paramètre de type real.
La liaison (le pointage) s'effectue tout naturellement à travers
une affectation :
L'affectation Proc1 := P1; indique que Proc1 pointe maintenant vers la
méthode P1 et peut être utilisé comme un identificateur
de procédure ayant la même signature que P1.
Exemple d'utilisation :
Proc2 := P2; // liaison du pointeur et de la procédure
P2
Proc2(45.8); // appel de la procédure vers
laquelle Proc2 pointe avec passage du paramètre 45.8
1.3 Un événement est un pointeur de méthode
Nous avons indiqué que les gestionnaires d'événements sont des méthodes, les champs du genre événements présents dans les classes Delphi sont en fait des pointeurs de méthode, qui peuvent pointer vers des gestionnaires d'événements.
Un type d'événement est donc un type pointeur de méthode,
Delphi possède plusieurs types d'événements par exemple
:
TNotifyEvent = procedure
( Sender: TObject) of
object
;
TMouseMoveEvent =
procedure ( Sender:
TObject
; Shift :
TShiftState
; X, Y : Integer
) of
object
;
TKeyPressEvent =
procedure ( Sender:
TObject
; var Key : Char )
of object ;
etc …
Un événement est donc une propriété de type
pointeur de méthode (type événement) :
property OnClick : TNotifyEvent ;
// événement click de souris
property OnMouseMove : TMouseMoveEvent ;
// événement passage de la souris
property OnKeyPress : TKeyPressEvent ;
// événement touche de clavier pressée
1.4 Quel est le code engendré pour gérer un événement
?
Intéressons nous maintenant au code engendré par un programme
simple constitué d'une fiche Form1 de classe TForm1 avec un objet
Button1 de classe Tbutton déposé sur la fiche :
Le code source apparent fournit au programmeur. Il permet l'intervention du programmeur sur la fiche et sur ses composants.
Le code intermédiaire caché au programmeur. Il permet l'initialisation automatique de la fiche et de ses composants.
unit Unit1 ;
interface
uses
Windows, Messages, SysUtils, Variants, Classes,
Graphics, Controls, Forms, Dialogs, StdCtrls ;
type
TForm1 = class (TForm)
Button1 : TButton ;
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;
var Form1 : TForm1 ;
implementation
{$R *.dfm}
end.object Form1 : TForm1
Left = 198
Top = 109
Width = 245
Height = 130
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = - 11
Font.Name = 'MS Sans Serif'
Font.Style = [ ]
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Button1 : TButtonend.
Left = 80
Top = 32
Width = 75
Height = 25
Caption = 'Button1'
TabOrder = 0
end
Demandons à Delphi de nous fournir (à partir de l'inspecteur
d'objet) les gestionnaires des 3 événements OnClick, OnMouseMove
et OnKeyPress de réaction de l'objet Button1 au click de souris,
au passage de la souris et à l'appui sur une touche du clavier:
Delphi engendre un code source visible en pascal objet modifiable et un
code intermédiaire (que l'on peut voir et éventuellement modifier)
:
- Le code source visible est celui qui nous sert à programmer nos algorithmes, nos classes, les réactions aux événements.
- Le code intermédiaire est généré au fur et à mesure que nous intervenons visuellement sur l'interface et contient l'initialisation de l'interface (affectations de gestionnaires d'événements, couleurs des fonds, positions des composants, taille, polices de caractères, conteneurs et contenus etc…) il sert au compilateur.
Nous venons de voir que Delphi a généré en code intermédiaire
pour nous, les affectations de chaque événement à un
gestionnaire :
OnClick = Button1Click
OnKeyPress = Button1KeyPress
OnMouseMove = Button1MouseMove
Et il nous a fournit les squelettes vides de chacun des trois gestionnaires
:
procedure TForm1.Button1Click (Sender: TObject); …
procedure TForm1.Button1MouseMove (Sender: TObject; Shift: TShiftState; X,Y: Integer); …
procedure TForm1.Button1KeyPress (Sender: TObject; var Key: Char); …
La dernière étape du processus de programmation de la réaction
du Button1 est de programmer du code à l'intérieur des squelettes
des gestionnaires.
Lors de l'exécution si nous cliquons avec la souris sur le Button1,
un mécanisme d'interception et de répartition figuré
ci-dessous appelle le gestionnaire de l'événément OnClick
dont le corps a été programmé :
2.1 Objectif de réalisation
Nous voulons faire saisir par l’utilisateur deux entiers et l’autoriser à effectuer leur somme et leur produit uniquement lorsque les deux entiers sont entrés. Aucune sécurité n’est apportée pour l’instant sur les données. |
Dès que les deux entiers
sont entrés, le bouton calcul est activé:
Lorsque l’on clique sur le
bouton calcul, le bouton effacer est activé et les résultats
apparaissent dans leurs zones :
Un clic sur le bouton effacer ramène l’interface à l’état initial.
2.2 Réalisation de l’implantation en Delphi
Un graphe événementiel complet de notre interface pourrait être :
Avec comme conventions sur les messages : X1 = exécuter les calculs sur les valeurs entrées. X2 = exécuter l’effacement des résultats. M1 = message à l’edit1 de tout effacer. M2 = message à l’edit2 de tout effacer. |
Changer
Edit1 |
Calcul activable
(si changer Edit2 a eu lieu) |
Changer
EDIT2 |
Calcul activable
(si changer Edit1 a eu lieu) |
Clic
CALCUL |
Exécuter X1
EFFACER activable Afficher les résultats |
Clic
EFFACER |
EFFACER désactivé
CALCUL désactivé Message M1 Message M2 |
Edit1 | activé |
Edit2 | activé |
Bcalcul | désactivé |
Beffacer | désactivé |
Implantation :
ButtonCalcul: TButton;
Buttoneffacer: TButton;
Edit1: TEdit;
Edit2: TEdit;
LabelSomme: TLabel;
LabelProduit: TLabel;
Implantation : deux variables booléennes
var Som_ok,Prod_ok:boolean; |
procedure
TForm1.Edit1Change(Sender: TObject); begin Som_ok:=true; // drapeau de Edit1 levé end; |
procedure
TForm1.Edit2Change(Sender: TObject); begin Prod_ok:=true; // drapeau de Edit2 levé end; |
Implantation du test des drapeaux :
procedure
TForm1.TestEntrees; {les drapeaux sont-ils levés tous les deux ?} begin if Prod_ok and Som_ok then Form1.ButtonCalcul.Enabled:=true // si et seulement si oui: le bouton calcul est activé end; |
Implantation n°2 du gestionnaire de OnChange :
procedure TForm1.Edit1Change(Sender:
TObject); begin Som_ok:=true; // drapeau de Edit1 levé TestEntrees; end; |
procedure TForm1.Edit2Change(Sender:
TObject); begin Prod_ok:=true; // drapeau de Edit2 levé TestEntrees; end; |
Implantation du gestionnaire
de OnClick du ButtonCalcul :
procedure TForm1.ButtonCalculClick(Sender:
TObject); var S,P:integer; begin S:=strtoint(Edit1.text); // transtypage : string à integer P:=strtoint(Edit2.text); // transtypage : string à integer LabelSomme.caption:=inttostr(P+S); LabelProduit.caption:=inttostr(P*S); Buttoneffacer.Enabled:=true // le bouton effacer est activé end; |
Edit1 | activé |
Edit2 | activé |
Bcalcul | désactivé |
Beffacer | désactivé |
procedure TForm1.RAZTout; begin with Form1 do begin Buttoneffacer.Enabled:=false; //le bouton effacer se désactive ButtonCalcul.Enabled:=false; //le bouton calcul se désactive LabelSomme.caption:='0'; // RAZ valeur somme affichée LabelProduit.caption:='0'; // RAZ valeur produit affichée Edit1.clear; // message M1 Edit2.clear; // message M2 Prod_ok:=false; // RAZ drapeau Edit2 Som_ok:=false; // RAZ drapeau Edit1 end end; |
Implantation du gestionnaire de OnClick du Buttoneffacer:
procedure TForm1.ButtoneffacerClick(Sender:
TObject); begin RAZTout; end; |
Implantation du gestionnaire de OnCreate de la fiche Form1:
procedure TForm1.FormCreate(Sender:
TObject); begin RAZTout; end; |
Lorsque nous essayons notre interface nous constatons que nous avons un problème de sécurité à deux niveaux dans notre saisie.
2.3 Améliorations de sécurité
du premier niveau par plan d’action
Delphi.Interfaces \Calcul.V.2.dlfi
Nous protégeons les calculs dans le logiciel, par un test sur la vacuité du champ text de l’Edit ; dans le cas favorable où le champ n’est pas vide, on autorise la saisie ; dans l’autre cas on désactive systématiquement le ButtonCalcul et l’on abaisse le drapeau qui avait été levé lors de l’entrée du premier chiffre, ce qui empêchera toute erreur ultérieure. L’utilisateur comprendra de lui-même que tant qu’il n’y a pas de valeur dans les entrées, le logiciel ne fera rien et on ne passera donc pas au plan d’action suivant (calcul et affichage). Cette amélioration s’effectue dans les gestionnaires d’événement OnChange des deux TEdit.
Implantation n°2 du gestionnaire de OnChange :
procedure TForm1.Edit1Change(Sender:
TObject); begin if Edit1.text<>'' then// champs text non vide ok ! begin Som_ok:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; // sinon bouton désactivé Som_ok:=false; // drapeau de Edit1 baissé end end; |
procedure TForm1.Edit1Change(Sender:
TObject); begin if Edit1.text<>'' then// champs text non vide ok ! begin Som_ok:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; // sinon bouton désactivé Som_ok:=false; // drapeau de Edit1 baissé end end; |
procedure TForm1.Edit2Change(Sender:
TObject); begin if Edit2.text<>'' then // champs text non vide ok ! begin Prod_ok:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; // sinon bouton désactivé Prod_ok:=false; // drapeau de Edit2 baissé end end; |
procedure TForm1.Autorise ( Ed : TEdit ; var flag : boolean ) ;
begin
if Ed.text < ' ' then // champ text non vide ok !
begin
flag := true ;
TestEntrees ;
end
else
begin
ButtonCalcul.enabled := false ; //bouton désactivé
flag := false ; // drapeau de Ed baissé
end
end;
procedure TForm1.Edit1Change( Sender: TObject) ;
begin
Autorise ( Edit1 , Som_ok ) ;
end;
procedure tform1.edit2change( sender: tobject) ;
begin
Autorise ( Edit2 , Prod_ok ) ;
end;
unit UFcalcul ;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ExtCtrls ;
type
TForm1 = class ( TForm )
Edit1 : TEdit ;
Edit2 : TEdit ;
ButtonCalcul : TButton ;
LabelSomme : TLabel ;
LabelProduit : TLabel ;
Buttoneffacer : TButton ;
procedure FormCreate( Sender: TObject) ;
procedure Edit1Change( Sender: TObject) ;
procedure Edit2Change( Sender: TObject) ;
procedure ButtonCalculClick( Sender: TObject) ;
procedure ButtoneffacerClick( Sender: TObject) ;
private { Déclarations privées }
Som_ok , Prod_ok :
procedure TestEntrees ;
procedure RAZTout ;
procedure Autorise ( Ed : TEdit ; var flag : boolean ) ;
public { Déclarations publiques }
end;
implementation
{ --------------- Méthodes privées ---------------------}
procedure TForm1.TestEntrees ;
{les drapeaux sont-ils levés tous les deux ?}
begin
if Prod_ok and Som_ok then
ButtonCalcul.Enabled := true // si oui: le bouton calcul est activé
end;
procedure TForm1.RAZTout ;
begin
Buttoneffacer.Enabled := false ; //le bouton effacer se désactive
ButtonCalcul.Enabled := false ; //le bouton calcul se désactive
LabelSomme.caption := '0'; // RAZ valeur somme affichée
LabelProduit.caption := '0'; // RAZ valeur produit affichée
Edit1.clear ; // message M1
Edit2.clear ; // message M2
Prod_ok := false ; // RAZ drapeau Edit2
Som_ok := false ; // RAZ drapeau Edit1
end;
procedure TForm1.Autorise ( Ed : TEdit ; var flag : boolean ) ;
begin
if Ed.text < ' ' then // champ text non vide ok !
begin
flag := true ;
TestEntrees ;
end
else
begin
ButtonCalcul.enabled := false ; //bouton désactivé
flag := false ; // drapeau de Ed baissé
end
end;
{ ------------------- Gestionnaires d'événements -------------------}
procedure TForm1.FormCreate( Sender: TObject) ;
begin
RAZTout ;
end;
procedure TForm1.Edit1Change( Sender: TObject) ;
begin
Autorise ( Edit1 , Som_ok ) ;
end;
procedure TForm1.Edit2Change( Sender: TObject) ;
begin
Autorise ( Edit2 , Prod_ok ) ;
end;
procedure TForm1.ButtonCalculClick( Sender: TObject) ;
var S , P : integer ;
begin
S := strtoint(Edit1.text) ; // transtypage : string è integer
P := strtoint(Edit2.text) ; // transtypage : string è integer
LabelSomme.caption := inttostr(P + S) ;
LabelProduit.caption := inttostr(P * S) ;
Buttoneffacer.Enabled := true // le bouton effacer est activé
end;
procedure TForm1.ButtoneffacerClick( Sender: TObject) ;
begin
RAZTout ;
end;
end.
procedure TForm1.Edit1Change( Sender: TObject) ;
begin
Autorise ( Edit1 , Som_ok ) ;
end;
procedure TForm1.Edit2change( Sender: tobject) ;
begin
Autorise ( Edit2 , Prod_ok ) ;
end;
procedure TForm1.TexteChange( Sender: TObject) ;
begin
if Sender is TEdit then
begin
if ( Sender as TEdit ) = Edit1 then
Autorise ( ( Sender as TEdit ) , Som_ok ) ;
else
Autorise ( ( Sender as TEdit ) , Prod_ok ) ;
end
end;
procedure TForm1.FormCreate( Sender: TObject) ;
begin
RAZTout ;
Edit1.OnChange := TexteChange ;
Edit2.OnChange := TexteChange ;
end;
procedure TForm1.FormCreate( Sender: TObject) ;
begin
RAZTout ;
Edit1.OnChange := TexteChange ;
Edit2.OnChange := TexteChange ;
end;
2.4 Améliorations de sécurité du second niveau par filtrage
Nous pouvons améliorer cet état de la saisie des caractères chiffres en construisant un analyseur de filtrage qui ne conserve que les caractères valides tapés dans chaque Tedit. La syntaxe de l’entrée est fournie par le diagramme suivant :
var i:integer; saisie :string; CarValides:set of char; begin CarValides:=['0'..'9']; // les chiffres seulement saisie:=entree; if length(saisie)<>0 then begin i:=1; while saisie[i] in CarValides do i:=i+1; // le premier caractère non juste if not(saisie[i] in CarValides)then delete(saisie,i,1); end; resultat:=saisie end; |
var sortie :string ; begin Filtrage(Edit1.text,sortie); Edit1.text:=sortie; Som_ok:=true; // drapeau de Edit1 levé TestEntrees; end; |
var sortie :string ; begin Filtrage(Edit2.text,sortie); Edit2.text:=sortie; Prod_ok:=true; // drapeau de Edit2 levé TestEntrees; end; |
Le filtrage est effectué lors de la saisie des entrées dans les gestionnaires d’événement OnChange des deux Tedit. Chaque changement du contenu du champ text d’un Edit (comme l’entrée d’un nouveau caractère) provoque le déclenchement de l’événement OnChange qui rejette alors les caractères non valides.
Implantation n°4 du gestionnaire de OnChange :
var sortie :string ; begin Filtrage(Edit1.text,sortie); Edit1.text:=sortie; if Edit1.text<>'' then// champs text non vide ok ! begin Som_ok:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; // sinon bouton désactivé Som_ok:=false; // drapeau de Edit1 baissé end end; |
var sortie :string ; begin Filtrage(Edit2.text,sortie); Edit2.text:=sortie; if Edit2.text<>'' then // champs text non vide ok ! begin Prod_ok:=true; TestEntrees; end else begin ButtonCalcul.enabled:=false; // sinon bouton désactivé Prod_ok:=false; // drapeau de Edit2 baissé end end; |
En combinant la sécurité du premier et du second niveau dans le gestionnaire d’événement OnChange nous obtenons un niveau acceptable de sécurisation de notre logiciel grâce à son interface. Il nous manque encore une protection sur les débordements de calcul (dépassement de l’intervalle sur les integer qui ne provoquent pas d’incidents mais induisent des résultats erronés) pour assurer une sécurité optimale de fiabilité.
Pour terminer la réalisation
de cet exemple, nous pouvions aussi procéder à un autre genre
de protection sans avoir à écrire un analyseur de filtrage
(qui était dans ce cas un AEFD), en utilisant directement les facilités
de protection fournies par les exceptions.
2.5 Améliorations de sécurité du second niveau par exceptions
On déporte le problème de la protection non plus sur le séquencement des plans d’actions mais au cœur même de l’action en protégeant directement le code où l’erreur se produit par un gestionnaire d’exception. Afin de présenter un traitement complet de l’exemple nous anticipons légèrement sur le chapitre sur la programmation défensive ; le lecteur pourra donc en première lecture sauter ce paragraphe s’il le désire et y revenir plus tard.
On fait exécuter le
programme en provoquant volontairement l’erreur (on entre des lettres au
lieu de chiffres) ; le logiciel signale la levée de l’exception EconvertError
et nous programmons le gestionnaire associé à cette levée
d’exception. Elle apparaît lorsque la fonction StrtoInt essaye de
transtyper le champ text de l’Edit et échoue ; c’est donc cette ligne
de code qui doit être protégée :
S:= StrtoInt(edit1.text); except on EconvertError do begin Edit1.text:='0'; S:=0 end end; |
Nous avons décidé
ici de mettre 0 dans le champ text de l’Edit1 et de forcer la valeur de
la variable de calcul S à 0. Ce traitement est identique pour la variable
P à partir du champ text de l’Edit2. Ces deux traitements de protection
sont effectués lors du click sur ButtonCalcul (traitement de l’erreur
après saisie).
var S,P:integer; begin try S:= StrtoInt(edit1.text); except on EconvertError do begin Edit1.text:='0'; S:=0 end end; try P:= StrtoInt(edit2.text); except on EconvertError do begin Edit2.text:='0'; P:=0 end end; LabelSomme.caption:=inttostr(P+S); LabelProduit.caption:=inttostr(P*S); Buttoneffacer.enabled:=true // le bouton effacer est activé end;{ButtonCalculClick} |
Ce logiciel d’interface simplifiée
met en œuvre tous les concepts de base d’une interface à l’exception
des temps d’attente qui ne sont pas pertinents dans cet exemple :
Un chapitre spécial
est consacré à la programmation défensive afin de nous
aider à améliorer la résistance aux erreurs de nos
logiciels de communication. Nous allons dans ce qui suit proposer une méthode
systématisée de mise en œuvre des concepts d’interaction
et de pilotage dans une interface.