LiveBindings de A à … : TPrototypeBindSource

Une base de données sans SGBD

Serge Girard

J'aurais pu aborder directement la relation LiveBindings et bases de données, même les possesseurs d'une version starter pouvant s'y frotter via un TClientDataset, mais je préfère aborder cette relation par une approche objet.

Pourquoi ? Tout d'abord parce que, pour de petites applications, on n'a pas toujours besoin d'utiliser un SGBD, mais aussi parce qu'une approche objet est intéressante d'un point de vue échange de données, en particulier avec JSON.

Commentez

- Titre : LiveBindings de A à … : TPrototypeBindSource

- Auteur : Serge Girard

- Parution : 11 novembre 2017

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

Sommaire

- I. Objectifs et démarche

- I-A. Les données

- I-B. Présentation de l'interface

- II. Première étape : le programme basique

- II-A. La liste

- II-B. Les données

- II-C. Les liaisons

- II-C-1. Le navigateur

- II-C-2. Les zones de saisie

- II-C-3. La liste

- II-D. Les données réelles

- II-D-1. Ajouter la classe au programme

- II-D-2. Ajouter une liste d'objets

- II-D-3. Création et ajout de mes données

- II-E. Résultat final

- III. Deuxième étape : amélioration du programme

- III-A. Les « données internes »

- III-B. Sauvegarder les données

- III-B-1. Sauvegarde CSV

- III-B-2. Sauvegarde JSON

- III-C. Charger les données

- III-C-1. Chargement CSV

- III-C-2. Chargement JSON

- III-D. Mise en place

- IV. Troisième étape : ajouter des informations dans l'interface utilisateur.

- IV-A. Combiner des données : CustomFormat

- IV-A-1. Afficher le nom complet dans un TLabel

- IV-A-2. Quelques notes sur la fonction Format de l'évaluateur d'expressions

- IV-A-2-a. Formater des nombres

- IV-A-2-b. Formater une date

- IV-A-2-c. Limites de Format

- IV-B. Informations sur l'ensemble de données

- IV-C. Afficher un âge

- IV-C-1. Premier pas : écrire la fonction de calcul

- IV-C-2. Deuxième pas : en faire une méthode invoquable

- IV-C-3. Pour finir : enregistrer cette méthode

- IV-C-4. Se servir de la nouvelle fonction

- IV-D. Faire une recherche

- V. Améliorer l'interface utilisateur

- V-A. De l'ordre

- V-B. Du style

- V-C. De la couleur à l'approche de l'événement

- V-D. Se positionner

- V-E. « Age or not Age ? »

- VI. Quid d'un programme identique mais FMX ?

- VI-A. Liaison des données

- VI-A-1. Modification de la TListView

- VI-A-2. Étape 1 : basculer en mode conception de liste

- VI-A-3. Étape 2 : la partie liaison

- VI-A-4. Étape 3 : codifier

- VI-B. Résultat

- VI-C. Du style

- VII. Conclusion

I - Objectifs et démarche

C'est au fil de mes lectures sur le forum Delphi que j'ai pu bâtir l'exemple qui va servir de support à ce tutoriel. Le but du programme est simple : avoir un « pense-bête » permettant de ne pas oublier les dates importantes, « ce que les hommes ont du mal à retenir » d'après les femmes. Comme, malgré tout, il n'y a pas une tonne de dates à retenir, utiliser un SGBD relationnel semble être largement excessif pour une petite application personnelle : c'est pour cette raison que j'utiliserai des objets, un peu à la manière de papillons adhésifs.

Plutôt que de fournir un résultat parfait, je vais essayer de faire partager les différentes étapes de construction et les optimisations successives que je vais apporter.

À chaque étape correspond un projet à retrouver dans les sources téléchargeables à cette adresse.

I-A - Les données

Que mettre sur un Post-it  ? Le nom de la personne ou l'intitulé de l'événement et, bien sûr, la date. Partant de cette constatation, je vais donc avoir besoin d'une classe avec au moins deux propriétés : le nom et la date. Pour faire bonne mesure (et surtout pour des besoins de démonstration) je vais même séparer le prénom du nom de famille.

Il est à noter que j'ai utilisé le terme de classe plutôt que d'objet, considérant que la classe est une recette de cuisine, les propriétés les ingrédients pour un résultat final : l'objet.

Au niveau programmation, ces considérations donnent quelque chose comme ceci :

Classe

TEvenement = Class
strict private
FNom : String;
FPrenom : String;
FDate : TDate;
private
...
public
property Nom : String read FNom write FNom;
property Prenom : String read FPrenom write FPrenom;
property Feterle : TDate read FDate write FDate;
end;

Cette classe sera améliorée par la suite en la complétant par des méthodes (procédures ou fonctions).

I-B - Présentation de l'interface

La fiche contiendra un TPageControl avec deux panneaux : le premier pour avoir une interface utilisateur, le second pour vérifier ce qui sera mis en place (un débogueur, en quelque sorte).

Panneau interface

(cliquez ici pour aggrandir l'image)

L'affichage principal contient une liste (TlistView), deux zones de saisie avec leur titre (des TLabeledEdit), une zone de saisie de date (TDateTimePicker) et, déjà, un navigateur (TBindNavigator) qui permettra de se déplacer dans l'ensemble des données.

Le second panneau permettant les tests contiendra un TMemo pour afficher les résultats et un bouton (TButton) pour programmer le test.

Panneau "Debug"

(cliquez ici pour aggrandir l'image)

Qu'en serait-il du programme sans les LiveBindings ? Il serait possible de le rendre opérationnel, sans aucun doute, mais il faudrait du code (beaucoup) pour remplir les différentes zones de saisie et d'affichage, de même que pour faire fonctionner le navigateur que l'on aurait, peut-être, même pas imaginé utiliser.

Avec les LiveBindings, tout va devenir beaucoup plus facile ; je ne dirais pas sans code, mais presque !

II - Première étape : le programme basique

Vous retrouverez les codes sources de ce premier projet de la série baptisé PenseBete1.

L'interface utilisateur étant présentée, je vais juste y ajouter un TPrototypeBindSource.

Squelette programme

unit Etape1;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.Components,
Data.Bind.ObjectScope, Vcl.StdCtrls, Vcl.ComCtrls, Data.Bind.Controls,
Data.Bind.GenData, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Bind.Navigator;

type
TFormPB1 = class(TForm)
Panneaux: TPageControl;
UI: TTabSheet;
Tests: TTabSheet;
ListView1: TListView;
Label1: TLabel;
Agenda: TPrototypeBindSource;
BindNavigator1: TBindNavigator;
EditPrenom: TLabeledEdit;
EditNom: TLabeledEdit;
EditDate: TDateTimePicker;
Memo1: TMemo;
Tester: TButton;
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;

var
FormPB1: TFormPB1;

implementation

{$R *.dfm}

end.

Squelette programme DFM

object FormPB1: TFormPB1
Left = 0
Top = 0
Caption = 'Pense B'#234'te Etape 1'
ClientHeight = 332
ClientWidth = 534
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Panneaux: TPageControl
Left = 0
Top = 0
Width = 534
Height = 332
ActivePage = UI
Align = alClient
TabOrder = 0
TabWidth = 120
object UI: TTabSheet
Caption = 'Affichage Principal'
object Label1: TLabel
Left = 3
Top = 13
Width = 22
Height = 13
Caption = 'Liste'
end
object ListView1: TListView
Left = 3
Top = 32
Width = 230
Height = 225
Columns = <>
TabOrder = 0
ViewStyle = vsList
end
object BindNavigator1: TBindNavigator
Left = 153
Top = 276
Width = 240
Height = 25
DataSource = Agenda
Orientation = orHorizontal
TabOrder = 1
end
object EditPrenom: TLabeledEdit
Left = 272
Top = 56
Width = 209
Height = 21
EditLabel.Width = 36
EditLabel.Height = 13
EditLabel.Caption = 'Pr'#233'nom'
TabOrder = 2
end
object EditNom: TLabeledEdit
Left = 272
Top = 104
Width = 209
Height = 21
EditLabel.Width = 21
EditLabel.Height = 13
EditLabel.Caption = 'Nom'
TabOrder = 3
end
object EditDate: TDateTimePicker
Left = 272
Top = 152
Width = 121
Height = 21
Date = 42890.489375162040000000
Time = 42890.489375162040000000
TabOrder = 4
end
end
object Tests: TTabSheet
Caption = 'Tests'
ImageIndex = 1
object Memo1: TMemo
Left = 2
Top = 0
Width = 521
Height = 270
Lines.Strings = (
'Memo1')
TabOrder = 0
end
object Tester: TButton
Left = 0
Top = 276
Width = 75
Height = 25
Caption = 'Tester'
TabOrder = 1
end
end
end
object Agenda: TPrototypeBindSource
AutoActivate = True
AutoPost = False
FieldDefs = <
item
Name = 'Prenom'
Generator = 'ContactTitles'
ReadOnly = False
end
item
Name = 'Nom'
Generator = 'ContactNames'
ReadOnly = False
end
item
Name = 'Feterle'
FieldType = ftDate
Generator = 'Date'
ReadOnly = False
end>
ScopeMappings = <>
Left = 460
Top = 32
end
end

Pourquoi un TPrototypeBindSource ? Parce que, intégré à ce composant, se trouve un ensemble de générateurs de valeurs aléatoires qui permettent de construire un prototype des données à placer par la suite dans le programme. Pour l'instant, il n'y a pas de données et il est par conséquent difficile de visualiser ce que pourrait donner l'interface. Pourtant, quelques liens posés, et la magie opère, toujours sans une ligne de code !

Remplissage des données

(cliquez ici pour aggrandir l'image)

Cependant, j'anticipe et il faut reprendre les étapes pour arriver à cette image.

II-A - La liste

J'ai modifié cette liste : tout d'abord en renseignant sa propriété ViewStyle à vsReport et, surtout, en ajoutant deux colonnes.

ListView1, ajout colonnes

(cliquez ici pour aggrandir l'image)

Ces modifications se sont répercutées sur le fichier dfm.

DFM modifié ListView1

object ListView1: TListView
Left = 3
Top = 32
Width = 246
Height = 225
Columns = <
item
Caption = 'Pr'#233'nom'
Width = 120
end
item
Caption = 'Nom'
Width = 120
end>
TabOrder = 0
ViewStyle = vsReport
end

II-B - Les données

Simuler les futures données est une étape importante du projet. Comme pour une table d'une base de données, il va falloir indiquer les colonnes (champs) voulues. La différence va être que, grâce à certains générateurs, un jeu d'essai de données contenant des valeurs pseudo-aléatoires sera fourni.

Pour procéder, il faut double-cliquer sur le composant TPrototypeBindSource nommé « Agenda ».

Il y a plusieurs manières d'obtenir le dialogue d'ajout de champs.

On peut double-cliquer sur le composant. En utilisant le menu contextuel (clic droit) du composant, on peut aussi choisir l'une des deux options de ce dernier (« éditeur de champs… » ou « ajouter un champ… »).

Définition des colonnes

(cliquez ici pour aggrandir l'image)

J'ajoute alors trois champs : Prenom et Nom de types ftString et FeterLe, et une date de type ftDate. À chaque ajout, je choisis un type de générateur.

Choisissez le générateur avant de saisir le nom du champ sous peine d'avoir un nom générique.

Comment choisir le générateur ? En premier lieu par son type (colonne de droite) puis le choix se fera par le nom de la colonne de gauche et les valeurs exemples qui sont affichées sous le tableau.

Personnellement, j'ai choisi les générateurs ContactTitles et ContactNames (respectivement pour les champs Prenom et Nom), relativement proches des données à utiliser, mais j'aurais très bien pu utiliser d'autres générateurs. Pour la colonne date (le champ FeterLe), j'ai par contre pris bien soin d'utiliser le bon type, car je ne voulais pas d'une date sous la forme d'une chaîne de caractères, mais sous la forme numérique rendue par TDate.

Vous pouvez remarquer, sous la liste des « Données de champ », deux cases à cocher. Décochées, ces dernières permettent un tri (case « Valeurs aléatoires ») et la non-redondance (case « Valeurs répétées »). Je rappelle qu'il s'agit d'un jeu d'essai de données et qu'il n'est donc pas nécessaire de s'en occuper. Toutefois, cela limitera le nombre d'éléments et permettra une lecture plus facile si les données sont triées : c'est pour ces raisons que, dans le code source du programme, j'ai utilisé ces propriétés.

Vous n'avez pas suivi ces étapes dans l'ordre, oublié de cocher les cases, obtenu un nom générique, etc. ? Pas de soucis : vous pouvez toujours utiliser l'éditeur de propriétés de chaque champ pour corriger les problèmes.

À propos de propriétés, je reviens sur celles du TPrototypeBindSource.

Propriétés PrototypeBindSource

(cliquez ici pour aggrandir l'image)

Les trois premières (AutoActivate, AutoEdit, AutoPost), peut-être parce qu'elles ont des noms significatifs, ne sont pas documentées ! Utilisant déjà des bases de données, d'aucuns pourraient penser que l'AutoActivate est l'équivalent de l'ouverture d'une table, l'AutoEdit l'équivalent de l'instruction Edit et l'AutoPost l'équivalent d'une mise à jour automatique des données. Les tests que j'ai pu faire à l'exécution de programmes modifiant ces propriétés me laissent perplexe, car elles ne font pas ce que je pouvais en attendre !

Par exemple :

AutoActivate

Agenda.AutoActivate:=True ; // n'a aucun effet sauf en mode design
{ Alors qu'au runtime, les lignes suivantes ont le comportement  souhaité, soit l'ouverture ou la fermeture de l'ensemble de données Agenda}
Agenda.Active:=True ;
Agenda.Active:=False ;

Par contre, en mode design, AutoActivate permet d'ouvrir ou de fermer le jeu d'essai généré.

Attention, de là à en déduire que ces propriétés ne sont utiles qu'au design, ce ne serait pas une bonne idée ! Elles seront utiles lorsqu'il s'agira de manipuler l'ensemble des données (voir chapitre III.C).

Je ferai l'impasse sur la propriété Concepteur LiveBindings. Comme son nom l'indique, cette propriété permettra de « jouer » avec le concepteur visuel ; elle n'apparaît donc pas avec la version Starter et n'a aucune influence à l'exécution.

La propriété FieldDefs permet de retrouver les champs : voilà encore une manière d'y accéder.

Les propriétés Name et Tag se passent de commentaires, mais mention spéciale à la propriété RecordCount qui permettra de limiter le nombre d'enregistrements générés : il faut bien noter le « générés » qui concerne le jeu d'essai et non les données futures (la valeur -1 indique en gros : « tout ce qu'il est possible d'obtenir »).

Reste la propriété ScopeMappings, peu documentée : on ne trouve aucune information concernant son utilisation.

Un seul événement pour ce composant : OnCreateAdapter, dont je reporte les explications au moment d'ajouter mes propres données.

II-C - Les liaisons

Passons à la partie la plus intéressante, objectif principal du tutoriel, à savoir les liaisons entre la source de données et les différents contrôles. Pour ceux qui ont une version pro ou supérieure, l'utilisation du concepteur de LiveBindings rend la chose aisée, et ils obtiendront rapidement, après quelques petits réarrangements visuels, la figure suivante :

Concepteur LiveBindings

(cliquez ici pour aggrandir l'image)

Que les possesseurs d'une version starter se rassurent, car il est tout à fait possible de faire sans.

Tout d'abord il faut poser sur la forme un TBindingsList.

Dans ce qui suit, je vais considérer les liaisons dans l'ordre, de haut en bas et de gauche à droite.

II-C-1 - Le navigateur

La seule chose à faire est d'indiquer sa propriété DataSource, égale dans le cas en cause à Agenda.

II-C-2 - Les zones de saisie

Toute zone de saisie, voire de libellé, sera liée à la source de données via un lien de type TLinkControlToField. Pour ce faire, il faut cliquer sur le bouton « Ajout » puis sélectionner le type souhaité.

(cliquez ici pour aggrandir l'image)

Il suffit ensuite de remplir les quelques propriétés nécessaires :

- Control : le contrôle qui va recevoir la valeur ;

- DataSource : la source de données, Agenda ;

- FieldName : le champ à associer.

Profitez-en pour renommer la liaison en quelque chose de plus parlant que LinkControlToField1 !

Choix de liaison

(cliquez ici pour aggrandir l'image)

On obtient alors une liste comme suit :

Liaisons zones de saisie

(cliquez ici pour aggrandir l'image)

Au passage, vous remarquerez que, à chaque liaison, la zone se remplit

(pour peu que la propriété AutoActivate soit cochée, bien sûr !).

II-C-3 - La liste

Lier la liste est à peine plus complexe. C'est, bien évidemment, d'un autre type de lien qu'il s'agit : TLinkListControlToField. Mais la procédure reste la même : après la demande d'ajout, il suffit de choisir ce type dans la liste.

Liaison avec une liste

(cliquez ici pour aggrandir l'image)

Il faut alors remplir les propriétés. Pour ce qui est de la première colonne, pas de difficultés, car il s'agit des mêmes que pour une zone de saisie déjà vue au-dessus, c'est-à-dire : Control, DataSource et FieldName. Pour ce qui est de la seconde colonne, par contre, il est nécessaire de passer par la propriété (moins évidente) FillExpressions.

Propriété de lien d'une liste

(cliquez ici pour aggrandir l'image)

Je vais donc double-cliquer ou cliquer sur les "…" pour obtenir une fenêtre de modification qui va permettre de remplir cette propriété. Il me suffit ensuite d'ajouter un nouveau membre pour associer colonne et valeur.

Reste quand même à savoir comment référencer cette colonne ! Heureusement, le choix peut se faire par l'intermédiaire d'une simple boîte de choix (ComboBox), car le nom, propriété ControlMemberName, n'est pas évident les premières fois. La seconde propriété, SourceMemberName, également sélectionnable dans une boîte de choix, est plus évidente puisqu'il s'agit des champs de la source de données.

Liaison colonne

(cliquez ici pour aggrandir l'image)

II-D - Les données réelles

Bien ! l'interface utilisateur est prête, mais seul le jeu d'essai est en place. Il faut maintenant fournir les données réelles à la source Agenda.

II-D-1 - Ajouter la classe au programme

Déjà décrit au début du chapitre I.A, le code de la classe TEvenement sera ajouté au programme. Je vais toutefois lui adjoindre deux constructeurs pour une manipulation plus rapide de l'objet :

Ajout de la classe

unit Etape1;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.Components,
Data.Bind.ObjectScope, Vcl.StdCtrls, Vcl.ComCtrls, Data.Bind.Controls,
Data.Bind.GenData, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Bind.Navigator, System.Rtti,
System.Bindings.Outputs, Vcl.Bind.Editors, Data.Bind.EngExt, Vcl.Bind.DBEngExt;

type
TEvenement = Class
strict private
FNom : String;
FPrenom : String;
FDate : TDate;
private

public
constructor Create(); overload;
constructor Create(Nom,Prenom : String; UneDate : TDate); overload;
property Nom : String read FNom write FNom;
property Prenom : String read FPrenom write FPrenom;
property Feterle : TDate read FDate write FDate;
end;

TFormPB1 = class(TForm)
Panneaux: TPageControl;
UI: TTabSheet;
Tests: TTabSheet;
ListView1: TListView;
Label1: TLabel;
Agenda: TPrototypeBindSource;
BindNavigator1: TBindNavigator;
EditPrenom: TLabeledEdit;
EditNom: TLabeledEdit;
EditDate: TDateTimePicker;
Memo1: TMemo;
Tester: TButton;
BindingsList1: TBindingsList;
LienListe: TLinkListControlToField;
LienPrenom: TLinkControlToField;
LienNom: TLinkControlToField;
LienDate: TLinkControlToField;
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;

var
FormPB1: TFormPB1;

implementation

{$R *.dfm}

{ TEvenement }

constructor TEvenement.Create;
// création par défaut
begin
FNom:='<Nom>';
FNom:='<Prénom>';
FDate:=Date;
end;

constructor TEvenement.Create(Nom, Prenom: String; UneDate: TDate);
// création objet
begin
FNom:=Nom;
FPrenom:=Prenom;
FDate:=UneDate;
end;

end.

Vous remarquerez, si besoin, qu'il n'y a toujours stricto sensu aucun code dans la partie implémentation du programme.

II-D-2 - Ajouter une liste d'objets

Tous les événements de mon agenda seront stockés dans une liste d'objets qu'il me faut donc déclarer. Une liste d'objets (TListObject) est une classe générique qui se trouve dans l'unité System.Generics.Collections qu'il me faudra adjoindre à la liste des unités utilisées.

Ajout de la liste d'objet

unit Etape1;

interface

uses
//...
// ajout
System.Generics.Collections
;

type
//...
TFormPB1 = class(TForm)
//...
private
{ Déclarations privées }
Evenements : TObjectList<TEvenement>;
public
{ Déclarations publiques }
end;

II-D-3 - Création et ajout de mes données

Cela va être le seul code de cette première partie : je vais utiliser pour ce faire l'événement OnCreateAdapter de la source de données Agenda :

Ajout de l'événement

procedure TFormPB1.AgendaCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin
Evenements:=TObjectList<TEvenement>.Create;
Evenements.Add(TEvenement.Create('Girard','Serge',EncodeDate(1956,6,29)));
// saint Valentin a été canonisé en 495
// mais la date minimum acceptable de TDateTimePicker est le 01/01/1900  Evenements.Add(TEvenement.Create('Saint','Valentin',EncodeDate(1900,2,14)));  Evenements.Add(TEvenement.Create('Mariage','Mon',EncodeDate(1978,10,30)));
Evenements.Add(TEvenement.Create('Mon Chaton','Grisou',EncodeDate(2017,5,1)));
ABindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self, Evenements, True);
end;

Cette partie de code mérite quelques explications. La liste d'objets est créée au moment de la création de l'adaptateur (Agenda). Jusque-là rien de particulier, si ce n'est que cette création, il faut le savoir, va intervenir avant même l'événement OnCreate de la forme.

Pour s'en convaincre, plusieurs possibilités :

- mettre un point d'arrêt sur la première ligne de cette procédure ;

- déplacer la création et le remplissage de la liste dans un événement OnCreate de la forme.

Dans les deux cas, il vous faudra ajouter l'événement OnCreate de la forme avec au moins une instruction.

Si l'on opte pour le premier test, voici un code minimaliste :

Vérification via débogage

procedure TFormPB1.FormCreate(Sender: TObject);
begin
Showmessage('Vérification de l''ordre de création');
end;

La mise en place de l'adaptateur (BindSourceAdapter) mérite, quant à elle, toute notre attention :

Adaptateur

AbindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create...

Cette instruction crée un TBindSourceAdapter qui va traiter un objet TEvenement.

Adaptateur partie entre ()

(Self, Evenements, True)

L'adaptateur appartient à la forme (Self), traite une liste d'objets (Evenements) et sera libéré à la destruction du propriétaire (True).

II-E - Résultat final

À l'exécution on obtient le résultat souhaité, totalement fonctionnel.

Pense Bête Etape 1

(cliquez ici pour aggrandir l'image)

L'interface paraît très « vieille mode », mais rien n'empêche d'ajouter des styles personnalisés à ce projet.

Plus critiquable : les données sont figées si bien que toute modification disparaîtra à la fermeture du programme. La seconde étape va donc consister à rendre plus professionnel le programme en séparant la partie métier de la partie interface, et à permettre la sauvegarde et le chargement des données.

III - Deuxième étape : amélioration du programme

Vous retrouverez les codes sources complets dans le projet PenseBete2.

Le premier pas va consister à déplacer tout ce qui concerne la classe TEvenement dans une nouvelle unité :

Agenda1

unit Agenda1;

interface
uses System.SysUtils; // SysUtils est nécessaire pour obtenir la date système

type
TEvenement = Class
strict private
FNom : String;
FPrenom : String;
FDate : TDate;
private

public
constructor Create(); overload;
constructor Create(Prenom,Nom : String; UneDate : TDate); overload;
property Nom : String read FNom write FNom;
property Prenom : String read FPrenom write FPrenom;
property Feterle : TDate read FDate write FDate;
end;

implementation

{ TEvenement }

constructor TEvenement.Create;
// création par défaut
begin
FNom:='<Nom>';
FNom:='<Prénom>';
FDate:=Date;
end;

constructor TEvenement.Create(Prenom, Nom: String; UneDate: TDate);
// création objet
begin
FNom:=Nom;
FPrenom:=Prenom;
FDate:=UneDate;
end;

end.

Il s'agira ensuite d'ajouter des fonctions de sauvegarde et de chargement des données.

III-A - Les « données internes »

À présent, comment faire pour passer les données que j'ai utilisées dans le chapitre II.D.3 ? La démarche sera ensuite applicable au chargement de données stockées hors programme.

Que faut-il pour lier les données à l'adaptateur ? Un TObjectList. Une fonction renvoyant ce type d'objet semble donc parfaitement indiquée pour remplir ce rôle.

Il suffit de déclarer cette fonction dans l'unité en n'oubliant pas d'ajouter System.Generics.Collections dans la liste des unités utilisées.

Ajout fonction F_DonneesInterne

unit Agenda1;

interface
uses System.SysUtils, // SysUtils est nécessaire pour obtenir la date système
// ajout
System.Generics.Collections;
type
TEvenement = Class
..  end;

function F_DonneesInternes : TObjectList<TEvenement>;

implementation

...
function F_DonneesInternes: TObjectList<TEvenement>;
begin
Result:=TObjectList<TEvenement>.Create;
Result.Add(TEvenement.Create('Serge','Girard',EncodeDate(1956,6,29)));
// saint Valentin a été canonisé en 495
// mais la date minimum acceptable de TDateTimePicker est le 01/01/1900
Result.Add(TEvenement.Create('Saint','Valentin',EncodeDate(1900,2,14)));
Result.Add(TEvenement.Create('Anniversaire','Mariage',EncodeDate(1978,10,30)));
Result.Add(TEvenement.Create('Grisou','mon chat',EncodeDate(2017,5,1)));
end;

end.

L'utilisation va devenir très simple. Dans le code source de la fiche principale, l'événement OnCreateAdapter du TPrototypeBindSource nommé Agenda est transformé en :

Appel de la fonction

procedure TFormPB2.AgendaCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin ABindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self,DonneesInternes, True);
end;

Après, bien évidemment, vient la déclaration d'utilisation de l'unité Agenda1.

Modifications effectuées, le code source de la forme principale est on ne peut plus succinct :

PenseBete2

unit Etape2;

interface

uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.Bind.Controls, Data.Bind.GenData,
Data.Bind.EngExt, Vcl.Bind.DBEngExt, System.Rtti, System.Bindings.Outputs,
Vcl.Bind.Editors, Data.Bind.Components, Data.Bind.ObjectScope, Vcl.StdCtrls,
Vcl.ComCtrls, Vcl.ExtCtrls, Vcl.Buttons, Vcl.Bind.Navigator;

type
TFormPB2 = class(TForm)
Panneaux: TPageControl;
UI: TTabSheet;
Label1: TLabel;
ListView1: TListView;
BindNavigator1: TBindNavigator;
EditPrenom: TLabeledEdit;
EditNom: TLabeledEdit;
EditDate: TDateTimePicker;
Tests: TTabSheet;
Memo1: TMemo;
Tester: TButton;
Agenda: TPrototypeBindSource;
BindingsList1: TBindingsList;
LienPrenom: TLinkControlToField;
LienNom: TLinkControlToField;
LienDate: TLinkControlToField;
LienListe: TLinkListControlToField;
procedure AgendaCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;

var
FormPB2: TFormPB2;

implementation

{$R *.dfm}

uses Agenda1;

procedure TFormPB2.AgendaCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin
AbindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self,F_DonneesInternes, True);
end;

end.

Cependant, le résultat est identique à notre programme basique.

III-B - Sauvegarder les données

En général, les vidéos et tutoriels passent sur cette partie (et la suivante), laissant le programmeur sur sa faim. Bien que m'écartant du sujet principal, à savoir les LiveBindings, je vais essayer de combler cette lacune.

Encore faut-il déterminer sous quel format ces sauvegardes seront faites. Je vais en choisir deux :

- le bon vieux CSV, format d'enregistrement de longueur variable, avec des champs séparés par des virgules ;

- le (relativement) tout nouveau format JSON.

Le premier format donnera un résultat très lisible et modifiable via un simple éditeur de texte ; le second, toujours un résultat lisible et modifiable, mais avec une présentation normalisée et qui paraîtra donc un peu moins naturelle.

III-B-1 - Sauvegarde CSV

Pour sauvegarder avec CSV, j'ai besoin d'une fonction qui permette de transformer un objet de ma classe TEvenement en texte. Je pourrais m'en passer, mais cela allégera d'autant plus le code du processus de sauvegarde en me donnant aussi l'occasion d'ajouter une fonction à ma classe.

Je vais ajouter la déclaration dans la partie privée de cette classe :

déclaration fonction EnCSV

type
TEvenement = Class
strict private
// ...
private
function EnCSV: String;
public
// ...
end ;

Ensuite, je vais écrire cette fonction dont le squelette aura été généré par l'utilisation du raccourci Ctrl+Maj+C :

fonction EnCSV

function TEvenement.EnCSV: String;
begin
Result:=Format('"%s","%s",%s',
[Prenom,
Nom,
FormatDateTime('dd/mm/yyyy',FeterLe)]);
end;

Vous remarquerez que j'ai mis entre guillemets les chaînes de caractères. Ce n'est pas obligatoire, mais seulement une habitude prise de longue date, donc difficile à perdre ! Vous en comprendrez la raison en lisant cette définition Wikipédia.

Une fois cette fonction écrite, il me reste la procédure de sauvegarde à écrire. Objectif : lire les objets de la liste et les enregistrer un à un dans un fichier texte.

À ce stade, un changement de stratégie s'impose. En effet, comment accéder à la liste d'objets du TPrototypeBindSource facilement ? Utiliser la fonction F_DonneesInternes, qui paraissait être une bonne idée de travail, n'est pas la solution adéquate : le TObjectList est créé par la fonction et renvoyé en résultat avant d'être libéré à la destruction du programme, mais ajouter d'autres fonctions identiques est impossible.

Or, j'ai besoin d'une liste d'objets unique et facile à utiliser. Il existe plusieurs solutions, mais, toujours pour rester dans la séparation de la partie métier (autrement dit la partie données) de la partie interface, j'ai choisi de déclarer une variable globale dans mon unité Agenda1. Cette variable sera créée dans la partie initialization de l'unité :

Variable globale

…
var MesEvenements : TObjectList<TEvenement>;

implementation

…
initialization
MesEvenements:=TObjectList<TEvenement>.Create;
finalization
// MesEvenements.Free;
// inutile car MesEvenements est libéré à la destruction du TProtypeBindSource
// puisque OwnsObject est égal à True au moment de la création de l'adaptateur
end.

Pour retrouver les données internes, il faudra abandonner la fonction F_DonneesInternes et créer une procédure qui remplira la liste d'objets (MesEvenements) :

P_DonneesInternes

//Déclaration
procedure P_DonneesInternes;

// implémentation
procedure P_DonneesInternes;
// Chargement de valeurs internes
begin
MesEvenements.Add(TEvenement.Create('Serge','Girard',EncodeDate(1956,6,29)));
// saint Valentin a été canonisé en 495
// mais la date minimum acceptable de TDateTimePicker est le 01/01/1900
MesEvenements.Add(TEvenement.Create('Saint','Valentin',EncodeDate(1900,2,14)));
MesEvenements.Add(TEvenement.Create('Anniversaire','Mariage',EncodeDate(1978,10,30)));
MesEvenements.Add(TEvenement.Create('Grisou','mon chat',EncodeDate(2017,5,1)));
end;

Le code de l'unité de l'interface utilisateur aura aussi à subir quelques changements mineurs :

modifications

procedure TFormPB2.AgendaCreateAdapter(Sender: TObject;
var ABindSourceAdapter: TBindSourceAdapter);
begin
P_DonneesInternes; // pour récupérer les données internes
// utilisation de F_DonneesInternes abandonnée
//  ABindSourceAdapter := //TListBindSourceAdapter<TEvenement>.Create(self,F_DonneesInternes, //True);
ABindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create(self,MesEvenements, True);
end;

Ces changements apportés, je peux alors coder, toujours dans l'unité métier, la partie sauvegarde, c'est-à-dire écrire une ligne par objet dans un fichier texte :

Sauvegarde CSV

procedure SauvegarderCSV(const destination : TFileName);
// Sauvegarde des données au format CSV
var F : TextFile;
unEvenement : TEvenement;
begin
AssignFile(F, Destination);
ReWrite(F);
for unEvenement in MesEvenements do
WriteLn(F, unEvenement.EnCSV);
CloseFile(F);
end;

J'obtiens alors un résultat facilement lisible et modifiable avec un simple éditeur de texte :

Contenu du fichier texte (sauvegarde à partir des données internes chargées)

"Serge","Girard",29/06/1956
"Saint","Valentin",14/02/1900
"Anniversaire","Mariage",30/10/1978
"Grisou","mon chat",01/05/2017

III-B-2 - Sauvegarde JSON

Transformer un objet Delphi en un élément JSON est simple grâce à l'unité Rest.JSON et sa fonction TJson.ObjectToJsonString qui va être l'équivalent de ma fonction EnCSV du chapitre au-dessus.

Premier réflexe, on peut même obtenir une transformation directe de la liste d'objets. C'est là que le panneau test intervient pour vérifier ce que cela donnerait :

Test

procedure TFormPB2.TesterClick(Sender: TObject);
begin
// tester l'export en JSON de la liste entière
memo1.Lines.text:=TJson.ObjectToJsonString(MesEvenements);
end;

Le résultat est lisible, certes, mais difficilement modifiable. En fait, il ne s'agit pas de plusieurs lignes, mais d'un seul paragraphe.

(cliquez ici pour aggrandir l'image)

Si mon objectif est de pouvoir modifier facilement ces données avec un éditeur de texte, il est évident qu'il faut simplement mettre un objet par ligne. Ma procédure aura donc le code suivant :

Sauvegarde JSON

procedure SauvegarderJSON(const destination : TFileName);
// sauvegarde au format JSON
var F : TextFile;
unEvenement : TEvenement;
begin
AssignFile(F, Destination);
ReWrite(F);
for unEvenement in MesEvenements do
// un objet par ligne
WriteLn(F, TJSON.ObjectToJSONString(unEvenement));
CloseFile(F);
end;

J'obtiens alors quelque chose de largement plus facile à modifier :

Contenu du fichier texte (sauvegarde à partir des données internes chargées) format JSON

{"nom":"Girard","prenom":"Serge","date":20635}
{"nom":"Valentin","prenom":"Saint","date":46}
{"nom":"Mariage","prenom":"Anniversaire","date":28793}
{"nom":"mon chat","prenom":"Grisou","date":42856}

Vous auriez pu vous attendre à ce que le champ « date » soit nommé comme la propriété, c'est-à-dire FeterLe. Il est à remarquer que les noms de champs sont ceux utilisés dans la partie private de la classe, sans le 'F' conventionnel.

Toutefois, pour la date, j'aurais quelques difficultés à effectuer une modification directe ! Comme quoi, le bon vieux CSV n'est pas si mal !

Un changement de type de la colonne de ftDate à ftString suffit pour régler ce problème de lecture.

III-C - Charger les données

Sauvegardes faites, il faut aussi pouvoir les restituer. C'est ce que je vais développer à présent.

III-C-1 - Chargement CSV

Comme j'ai eu besoin d'une fonction (EnCSV) qui a permis de transformer l'objet en une chaîne selon le format, je vais avoir besoin de son pendant pour décoder ma ligne :

CSVversObjet

procedure TEvenement.CSVversObjet(Data: String);
// transformation d'une ligne CSV en objet TEvenement
var SL : TStringList; // sert à parser la ligne
begin
SL:=TStringList.Create;
try
SL.QuoteChar:='"';  // indique l'utilisation des " pour les chaînes
SL.CommaText:=Data;
Prenom:=SL[0];
Nom:=SL[1];
Feterle:=StrToDate(SL[2]);
finally
SL.Free;
end;
end;

J'ai aussi besoin d'une procédure de chargement. Ligne par ligne, je crée un objet TEvenement pour ensuite l'ajouter à ma liste (MesEvenements) préalablement vidée :

Chargement CSV

procedure ChargerCSV(const source : TFileName);
// Chargement du fichier au format CSV
var F : TextFile;
Data : String;
unEvenement : TEvenement;
begin
MesEvenements.Clear;
AssignFile(F, source);
Reset(F);
while not EOF(F) do
begin
Readln(F,Data);
unEvenement:=TEvenement.Create;
unEvenement.CSVversObjet(Data);
MesEvenements.Add(unEvenement);
end;
CloseFile(F);
end;

III-C-2 - Chargement JSON

Avec des données au format JSON, c'est l'objet qui est sauvegardé, le processus de chargement étant donc simplifié. J'ajoute à la liste un objet obtenu en décodant ligne par ligne grâce à la fonction TJSON.JsonToObject :

Chargement JSON

procedure ChargerJSON(const source : TFileName);
// chargement du fichier au format JSON
var F : TextFile;
Data : String;
begin
MesEvenements.Clear; // effacer la liste d'objet
AssignFile(F, source);
Reset(F);
while not EOF(F) do
begin
Readln(F,Data);
MesEvenements.Add(TJSON.JsonToObject<TEvenement>(Data));
end;
CloseFile(F);
end;

III-D - Mise en place

Il n'y a plus qu'à mettre en place ces fonctionnalités dans l'interface utilisateur.

Etape 2 Fichiers CSV ou JSON

(cliquez ici pour aggrandir l'image)

Je me suis contenté d'ajouter un TFileOpenDialog et quatre boutons dont les méthodes OnClick sont renseignées de cette manière :

OnClick Boutons

procedure TFormPB2.CSVLoadClick(Sender: TObject);
// Chargement CSV
begin
if FileOpenDialog1.Execute then
begin
Agenda.Active:=False;
ChargerCSV(FileOpenDialog1.FileName);
Agenda.Active:=True;
end;
end;

procedure TFormPB2.CSVSaveClick(Sender: TObject);
// Sauvegarde CSV
begin
if FileOpenDialog1.Execute then
SauvegarderCSV(FileOpenDialog1.FileName);
end;

procedure TFormPB2.JSONLoadClick(Sender: TObject);
// Chargement JSON
begin
if FileOpenDialog1.Execute then
begin
Agenda.Active:=False;
ChargerJSON(FileOpenDialog1.FileName);
Agenda.Active:=True;
end;
end;

procedure TFormPB2.JSONSaveClick(Sender: TObject);
// Sauvegarde JSON
begin
if FileOpenDialog1.Execute then
SauvegarderJSON(FileOpenDialog1.FileName);
end;

Pour les chargements, après choix confirmé du fichier à traiter, je ferme la source de données (TPrototypeBindSource) avant d'appeler la procédure souhaitée et réouvre ensuite cette source, ce qui aura pour effet de rafraîchir l'affichage des données.

IV - Troisième étape : ajouter des informations dans l'interface utilisateur.

Retour au cœur de cible du tutoriel : les LiveBindings et les sources de données.

Les codes sources sont dans le projet PenseBete3.

Etape 3 Manipulation des données

IV-A - Combiner des données : CustomFormat

Toutes les données liées peuvent être formatées en sortie via l'interpréteur d'expressions des LiveBindings. C'est cette possibilité que j'utilise pour combiner ou modifier l'affichage de celles-ci, et ce, dans les limites que l'interpréteur d'expressions impose.

IV-A-1 - Afficher le nom complet dans un TLabel

La méthode la plus simple consiste à créer un lien entre la zone d'affichage et l'une des deux colonnes (prénom ou nom).

Cette méthode demande de double-cliquer sur le composant BindingsList puis d'ajouter un nouveau TLinkPropertyToField pour renseigner les propriétés Control, DataSource et FieldName, comme vu au chapitre II.C.2. La différence est qu'il faut à présent spécifier la propriété CustomFormat avec la formule suivante (si FieldName=Prenom), la formule étant une simple concaténation de chaînes :

CustomFormat

%s+' '+Owner.NOM.value

Voici le sens à donner à ses éléments :

- %s correspond à la valeur de la colonne ;

- Owner correspond au DataSource.

Il est également possible d'utiliser la fonction Format de l'interpréteur d'expressions des Livebindings :

CustomFormat

Format('Prénom : %%s  Nom : %%s',%s, Owner.NOM.Value)
ou
Format('Prénom : %%s  Nom : %%s',value, Owner.NOM.Value)
ou
Format('Prénom : %%s  Nom : %%s',Owner.PRENOM.Value, Owner.NOM.Value)

Vous noterez la syntaxe très particulière du Format des LiveBindings. En Delphi, nous aurions écrit :

Format('Prénom : %s Nom : %s',[owner.PRENOM.value,owner.NOM.Value]), soit la chaîne de format suivie d'un tableau de valeurs.

Vous noterez également le double caractère % utilisé dans la chaîne.

IV-A-2 - Quelques notes sur la fonction Format de l'évaluateur d'expressions

Cette fonctionnalité mériterait à elle seule un tutoriel tellement son comportement peut être différent en fonction du type de lien utilisé ! Je limiterai ici mes explications à la liaison TLinkControlToField employée.

Le caractère % est compris par l'interpréteur d'expressions des LiveBindings comme un caractère d'échappement. Dans le cas d'un TLinkControlToField, il semblerait que l'expression entière soit interprétée en deux passes, ce qui explique la nécessité de dédoubler les « % » de la chaîne de format.

IV-A-2-a - Formater des nombres

Ce n'est pas le cas dans la source de données utilisée, mais si j'avais, par exemple, une valeur numérique comme un nombre réel (ftSingle,ftCurrency) ou encore un nombre entier (ftInteger) ?

Type

CustomFormat

Valeur %s

Résultat

ftInteger

Format('%%.3d',%s)

10

10

ftSingle

Format('%%3.2f',%s)
Format('%%5.2f',%s)
Format('%%5,4f',%s)

10.54

10.54
10,54
10,5400

ftCurrency

Format('%%3.2m',%s)

10.54

10,54 €

Pour afficher un pourcentage, cela devient encore plus complexe :

CustomFormat

Valeur %s

Résultat

Format('%%3.2f %%%%',%s)

10.54

10,54 %

Explication : la première passe change la chaîne de format en '%3.2f %%' et la valeur %s en 10.54 ; la seconde passe permet d'obtenir le résultat final comme l'instruction Format le ferait dans le code du programme.

IV-A-2-b - Formater une date

Pour une date ou un temps, on utilisera FormatDateTime.

IV-A-2-c - Limites de Format

Une erreur dans l'expression et c'est la liaison qui en pâtit et risque de ne plus être active. Au design, on obtiendra le message suivant :

« Le format <chaine de format après la première passe> est incorrect ou incompatible avec l'argument. »

Toute modification d'affichage d'une valeur (par exemple, l'ajout du signe pourcentage) ne pourra être interprété correctement. Il faudra donc, en cas de zone de saisie, avoir une fonction qui permettra de procéder à ces modifications et qui sera indiquée dans la propriété CustomParse.

IV-B - Informations sur l'ensemble de données

Il est toujours intéressant de connaître sa position et le nombre de lignes de l'ensemble, et il est relativement facile d'obtenir ces renseignements. Toutefois, il n'est pas possible de le faire via le concepteur visuel qui, je le rappelle, ne permet de créer que des expressions expresses.

Un double-clic sur le composant BindingsList pour ajouter une liaison de type BindLink, me permet de créer un nouveau type de liaison.

Il faut alors lier la zone de sortie, propriété ControlComponent, à la source de données, propriété SourceComponent. Une fois cette opération effectuée, il reste à renseigner la valeur que nous voulons afficher et ce dans la liste des expressions de format.

Il y a plusieurs manières de procéder. Si vous êtes chevronné, vous pouvez passer directement par l'éditeur d'expressions de la propriété FormatExpressions. Dans le cas contraire, en double-cliquant sur le lien, vous pourrez tester vos expressions (cf. image).

BindLink

(cliquez ici pour aggrandir l'image)

J'utilise dans l'expression de la source : « ToStr(ItemIndex+1)+' de '+ToStr(ItemCount) », les appels aux valeurs ItemIndex et ItemCount, propriétés accessibles via l'interface IBindSourceAdapter de notre TPrototypeBindSource Agenda.

IV-C - Afficher un âge

« ça lui fait quel âge ? » La question qui tue quand on est un peu fatigué et que l'on n'a pas envie de faire de calcul mental !

Plusieurs solutions sont envisageables :

- ajouter une nouvelle propriété Age ? Très mauvaise idée d'un point de vue administrateur de données, car l'âge est une valeur calculée à partir d'une date, ce n'est donc pas une valeur à stocker ;

- ajouter une fonction à l'objet ? Parfait, sauf que seules les propriétés sont accessibles par les LiveBindings et qu'il faut par conséquent coder l'appel dans le programme principal ;

- faire en sorte d'avoir une fonction accessible via le moteur des LiveBindings ? Voilà la solution qui correspond le plus à mes attentes ou du moins à l'objectif du tutoriel. Cela correspondrait à l'équivalent d'une fonction définie par l'utilisateur des SGBD modernes. De plus, le tutoriel précédent a permis d'apprendre qu'il est possible d'implémenter ses propres méthodes au moteur.

Rappel du principe : il faut créer une méthode qui pourra être appelée (Invokable) par le moteur d'expressions et inscrire celle-ci à la liste des méthodes usine (TBindingMethodsFactory).

IV-C-1 - Premier pas : écrire la fonction de calcul

La fonction de calcul fait partie des méthodes attachées à l'objet TEvenement :

Calcul Age

Uses … System.DateUtils … ;
…

function CalculAge(uneDate : TDate) : String;
// fonction de calcul de l'age
var Age : integer;
begin
Age:=YearsBetween(UneDate,Date);
case Age of
0 : begin
Age:=MonthsBetween(UneDate,Date);
case Age of
0 : begin
Age := WeeksBetween(uneDate,Date);
case Age of
0 : Exit(Format('%d Jours',[DaysBetween(uneDate,Date)]));
1 : Exit(Format('%d Semaine',[Age]));
else Exit(Format('%d Semaines',[Age]));
end;
end
else Exit(Format('%d Mois',[Age]));
end;
end;
1 : Exit(Format('%d An',[Age]));
else Exit(Format('%d Ans',[Age]));
end;
end;

IV-C-2 - Deuxième pas : en faire une méthode invoquable

function CalcAge: IInvokable;
// mise en place de la fonction
// on rend la fonction CalculAge "invoquable"
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
D : TDate;
begin
if Length(Args) <> 1 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [1,Length(Args)]))
else begin
v1:=Args[0];
d:=VarToDateTime(v1.GetValue.AsVariant);
Exit(TValueWrapper.Create(CalculAge(d)));
end;
end);
end;

IV-C-3 - Pour finir : enregistrer cette méthode

Incription méthode

initialization
// Ajout d'une "fonction utilisateur UDF"
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
CalcAge,
'Age',
'Calcul Age', '', True,
'',
nil));
MesEvenements:=TObjectList<TEvenement>.Create;
finalization
// ne pas oublier de libérer la fonction
TBindingMethodsFactory.UnRegisterMethod('Age');

Une autre technique est possible en utilisant une unité (MethodUtils) créée par Jim Tierney. Le principe est le même, mais l'implémentation différente.

J'ai inclus cette technique dans le projet PenseBete3. Un bon exemple de son utilisation est à retrouver dans le blog de Malcolm Groves.

IV-C-4 - Se servir de la nouvelle fonction

Pour se servir de la nouvelle fonction, il suffira d'utiliser celle-ci dans la propriété CustomFormat d'une liaison.

Je dois toutefois vous avertir que l'utilisation de méthodes « maison » fait parfois lever des erreurs au cours du design des écrans, ce qui est parfois très frustrant ! La raison principale est que la source de données est active et que l'EDI tente de mettre à jour votre dessin d'écran. Mettre la propriété AutoActivate à False pour le lien concerné est une bonne parade à ce genre de problème.

CustomFormat Utilisation de la fonction Age

(cliquez ici pour aggrandir l'image)

IV-D - Faire une recherche

La ListView de la VCL est pauvre par rapport à celle de la FMX qui nous permet d'inclure directement une barre de recherche (voir le projet PenseBete5FMX). C'est uniquement par code que je pourrai implémenter des recherches. Comme cela ne concerne pas directement les LiveBindings, je n'en livre que quelques lignes de code.

Vous pourrez retrouver tout ceci dans le projet PenseBete3.

Je propose deux types de recherche. La première s'appuie sur l'utilisation du TPrototypeBindSource et des RTTI :

Recherche

procedure TFormPB3.RechercheEntiereChange(Sender: TObject);
// faire une recherche texte complet case sensitive
var colonne: String;
begin
case RechercheSur.ItemIndex of
0 : Colonne:='Prenom';
1 : Colonne:='Nom';
end;
Agenda.Locate(Colonne,RechercheEntiere.Text);
end;

L'inconvénient de cette méthode est que seules des recherches sur des valeurs entières et de surcroît en respectant la casse peuvent être effectuées ainsi.

La seconde méthode est un simple balayage de ma liste d'objets et donc offre la possibilité de traiter les résultats à ma convenance :

Balayage

procedure TFormPB3.RecherchePartielleChange(Sender: TObject);
// faire une recherche texte partiel, case insensitive
var unEvenement : TEvenement;
Quoi : String;
begin
for unEvenement in MesEvenements do
begin
case RechercheSur.ItemIndex of
0 : Quoi:= unEvenement.Prenom;
1 : Quoi:= unEvenement.Nom;
end;
if Quoi.StartsWith(RecherchePartielle.Text,true) then
begin
// se positionner
Agenda.ItemIndex :=MesEvenements.IndexOf(unEvenement);
Break;
end;
end;
end;

V - Améliorer l'interface utilisateur

V-A - De l'ordre

Un affichage chronologique, c'est mieux. Pour pouvoir trier une liste d'objets, il faut créer un comparateur qui sera utilisé par la méthode Sort. J'ai choisi de faire un tri selon le mois et le jour, mais j'aurais aussi pu utiliser la fonction DayOfTheYear pour arriver au même résultat.

AgendaFinal.pas

...

var MesEvenements : TObjectList<TEvenement>;
OrdreMMJJ :IComparer<TEvenement>;

implementation
...
initialization
// créer un comparateur
OrdreMMJJ:=TComparer<TEvenement>.Construct(
function (const L, R: TEvenement): integer
var A,B : integer;
aa,mm,jj : Word;
begin
DecodeDate(L.Feterle,aa,mm,jj);
A:=mm*100+jj;
// ou A:=DayOfTheYear(L.Feterle);
DecodeDate(R.Feterle,aa,mm,jj);
B:=mm*100+jj;
// ou B:=DayOfTheYear(R.FeterLe);
if A=B then
Result:=0
else
if A<B then
Result:=-1
else
Result:=1;
end
);

V-B - Du style

Ajouter une apparence au projet VCL lui donnera un cachet supplémentaire.

Options de projet

(cliquez ici pour aggrandir l'image)

Application du Style

(cliquez ici pour aggrandir l'image)

V-C - De la couleur à l'approche de l'événement

Pour renforcer l'effet visuel, il serait bon d'ajouter de la couleur sur l'élément de liste. Ce sera chose faite en travaillant sur l'événement OnDrawItem de la liste. J'ai choisi de mettre en jaune l'item si l'on s'approchait de la date à moins de six jours et en rouge le jour J.

Deux approches sont possibles :

- coder directement le calcul de la couleur (partie en commentaire dans le code) ;

- utiliser une fonction de l'objet pour obtenir la couleur.

Fonction CouleurItem

// nécessité de rajouter l'unité VCL.Graphics pour les couleurs
uses ... VCL.Graphics ;
...
public
// ajout de la fonction de classe
function CouleurItem : Tcolor;
...
function TEvenement.CouleurItem: TColor;
var i : Integer;
aa,mm,jj,curaa : word;
ADate : TDate;
begin
DecodeDate(Date,curaa,mm,jj);
DecodeDate(FDate,aa,mm,jj);
if isLeapYear(aa) and (not IsLeapYear(curaa))
and (mm=2) and (jj=29)
then jj:=28;
ADate:=EncodeDate(curaa,mm,jj);
i:=DayOfTheYear(ADate)-DayOfTheYear(Date);
case i of
0 : Result:=clRed;
1..5 : Result:=clYellow;
else result:=clWindow;
end;
end;

EtapeFinale.pas

procedure TForm9.ListView1DrawItem(Sender: TCustomListView; Item: TListItem;
Rect: TRect; State: TOwnerDrawState);
// Dessin d'un Item de la liste
var unEvenement: TEvenement;
labelRect : TRect;
i : integer;
aa,mm,jj,curaa : word;
Adate: TDate;
{curaa : Word;
ADate : TDate}

begin
// changer de couleur selon la date
unEvenement:=MesEvenements.Items[Item.Index];
{ sans fonction métier
DecodeDate(Date,curaa,mm,jj);
DecodeDate(unEvenement.Feterle,aa,mm,jj);
if isLeapYear(aa) and (not IsLeapYear(curaa))
and (mm=2) and (jj=29) then jj:=28;
ADate:=EncodeDate(curaa,mm,jj);
i:=DayOfTheYear(ADate)-DayOfTheYear(Date);
case i of
0 :  ListView1.Canvas.Brush.Color :=clRed;
1..5 : ListView1.Canvas.Brush.Color :=clYellow;
else ListView1.Canvas.Brush.Color :=ListView1.Color;
end; }

{Utilisation de la fonction CouleurItem}
ListView1.Canvas.Brush.Color :=unEvenement.CouleurItem;
if Item.Selected then ListView1.Canvas.Brush.Color :=clActiveCaption;

// partie coloration
ListView1.Canvas.FillRect(Rect);
LabelRect := Item.DisplayRect(drLabel);
DrawText (ListView1.Canvas.Handle, PChar(Item.Caption),length(Item.Caption), LabelRect,
DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_END_ELLIPSIS);
i := 0;
while (i < Item.SubItems.Count-1) do
begin
OffsetRect (LabelRect, ListView1.Columns[i].Width,0);
LabelRect.Right := LabelRect.Left + ListView1.Columns[1+i].Width;
DrawText (ListView1.Canvas.Handle, PChar(Item.SubItems[i]),length(Item.SubItems[i]),
LabelRect, DT_LEFT or DT_VCENTER or DT_SINGLELINE or DT_END_ELLIPSIS);
inc (i);
end;
end;

Il y a du pour et du contre pour chacune de ces deux approches : je vous laisse juge !

V-D - Se positionner

Une autre amélioration est possible en proposant la possibilité de se positionner sur la date la plus proche. Ce sera chose faite avec cette fonction :

function ApprocheFete(D : TDate) : Integer;
var unEvenement : TEvenement;
i : Integer;
begin
for i:=0 to Pred(MesEvenements.Count) do
begin
unEvenement:=MesEvenements.Items[i];
if DayOfTheYear(unEvenement.Feterle)-DayOfTheYear(D)>0 then Break;
end;
result:=i;
end;

V-E - « Age or not Age ? »

Afficher l'âge de la Saint Valentin, qui de plus sera faux puisque le calendrier ne commence qu'en 1900, n'est évidemment pas utile. Le mieux est donc d'ajouter une propriété booléenne nommée ShowAge qui gérera l'affichage ou non de la zone :

Classe Evenement

type
TEvenement = Class
strict private
FNom : String;
FPrenom : String;
FDate : TDate;
FShowAge : Boolean;
private
function EnCSV : String;
procedure CSVversObjet(Data : String);
procedure SetDate(const Value: TDate);
public
constructor Create(); overload;
constructor Create(Prenom,Nom : String; UneDate : TDate; MontrerAge : Boolean); overload;
property Nom : String read FNom write FNom;
property Prenom : String read FPrenom write FPrenom;
property Feterle : TDate read FDate write SetDate;
property ShowAge : Boolean read FShowAge write FShowAge;
function CouleurItem : TColor;
end;

// modification des constructeurs
constructor TEvenement.Create;
// création par défaut
begin
FNom:='<Nom>';
FNom:='<Prénom>';
FDate:=Date;
FshowAge:=True; // ajout
end;

constructor TEvenement.Create(Prenom, Nom: String; UneDate: TDate; MontrerAge : Boolean);
// création objet
begin
FNom:=Nom;
FPrenom:=Prenom;
FDate:=UneDate;
FshowAge:=MontrerAge;  // ajout
end;

Toutefois, l'ajout de cette propriété oblige également à modifier légèrement les fonctions de sauvegarde et de lecture du format CSV :

Format CSV

function TEvenement.EnCSV: String;
// Transformation de l'objet en chaîne au format CSV choisi
begin
Result:=Format('"%s","%s",%s,%s',
[Prenom,
Nom,
FormatDateTime('dd/mm/yyyy',FeterLe),
BoolToStr(ShowAge,True)
]);
end;

procedure TEvenement.CSVversObjet(Data: String);
// transformation d'une ligne CSV en objet TEvenement
var SL : TStringList; // sert à parser la ligne
begin
SL:=TStringList.Create;
try
SL.QuoteChar:='"';  // indique l'utilisation des " pour les chaînes
SL.CommaText:=Data;
Prenom:=SL[0];
Nom:=SL[1];
Feterle:=StrToDate(SL[2]);
ShowAge:=StrToBool(SL[3]);
finally
SL.Free;
end;
end;

VI - Quid d'un programme identique mais FMX ?

Le principe reste le même pour une transposition de ce programme en FMX, seuls quelques spécificités de la TListView et les dialogues d'ouverture de fichiers étant alors à prendre en compte. Comme la conception de l'interface est bien sûr à revoir en fonction des tailles d'écran, j'ai opté pour une séparation en trois onglets :

- une liste :

(cliquez ici pour aggrandir l'image)

- un mode édition :

(cliquez ici pour aggrandir l'image)

- les outils de sauvegarde/chargement :

Le projet FMX est séparé des projets VCL, vous le retrouverez en téléchargement à cette adresse.

(cliquez ici pour aggrandir l'image)

VI-A - Liaison des données

Les liaisons s'établissent de la même manière que pour la VCL. Seule la modification de l'apparence de l'élément de liste va imposer quelques changements.

VI-A-1 - Modification de la TListView

Point positif : il est possible d'intégrer facilement la partie recherche en mettant les propriétés de la TListView SearchAlwaysOnTop et SearchVisible à True.

(cliquez ici pour aggrandir l'image)

Point négatif : mettre l'événement en valeur à son approche sera plus problématique.

Pour ce dernier point, j'ai choisi la solution la plus simple qui consiste à associer une TImageList (contenant deux petites icônes du programme Emule) et donc à utiliser les images de celle-ci au lieu de tenter de colorer tout l'élément. La colorisation de l'élément est possible en ajoutant un TBitmap vide dans les propriétés de l'objet.

Vous retrouverez cette technique dans ce tutoriel : Mettre de la couleur dans un TListView

Pour réaliser cela, la première étape est de changer l'apparence des éléments (ItemAppearance) en définissant la propriété à Dynamic.

VI-A-2 - Étape 1 : basculer en mode conception de liste

Tout en bas de l'inspecteur d'objets du composant TlistView, il est possible de sélectionner l'option « Basculer en mode conception ».

(cliquez ici pour aggrandir l'image)

Il est toutefois nettement plus facile d'utiliser le menu contextuel (clic droit) de ce composant pour accéder à cette fonctionnalité ! Je vais rajouter deux objets à mon élément de liste : un TImageObjectAppearence et un TAccessoryObjectAppearence.

(cliquez ici pour aggrandir l'image)

Je vais ensuite les arranger de façon à obtenir la présentation suivante :

Conception élément de liste

(cliquez ici pour aggrandir l'image)

Vous êtes perdus ? Retrouvez toutes les explications sur la conception d'un élément de liste d'apparence dynamique dans le DocWiki Embarcadero mais aussi dans le tutoriel déjà cité plus haut.

VI-A-3 - Étape 2 : la partie liaison

Du fait de l'utilisation d'une apparence de liste dynamique, les éléments à lier sont légèrement différents. En effet, l'élément text, bien que visible au niveau du concepteur visuel, n'est plus pris en compte. Par contre, les objets que nous avons ajoutés sont présents.

(cliquez ici pour aggrandir l'image)

Si établir les liaisons avec le concepteur visuel est une simple question de jeu de souris, ceux qui n'ont qu'une version Starter devront, une fois encore, passer par l'édition des éléments du TBindingsList.

(cliquez ici pour aggrandir l'image)

Il faudra ajouter deux expressions au lien établi avec la liste (TLinkListControlToField) établie comme au chapitre II.C.3 pour obtenir ce qui suit :

(cliquez ici pour aggrandir l'image)

Vous remarquerez que les ControlMemberName font référence aux objets ajoutés.

VI-A-4 - Étape 3 : codifier

Bien sûr, indiquer quelle icône utiliser ne va pas se faire sans un peu de programmation. De fait, deux événements sont à prendre en compte au moment où le composant TListview charge les objets (OnUpdatingObjects), mais aussi si une modification est faite dans les données.

Pour ce dernier point, je me suis contenté d'utiliser l'événement OnClick du navigateur du second onglet :

gestion des icônes de la liste

// Gestion couleur (icône) de l'item de liste ----------------------------------
procedure TFormPBFMX.ListView1UpdatingObjects(const Sender: TObject;
const AItem: TListViewItem; var AHandled: Boolean);
// à la création de la liste
var ABitmap: TListItemImage;
unEvenement : TEvenement;
AColor : TAlphaColor;
begin
unEvenement:=MesEvenements[AItem.Index];
ABitmap:=AItem.Objects.FindObjectT<TListItemImage>('Image1');
if assigned(ABitmap) then
begin
Abitmap.ImageIndex:=UnEvenement.CouleurItem;
end;
end;

procedure TFormPBFMX.NavigatorAgendaClick(Sender: TObject; Button: TNavigateButton);
// Gestion couleur si modification de la date
var ABitMap : TListItemImage;
unEvenement : TEvenement;
begin
if Button=nbPost then
begin
ListView1.BeginUpdate;
unEvenement:=MesEvenements.Items[ListView1.ItemIndex];
ABitmap:=ListView1.Items[ListView1.ItemIndex].Objects.FindObjectT<TListItemImage>('Image1');
if assigned(ABitmap) then Abitmap.ImageIndex:=UnEvenement.CouleurItem;
ListView1.EndUpdate;
end;
if Button=nbEdit then
begin
TabControl1.ActiveTab:=TabItem1;
end;
end;

VI-B - Résultat

Le résultat est alors conforme à mes espérances.

1. Bien sûr, tout dépend du jour de l'exécution du programme et aussi des données ! Pour obtenir cette image, j'ai « triché » en modifiant les dates anniversaires...

(cliquez ici pour aggrandir l'image)

VI-C - Du style

Les choix d'affichage sont une affaire de goût. L'ajout d'un style TStyleBook (ici Transparent.style) finalise le programme.

(cliquez ici pour aggrandir l'image)

VII - Conclusion

À travers ces différentes étapes, j'ai voulu montrer :

- l'utilisation d'un prototype de données (TPrototypeBindSource) pour simuler une source de données au cours du design de l'interface utilisateur ;

- les premières techniques de liaisons entre des composants et les données ainsi obtenues.

Cette première approche montre les avantages, mais aussi les petits défauts encore présents dans l'utilisation des LiveBindings. L'objectif « zéro code » voulu par les concepteurs est presque atteint, si l'on ne tient pas compte des fioritures que j'ai pu ajouter dans ma quête de perfectionnements. Il y a là de quoi, à mon avis, encourager tout un chacun à l'utilisation de ces techniques !

Point de conclusion sans remerciements à mon premier « cobaye-lecteur » : Nabil et bien sûr aux relecteurs officiels du forum. Que cela soit de la relecture technique patiente de Gilles au correcteur orthographique et grammatical Jacques_Jean. Sans eux, point de publication, donc encore merci à eux !

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article