I. Introduction▲
L'évaluateur d'expressions fait partie du cœur des LiveBindings. Ce tutoriel a pour but de comprendre comment il fonctionne, mais aussi comment nous pourrions détourner cette fonctionnalité pour l'utiliser dans un programme. En bref, nous souhaiterions avoir une procédure qui pourrait interpréter des formules simples.
Point de mise en garde aux détenteurs d'une version Starter : aucun concepteur visuel de liaison ne sera utilisé dans cet épisode.
Découvrir comment les expressions sont traitées nous permettra de mieux aborder l'utilisation des LiveBindings.
Vous pourrez retrouver les sources de tous les programmes Formules.zip.
II. Premiers pas : expressions simples▲
Avant des explications plus techniques, un premier programme (FormulesSimples.exe) est proposé pour visualiser les explications qui suivent.
L'objectif est simple : pouvoir saisir une expression (une formule arithmétique simple) dans une zone de texte et en fournir le résultat dans une zone d'affichage.
Jusqu'à l'apparition des LiveBindings, RAD Studio ne nous permettait pas de faire ce genre de chose : il fallait ajouter un interpréteur de formules venant de sources tierces ou faire le nôtre. C'est maintenant devenu possible, pour peu que l'on ait connaissance des unités nécessaires à inclure dans nos projets.
Il existe plusieurs manières de trouver ces unités :
- ouvrir un projet existant utilisant déjà les LiveBindings ;
- faire une recherche avec le mot clé Bind dans notre installation de RAD Studio (<répertoire d'installation>\source\databinding\engine).
Puisque les unités sources ne sont pas fournies avec la version Starter, il faudra chercher dans le répertoire <répertoire d'installation>\include\windows\rtl.
La seconde méthode est particulièrement intéressante puisqu'elle permet d'avoir un aperçu de ces unités.
Évidemment, la partie délicate est de retrouver les unités utiles !
System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys
Le code pour évaluer notre expression est relativement compact :
procedure
TMainForm.EvaluerClick(Sender: TObject);
var
Scope: IScope; /// une portée
Expression: TBindingExpression; /// une expression
ValueInt: IValue; /// le résultat de l'expression
Value: TValue; /// interprétation du résultat
begin
Scope := TDictionaryScope.Create;
try
// création de l'expression, celle-ci est déjà vérifiée syntaxiquement
Expression := TBindings.CreateExpression(Scope, maFormule.Text);
// récupération de l'évaluation de la formule
ValueInt := Expression.Evaluate;
// Rechercher de quel type il s'agit (via les RTTIs)
Value := ValueInt.GetValue;
if
Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
else
// affichage du résultat
Resultat.Caption := Value.ToString;
except
On
E:Exception do
Showmessage(E.Message
);
end
;
end
;
II-A. Concepts importants : scopes et wrappers▲
S'il n'est pas facile de vulgariser ces concepts informatiques de scopes et de wrappers, nous allons cependant essayer !
II-A-1. Scopes▲
Notion apparue dans le milieu des années 50 avec LISP, il est fort à parier que selon les langages et leur modernisation, la définition des scopes a évolué dans le temps. Traduit littéralement, nous obtenons des résultats comme : portée, étendue, champ ou périmètre d'une recherche, domaine de compétence, etc.
En fait, avec Delphi, vous utilisez un scope sans le savoir : l'opérateur « . »
Si nous recherchons sa définition dans certains dictionnaires francophones, négligeant le grec ancien, ces derniers nous indiquent que ce mot est d'origine anglophone, du moins pour son usage contemporain. Mais c'est oublier nos périscopes, télescopes, stéthoscopes et autres préfixes !
C'est plutôt de ces derniers que je vais tirer mon explication. Scope vient du grec ancien ?????? (prononcez « skopéô ») qui signifie observer. En français, scope est utilisé comme suffixe pour former des noms d'appareils d'observation. Et c'est bien ce que nous allons faire : créer un système permettant d'observer quelque chose. Si nous remplaçons les miroirs, loupes, moteurs et autres d'un télescope par des constantes, variables, méthodes et autres, nous sommes alors assez proches du concept. Toutefois, pour finaliser notre image, à cet élément informatique, il faut ajouter la notion d'étendue : la racine du mot est alors le terme informatique suivi, bien entendu, de son suffixe scope.
Compliqué ? Oui, mais voyez ça comme une sorte de trousseau de clés magnétiques qui va permettre d'atteindre quelque chose. Dans le source ci-dessus, j'ai créé une sorte de formuloscope !
II-A-2. Wrappers▲
Que serait un télescope sans son opérateur pour interpréter les images observées ? Rien et il en est de même pour les scopes. À ces derniers, il faut associer les wrappers.
Là encore, une traduction littérale est limitée : emballages, paquets ou encore bande d'adhésif. Ces traductions ne nous aident pas vraiment à comprendre le sens exact du mot !
Pour continuer dans les métaphores, je vais recourir à mon trousseau de clés magnétiques. Il faut bien que celles-ci ouvrent quelque chose ! Imaginez maintenant un entrepôt dans lequel il y a des locaux fermés, eux-mêmes contenant des malles verrouillées, etc. Dans chaque endroit seront stockés des objets (terme bien connu informatiquement), mais aussi des outils (chariots, élévateurs, etc.) spécifiques ou non, en un mot :des méthodes. Voilà nos wrappers posés.
Le premier entrepôt, c'est l'unité System.Rtti qui contient des informations sur les types (ancêtre de classe, champs déclarés, attributs, etc.), mais nous n'utiliserons pas que celui-ci. C'est en effet à dessein que j'avais utilisé l'image des clés magnétiques, car ces informations pourraient aussi être dans la clé elle-même. Par exemple : une clé de couleur rouge n'ouvre que des serrures rouges. L'information est alors la couleur.
II-A-3. Débriefing ▲
Tout cela a quand même un prix : la taille de l'exécutable. Tous les anciens "delphistes"
auront remarqué qu'au fil du temps les applications sont devenues de plus en plus importantes en taille. Une des raisons en est bien évidemment tout ce code en plus.
En savoir plus sur les RTTI :
II-B. Retour au programme ▲
Ce premier programme ne nous propose que peu de choses :
- des éléments mathématiques :
- constantes : nil, True, False, Pi ;
- opérateurs arithmétiques : + - * / ;
- opérateurs logiques : = <> < <= > >= ;
- parenthèses, (), pour changer l'ordre des opérations.
- de la concaténation de chaînes :
- une chaîne sera encadrée par des apostrophes ' ou des guillemets " ;
- pour ajouter (concaténer) deux chaînes, on utilise l'opérateur +.
Cette formule n'est connectée à aucun élément de ma forme et n'offre aucune des opérations qu'un BindingsLists nous propose et auxquelles j'avais fait allusion lors de mon Introduction aux LiveBindings - Épisode 1Introduction aux LiveBindings - Episode 1 (Chapitre III.2.2), à savoir la possibilité d'utiliser des méthodes prédéfinies de chaîne telles qu'UpperCase, LowerCase, SubString, de formatage telles que Format, FormatDateTime, mais aussi de petites fonctions de test comme IfAll, IfAny, IfThen.
III. Ajout des méthodes « usine »▲
Avec le deuxième programme (FormulesPlus.exe) de la série, nous allons ajouter à notre évaluateur de formules, les méthodes proposées par Embarcadero. Pour cela, nous allons améliorer notre formuloscope en lui adjoignant un wrapper.
III-A. Modifications apportées au programme précédent▲
Important : il ne faut pas oublier de mentionner l'unité utilisée dans la liste de la clause uses.
Dans la liste des uses, j'ajoute donc l'unité contenant ces méthodes :
// Ajout
System.Bindings.Methods
// ,Data.Bind.EngExt {BindingsList méthodes}
// ,Data.Bind.DBScope {méthodes préfixées par DBUtils_}
Puis, dans le programme, je vais indiquer que mon scope va utiliser les fonctions de cette unité.
Scope := TDictionaryScope.Create;
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
Pour faire bonne mesure et obtenir une liste des nouvelles méthodes proposées, j'ai aussi ajouté au programme un panneau contenant un TMemo qui se remplira via un bouton.
procedure
TMainForm.ObtenirClick(Sender: TObject);
var
AnArray : TArray<TMethodDescription>;
AMethode : TMethodDescription;
begin
// Obtenir les méthodes enregistrées
AnArray:= TBindingMethodsFactory.GetRegisteredMethods;
MethodesMemo.Clear;
for
AMethode in
AnArray do
begin
MethodesMemo.Lines.Add(AMethode.Name);
end
;
end
;
Cependant, si vous avez été assez curieux pour vérifier quelles étaient les méthodes proposées par un TBindingsList, vous ferez certainement remarquer qu'elles n'y sont pas toutes !
Vous vous apercevrez aussi, au niveau de l'image au-dessus, que certaines d'entre elles sont dans une unité différente (Data.Bind.EngExt) et que d'autres, également manquantes, ont le préfixe DBUtils.
Dans le code source du programme, j'ai mis en commentaire les unités à ajouter pour avoir accès aux mêmes méthodes. Le tableau ci-dessous récapitule les utilisations : en gras, les méthodes obtenues avec l'unité Data.Bind.EngExt, en italique, celles de l'unité Data.Bind.DBScope.
Attention, vous ne devrez pas simplement recompiler le programme, mais bien le reconstruire si vous voulez inclure ces unités.
Méthodes TBindingsList |
Méthodes « usine » System.Bindings.Methods |
CheckedState |
- |
III-B. Perspectives ouvertes▲
L'évaluateur de formules est donc modulable quant aux méthodes que nous pouvons y ajouter. De là à pouvoir ajouter nos propres méthodes, il n'y a qu'un petit pas à franchir. Ceux qui ont les versions de Delphi avec sources plongeront peut-être avec délice dans l'une de ces unités pour y voir d'un peu plus près (pour un coup d'œil rapide, je vous conseille Data.Bind.EngExt).
Pour les autres, il va falloir attendre (ou pas) le chapitre VI.
IV. Accéder aux classes (contrôles de la forme et objets) ▲
Avant d'ajouter nos propres méthodes, il est temps de voir comment nous devons procéder pour accéder aux éléments de notre forme, mais aussi, pourquoi pas, à des classes de notre unité.
C'est l'objectif de ce troisième programme (FormulesEtObjets.exe). Pour y parvenir, il faut inclure une unité dans la liste des unités à utiliser : System.Bindings.ObjEval.
// Ajout
,System.Bindings.ObjEval
;
IV-A. Accéder à Self▲
Cette partie sera purement anecdotique et n'est pas dans le programme. Je me permets de ne fournir que le code permettant d'accéder à Self dans une formule.
var
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Scope := WrapObject(MaFormule);
// ajout des méthodes «usine»
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
Expression := TBindings.CreateExpression(Scope, MaFormule.Text);
ValueInt := Expression.Evaluate;
Value := ValueInt.GetValue;
if
Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
else
if
Value.IsObject then
Resultat.Caption := Value.AsObject.ClassName
else
Resultat.Caption := Value.ToString;
end
;
L'astuce réside dans la création du scope de départ qui n'est pas un TDictionaryScope comme dans le programme précédent, mais directement une clé magnétique sur l'objet lui-même. Avec ce code, il serait possible d'utiliser une formule du genre Self.Height*Self.Width, truc presque totalement inutile, mais qui nous permettrait cependant de visualiser un peu ce que fait l'interpréteur d'expression d'un TBindingsList.
IV-B. Accéder à des contrôles nommés ▲
Le troisième programme va s'attacher à utiliser les propriétés des contrôles posés sur la forme. Après cette légère digression sur le Self, revenons au schéma établi par notre précédent programme.
Pour accéder aux contrôles posés sur la forme, nous allons en quelque sorte ajouter un plan des lieux à notre trousseau de clés magnétiques. Chaque clé aura un nom unique, pas forcément identique au nom de l'objet auquel nous voulons accéder. En bref, elle aura un alias.
TDictionaryScope(Scope).Map.Add('Self'
, WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Formule'
, WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Bouton'
, WrapObject(Evaluer));
Vous remarquerez que j'ai deux noms différents pour un même objet : c'est une façon un peu cavalière d'utiliser le Self du chapitre précédent, je vous l'accorde, mais je n'ai pas trouvé mieux.
Désormais, nous pouvons accéder aux propriétés de ces objets dans nos formules à évaluer et ce via le nom déclaré.
Par exemple : Formule.width (ou Self.width) renverra la largeur de la zone de saisie (TEdit).
Les noms utilisés sont sensibles à la casse, contrairement aux noms de propriétés.
IV-C. Accéder à ses propres classes▲
À partir de cet instant, il devient évident de se poser la question : « Pourquoi ne pourrais-je pas accéder à mes propres classes et à leurs méthodes ? ». Eh bien oui, aussi simplement que pour les contrôles de notre forme, nous allons pouvoir accéder à des classes de notre propre cru.
Pour les besoins de la démonstration, j'ai donc créé deux classes. La première, baptisée TBarrillet, très simple, ne contient qu'une méthode nommée FaireTourner.
TBarillet = class
function
FaireTourner(Value : Integer
): Integer
;
end
;
La seconde, TPersonne, comprend deux propriétés (Nom et Prenom) et trois méthodes (NomComplet, EnVerlan et MettreUneBalle).
TPersonne = class
private
FNom : String
;
FPrenom : String
;
procedure
SetNom(const
Value: String
);
procedure
SetPrenom(const
Value : String
);
public
property
Nom : string
read
FNom write
SetNom;
property
Prenom : string
read
FPrenom write
SetPrenom;
constructor
Create(Nom,Prenom : String
);
function
NomComplet(Value : String
=''
) : String
;
function
EnVerlan(Value : String
=''
) : String
;
function
MetBalle(Value : Integer
): Integer
;
end
;
{ TPersonne }
constructor
TPersonne.Create(Nom,Prenom : String
);
begin
FNom:=Nom;
FPrenom:=Prenom;
end
;
function
TPersonne.NomComplet(X : String
) : String
;
begin
result:=Format('%s %s'
,[FPrenom,FNom]);
end
;
function
TPersonne.EnVerlan(Value : String
): String
;
begin
if
Value.IsEmpty then
Value:=NomComplet;
Result:=ReverseString(Value)
end
;
function
TPersonne.MetBalle(Value: Integer
): Integer
;
begin
result:=System.Random(Value) mod
6
+ 1
;
end
;
procedure
TPersonne.SetNom(const
Value: String
);
begin
FNom:=Value;
end
;
procedure
TPersonne.SetPrenom(const
Value: String
);
begin
FPrenom:=Value;
end
;
Pour y accéder, il ne reste plus qu'à les ajouter à notre plan des lieux.
// Objets créés au runtime
Personne:=TPersonne.Create('Girard'
,'Serge'
);
Pistolet:=TBarillet.Create;
...
// Classes à ajouter,
TDictionaryScope(Scope).Map.Add('Auteur'
, WrapObject(Personne));
TDictionaryScope(Scope).Map.Add('Roulette'
,WrapObject(Pistolet));
Qui a dit que l'informatique ne pouvait pas être amusante ? Vous pouvez faire jouer l'auteur à la roulette russe en testant la formule :
IfThen(Auteur.MetBalle(6
)=Roulette.FaireTourner(50
),'Pan'
,'Clic'
)
Une petite explication s'impose toutefois pour les méthodes NomComplet et EnVerlan de notre classe TPersonne. En effet, si vous étudiez leur déclaration, vous remarquerez que j'ai été obligé d'ajouter un paramètre à ces fonctions, sinon l'évaluateur d'expressions me renvoyait une valeur vide. Je n'ai aucune idée quant à l'explication de ce problème (version, bogue ou autre).
Voici le code source complet du programme :
unit
MainObjetsNommes;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys,
System.Bindings.Methods
// Ajouts
,System.Bindings.ObjEval
,System.StrUtils {pour ReverseString utilisé par TPersonne}
;
type
TPersonne = class
private
FNom : String
;
FPrenom : String
;
procedure
SetNom(const
Value: String
);
procedure
SetPrenom(const
Value : String
);
public
property
Nom : string
read
FNom write
SetNom;
property
Prenom : string
read
FPrenom write
SetPrenom;
constructor
Create(Nom,Prenom : String
);
function
NomComplet(const
Value : String
=''
) : String
;
function
EnVerlan(const
Value : String
=''
) : String
;
function
MetBalle(const
Value : Integer
): Integer
;
end
;
TBarillet = class
function
FaireTourner(Value : Integer
): Integer
;
end
;
TMainForm = class
(TForm)
Memo1: TMemo;
Label2: TLabel;
MaFormule: TEdit;
Evaluer: TButton;
Label1: TLabel;
Resultat: TLabel;
procedure
EvaluerClick(Sender: TObject);
procedure
FormCreate(Sender: TObject);
procedure
FormDestroy(Sender: TObject);
private
{ Déclarations privées }
Personne : TPersonne;
Pistolet : TBarillet;
public
{ Déclarations publiques }
end
;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure
TMainForm.EvaluerClick(Sender: TObject);
var
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Scope := TDictionaryScope.Create;
// des contrôles présents sur la forme
TDictionaryScope(Scope).Map.Add('Self'
, WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Formule'
, WrapObject(MaFormule));
TDictionaryScope(Scope).Map.Add('Bouton'
, WrapObject(Evaluer));
// classes créées au runtime
TDictionaryScope(Scope).Map.Add('Auteur'
, WrapObject(Personne));
TDictionaryScope(Scope).Map.Add('Roulette'
,WrapObject(Pistolet));
// ajout des "méthodes usine"
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
// création de l'expression, celle-ci est déjà vérifiée syntaxiquement
Expression := TBindings.CreateExpression(Scope, MaFormule.Text);
// rechercher de quel type il s'agit (via les RTTIs)
ValueInt := Expression.Evaluate;
Value := ValueInt.GetValue;
if
Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
// cette fois, je vérifie qu'il ne s'agit pas d'un objet
else
if
Value.IsObject then
// s'il s'agit d'un objet, mon résultat sera son nom de classe
Resultat.Caption := Value.AsObject.ClassName
else
// affichage du résultat
Resultat.Caption := Value.ToString;
end
;
procedure
TMainForm.FormCreate(Sender: TObject);
begin
Personne:=TPersonne.Create('Girard'
,'Serge'
);
Pistolet:=TBarillet.Create;
end
;
procedure
TMainForm.FormDestroy(Sender: TObject);
begin
Personne.Free;
Pistolet.Free;
end
;
{ TPersonne }
constructor
TPersonne.Create(Nom,Prenom : String
);
begin
FNom:=Nom;
FPrenom:=Prenom;
end
;
function
TPersonne.NomComplet(const
Value : String
) : String
;
begin
result:=Format('%s %s'
,[FPrenom,FNom]);
end
;
function
TPersonne.EnVerlan(const
Value : String
): String
;
begin
if
Value.IsEmpty
then
Result:=ReverseString(NomComplet)
else
Result:=ReverseString(Value);
end
;
function
TPersonne.MetBalle(const
Value: Integer
): Integer
;
begin
result:=System.Random(Value) mod
6
+ 1
;
end
;
procedure
TPersonne.SetNom(const
Value: String
);
begin
FNom:=Value;
end
;
procedure
TPersonne.SetPrenom(const
Value: String
);
begin
FPrenom:=Value;
end
;
{ TBarillet}
function
TBarillet.FaireTourner(Value: Integer
): Integer
;
begin
result:=system.Random(Value) mod
6
+ 1
;
end
;
end
.
V. Utiliser l'évaluateur dans une DLL▲
Où est-ce que je veux en venir ? Au cours de ma carrière, je me suis heurté plusieurs fois à des besoins de conversion de quantité entre deux mesures et donc à la nécessité d'utiliser des formules de conversion pas toujours aussi simples qu'une simple division ou multiplication (comme entre mètres et centimètres). Actuellement, j'utilise principalement Firebird comme SGBDRSystème de Gestion de Base de Données Relationnel. Or, ce dernier permet d'utiliser des fonctions définies par l'utilisateur (UDFUser Defined Functions) : de là à envisager d'utiliser ce que nous venons de voir, le saut était encore une fois tentant.
V-A. La DLL▲
Cette fois, il ne s'agit plus d''écrire un application fiche VCL, mais une bibliothèque de liaison dynamique (DLL)
et de pouvoir exporter une fonction adaptée à Firebird. Une fonction avec deux arguments sera nécessaire, l'un pour la colonne numérique à traiter, l'autre pour contenir la formule et retournant une valeur numérique. Il faudra bien entendu tenir compte des spécificités de Firebird et passer pour notre formule, non pas une chaîne de caractères, mais un pointeur sur le début de cette chaîne (et qui plus est, un pointeur de type PAnsiChar).
function
EvalFormule(MaColonne: Extended
; Maformule : PAnsiChar) : Extended
; cdecl
;
Le reste du code utilise le schéma déjà vu dans nos différents programmes :
unit
DllUnit;
interface
uses
System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys;
function
EvalFormule(MaColonne: Extended
; Maformule : PAnsiChar) : Extended
; cdecl
;
implementation
function
EvalFormule(MaColonne: Extended
; Maformule : PAnsiChar) : Extended
;
var
Formule : String
;
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Formule:=Maformule;
Scope := TDictionaryScope.Create;
try
// création de l'expression, celle-ci est déjà vérifiée syntaxiquement
Expression := TBindings.CreateExpression(Scope, Formule);
// récupération de l'évaluation de la formule
ValueInt := Expression.Evaluate;
// Rechercher de quel type il s'agit (via les RTTIs)
Value := ValueInt.GetValue;
if
Value.IsEmpty then
Result:=0
else
Result:=MaColonne*Value.AsExtended;
except
Result:=0
;
end
;
end
;
end
.
V-B. Tester la DLL▲
Avant d'utiliser cette DLL avec Firebird, il vaut mieux passer par une phase de test, ce qui est l'objectif du petit programme suivant (FormuleTestDLL) :
unit
MainDllTest;
// objectif : tester la dll produite précédemment
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TMainForm = class
(TForm)
Label1: TLabel;
Valeur: TEdit;
Label3: TLabel;
resultat: TLabel;
Evaluer: TButton;
procedure
EvaluerClick(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
MainForm: TMainForm;
// déclaration de la fonction
function
EvalFormule(MaColonne: Extended
; Maformule : PAnsiChar) : Extended
; cdecl
; external
{ ici, bien sûr, vous changerez le chemin de la DLL en fonction de
votre environnement}
'F:\Livebindings\Programmes\VCL\Formules\Win32\Debug\FormuleDLLFB.dll'
;
implementation
{$R *.dfm}
// exécution du test
procedure
TMainForm.EvaluerClick(Sender: TObject);
var
VColonne, R : Extended
;
Formule : AnsiString;
begin
// valeur saisie (ma future colonne)
VColonne:=StrToFloatDef(Valeur.Text,0
);
// formule à utiliser (libellé du bouton)
Formule:=Evaluer.Caption;
// utilisation de la fonction externe (contenue dans la DLL)
R:=EvalFormule(VColonne,PAnsiChar(Formule));
// affichage du résultat
Resultat.Caption:=R.ToString;
end
;
end
.
V-C. Utiliser la DLL avec Firebird (UDF)▲
Pour utiliser cette DLL avec Firebird, quelques étapes seront nécessaires :
- Copier celle-ci dans le répertoire UDF de l'installation de Firebird, sauf indication contraire contenue dans le fichier de configuration de Firebird (firebird.conf section « External Function (UDF) Paths/Directories » soit en gros, la valeur d'UdfAccess). Bien sûr, nous devrons tenir compte de la version en bits de Firebird (32 ou 64) et copier la version correspondante ;
- Déclarer la fonction au niveau de la base de données qui va l'utiliser.
DECLARE
EXTERNAL
FUNCTION
F_FORMULE
DOUBLE
PRECISION
, CSTRING(
250
)
RETURNS
DOUBLE
PRECISION
BY
VALUE
ENTRY_POINT 'EvalFormule'
MODULE_NAME 'FormuleDLLFB'
;
Il ne nous reste plus qu'à faire un test, par exemple via ce code SQL :
select
f_Formule(
1
,'1*100'
)
from
RDB$DATABASE
; -- 100.00000000
select
f_Formule(
1
,'1.24*100'
)
from
RDB$DATABASE
; -- 124.00000000
VI. Étoffer par l'ajout de méthodes personnelles▲
Le sujet du dernier programme de la série (FormulesEtMethodes) est d'ajouter nos propres méthodes pour ne pas rester déçu du peu d'opérateurs mathématiques ou de routines de chaînes proposés et ce malgré l'ajout des méthodes du chapitre III. Bien sûr, dans le chapitre IV.C, nous avons vu que nous pouvions, à condition de les recenser, utiliser les méthodes d'une classe, mais même si cela peut répondre à certains besoins, cela reste insuffisant.
Ce que nous devons faire est, je cite : « définir des fonctions qui renverront un emplacement pour des expressions bidirectionnelles ». Oui, écrit comme cela, c'est totalement obscur. Maintenant, si je vous fais part du fait qu'une interface (IInvokable) a déjà été prévue à cet effet, je pense que, comme moi, vous serez soulagé !
Selon mon système bien rodé, je vais commencer par ajouter deux autres unités :
// Ajout
System.Bindings.Consts, // pour la gestion des erreurs en utilisant les messages prédéfinis
System.StrUtils // pour la routine de chaînes (ReverseString)
VI-A. Créer une méthode ou deux▲
Pour cet exemple , je vais créer deux nouvelles méthodes afin de pouvoir faire les tests :
- une méthode simple renvoyant une valeur, le reste de la division de deux entiers (modulo), mais utilisant quand même deux paramètres (le dividende et le diviseur) qui devront être des entiers ;
- une méthode renvoyant une chaîne de caractères : je resterai sur la classique ReverseString.
Dans un premier temps, je les déclarerai directement dans la partie privée de ma forme :
private
{ Déclarations privées }
function
Modulo : IInvokable;
function
Alenvers : IInvokable;
VI-A-1. Alenvers ▲
Commençons par la plus facile des deux pour en comprendre l'astuce.
function
TMainForm.Alenvers: IInvokable;
begin
Result := MakeInvokable(function
(Args: TArray<IValue>): IValue
var
v1: IValue;
begin
if
Length(Args) <> 1
then
raise
EEvaluatorError.Create(Format(sUnexpectedArgCount, [1
,Length(Args)]))
else
begin
v1:=Args[0
];
Exit(TValueWrapper.Create(ReverseString(v1.GetValue.AsString)));
end
;
end
);
end
;
Quoi ? À l'intérieur d'une fonction, nous retrouvons le corps d'une autre fonction ! Pour les débutants comme pour les vieux "delphistes" qui ne sont pas au jus des dernières nouveautés du langage, il y a de quoi être surpris ! Étudions ce mécanisme pas à pas.
VI-A-1-a. Result:=MakeInvokable ▲
À la lecture de l'introduction du chapitre, c'est compréhensible. Nous voulons obtenir une adresse vers une fonction « que nous pourrions utiliser pour des expressions bidirectionnelles ». D'ailleurs, je trouve qu'une mauvaise traduction littérale de MakeInvokable : « rendre invocable » est assez drôle, du genre « j'invoque les esprits ». Du coup, il est moins surprenant de comprendre que c'est une fonction que nous allons retrouver en argument.
VI-A-1-b. function(Args: TArray<IValue>): IValue ▲
La déclaration de la fonction, ainsi que son type de résultat, surprend également. Pourquoi des IValue que je définirais en gros comme des interfaces vers un type de valeur encore inconnu ?
N'oubliez pas que nous allons utiliser cela via l'interpréteur et que ce dernier utilise les RTTI. Laissons donc les RTTI faire le travail à notre place. Gros avantage pour nous en ce qui concerne les arguments de la fonction, nous n'avons qu'à en mettre un certain nombre, c'est tout, et il sera ensuite à la charge de la fonction, dans son corps, de vérifier ces derniers.
VI-A-1-c. if Length(Args) <> 1 then raise EEvaluatorError ▲
Cette instruction nous permettra de vérifier que le nombre d'arguments est correct et, cerise sur le gâteau, de lever une erreur dans le cas contraire. De plus, comme ce type d'erreur est déjà prévu, nous avons même déjà le message tout prêt dans l'unité System.Bindings.Consts ( sUnexpectedArgCount).
VI-A-1-d. Résultat de la fonction : Exit▲
Reste à fournir le résultat de notre fonction appelée, mais pour cela il faut d'abord obtenir la valeur de notre argument, ce qui sera chose fait grâce aux RTTI et leur fonction GetValue, et enfin utiliser ReverseString pour obtenir ce que nous voulons. Il nous faut également renvoyer, non pas une chaîne de caractères, mais cette chaîne « dans son emballage », donc une IValue. Ceci sera fait via TvalueWrapper.Create. Revoilà donc nos fameux wrappers du chapitre II.A.2 !
VI-A-2. Modulo▲
Une fois le mécanisme démonté, il est plus facile d'écrire cette seconde fonction. La seule différence réside surtout dans le nombre d'arguments et, bien sûr, dans la récupération de ces derniers ( GetValue.AsInteger).
function
TMainForm.Modulo: IInvokable;
begin
Result := MakeInvokable(function
(Args: TArray<IValue>): IValue
var
v1: IValue;
v2: IValue;
begin
if
Length(Args) <> 2
then
raise
EEvaluatorError.Create(Format(sUnexpectedArgCount, [2
,Length(Args)]))
else
begin
v1:=Args[0
];
v2:=Args[1
];
Exit(TValueWrapper.Create(v1.GetValue.AsInteger mod
v2.GetValue.AsInteger));
end
;
end
);
end
;
Pour les puristes, les deux arguments devraient aussi être vérifiés : il faut que ce soit des entiers.
Je n'ai pas voulu trop surcharger le code : pour vérifier que ce sont bien des entiers, il faudrait faire un test sur leur type.
If
NOT
((v1.GetType.Kind in
[tkInteger,tkInt64])
AND
(v2.GetType.Kind in
[tkInteger,tkInt64]))
then
raise
EevaluatorError.Create('Uniquement applicable à des entiers'
) ;
VI-B. Inclure ces unités▲
Tout cela est bien beau, mais encore faut-il que ces nouvelles clés soient accrochées à notre trousseau. Autrement écrit, il est nécessaire d'appliquer ce que nous avons fait à nos propres classes au chapitre IV.C, mais cette fois-ci à nos fonctions.
Ce sera chose faite grâce à la fonction RegisterMethod de l'unité TBindingMethodsFactory.
En argument, nous lui fournirons non seulement l'adresse de la méthode créée, mais aussi le nom que nous utiliserons pour l'appeler, une description, etc. Je vous invite en en lire la syntaxe complète dans DocWiki Embarcadero : syntaxe TMethodDescription.
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Alenvers,
'Reverse'
,
'ReverseString'
, ''
, True
,
''
,
nil
));
Vous remarquerez que le nom qui sera utilisé, à savoir Reverse, n'est pas forcément le même que celui de la fonction. Par contre, je vous le rappelle encore une fois, la casse est importante.
Enfin, tout comme vous le feriez pour un objet, n'oubliez pas de libérer cette fonction quand vous n'en avez plus besoin.
TBindingMethodsFactory.RegisterMethod('Reverse'
);
Voici le code source complet :
unit
MainMethodes;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
System.Rtti,
System.Bindings.Helper,
System.Bindings.EvalProtocol,
System.Bindings.Expression,
System.Bindings.EvalSys,
System.Bindings.Methods,
// Ajout
System.Bindings.Consts, // pour la gestion des erreurs en utilisant les messages prédéfinis
System.StrUtils, Vcl.ComCtrls // pour la routine de chaînes (ReverseString)
;
type
TMainForm = class
(TForm)
Evaluer: TButton;
PageControl1: TPageControl;
ZoneTest: TTabSheet;
Plus: TTabSheet;
Memo1: TMemo;
Label2: TLabel;
MaFormule: TEdit;
Label1: TLabel;
Resultat: TLabel;
Obtenir: TButton;
MethodesMemo: TMemo;
procedure
EvaluerClick(Sender: TObject);
procedure
FormCreate(Sender: TObject);
procedure
ObtenirClick(Sender: TObject);
procedure
FormDestroy(Sender: TObject);
private
{ Déclarations privées }
function
Modulo : IInvokable;
function
Alenvers : IInvokable;
procedure
EnregistrerMesMethodes;
procedure
LibererMesMethodes;
public
{ Déclarations publiques }
end
;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
function
TMainForm.Alenvers: IInvokable;
begin
Result := MakeInvokable(function
(Args: TArray<IValue>): IValue
var
v1: IValue;
begin
if
Length(Args) <> 1
then
raise
EEvaluatorError.Create(Format(sUnexpectedArgCount, [1
,Length(Args)]))
else
begin
v1:=Args[0
];
Exit(TValueWrapper.Create(ReverseString(v1.GetValue.AsString)));
end
;
end
);
end
;
procedure
TMainForm.EnregistrerMesMethodes;
begin
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Modulo,
'Mod'
,
'Modulo'
, ''
, True
,
''
,
nil
));
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Alenvers,
'Reverse'
,
'ReverseString'
, ''
, True
,
''
,
nil
));
end
;
procedure
TMainForm.EvaluerClick(Sender: TObject);
var
Scope: IScope;
Expression: TBindingExpression;
ValueInt: IValue;
Value: TValue;
begin
Scope := TDictionaryScope.Create;
Scope := TNestedScope.Create(Scope, TBindingMethodsFactory.GetMethodScope());
Expression := TBindings.CreateExpression(Scope, MaFormule.Text);
ValueInt := Expression.Evaluate;
Value := ValueInt.GetValue;
if
Value.IsEmpty then
Resultat.Caption := 'Rien à évaluer'
else
if
Value.IsObject then
Resultat.Caption := Value.AsObject.ClassName
else
Resultat.Caption := Value.ToString;
end
;
procedure
TMainForm.FormCreate(Sender: TObject);
begin
EnregistrerMesMethodes;
end
;
procedure
TMainForm.FormDestroy(Sender: TObject);
begin
LibererMesMethodes;
end
;
procedure
TMainForm.LibererMesMethodes;
begin
TBindingMethodsFactory.UnRegisterMethod('Mod'
);
TBindingMethodsFactory.UnRegisterMethod('Reverse'
);
end
;
function
TMainForm.Modulo: IInvokable;
begin
Result := MakeInvokable(function
(Args: TArray<IValue>): IValue
var
v1: IValue;
v2: IValue;
begin
if
Length(Args) <> 2
then
raise
EEvaluatorError.Create(Format(sUnexpectedArgCount, [2
,Length(Args)]))
else
begin
v1:=Args[0
];
v2:=Args[1
];
Exit(TValueWrapper.Create(v1.GetValue.AsInteger mod
v2.GetValue.AsInteger));
end
;
end
);
end
;
procedure
TMainForm.ObtenirClick(Sender: TObject);
var
AnArray : TArray<TMethodDescription>;
AMethode : TMethodDescription;
AStringList : TStringList;
begin
AStringList:=TStringList.Create;
try
// Obtenir les méthodes enregistrées
// pour en avoir une liste triée, je passe par une TStringList intermédiaire
AnArray:= TBindingMethodsFactory.GetRegisteredMethods;
for
AMethode in
AnArray do
begin
AStringList.Add(AMethode.Name);
end
;
AStringList.Sort;
MethodesMemo.Lines:=AStringList;
finally
AStringList.Free;
end
;
end
;
end
.
Il ne nous reste plus qu'à valider tout ça, par un test de ces méthodes
ou en vérifiant que ces dernières sont bien là.
Nous pourrons également tester les erreurs de syntaxe en saisissant une formule incorrecte.
VI-C. Un dernier pas : une unité indépendante▲
Bien, ces méthodes sont dans notre unité principale, pas mal, mais si nous voulions les utiliser dans plusieurs de nos programmes ?
Qu'à cela ne tienne, il suffit de mettre nos fonctions et l'enregistrement des méthodes dans une unité indépendante que nous utiliserons de la même manière que nous avons utilisé System.Bindings.Methods au chapitre III.
unit
MesMethodes;
interface
implementation
uses
System.Classes, System.SysUtils, System.Rtti, System.StrUtils,
System.Bindings.EvalProtocol,System.Bindings.Methods, System.Bindings.Consts;
function
Alenvers: IInvokable;
begin
Result := MakeInvokable(function
(Args: TArray<IValue>): IValue
var
v1: IValue;
begin
if
Length(Args) <> 1
then
raise
EEvaluatorError.Create(Format(sUnexpectedArgCount, [1
,Length(Args)]))
else
begin
v1:=Args[0
];
Exit(TValueWrapper.Create(ReverseString(v1.GetValue.AsString)));
end
;
end
);
end
;
function
Modulo: IInvokable;
begin
Result := MakeInvokable(function
(Args: TArray<IValue>): IValue
var
v1: IValue;
v2: IValue;
begin
if
Length(Args) <> 2
then
raise
EEvaluatorError.Create(Format(sUnexpectedArgCount, [2
,Length(Args)]))
else
begin
v1:=Args[0
];
v2:=Args[1
];
Exit(TValueWrapper.Create(v1.GetValue.AsInteger mod
v2.GetValue.AsInteger));
end
;
end
);
end
;
procedure
EnregistrerMesMethodes;
begin
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Modulo,
'Mod'
,
'Modulo'
, 'MesMethodes'
, True
,
''
,
nil
));
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
Alenvers,
'Reverse'
,
'ReverseString'
, 'MesMethodes'
, True
,
''
,
nil
));
end
;
procedure
LibererMesMethodes;
begin
TBindingMethodsFactory.UnRegisterMethod('Mod'
);
TBindingMethodsFactory.UnRegisterMethod('Reverse'
);
end
;
initialization
EnregistrerMesMethodes;
finalization
LibererMesMethodes;
end
.
Notez l'utilisation de initialization et finalization qui vont faire le travail à notre place (pas besoin d'écrire quoi que ce soit dans le programme qui va utiliser l'unité).
VII. Conclusion ▲
Au terme de cet article, j'espère que mon objectif qui était de faire comprendre une partie des mécanismes des LiveBindings est atteint. Rassurez-vous, vous n'aurez pas à faire à chaque fois toutes ces manipulations (et heureusement !). Posez un TBindingsList sur votre forme et tous ces mécanismes sont réalisés : un grand merci aux développeurs d'Embarcadero qui ont rendu le mécanisme transparent.
VIII. Sources de référence▲
Pour réaliser ce tutoriel, je me suis beaucoup appuyé sur quelques vidéos et blogs.
Source d'inspiration principale de ce tutoriel, la présentation de Cary Jensen lors du CodeRage 7 de fin 2012 : CodeRage 7 - Cary Jensen - LiveBindings Expressions and Side Effects.
D'autres blogs, vidéos ou tutoriels peuvent utiliser d'autres techniques ; je pense en particulier à l'article The Road To Delphi - A Quick guide to compile espressions ... ou au tutoriel de Jhon Colibri Jhon Colibri - LiveBindings Delphi XE2.
IX. Remerciements▲
Tous mes remerciements vont à Gilles (gvasseur58) pour sa relecture, ses conseils techniques et ses premières corrections. À Jacques (jacques_jean), 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.
Rappel : vous pourrez retrouver les sources des programmes Formules.zip.