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 :
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).
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.
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.
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
.
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 !
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.
Ces modifications se sont répercutées sur le fichier dfm.
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… »).
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.
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 :
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 :
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é.
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 !
On obtient alors une liste comme suit :
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.
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.
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.
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 :
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.
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 :
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 :
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 :
AbindSourceAdapter:=TListBindSourceAdapter<TEvenement>.Create...
Cette instruction crée un TBindSourceAdapter qui va traiter un objet TEvenement.
(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.
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é :
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.
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 :
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 :
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 JSONJavaScript Object Notation.
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 :
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 :
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é :
…
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) :
//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 :
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 :
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 |
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 :
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.
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 :
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} |
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 :
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 :
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 :
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.
Je me suis contenté d'ajouter un TFileOpenDialog et quatre boutons dont les méthodes OnClick sont renseignées de cette manière :
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.
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 :
%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 :
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) |
10.54 |
10.54 |
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).
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 SGBDSystèmes de Gestion de Bases de Données 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 :
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 ▲
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'EDIEnvironnement de Développement Intégré 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.
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 :
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 :
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.
...
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.
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.
// 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
;
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 :
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 :
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 :
-
un mode édition :
- les outils de sauvegarde/chargement :
Le projet FMX est séparé des projets VCL, vous le retrouverez en téléchargement à cette adresse.
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.
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 ».
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.
Je vais ensuite les arranger de façon à obtenir la présentation suivante :
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.
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.
Il faudra ajouter deux expressions au lien établi avec la liste (TLinkListControlToField) établie comme au chapitre II.C.3 pour obtenir ce qui suit :
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 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.
- 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...
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.
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 !