I. Piqûre de rappel : Liaison manuelle : TBindExpression▲
Dans l'épisode précédent, nous avons utilisé une propriété d'un composant TTrackBar pour déplacer un cercle. Dans ce premier chapitre, nous allons étudier d'un peu plus près ce type de lien.
Ce premier programme est un autre exemple d'utilisation basique et va nous permettre de comparer encore une fois lien rapide et lien manuel afin de plonger un peu plus dans l'univers des LiveBindings. Notre objectif est simple : saisir une phrase (par exemple, le fameux « Hello World ») pour répercuter cette saisie sur quelques zones de l'écran (trois composants TLabel nommés Capture1 à 3).
Mise en garde aux détenteurs d'une version starter !
Malheureusement pour les détenteurs de cette version, le concepteur visuel de liens n'est pas disponible (ainsi que les raccourcis y donnant accès). Pour eux, seule la méthode manuelle sera accessible ! Ces points ou visuels non accessibles seront indiqués par un astérisque (*).
I-A. Liaison Rapide 1▲
Sur cette image écran, j'ai déjà établi un lien rapide entre la zone de saisie (TEdit1) et la première zone de « capture ».
Avant d'aller plus loin, voici les sources à ce stade :
- le DFM :
object
Form8: TForm8
Left = 0
Top = 0
Caption = 'Liaison rapide vs Liaison manuelle'
ClientHeight = 128
ClientWidth = 433
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object
Capture1: TLabel
Left = 280
Top = 27
Width = 45
Height = 13
Caption = 'Capture1'
end
object
Capture2: TLabel
Left = 280
Top = 56
Width = 45
Height = 13
Caption = 'Capture2'
end
object
Label3: TLabel
Left = 184
Top = 27
Width = 55
Height = 13
Caption = 'Lien Rapide'
end
object
Label4: TLabel
Left = 184
Top = 56
Width = 56
Height = 13
Caption = 'Lien Manuel'
end
object
Capture3: TLabel
Left = 280
Top = 88
Width = 45
Height = 13
Caption = 'Capture3'
end
object
Edit1: TEdit
Left = 32
Top = 24
Width = 121
Height = 21
TabOrder = 0
Text = 'Label1'
OnChange = Edit1Change
end
object
CheckBox1: TCheckBox
Left = 126
Top = 87
Width = 114
Height = 17
Caption = 'Pister le lien (track)'
TabOrder = 1
end
object
BindingsList1: TBindingsList
Methods = <>
OutputConverters = <>
Left = 372
Top = 13
object
LinkControlToPropertyCaption: TLinkControlToProperty
Category = 'Liaisons rapides'
Control = Edit1
Track = True
Component = Capture1
ComponentProperty = 'Caption'
end
end
end
- le programme PAS :
unit
U_Prog2_1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Data.Bind.EngExt, Vcl.Bind.DBEngExt,
System.Rtti, System.Bindings.Outputs, Vcl.Bind.Editors, Data.Bind.Components;
type
TForm8 = class
(TForm)
Edit1: TEdit;
Capture1: TLabel;
Capture2: TLabel;
Capture3: TLabel;
CheckBox1: TCheckBox;
Label3: TLabel;
Label4: TLabel;
BindingsList1: TBindingsList;
LinkControlToPropertyCaption: TLinkControlToProperty;
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
Form8: TForm8;
implementation
{$R *.dfm}
end
.
À ce stade, si l'on exécute le programme, tant que l'on frappe quelque chose dans la zone de saisie, rien ne se passe ! Ce n'est qu'en appuyant sur la touche [Entrée] ou [Tab] qui permet de passer à la zone active suivante que la capture récupère la valeur saisie. Pas vraiment l'effet escompté, n'est-ce pas ? Vous auriez certainement préféré quelque chose de plus réactif où chaque caractère saisi se répercute automatiquement dans la capture. Donc il n'y a pas de quoi pavoiser, surtout que, sans les LiveBindings, vous auriez pu faire beaucoup plus simplement grâce à la méthode OnChange de la zone de saisie :
procedure
TForm8.Edit1Change(Sender: TObject);
begin
Capture1.Caption:=TEdit(Sender).text;
end
;
Bien évidemment, comme nous avons trois zones de capture, il aurait fallu ajouter deux autres instructions dans cette procédure, une pour chaque capture.
I-B. Liaison manuelle▲
Dans l'épisode précédent, nous avons déjà vu que, pour avoir quelque chose de plus réactif, il fallait passer par un TBindExpression.
Petit rappel : sélectionnez le composant conteneur des liaisons (BindingsList1) et, dans l'inspecteur d'objets, l'option du menu « Nouveau composant LiveBinding... »*.
Après sélection du type de lien, il ne reste qu'à remplir les propriétés de lien. Pour rappel, nous allons remplir les propriétés ControlComponent, ControlExpression, Direction, SourceComponent, SourceExpression. Les deux premières font référence au contrôle que nous voulons lier (c'est-à-dire Controle2), les deux dernières à la provenance (ou source, c'est-à-dire la zone de saisie), la direction étant déjà renseignée dans le bon sens.
Les modifications de propriétés se répercutent sur l'écran du concepteur visuel de liaisons*. Il nous reste à faire en sorte que l'expression soit prise en compte. Nous allons donc ajouter l'événement OnChange du composant TEdit de la forme avec le code suivant :
procedure
TForm8.Edit1Change(Sender: TObject);
begin
BindingsList1.Notify(Sender,''
);
end
;
- Nouveau DFM :
object
Form8: TForm8
Left = 0
Top = 0
Caption = 'Liaison rapide vs Liaison manuelle'
ClientHeight = 128
ClientWidth = 433
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object
Capture1: TLabel
Left = 280
Top = 27
Width = 45
Height = 13
Caption = 'Capture1'
end
object
Capture2: TLabel
Left = 280
Top = 56
Width = 45
Height = 13
Caption = 'Capture2'
end
object
Label3: TLabel
Left = 184
Top = 27
Width = 55
Height = 13
Caption = 'Lien Rapide'
end
object
Label4: TLabel
Left = 184
Top = 56
Width = 56
Height = 13
Caption = 'Lien Manuel'
end
object
Capture3: TLabel
Left = 280
Top = 88
Width = 45
Height = 13
Caption = 'Capture3'
end
object
Edit1: TEdit
Left = 32
Top = 24
Width = 121
Height = 21
TabOrder = 0
Text = 'Capture1'
OnChange = Edit1Change
end
object
CheckBox1: TCheckBox
Left = 126
Top = 87
Width = 114
Height = 17
Caption = 'Pister le lien (track)'
TabOrder = 1
end
object
BindingsList1: TBindingsList
Methods = <>
OutputConverters = <>
Left = 372
Top = 13
object
LinkControlToPropertyCaption: TLinkControlToProperty
Category = 'Liaisons rapides'
Control = Edit1
Track = True
Component = Capture1
ComponentProperty = 'Caption'
end
object
BindExpression1: TBindExpression
Category = 'Expressions de liaison'
ControlComponent = Capture2
SourceComponent = Edit1
SourceExpression = 'Text'
ControlExpression = 'Caption'
NotifyOutputs = False
Direction = dirSourceToControl
end
end
- Nouveau programme :
unit
U_Prog2_1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
Data.Bind.EngExt, Vcl.Bind.DBEngExt,
System.Rtti, System.Bindings.Outputs, Vcl.Bind.Editors, Data.Bind.Components;
type
TForm8 = class
(TForm)
Edit1: TEdit;
Capture1: TLabel;
Capture2: TLabel;
Capture3: TLabel;
CheckBox1: TCheckBox;
Label3: TLabel;
Label4: TLabel;
BindingsList1: TBindingsList;
LinkControlToPropertyCaption: TLinkControlToProperty;
BindExpression1: TBindExpression;
procedure
Edit1Change(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
Form8: TForm8;
implementation
{$R *.dfm}
procedure
TForm8.Edit1Change(Sender: TObject);
begin
BindingsList1.Notify(Sender,''
);
end
;
end
.
À l'exécution, Capture2 réagit au doigt et à l'œil alors que Capture1 attend toujours le « signal » émis par les touches [Entrée] ou [Tab].
I-C. Les propriétés de la liaison rapide▲
Pour faire le tour complet de ce programme (sinon, pourquoi aurais-je ajouté une case à cocher et une troisième capture ?), nous allons nous attarder sur quelques propriétés supplémentaires de la liaison rapide.
La première étape consiste à créer un lien rapide entre Edit1 et Capture3 : c'est très facile à faire à l'aide de la souris et une opération de glisser-déposer*.
Les propriétés suivantes sont à prendre en compte :
AutoActivate : il tombe sous le sens qu'elle doit être activée, sinon la liaison ne fonctionnera pas. Bien sûr, cette propriété peut être modifiée lors de l'exécution du programme via l'instruction :
LinkControlToPropertyCaption2.AutoActivate:=True
; //ou False
// mais nous pouvons aussi activer ou désactiver le lien ainsi
LinkControlToPropertyCaption2.Active:=True
; // ou False au runtime
Category : par défaut, « Liaisons rapides » avec un environnement de travail en français. Vous pouvez y écrire autre chose : cela ne se répercutera que sur le BindingsList1 et permettra de ranger un peu les choses lors de l'utilisation de ce dernier ;
Component, ComponentProperty, Control : les propriétés sont remplies directement lors de la création via le glisser-déposer ;
CustomFormat : nous y avons déjà fait appel dans l'épisode précédent pour y mettre notre formule (ou expression) de calcul. Le sens de fonctionnement est de Control vers Component, c'est-à-dire d'Edit1 vers Capture3.
Oui, je sais, c'est très frustrant et donc pénible, cette histoire de nom de propriétés !
Pour illustrer tout cela, je vais demander que la phrase saisie soit mise en majuscules en utilisant la méthode UpperCase.
CustomParse : pour faire simple, c'est l'inverse de CustomFormat, c'est-à-dire que le sens de l'expression est inversé (donc de Control3 vers Edit1). Je vais aussi utiliser la méthode Uppercase. Vous pourrez voir l'effet de ceci un peu plus loin lors de l'activation de la propriété suivante.
Mais où trouver ces méthodes ? Ayez la curiosité de regarder les propriétés de BindingsList1 et vous y verrez un (TMethods). Cliquez sur les « … » pour avoir la liste des méthodes recensées.
Attention, même si les noms sont les mêmes, il ne s'agit pas des fonctions des unités du genre StrUtils.
InitializeControlValue : par défaut, la case est décochée ; cochez-la et vous verrez tout de suite la zone de saisie se remplir avec UpperCase('Controle3') soit « CONTROLE3 », l'expression de CustomParse étant bien appliquée.
Name : un bon conseil, prenez le temps de mettre un nom à chaque lien, car ce qui est proposé par défaut, « LinkControlToPropertyCaption2 », est loin d'être clair quand on a des applications contenant des dizaines de liens.
Tag : pas de commentaire.
Track : cette propriété mérite que l'on s'y attarde un peu, et c'est d'ailleurs pour cela que, lors du design de l'écran, j'ai mis une case à cocher. Implémentons l'événement OnClick de ce composant qui va nous permettre d'activer ou de désactiver cette fonction :
procedure
TForm8.CheckBox1Click(Sender: TObject);
begin
LinkControlToPropertyCaption2.Track:=CheckBox1.Checked;
end
;
À ma connaissance, on ne peut pas lier une propriété d'un LiveBinding à un composant. On est donc obligé de coder un peu.
Auparavant, décochez la propriété Track du lien pour être en cohérence avec la case à cocher.
Exécutons maintenant le programme.
Saisissez une phrase du genre « Hello World », puis appuyez sur [Entrée] :
- Le curseur reste en fin de saisie ;
- Le contrôle 1 s'est mis à jour ;
- Le contrôle 2 a suivi la saisie lettre par lettre ;
- Le contrôle 3 n'a pas bougé !
- Appuyez sur [Tab], ça y est ! Le contrôle 3 est à jour en contenant « HELLO WORLD » tandis que la case à cocher devient le contrôle actif.
Comme le contrôle actif est maintenant sur la case à cocher, activez-la puis repassez dans la zone de saisie pour modifier la phrase en « Bonjour à tous ».
Après appui sur la touche [Entrée], les étapes 1,2,3 sont identiques. Par contre, à l'étape 4, le contrôle 3 a changé lui aussi. La touche [Tab] ne fait que passer au prochain contrôle actif, à savoir la case à cocher.
Grâce à la propriété Track, on peut donc un peu changer le comportement de la saisie.
Ce programme n'est pas fourni dans les exemples accompagnant le tutoriel. Tout d'abord parce qu'il est loin d'être remarquable, mais aussi parce que la meilleure manière d'aborder les LiveBindings est bien de tester par soi-même ces quelques lignes. De plus, tout le code source est indiqué dans ce chapitre.
II. Effets secondaires▲
Baptisés comme tels par Cary Jensen, je vous livre d'abord la vision qu'il a des effets secondaires, soit le pourquoi des termes :
- un effet secondaire est une opération supplémentaire réalisée à la suite de l'exécution d'une méthode ;
- les effets secondaires sont normalement associés aux méthodes d'accessibilité de la propriété ;
- les effets secondaires peuvent également être initiés à partir de l'invocation de la méthode de l'objet, et ce à la suite de l'évaluation d'une expression par le moteur d'expression.
Bon, tout cela est très théorique et comme rien ne vaut la pratique, je vous propose de créer un petit convertisseur de monnaie. Le dessin d'écran en est très simple, car il ne comporte que quelques libellés et deux zones de saisie (une pour un montant en euros l'autre pour un montant en dollars US). On améliorera ensuite ce premier jet afin d'avoir le choix de la devise de conversion, toujours par rapport à la monnaie de référence, c'est-à-dire l'euro.
Toutefois, on veut pouvoir saisir un montant dans l'une de ces deux monnaies. Comment procéder ?
II-A. Avant l'apparition des LiveBindings▲
Challenge : sans composants supplémentaires, boutons ou autres, seuls les événements des deux TEdit étant disponibles, il va falloir jongler avec quelques variables ou tout autre moyen si l'on veut utiliser les événements OnChange de ces deux composants, sous peine de boucler.
Je vais utiliser la propriété Tag pour cela.
Taux est pour l'instant une constante (=1.10).
procedure
TForm9.EurosChange(Sender: TObject);
var
V : Extended
; // récupère la valeur de transformation
begin
case
TEdit(Sender).Tag of
0
: Begin
if
TryStrToFloat(TEdit(Sender).Text,V)
then
Conversion.Text:=FloatToStr(V*taux)
else
begin
TEdit(Sender).Text:=''
;
Conversion.Text:=''
;
end
;
End
;
1
: Begin
if
TryStrToFloat(TEdit(Sender).Text,V)
then
Euros.Text:=FloatToStr(V/taux)
else
begin
TEdit(Sender).Text:=''
;
Euros.Text:=''
;
end
;
End
;
end
;
II-B. Depuis les LiveBindings▲
Avec les LiveBindings, nous allons utiliser ces effets secondaires. Pour cela, nous allons créer un nouveau lien d'un nouveau type : un TBindExprItems.
Rappel : la méthode pour ajouter un lien est déjà indiquée au chapitre I.B Liaison manuelle, la seule différence résidant dans le choix du type de lien.
Il vous faut ensuite exprimer les expressions qui vont être cette fois au nombre de deux !
La première sera dans le sens contrôle vers source, l'autre en direction inverse. Cela vous rappelle quelque chose ? Non ? Refaites un tour au chapitre Les propriétés de la liaison rapide, vous y êtes, les « fameux » CustomFormat et CustomParse !
Pour procéder à cela, vous remarquerez cette fois que tous les boutons de la fenêtre de modification d'expressions sont disponibles.
Rappel, pour accéder à la fenêtre de modification d'expressions le plus rapidement possible, sélectionnez « Expressions » dans l'inspecteur d'objets *.
Chaque expression de format créée n'a que trois propriétés :ControlExpression, Direction et SourceExpression. Il ne nous reste donc qu'à remplir ces deux expressions.
Format[0] |
Format[1] |
|
ControlExpression |
Text |
Text/1.10 |
Direction |
dirSourceToControl |
dirControlToSource |
SourceExpression |
Text*1.10 |
Text |
N'oublions pas de gérer les événements des deux TEdit. De manière classique, une procédure OnChange suffira pour ces derniers. Il faut juste ne pas oublier de bien l'indiquer !
procedure
TForm9.EurosChange(Sender: TObject);
begin
BindingsList1.Notify(Sender,''
);
end
;
Exécutons le programme et saisissons 1 dans la zone de saisie des euros.
Aïe ! Nous venons de nous faire jeter comme des malpropres…
Et la surprise est de taille, l'erreur plutôt étrange : tout le monde sait diviser 1,1 par 1,1 ! Pourquoi pas notre interpréteur d'expressions ?
Tout d'abord, parce qu'il ne s'agit pas à proprement parler des nombres, mais de deux chaînes de caractères. Dans la formule, nous avons écrit 1.10 pour le taux de conversion, pas 1,10.
Ensuite, parce que notre séparateur décimal est la virgule et non le point comme nos amis anglo-saxons qui n'auraient pas eu cette erreur (dommage de passer à côté quand même;-) ).
Pour vous en convaincre, ajoutez, dans l'événement OnCreate de la forme, l'instruction FormatSettings.DecimalSeparator:='.';
Il faut donc procéder autrement, mais n'est-ce pas un peu le sujet de l'article ? De toute façon, cette solution n'était pas très propre : écrire « en dur » le taux de conversion dans l'expression n'est pas vraiment professionnel !
II-B-1. Création d'un objet▲
Pour résoudre le problème rencontré, nous allons créer un objet ayant deux fonctions :
TConvertisseur = Class
(TComponent)
private
public
function
ConvertirEUR2USD(const
Valeur : String
) : String
;
function
ConvertirUSD2EUR(const
Valeur : String
) : String
;
end
;
…
function
TConvertisseur.ConvertirEUR2USD(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:=FloatToStr(f*taux);
end
;
function
TConvertisseur.ConvertirUSD2EUR(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:= FloatToStr(f/taux);
end
;
Un objet public de ce type sera créé au moment de la création de la forme :
procedure
TForm9.FormCreate(Sender: TObject);
begin
Convertisseur:=TConvertisseur.Create(Self
);
end
;
Ensuite, nous appellerons ces fonctions pour effectuer nos conversions dans les expressions de format.
Format[0] |
Format[1] |
|
ControlExpression |
Text |
Owner.Convertisseur.ConvertirUSD2EUR(Text) |
Direction |
dirSourceToControl |
dirControlToSource |
SourceExpression |
Owner.Convertisseur.ConvertirEUR2USD(Text) |
Text |
Étudions de plus près une de ces formules.
Owner : fait référence au propriétaire du composant lié, c'est-à-dire le ControlComponent, soit le TEdit nommé « Conversion ».
Convertisseur : est bien sûr l'objet TConvertisseur créé un peu plus haut.
Ensuite vient l'appel de la fonction qui va faire le traitement dépendant de la direction :
- la valeur de la propriété Text du TEdit « Euros » pour l'expression Format[0] ;
- la valeur de la propriété Text du TEdit « Conversion » pour l'expression Format[1].
Rappelons, si besoin, que cette propriété est de type String.
II-B-2. À ce stade, comment se présentent les sources ? ▲
object
Form9: TForm9
Left = 0
Top = 0
Caption = 'Convertisseur'
ClientHeight = 134
ClientWidth = 343
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object
Label1: TLabel
Left = 24
Top = 43
Width = 27
Height = 13
Caption = 'Euros'
end
object
DateXML: TLabel
Left = 24
Top = 8
Width = 23
Height = 13
Caption = 'Date'
end
object
Label2: TLabel
Left = 24
Top = 80
Width = 48
Height = 13
Caption = 'Dollars US'
end
object
Euros: TEdit
Left = 80
Top = 40
Width = 121
Height = 21
TabOrder = 0
Text = '0'
OnChange = EurosChange
end
object
Conversion: TEdit
Tag = 1
Left = 80
Top = 77
Width = 121
Height = 21
TabOrder = 1
Text = '0'
OnChange = EurosChange
end
object
BindingsList1: TBindingsList
Methods = <>
OutputConverters = <>
Left = 252
Top = 13
object
BindExprItems1: TBindExprItems
Category = 'Expressions de liaison'
ControlComponent = Conversion
SourceComponent = Euros
FormatExpressions = <
item
ControlExpression = 'Text'
SourceExpression = 'Owner.Convertisseur.ConvertirEUR2USD(Text)'
end
item
ControlExpression = 'Owner.Convertisseur.ConvertirUSD2EUR(Text)'
SourceExpression = 'text'
Direction = dirControlToSource
end
>
ClearExpressions = <>
NotifyOutputs = False
end
end
end
unit
U_MainConvertisseur;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.EngExt, Vcl.Bind.DBEngExt,
System.Rtti, System.Bindings.Outputs, Vcl.Bind.Editors, Data.Bind.Components;
type
TConvertisseur = Class
(TComponent)
private
public
function
ConvertirEUR2USD(const
Valeur : String
) : String
;
function
ConvertirUSD2EUR(const
Valeur : String
) : String
;
end
;
TForm9 = class
(TForm)
Euros: TEdit;
Label1: TLabel;
Conversion: TEdit;
BindingsList1: TBindingsList;
BindExprItems1: TBindExprItems;
DateXML: TLabel;
Label2: TLabel;
procedure
EurosChange(Sender: TObject);
procedure
FormCreate(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
Convertisseur : TConvertisseur;
end
;
var
Form9: TForm9;
Const
taux = 1
.10
;
implementation
{$R *.dfm}
procedure
TForm9.EurosChange(Sender: TObject);
begin
BindingsList1.Notify(Sender,''
);
end
;
{ TConvertisseur }
function
TConvertisseur.ConvertirEUR2USD(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:=FloatToStr(f*taux);
end
;
function
TConvertisseur.ConvertirUSD2EUR(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:= FloatToStr(f/taux);
end
;
procedure
TForm9.FormCreate(Sender: TObject);
begin
Convertisseur:=TConvertisseur.Create(Self
);
end
;
end
.
Plusieurs constatations s'imposent :
- N'avoir qu'un taux fixe en constante est pratique pour les essais, mais frustrant dans le cadre d'un programme plus abouti ;
- N'avoir qu'une seule possibilité de conversion EUR?USD manque un peu de charme ;
- Le code source est loin d'être très lisible et c'est en partie dû à l'EDI, mais quand même ! La partie concernant l'objet TConvertisseur se situe entre deux constituants de l'interface graphique ;
- Je n'ai introduit aucun commentaire dans le code ;
- J'ai fait l'impasse sur les quelques noms de composants non renseignés et donc fournis par défaut, tels que Label1, Label2, Form8…
III. Conclusion▲
Nous avons vu qu'une liaison peut être bidirectionnelle, que l'on pouvait utiliser des méthodes internes, mais aussi, avec un peu de code, des fonctions et propriétés d'objets. C'est une introduction toute trouvée vers des liaisons à des données. La seconde partie sur les effets de bord abordera l'utilisation de l'interpréteur d'expressions.
Toutefois, je suis sûr que vous en êtes encore sur votre faim ! Cette expérimentation est très jolie, mais il est frustrant de ne pas avoir un programme plus abouti, surtout après les constations de la dernière partie.
La mise en forme du programme final débordant stricto sensu le cadre des LiveBindings, j'ai préféré en faire un chapitre à part.
IV. Finalisation du programme▲
Vous pouvez retrouver le code ici.
IV-A. Isoler la partie interface de la partie métier ▲
En fait, il s'agit d'isoler la partie concernant l'objet TConvertisseur de la partie du programme concernant la forme. Cela aura un effet immédiat : alléger la lecture du programme.
Nous allons donc créer une nouvelle unité qui va contenir tout ce qui s'y rattache. Quelques opérations de couper-coller plus tard, nous obtenons :
unit
Convertisseur_Objet;
interface
uses
System.SysUtils, System.Classes;
type
TConvertisseur = Class
(TComponent)
private
public
function
ConvertirEUR2USD(const
Valeur : String
) : String
;
function
ConvertirUSD2EUR(const
Valeur : String
) : String
;
end
;
implementation
function
TConvertisseur.ConvertirEUR2USD(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:=FloatToStr(f*taux);
end
;
function
TConvertisseur.ConvertirUSD2EUR(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:= FloatToStr(f/taux);
end
;
end
.
la variable Taux est encore la constante de l'unité principale, mais je vais revenir dessus un peu plus tard.
Le code source de l'unité principale est réduit. Avant d'enregistrer mon travail sous un nouveau nom, j'en profite pour utiliser une des nouvelles fonctionnalités (depuis Berlin 10.1 update 2 je crois) de l'EDI : les modifications rapides, et ainsi tordre le cou du point 5 sur les noms par défaut laissés tels quels.
Je remplace également le libellé « Dollar US » et le remplace par un TComboBox qui permettra de choisir la devise (point numéro 2). Il y a donc peu de changements concernant la partie interface.
object
InterfaceUtilisateur: TInterfaceUtilisateur
Left = 0
Top = 0
Caption = 'Convertisseur'
ClientHeight = 134
ClientWidth = 343
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object
LabelEuro: TLabel
Left = 24
Top = 43
Width = 27
Height = 13
Caption = 'Euros'
end
object
DateXML: TLabel
Left = 24
Top = 8
Width = 23
Height = 13
Caption = 'Date'
end
object
Euros: TEdit
Left = 80
Top = 40
Width = 121
Height = 21
TabOrder = 0
Text = '0'
OnChange = EurosChange
end
object
Conversion: TEdit
Tag = 1
Left = 80
Top = 77
Width = 121
Height = 21
TabOrder = 1
Text = '0'
OnChange = EurosChange
end
object
Devise: TComboBox
Left = 24
Top = 77
Width = 50
Height = 21
TabOrder = 2
Text = 'USD'
end
object
ListeDesLiaisons: TBindingsList
Methods = <>
OutputConverters = <>
Left = 252
Top = 13
object
LiaisonConversion_Euro: TBindExprItems
Category = 'Expressions de liaison'
ControlComponent = Conversion
SourceComponent = Euros
FormatExpressions = <
item
ControlExpression = 'Text'
SourceExpression = 'Owner.Convertisseur.ConvertirEUR2USD(Text)'
end
item
ControlExpression = 'Owner.Convertisseur.ConvertirUSD2EUR(Text)'
SourceExpression = 'text'
Direction = dirControlToSource
end
>
ClearExpressions = <>
NotifyOutputs = False
end
end
end
Les modifications sont un peu plus notables dans le source de l'unité principale :
unit
Convertisseur_MainUI;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.EngExt, Vcl.Bind.DBEngExt,
System.Rtti, System.Bindings.Outputs, Vcl.Bind.Editors, Data.Bind.Components,
Vcl.StdCtrls, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient,
IdHTTP, Xml.xmldom, Xml.XMLIntf, Xml.XMLDoc,
System.RegularExpressions, Convertisseur_Objet;
type
TInterfaceUtilisateur = class
(TForm)
Euros: TEdit;
LabelEuro: TLabel;
Conversion: TEdit;
ListeDesLiaisons: TBindingsList;
LiaisonConversion_Euro: TBindExprItems;
DateXML: TLabel;
Devise: TComboBox;
procedure
EurosChange(Sender: TObject);
procedure
FormCreate(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
Convertisseur : TConvertisseur;
end
;
var
InterfaceUtilisateur: TInterfaceUtilisateur;
Const
taux = 1
.10
;
implementation
{$R *.dfm}
procedure
TInterfaceUtilisateur.EurosChange(Sender: TObject);
begin
ListeDesLiaisons.Notify(Sender,''
);
end
;
procedure
TInterfaceUtilisateur.FormCreate(Sender: TObject);
begin
Convertisseur:=TConvertisseur.Create(Self
);
end
;
end
.
Le plus important est ne pas oublier d'indiquer, dans la partie interface, l'utilisation de notre unité métier !
IV-B. Les taux de change▲
Jusqu'à présent, Taux était une constante de l'unité principale. Nous pourrions bien sûr mettre cette constante dans l'unité métier, mais il est préférable d'améliorer notre objet en lui ajoutant une propriété qui contiendra le taux de conversion euro vers dollar.
unit
Convertisseur_Objet;
interface
uses
System.SysUtils, System.Classes;
type
TConvertisseur = Class
(TComponent)
private
function
GetConversionEuro2USD : Extended
;
public
function
ConvertirEUR2USD(const
Valeur : String
) : String
;
function
ConvertirUSD2EUR(const
Valeur : String
) : String
;
property
CoursEuro: Extended
read
GetConversionEuro2USD;
end
;
implementation
function
TConvertisseur.ConvertirEUR2USD(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:=FloatToStr(f*CoursEuro);
end
;
function
TConvertisseur.ConvertirUSD2EUR(const
Valeur: String
): String
;
var
f : Extended
;
begin
result:=''
;
if
TryStrToFloat(Valeur,f)
then
result:= FloatToStr(f/CoursEuro);
end
;
function
TConvertisseur.GetConversionEuro2USD: Extended
;
begin
Result:=1
.10
; // notre constante
end
;
end
.
Nous avançons sauf que, pour l'instant, nous ne répondons pas aux points 1 et 2 soulevés juste avant la conclusion : le taux est fixe et ne concerne que les échanges entre dollars et euros.
Comme on peut trouver les cours journaliers sur internet, il devrait donc être possible de récupérer ces derniers. Je vais utiliser le site officiel de la BCEBanque Centrale Européenne :
BCE cours journalierCours de l'Euro
On nous y propose quatre choix de téléchargement, dont deux vont plus particulièrement attirer notre attention : les formats XML et CSV. Notez que ces fichiers sont mis à jour aux environs de 16 h 00 CETCommon European Time les jours ouvrables, sauf en cas de fermeture du TARGETSystème de gestion des échanges.
IV-C. Récupérer un fichier ▲
Je vais rapidement passer sur cet aspect de l'application, car ce n'est pas le sujet du tutoriel. Je vais utiliser les composants Indy fournis, et plus particulièrement TidHttp. FAQ Delphi sur IdHTTPFAQ Delphi sur IdHTTP
function
GetFichierCours: String
;
var
FStream : TFileStream;
Emplacement : String
;
begin
Emplacement:=TPath.GetPublicPath; //obtenir le dossier public
FStream:=TFileStream.Create(TPath.Combine(Emplacement,'eurofxref-daily.xml'
), fmCreate);
try
With
TIdHttp.Create(nil
) do
try
try
// adresse constante, qui pourrait être pourtant sujette
// à changements. Prévoir une adresse passée par paramètre
// serait peut-être mieux
Get('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'
, FStream);
result:=FormatDateTime('aaaa-mm-jj'
,date); // attention il ne s'agit pas de la date du cours
except
on
e: Exception do
Result:='Pas de Connexion à la BCE'
;
end
;
finally
Free;
end
;
finally
FStream.Free;
end
;
end
;
Récupérer un fichier de cette manière permet de toujours avoir un fichier de référence au cas où il serait impossible de charger le fichier du jour pour une raison quelconque. Une vérification de la date du fichier pourrait éviter un téléchargement à chaque lancement de l'application. Attention toutefois : la date du cours n'est pas forcément la date du fichier.
A contrario, nous avons donc ce que je traiterais d'une « petite pollution » dans notre espace de stockage.
Bien sûr, la même chose peut se faire pour le format CSV. Toutefois, nous avons affaire à un fichier compressé (ZIP) qu'il va donc falloir décompresser pour obtenir ce fameux fichier CSV. Depuis peu, les versions de Delphi ont une unité System.Zip qui permettra de le faire. Pour la suite, je me contenterai du fichier XML.
IV-D. « XML, vous avez dit XML ? » ▲
Premier face à face ou pas avec ce genre de fichier ? RAD Studio peut nous aider à traiter les documents XML et nous fournir, par l'intermédiaire d'un « Expert de liaison de données XML », tout le code nécessaire pour traiter le fichier téléchargé.
Pour obtenir la génération de l'unité de traitement :
- Créez une fiche vierge ;
- Posez un composant TXMLDocument ;
- Indiquez le nom du fichier préalablement téléchargé ;
- Cliquez avec le bouton droit de la souris sur le composant et utilisez l'expert.
Malheureusement, l'expert ne réussit pas à nous fournir une unité totalement utilisable pour le document eurofxref-daily.xml. Si l'on arrive facilement à accéder aux informations générales du document (envoyeur, sujet), il est impossible d'accéder à ce qui nous intéresse le plus : les cours de conversion. Qui est le coupable ? Soit le gabarit utilisé par la BCE, soit l'expert qui n'arrive pas à comprendre ce gabarit. Je vous laisse le choix du verdict.
Notons que cela aurait fait très propre et très professionnel !
Sans m'étendre trop sur le sujet, il est toujours possible de modifier un peu le fichier puis de réutiliser l'expert pour obtenir une unité qui exploitera parfaitement le XML. Voir une discussion sur ce sujet1. Toutefois, cela déborde largement d'un niveau débutant, voire confirmé.
IV-E. Ma solution pour exploiter le fichier▲
L'objectif est d'obtenir une liste de couples de valeurs (Devise, Cours) et au passage la date de publication de cette liste, soit, en étudiant le document XML, tout ce qui est compris entre les balises <Code> et </Code>. Il me faut donc trouver un moyen pour n'avoir que cette partie en mémoire.
Au lieu d'utiliser un TfileStream, je vais utiliser un TMemoryStream au moment du téléchargement.
Après récupération du fichier, je fais une recherche de position des deux balises pour modifier le contenu de mon fichier.
var
MStream : TMemoryStream;
EuroDaily : IXMLDocument;
posDebutcube,posFinCube : Integer
;
// ...
begin
EuroDaily:=TXMLDocument.Create(Self
);
MStream:=TMemoryStream.Create;
try
With
TIdHttp.Create(nil
) do
try
try
// récupération du fichier via internet
Get('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'
, MStream);
fStream.Position:=0
;
EuroDaily.LoadFromStream(MStream);
// recherche de position des balises
posDebutCube:=Pos('<Cube>'
,EuroDaily.XML.Text);
posFinCube:=Pos('</Cube>'
,EuroDaily.XML.Text)+7
;
// modification du contenu (uniquement ce qui est entre les deux balises EuroDaily.XML.Text:=Copy(EuroDaily.XML.Text,posDebutCube,posFinCube-PosDebutCube);
// ...
finally
Free;
end
;
finally
EuroDaily.Free;
MStream.Free;
End
;
À ce stade, j'obtiens quelque chose de plus compact, mais d'encore peu exploitable.
<Cube
time
=
"2017-03-31"
>
<Cube
currency
=
"USD"
rate
=
"1.0691"
/>
<Cube
currency
=
"JPY"
rate
=
"119.55"
/>
<Cube
currency
=
"BGN"
rate
=
"1.9558"
/>
<Cube
currency
=
"CZK"
rate
=
"27.030"
/>
<Cube
currency
=
"DKK"
rate
=
"7.4379"
/>
<Cube
currency
=
"GBP"
rate
=
"0.85553"
/>
<Cube
currency
=
"HUF"
rate
=
"307.62"
/>
<Cube
currency
=
"PLN"
rate
=
"4.2265"
/>
<Cube
currency
=
"RON"
rate
=
"4.5525"
/>
<Cube
currency
=
"SEK"
rate
=
"9.5322"
/>
<Cube
currency
=
"CHF"
rate
=
"1.0696"
/>
<Cube
currency
=
"NOK"
rate
=
"9.1683"
/>
<Cube
currency
=
"HRK"
rate
=
"7.4465"
/>
<Cube
currency
=
"RUB"
rate
=
"60.3130"
/>
<Cube
currency
=
"TRY"
rate
=
"3.8894"
/>
<Cube
currency
=
"AUD"
rate
=
"1.3982"
/>
<Cube
currency
=
"BRL"
rate
=
"3.3800"
/>
<Cube
currency
=
"CAD"
rate
=
"1.4265"
/>
<Cube
currency
=
"CNY"
rate
=
"7.3642"
/>
<Cube
currency
=
"HKD"
rate
=
"8.3074"
/>
<Cube
currency
=
"IDR"
rate
=
"14237.94"
/>
<Cube
currency
=
"ILS"
rate
=
"3.8853"
/>
<Cube
currency
=
"INR"
rate
=
"69.3965"
/>
<Cube
currency
=
"KRW"
rate
=
"1194.54"
/>
<Cube
currency
=
"MXN"
rate
=
"20.0175"
/>
<Cube
currency
=
"MYR"
rate
=
"4.7313"
/>
<Cube
currency
=
"NZD"
rate
=
"1.5309"
/>
<Cube
currency
=
"PHP"
rate
=
"53.658"
/>
<Cube
currency
=
"SGD"
rate
=
"1.4940"
/>
<Cube
currency
=
"THB"
rate
=
"36.724"
/>
<Cube
currency
=
"ZAR"
rate
=
"14.2404"
/>
J'ai encore de nombreuses balises, ce qui est normal, mais aussi une mise en forme (tabulations, sauts de lignes). Fort de mes tentatives du chapitre II.B, je vais tout d'abord remplacer les points décimaux par le séparateur décimal utilisé par le poste :
EuroDaily.XML.Text:=StringReplace(EuroDaily.XML.Text,'.'
,FormatSettings.DecimalSeparator,[rfReplaceAll]);
Reste à exploiter ce résultat ! Depuis plusieurs années déjà, nous avons la possibilité d'utiliser les expressions régulières. Récemment, c'est devenu une de mes marottes, et c'est ce que je vais utiliser pour récupérer les informations en tant que résultat de ma fonction pour la date et dans un TStringList pour les cours :
regExp:=TRegEx.Create('.*<Cube\stime="(?<Date>.{10}).*|.*<Cube\scurrency="(?<Devise>.{3}).*="(?<Cours>.*)"/>.*'
);
Matches:=regExp.Matches(EuroDaily.XML.Text);
for
unMatch in
Matches do
begin
if
Trim(unMatch.Groups.Item['Date'
].Value)<>''
then
result:=unMatch.Groups.Item['Date'
].Value
else
Liste.AddPair(unMatch.Groups.Item['Devise'
].Value,unMatch.Groups.Item['Cours'
].Value);
end
;
Contenu de Liste au 31 Mars 2017 |
USD=1,0691 |
Un tri s'impose pour avoir un accès rapide aux couples de valeurs (Liste.Sort).
Toutes ces étapes peuvent être testées dans un premier temps dans l'unité principale à l'aide de composants (mémo et bouton), mais pour la finalisation du projet, c'est l'unité métier qui va récupérer ce code.
Il nous faudra pour cela ajouter à l'objet :
- Une propriété Liste de type TStringList (ne pas oublier de créer/libérer l'objet) ;
- Une propriété qui contiendra le texte de la date (<Cube time="2017-03-31">).
À la création de l'interface, après la création de l'objet TConvertisseur, nous pourrons facilement mettre à jour les zones de notre interface utilisateur : la ComboBox et l'indication de la date du cours.
Le choix d'une TStringList s'impose en première instance si l'on regarde le contenu obtenu : un ensemble de couples « nom=valeur » que cette classe sait parfaitement gérer. Toutefois, j'utiliserai dans mon programme final un TDictionnary tout aussi efficace et qui me permettra « d'oublier » de libérer son utilisation.
Tout est maintenant en place pour finaliser le programme que vous pourrez retrouver ici
V. Remerciements▲
Je tiens tout d'abord à citer Cary Jensen et ses différentes vidéos sur ce sujet, sources de référence principales de ce tutoriel.
CodeRage 7 - Cary Jensen - LiveBindings Expressions and Side Effects
Différentes vidéos de Cary-Jensen sur le sujet
Tous mes remerciements vont à Gilles (gvasseur58) et Jean-Luc (Alcatîz) pour leur relecture et leurs conseils techniques et à Claude (ClaudeLeloup) pour ses corrections orthographiques et grammaticales. Enfin une pensée particulière à mon « cobaye » Nabil (Nabil74) qui, tel le Candide de Voltaire, par ses questions, m'a permis d'améliorer mes arguments et programmes.