Grilles et Livebindings

Les Livebindings de A à Z

Serge Girard

L'affichage des données sous forme de grille sera plus utilisé pour des applications de bureau que pour des applications mobiles pour lesquelles les ListView sont plus adaptées.

Si en VCL nous utilisons plus facilement le composant TDBGrid, en FMX seuls les composants TGrid et TStringGrid nous sont accessibles. Pour les remplir avec des données, nous avons deux solutions : coder ou utiliser les fonctions de liaisons (LiveBindings).

J'ai déjà abordé dans le tutoriel précédent la liaison simple entre une source de données et une grille : l'objectif est maintenant d'approfondir notre connaissance de ce type de liaison.

Commentez

- Titre : Grilles et Livebindings

- Auteur : Serge Girard

- Parution : 31 août 2018

- Mise à jour : 2 septembre 2018

- Public visé :

- 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 ® 2018 sergiomaster. 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. Mise en place

- I-A. La source de données

- I-B. Le programme de démonstration

- I-C. Démarche du tutoriel

- II. Approche avec la VCL

- III. StringGrid ou Grid

- IV. Liaison rapide ou manuelle

- IV-A. Liaison rapide

- IV-B. Liaison manuelle

- IV-B-1. À mémoriser

- IV-B-1-a. Le positionnement

- IV-B-1-b. Le remplissage des colonnes d'une TStringList

- IV-B-1-c. Le remplissage des colonnes d'une TGrid

- IV-B-2. En détail

- IV-B-2-a. Expressions pour un TStringGrid

- IV-B-2-b. Expressions pour un TGrid

- IV-B-2-c. L'expression de la source DisplayText

- IV-B-2-d. À propos de SelectedText(Self)

- IV-B-2-e. Événements de liaison

- IV-B-2-f. L'utilisation de ScopeMappings du TBindSourceDB

- IV-B-2-g. Récupération de données d'une liste

- IV-B-2-h. Les expressions FormatControl et ColFormat

- V. Typer les colonnes

- V-A. À partir d'une liaison rapide

- V-B. Les différents types de colonne

- V-B-1. TColumn

- V-B-2. TStringColumn

- V-B-3. TCheckColumn

- V-B-4. TDateColumn

- V-B-5. THourColumn

- V-B-6. TGlyphColumn

- V-B-7. TFloatColumn

- V-B-8. TCurrencyColumn

- V-B-9. TInteger

- V-B-10. TImageColumn

- V-B-11. TProgressColumn

- V-B-12. TPopupColumn

- V-B-13. Ajout de ses propres types

- VI. Les événements pour aller plus loin

- VI-A. Un menu contextuel selon la colonne

- VI-B. Mettre de la couleur

- VI-B-1. Premier exemple : mettre la colonne « Nom » en couleur

- VI-B-2. Deuxième exemple : mettre en couleur les poissons « nobles » (Bar, Sole)

- VI-B-3. Derniers retranchements : mettre en couleur selon la valeur d'une autre colonne

- VII. Astuces, le coin du mécano

- VII-A. Hauteur de l'en-tête de grille

- VII-B. Une colonne indicateur d'enregistrement

- VII-B-1. Dessin d'un triangle dans une cellule

- VII-B-2. Récupérer les couleurs à utiliser

- VII-C. Remplacer FillText, les possibilités de TTextLayout

- VII-C-1. Grille avec des valeurs de cellules en langues RTL

- VII-C-2. Mixer fontes et couleurs au sein d'un texte

- VIII. Conclusions

I - Mise en place

Ce tutoriel ne peut s'appliquer qu'à partir des versions 10.1 (Berlin) de Delphi. Le programme de démonstration a été fait avec la version 10.2.3 (Tokyo).

I-A - La source de données

L'intérêt du travail qui suit est bien d'afficher des données « réelles » dans une grille et non d'utiliser des données aléatoires comme peut le fournir un TPrototypeBindSource, trop théorique.

Le dilemme suivant qui s'est imposé était de trouver quel fichier, permettant de couvrir, à peu près, tous les types de colonnes que je pouvais utiliser. De surcroît, je voulais fournir un mode d’accès compatible avec une version starter de Delphi, c'est-à-dire avec un TClientDataset. Mes recherches dans les fichiers proposés en exemple n'aboutissant à rien d'exploitable, j'ai décidé d'en créer un propre autour d’un tableau de pêche. Cette application me sera ainsi utile pour vanter mes mérites quand je pratique mon passe-temps favori  : la chasse sous-marine.

Sans plus tarder, en voici la structure :

Nom colonne

Type/Taille

Commentaire

Nom

String / 30

Prise faite

DatePrise

Date

HeurePrise

Time

Plus pour le type de colonne que pour la précision des faits

Poids

Float

Exprimé en kg.

PrixduJour

Currency

Le prix chez le poissonnier, histoire de calculer si mon investissement en matériel est rentable.

Quantité

Word

Catégorie

Word

Un entier qui va me permettre de classer mes prises entre :

- poissons,

- mollusques,

- crustacés,

- coquillages

Il me permettra par ailleurs d'afficher le glyphe correspondant.

ModePrise

Word

Aussi un entier, mais je vais le relier à une liste des modes de prises possibles :

- à pied,

- à la ligne,

- au filet,

- aux casiers,

- en chasse sous-marine.

Vantardise

Booléen

Il me manquait un type booléen jusqu'à ce que je pense à l'adage « En tout pêcheur il y a un menteur » !

Circonstances

Memo

Description des lieux et autres anecdotes

Images

Graphic

La preuve en photo

Création fichier

if not FileExists('..\..\PecheurMenteur.cds') then
begin
// Création de la table de données
with cdscreate.FieldDefs do
begin
with AddFieldDef do begin
Name := 'Nom';
DataType := ftString;
Size := 30;
Required := False;
end;
with AddFieldDef do begin
Name := 'DatePrise';
DataType := ftDate;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Heure';
DataType := ftTime;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Categorie';
DataType := ftInteger;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Poids';
DataType := ftFloat;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'PrixduJour';
DataType := ftCurrency;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Quantite';
DataType := ftWord;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'ModePrise';
DataType := ftWord;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Vantardise';
DataType := ftBoolean;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Circonstances';
DataType := ftMemo;
Size := 0;
Required := False;
end;
with AddFieldDef do begin
Name := 'Image';
DataType := ftGraphic;
Size := 0;
Required := False;
end;
end;
cdsCreate.CreateDataSet;
end;

La création d'une table vide est incluse dans le programme, mais vous trouverez également une table préremplie (légèrement).

Je vous dévoile donc quelques-uns de mes coins de pêche, encore que, juste avec le nom du lieu-dit vous risquez d'avoir du mal à piller mes ressources !

I-B - Le programme de démonstration

Une fois n'est pas coutume, quelques explications s'imposent. Le programme a été écrit pour tester à la fois l'utilisation des TStringGrid et TGrid mais aussi le mode de liaison. Par conséquent, pour chacun de ces deux composants, il y a à la fois la liaison rapide (TLinkGridToDataSource) et une liaison plus manuelle (TBindGridLink).

Un TTabControl de quatre panneaux recoupe les différentes parties du tutoriel :

Le premier panneau concerne le TStringGrid, la grille du haut etant liée rapidement via les experts disponibles, la grille du bas étant quant à elle liée manuellement.

Le second panneau concerne le TGrid. Comme le premier panneau, la grille du haut est liée via les experts alors que la seconde est liée « à la main ».

Le troisième panneau permet d'introduire les différentes personnalisations possibles d’une TGrid (couleurs, tailles de colonnes, menus contextuels, etc.).

Le dernier panneau concerne la saisie d'un seul enregistrement de la table :

Chaque grille de l'interface utilisateur, de même que la forme de saisie des données, est traitée dans des cadres (Frames) distincts. Les raisons de ce choix ?

- En premier lieu le concepteur visuel de liaison : tout mettre sur une seule forme aboutit à un affreux salmigondis très difficile à embrasser d'un seul coup d’œil.

- En second argument, chaque cadre ayant son propre TBindingsList il est beaucoup plus simple alors de comparer les différents choix qui sont faits.

- Enfin cela peut permettre de comparer les différents sources .dfm, afin de voir ce qu'il est possible de faire à ce niveau.

Vous trouverez le code source de cet article Ici.

I-C - Démarche du tutoriel

Étant donné que les techniques de liaison peuvent s'appliquer aussi bien au TStringGrid qu'au TGrid, à quelques exceptions près ; je n'allais pas répéter à chaque fois les mêmes informations.

J'ai donc préféré parler des liaisons (expresse ou manuelle) puis des améliorations à apporter aux grilles elles-mêmes bien que cela soit imbriqué. Si je devais faire un classement, je hiérarchiserais par raffinement, rendu final et complexité :

1. TStringGrid liaison expresse ;

2. TGrid liaison expresse ;

3. TStringGrid liaison manuelle ;

4. TGrid liaison manuelle.

En fait, l'habitude aidant, le TGrid se révèle beaucoup plus avantageux ce qui explique peut-être qu'au fil de la rédaction j'aborde de plus en plus souvent TGrid au détriment de TStringGrid.

II - Approche avec la VCL

En guise de mise en bouche, j'ai d'abord voulu voir ce que les LiveBindings pouvaient apporter sur une grille en VCL. Il est à noter que seule la TStringGrid peut être liée.

En haut la classique TDBGrid, en bas une TStringGrid liée avec l'expert sans aucune modification. Les premiers constats figurent dans les encadrés rouges. Le plus déplorable est la perte de l'indicateur de l'enregistrement en cours. En second lieu nous remarquons que les colonnes ont toutes la même taille alors que la TDBGrid prenait en compte la taille des colonnes. Plus en détail, notons le format des colonnes date et heure et les valeurs pour un contenu booléen. Seul point en faveur des LiveBindings, une colonne de type mémo texte affiche une partie du contenu.

J'ai quand même voulu aller plus loin pour voir ce qu'il était possible de faire en modifiant quelque peu une grille liée, et c'est l'objet de la seconde image écran.

En haut, la même grille liée par l'expert (LinkGridToDataSource) mais avec quelques modifications sur les propriétés ParseFormat, en bas une liaison plus manuelle (BindGridLink) qui montre les quelques différences encadrées en rouge.

Croyez-moi, pour la liaison manuelle, le jeu n'en vaut pas la chandelle ! À tel point que je ne vous fournis même pas le source de ce programme.

N.B. L'encadré jaune montre un espace inesthétique sur lequel je reviendrai plus tard IV.B.2.h .

III - StringGrid ou Grid

Voici un piège dans lequel je suis tombé à pieds joints ! La VCL ne me proposant de lier que la TStringGrid et beaucoup de vidéos ne faisant les liaisons que sur cette grille, mon réflexe a été de poser d'abord ce composant plutôt qu'un TGrid. Quelques différences par rapport à la VCL sautent immédiatement aux yeux !

Des glyphes, des cases à cocher et des mémos qui semblent extensibles apparaissent, et ça sans une ligne de code !

Je l'avoue, pour les glyphes je triche un peu. Vous verrez dans le prochain chapitre comment j'ai pu afficher ces glyphes alors que la colonne est une donnée numérique.

Quand nous passons à la TGrid, c'est encore plus flagrant car même les photos apparaissent !

L'image est très petite ? C'est dû à la hauteur par défaut des lignes.
Changer la propriété RawHeight de la grille permet d'agrandir automatiquement.

IV - Liaison rapide ou manuelle

La liaison rapide est certes beaucoup plus facile à mettre en place que d'utiliser un TBindGridLink. Toutefois, même si cette dernière méthode est plus longue, elle offre des possibilités que la facilité de la liaison rapide occulte, à savoir pouvoir utiliser plusieurs expressions dans une collection.

Collection = Liste d’objets homogènes, dans notre cas, d'expressions à appliquer.

IV-A - Liaison rapide

Que ce soit pour un TStringList ou un TGrid, rien de plus simple pour lier notre source de données à la grille. Par l'intermédiaire de l'expert un simple dialogue de trois pages suffit, la première page permettant même de créer la grille.

La quatrième étape de l'expert est optionnelle mais permet d’ajouter au besoin un navigateur :

Par le concepteur de liaisons visuel, c'est encore plus simple : une manipulation de glisser et déposer entre la grille et la source de données avec la souris !

Pas une ligne de code dans le source, pourtant nos grilles sont prêtes, comme avec la VCL et sa TDBGrid.

Dans le cas où nous posons une grille pour tout de suite la lier à une source de données ouverte,

Attention, notez bien : une source de données ouverte.

toutes les colonnes sont créées et taillées au mieux automatiquement. Ce n'est, ma foi, pas trop mal fait puisque les colonnes ainsi créées détectent le type de données autant que possible. Ainsi les mémos texte ou les images, uniquement dans un TGrid pour ces dernières, vont déjà s'afficher.

Pour rappel aux utilisateurs d'une version starter : vous devez préalablement poser un composant TBindingsList pour pouvoir utiliser l'expert de liaison.

De même, si vous voulez directement passer aux liaisons manuelles vous devez avoir déposé et renseigné un composant TBindScopeDB que l'expert de liaison rapide aurait créé de son propre chef.

Inconvénients des liaisons express, peu de modifications sont possibles et il est impossible d'ajouter des expressions à une collection.

IV-B - Liaison manuelle

Pas de secret, pour aborder ce type de procédé de liaison, il faut regarder ce qui se fait avec une liaison rapide puis avancer par touches successives. Il y a très peu de documentation sur ce sujet. Je vous propose, en premier lieu, quelques généralités à mémoriser.

IV-B-1 - À mémoriser

IV-B-1-a - Le positionnement

Le positionnement est le plus important puisqu'il agit sur le déplacement dans la source de données.

Surtout ne pas oublier de renseigner ces deux expressions.

ControlExpression

SourceExpression

PosControlExpressions

Selected

Math_Max(0,DBUtils_ActiveRecord(Self))

PosSourceExpressions

Math_Max(1,Selected+1)

DBUtils_ValidRecNo(Self)

IV-B-1-b - Le remplissage des colonnes d'une TStringList

Expression de contrôle

Expression de la source

CellFormat

Cells[n] où n est le numéro de colonne

Text

CellParse

SelectedText(Self)

Value

IV-B-1-c - Le remplissage des colonnes d'une TGrid

Expression de contrôle

Expression de la source

CellFormat

Data

Text ou Value

CellParse

SelectedText(Self)

Value

IV-B-2 - En détail

Ce serait la simplicité même si les règles exposées ci-dessus s'appliquaient tout le temps et pour toutes les associations colonnes/données. Il y a, hélas, des exceptions dépendant généralement du type de champ à associer mais aussi, bien évidemment, de ce que nous désirons afficher.

IV-B-2-a - Expressions pour un TStringGrid

Sauf cas particulier, comme l'affichage d'un glyphe, une cellule de ce type de grille TStringGrid ne peut contenir que du texte. Nous accèdons à la cellule par l'index de colonne Cells[n] et la valeur de la donnée doit être transformée en chaîne, ce qui se traduit, dans l'expression de la source, par Text. Restent les cas particuliers correspondant au type de champ :

- pour un booléen il faut interpréter la valeur, ce qui sera fait par DBUtils_BoolValue(Self) ;

- pour un mémo de type texte il faut transformer le contenu du champ en chaîne par AsString ;

- pour un mémo de type graphique (une image), impossible à afficher dans un TStringGrid, nous utiliserons une constante, par exemple : '(Bitmap)'.

IV-B-2-b - Expressions pour un TGrid

Principale différence avec la TStringGrid, et, à mon avis, gros avantage, il n'est plus question de se référer à une cellule indexée avec un TGrid, nous nous contenterons d'indiquer qu'il s'agit d'une donnée (Data).

S’il n’y a pas de différence en ce qui concerne les champs booléens ou mémo texte, ce qui change est bien sûr la possibilité de traiter les mémos graphiques. Comme bien sûr il n'est pas question d'utiliser Text pour y accéder, nous mettrons Self.

IV-B-2-c - L'expression de la source DisplayText

Comme le typage de colonne n'a pas encore été vraiment abordé, sauf marginalement pour vous allécher avec les colonnes contenant les glyphes, je peux aborder la notion de DisplayText.

Prenons l'exemple de la colonne contenant une date. Par défaut, la date est affichée selon nos paramètres de poste (DefaultFormatSettings), dans mon cas sous la forme dd/mm/yyyy.

Que faire pour afficher sous une autre forme, par exemple nom du jour abrégé, jour nom du mois abrégé et année sur deux chiffres grâce au formatddd, dd mmm yy ? C'est ce que permet DisplayText dès que le format de la colonne de données est précisé dans la déclaration du champ.

Cependant, DisplayText est, selon moi, un faux ami. Effectivement la partie affichage dans la grille se fera selon nos directives, mais l'interprétation du contenu de la cellule (CellParse) ne va pas se faire correctement (à l'exception du format dd/mm/yy).

C'est encore plus flagrant dans le cas d'un champ monétaire que nous voudrions voir affiché avec la devise. Soit, par exemple, une valeur affichée de 120,50 € : pour modifier cette valeur l'utilisateur devra taper 110.25 donc effacer pratiquement toute la cellule, saisir les chiffres et faire attention à bien utiliser le point du clavier (et non le point décimal du pavé numérique, qui sera interprété comme une virgule !) et ce sous peine de se voir infliger une erreur du style : 110,25 n'est pas une valeur numérique !

Il est possible de pallier ces inconvénients en créant ses propres méthodes à ajouter aux moteurs d'expressions (Tutoriel : LiveBindings - l'évaluateur d'expressions).

Vous trouverez un mode opératoire dans l'unité UnitMethodes.pas contenu dans le fichier des sources.

Plus efficace sera de typer les colonnes comme nous le verrons chapitre V.
Mais typer les colonnes réduit à néant l'utilisation de DisplayText.

IV-B-2-d - À propos de SelectedText(Self)

Selon la documentation, utilisé dans les expressions permettant d'interpréter les données saisies de la cellule (CellParse),SelectedText(Self), où Self représente l'éditeur utilisé, peut être remplacé, en fonction du type de colonne, par :

SelectedDateTime(éditeur)

SelectedItem(éditeur)

SelectedLookupValue(éditeur)

SelectedValue(éditeur)

IV-B-2-e - Événements de liaison

S'il y a quelque chose de frustrant, c'est bien la colonne « Mode de prise » qui n’apparaît pas de manière lisible par l’utilisateur final. En effet, lors de la conception de la table j'ai préféré utiliser une valeur numérique entière, valeur de l'index dans une liste, plutôt qu'un libellé. Utiliser un événement sur la liaison peut être envisagé pour résoudre ce problème.

Les événements disponibles sont au nombre de cinq : OnActivated, OnActivating, OnAssignedValue, OnAssigniningValue et OnEvalError ; celui qui me semble le plus important est le OnAssigningValue.

Ayant déjà utilisé avec succès OnAssigningValue pour d'autres composants, j'ai pensé m'en servir. Toutefois les deux événements qui nous permettent d'accéder aux valeurs n'ont aucune indication simple sur l'objet concerné (comme l'index de la colonne), juste AssignValueRec qui nous renseigne sur l'expression, la propriété et l'objet.

Première difficulté, le type TGridDataObject n'est pas accessible, il faut le re-déclarer pour pouvoir travailler avec !

type
TGridDataObject = class
private
FColumn: Integer;
[Weak] FOwner: TCustomGrid;
public
property Owner: TCustomGrid read FOwner;
property Column: Integer read FColumn;
end;

Second problème, l'objet n'est pas toujours le même, il faut donc avant toute chose tester le type d'objet pour pouvoir ensuite le transtyper correctement afin de l'utiliser, ce qui me permet, enfin, d'accéder au numéro de colonne.

procedure TMainForm.BindGridLink1AssigningValue(Sender: TObject;
AssignValueRec: TBindingAssignValueRec; var Value: TValue;
var Handled: Boolean);
var AnObject : TObject;
colonne : integer;
begin
AnObject:=AssignValueRec.OutObj;
if AnObject.ClassNameIs('TGridDataObject') then
begin
colonne:=TGridDataObject(AnObject).Column;
if colonne=7 then // colonne 7 correspond à mode de prise
case Value.AsInteger of
0 : Value := 'Casier';
1 : Value := 'Filet';
2 : Value := 'Drague';
3 : Value := 'A la ligne';
4 : Value := 'Pêche à pied';
5 : Value := 'Chasse sous-marine';
end;
end;

Il est donc possible de changer la valeur qui sera affichée. Cependant, changer la valeur affichée ne répondra pas à la question subsidiaire : comment obtenir l'inverse, c'est-à-dire obtenir une valeur par rapport au libellé ?

Cette méthode a quand même un inconvénient : ces valeurs affichées sont des constantes que je retrouverai aussi à d'autres endroits plus tard

soit dans l'utilisation d'un type de colonne TColumnPopup V.B.12, soit en utilisant un TPopupMenu VI.A.

Donc des sources potentielles d’erreurs orthographiques ou d’ordre dans la forme.

IV-B-2-f - L'utilisation de ScopeMappings du TBindSourceDB

Avant d'aborder ce que peuvent faire les collections d'expressions FormatControl et ColFormat, il me faut aborder cette notion.

Longtemps je me suis posé la question de savoir à quoi cette propriété pouvait servir. C'est presque par hasard, au cours de l'écriture de l'exemple associé à ce tutoriel, que j'ai découvert une de ses utilisations.

Je n'arrivais pas, dans l'expression de la source, à accéder aux composants visuels de ma forme. Or, j'avais deux objectifs en tête : remplacer la valeur de la colonne ModePrise, un entier, par son intitulé pris dans une liste (TPopupMenu) mais également accéder à la grille pour par exemple pouvoir retailler mes colonnes en largeur, avoir en quelque sorte une colonne « élastique ».

C'est là que la propriété ScopeMappings intervient.

J'ai défini deux observateurs (Scopes) supplémentaires à ce qui permet de lier les données aux composants : le TBindScopeDB.

Je peux ainsi accéder à ces deux éléments (ou plutôt à leurs propriétés) dans mes expressions de source, comme nous allons le voir dans les chapitres qui suivent.

IV-B-2-g - Récupération de données d'une liste

Comment alors faire en sorte d'afficher un libellé se trouvant, dans ce cas précis et pour des raisons qui seront exposées ultérieurement (voir VI.A), dans un PopupMenu ?

Le fait d'avoir créé un ScopeMappings avec le menu permet de fournir une solution LiveBindings. Il suffit de changer l'expression de la source de la collection CellFormat pour la colonne en Prise.Items[value].text.

(cliquez ici pour aggrandir l'image)

Il est aisé de vérifier que cette expression fonctionne en utilisant le bouton « Évaluer la source ».

IV-B-2-h - Les expressions FormatControl et ColFormat

Ces deux collections, accessibles uniquement via des liaisons manuelles (TBindGridLink), permettent d'accéder à certaines propriétés des colonnes de la grille comme leur largeur ou leur intitulé.

Comme premier exemple d'utilisation, je choisirais la colonne « élastique ».

Je trouve, en effet, inesthétique la partie droite de la grille, nous retrouvons souvent un espace perdu entre la dernière colonne et la gouttière (encadré jaune).

Maintenant que je peux accéder, grâce aux ScopeMappings, aux propriétés de la grille, je peux très bien recalculer la taille de ma colonne « Circonstances » de façon à ce qu'elle récupère cet espace perdu.

Notez que j'ai utilisé la formule dans FormatControl plutôt que ColFormat. Car FormatControl s'applique à l'entête de colonne (donc une seule fois), alors que ColFormat s'appliquera à chaque cellule de la colonne.

Formule

Math_Max(30,  // Minimum 30
Grille.Width - // taille de la grille
// - la taille de toutes les autres colonnes
-Grille.Columns[0].Width-Grille.Columns[1].Width-Grille.Columns[2].Width-Grille.Columns[3].Width-Grille.Columns[4].Width-Grille.Columns[5].Width-Grille.Columns[6].Width-Grille.Columns[7].Width-
Grille.Columns[9].Width-Grille.Columns[10].Width
-42 // largeur glissière et lignes verticales)

Pour que cela fonctionne, il me faudra alors faire appel à une fonction de la liaison entre la grille et les données BindGridLink.ResetColumns ; au sein de l’événement OnResize du parent de la grille (généralement une forme ou un cadre).

Encore une fois, il est tout à fait possible de coder dans le source.

Par exemple, le OnResize de la forme pourrait très bien gérer cette colonne « élastique ».

procedure TForm.FormResize(Sender: TObject);
var ltc : Single;
begin
ltc:=Grid1.Width-Grid1.Columns[0].Width
-Grid1.Columns[1].Width
-Grid1.Columns[2].Width-Grid1.Columns[3].Width
-Grid1.Columns[4].Width-Grid1.Columns[5].Width
-Grid1.Columns[6].Width-Grid1.Columns[7].Width
-Grid1.Columns[9].Width-Grid1.Columns[10].Width-42;
Grid1.Columns[8].Width:=Max(30,ltc);
end;

À présent voici un autre exemple avec l'utilisation de ColFormat, qui va me permettre également d'introduire la notion d'ajout de méthodes au moteur d'expressions : la colonne taillée au plus juste.

L'objectif est de faire en sorte qu'une colonne (« Nom » dans le programme test) soit de la largeur maximum en fonction des données. Le problème est que, si je peux obtenir la taille en nombre de caractères cela ne me donne pas cette taille en pixels.

Dans mon tutoriel sur l'évaluateur d'expressions, j'ai introduit la possibilité d'ajout de méthodes personnelles (chapitre IV) : c'est ce que je vais mettre en pratique.

Je rajoute cette unité à ma liste des utilisations.

unit UnitMethodes;

interface

implementation

uses
System.Classes, System.SysUtils, System.Rtti, System.StrUtils,
System.Bindings.EvalProtocol,System.Bindings.Methods, System.Bindings.Consts,
FMX.TextLayout, FMX.PlatForm, FMX.Graphics;

function TextSize: IInvokable;
begin
Result := MakeInvokable(function(Args: TArray<IValue>): IValue
var
v1: IValue;
Ts : Single;
ATLayout : TTextLayout;
FontService : IInterface;
FontName : String;
FontSize : Single;
begin
if Length(Args) <> 1 then
raise EEvaluatorError.Create(Format(sUnexpectedArgCount, [1,Length(Args)]))
else begin
// tentative de récupération de la fonte par défaut
if TPlatformServices.Current.SupportsPlatformService(IFMXSystemFontService) then
begin
FontService:=TPlatformServices.Current.GetPlatformService(IFMXSystemFontService);
FontName:=IFMXSystemFontService(FontService).GetDefaultFontFamilyName;
FontSize:=IFMXSystemFontService(FontService).GetDefaultFontSize;
end
else begin
// fonte par défaut Windows
FontName:='Segoe UI';
FontSize:=12;
end;
v1:=Args[0];
ATLayout:=TTextLayoutManager.DefaultTextLayout.Create;
ATLayout.BeginUpdate;
ATlayout.Font.Size:=FontSize;
ATlayout.Font.Family :=FontName;
// 'H' est la lettre la plus large
// contruire la chaine StringofChar de la taille de la valeur
ATlayout.Text:=StringOfChar('H',Length(v1.GetValue.asString));
ATLayout.EndUpdate;
Ts:=ATLayout.TextWidth;
Exit(TValueWrapper.Create(Ts));
end;
end);
end;

procedure EnregistrerMesMethodes;
begin
TBindingMethodsFactory.RegisterMethod(
TMethodDescription.Create(
TextSize,
'TailleTexte',
'TailleTexte', 'MesMethodes', True,
'',
nil));
end;

procedure LibererMesMethodes;
begin
TBindingMethodsFactory.UnRegisterMethod('TailleTexte');
end;

initialization
EnregistrerMesMethodes;
finalization
LibererMesMethodes;

end.

Je peux alors utiliser cette méthode à l'intérieur de mes liens !

Remarquez la taille de la colonne « Nom » encadrée en rouge, qui est réduite. Pourtant, à l'exécution j'obtiendrai des noms complets sans troncatures.

Seul inconvénient à l'ajout de méthodes « maison » : au moment du design les données n'apparaissent plus, la méthode étant inconnue dans l'environnement de développement.

Ce problème est contournable en ajoutant l'expression au début de l'exécution du programme de la manière suivante :

procedure TForm15.FormCreate(Sender: TObject);
var AnExpression : TExpressionItem;
begin
// Mémorisation PopupMenu par défaut
TFMXObject(Grid1).TagObject:=Grid1.PopupMenu;
// Ajout de l'expression pour retailler la colonne "Nom"
with BindGridLink1.ColumnExpressions.Items[0] do
begin
AnExpression:=FormatColumnExpressions.AddExpression;
AnExpression.ControlExpression:='Columns[0].Width';
AnExpression.SourceExpression:='Math_Max(Grille.Columns[0].Width,TailleTexte(Self.asString))';
end;
// Chargement du fichier
cds.LoadFromFile('F:\Livebindings\FMX\DBGrid\Win32\Debug\PecheurMenteur.cds');
// Ajout d'un index, qui va mettre en ordre de Date
cds.IndexFieldNames:='DatePrise;Heure';
// Ouverture du fichier
cds.Active:=True;
end;

L'utilisation de cadres (TFrame) est, dans ce cas de figure, pénalisante car l'événement OnCreate n'existe pas, il faudra substituer cette méthode.

Voir l'unité framegridbis.pas.

V - Typer les colonnes

J'ai découvert le truc consistant à typer les colonnes par hasard, il ne me semble pas avoir vu d'informations à ce propos dans l'aide si ce n'est à travers une liste des possibilités Docwiki Embarcadero FMX.Grid.TColumn.

Tout va dépendre si la grille (que ce soit un TGrid ou un TStringGrid) est déjà liée par l'intermédiaire d'un lien rapide ou non.

V-A - À partir d'une liaison rapide

Dans le menu contextuel, obtenu par un clic droit sur la grille, peut-être déjà utilisé pour obtenir l'expert de liaison, se trouve une option permettant d'éditer les colonnes.

Pour l'instant non rempli, cet éditeur va permettre de définir les colonnes de la grille.

Avez-vous remarqué le titre de cet éditeur ? Nous sommes sur une modification d'un lien !

Le troisième bouton à partir de la droite permet d'ajouter tous les champs, sélectionner ensuite un élément de cette liste permet de modifier le type de colonne.

Par défaut le type est TColumn pour une TGrid, TStringColumn dans le cas d'un TStringGrid.

Je me suis positionné sur la colonne « Catégorie » et vais choisir le type GlyphColumn.

En dehors de la valeur qui disparaît, rien ne se passe ; pour obtenir le glyphe, il ne faut pas oublier de renseigner la propriété Images de la grille.

J'avais, auparavant, ajouté un TImageList à ma forme.

Une fois la propriété Images renseignée, le glyphe apparaît bien en lieu et place des valeurs.

V-B - Les différents types de colonne

Si la plupart des types n'ont pas besoin de précision particulière, il y a pour chacun d'eux des pour et des contre. Il faut considérer que typer une colonne permet dans la plupart des cas de lui associer un éditeur ou un comportement particulier. Je ne vais pas faire une liste exhaustive de toutes les propriétés de chaque type de colonne, mais plutôt indiquer les quelques propriétés intéressantes selon le cas. J'en profiterai également pour donner mon ressenti pour chacune.

V-B-1 - TColumn

TColumn est La version colonne fourre-tout pour TGrid. Non liée, c'est par défaut une colonne de type chaîne de caractères.

Pour

Contre

Type par défaut, pratique lors des liaisons rapides.

Colonne qui distingue les différents types de données mais applique des formats « standard », par exemple un réel sera affiché avec deux chiffres après la virgule.

V-B-2 - TStringColumn

Colonne de base pour TStringGrid, comme pour TColumn, il n'y a pas grand-chose à dire de TStringColumn si ce n'est qu’elle présente les mêmes avantages et inconvénients que sa consœur.

V-B-3 - TCheckColumn

Colonne case à cocher, TCheckColumn est, par conséquant, à lier avec une valeur booléenne. Comme certaines bases de données (par exemple les versions de Firebird inférieures à 3) ne connaissent pas ce type de colonne, il faudra jouer sur la propriété ParseFormat de la liaison pour obtenir un bon comportement.

Pour

Contre

Le fait que ce soit une case à cocher.

Outre le problème du traitement au niveau des liaisons, la case à cocher traite trois états : vrai, faux mais aussi non renseigné (null) - dans ce cas la case n'apparaît pas, ce qui peut être un peu perturbant !

V-B-4 - TDateColumn

TDateColumn est une colonne permettant de saisir des dates.

Pour

Contre

Bien évidemment l'éditeur mais aussi la possibilité de formater la date.

J'ai un avis mitigé sur la propriété ShowClearButton(1).

V-B-5 - THourColumn

THourColumn est une colonne permettant de saisir des heures.

Pour

Contre

Comme pour TDateColumn l'éditeur et la propriété Format.

La propriété ShowClearButton.
On ne peut pas saisir une durée, juste une heure de la journée (de 0 à 24h).

V-B-6 - TGlyphColumn

TGlyphColumn permet d'afficher un glyphe contenu dans une liste d'images selon la valeur (index dans la liste).

Pour

Contre

L'affichage d'une image en fonction de la valeur.

La colonne ne peut être qu'en lecture seule.
La liste d'image est liée à la grille et non à la colonne, ce qui sous-entend que si nous voulons utiliser plus d'une colonne glyphe il faudra certainement jouer sur les valeurs.

Il est vraiment regrettable que la liste d'images soit liée à la grille et non une propriété de la colonne ! S'il devait y avoir plus d'une colonne glyphe il faudrait, soit revoir les valeurs possibles dans la base de données, soit jouer avec les valeurs pour obtenir les bons index.

Pour pallier le problème de la lecture seule, nous pouvons nous rabattre sur l'utilisation d'un menu contextuel (voir VI.A).

V-B-7 - TFloatColumn

TFloatColumn est une colonne permettant la saisie de nombres.

Pour

Contre

Outre le fait que l'éditeur ne permette que la
saisie de nombre, ce qui est intéressant est que l'on puisse jouer sur le format grâce aux propriétés :
DecimalDigits (nombre de décimales),
ShowThousandSeparator (séparateur de milliers).

V-B-8 - TCurrencyColumn

TCurrencyColumn permet la saisie de nombres représentant des valeurs monétaires.

Pour

Contre

Comme pour TFloatColumn

On ne voit pas quelle est la différence avec TFloatColumn si ce n'est que le séparateur de milliers, activé par défaut.
Je m'attendais à voir le symbole monétaire mais non !

V-B-9 - TInteger

TInteger permet de saisir un entier.

Pour

Contre

L'éditeur.

V-B-10 - TImageColumn

TImageColumn permet l’affichage d'images donc uniquement pour TGrid.

Pour

Contre

Une image dans une grille sans difficulté.

Uniquement pour TGrid.
Il faut que l'image soit une donnée (blob binaire) de la source.

V-B-11 - TProgressColumn

Absent de la version Berlin, TProgressColumn est le seul type de colonne que je n'ai pas réussi à intégrer dans mon application, faute d'imagination. Les propriétés qu'il faut utiliser : Min et Max.

V-B-12 - TPopupColumn

En théorie, le type de colonne TPopuColumn semble pratique pour tout ce qui ressemble à des listes prédéfinies.

Pour

Contre

Sélection dans une liste.

Il ne semble pas possible de l'appliquer pour des couples valeur/intitulé ou index/intitulé.

Je n'ai pas réussi à lier un index de liste à la cellule sauf en passant par des événements et encore uniquement pour la partie affichage, donc pour l'équivalent d'une colonne en lecture seule !

Vous préférerez utiliser un menu contextuel sur la colonne, comme indiqué au paragraphe VI.A, qui permettra de modifier facilement les données.

V-B-13 - Ajout de ses propres types

Oui, vous avez bien lu, nous pouvons ajouter nos propres types de colonnes. Vous trouverez à cette adresse un exemple édifiant quoiqu'un peu daté (version XE3) des possibilités (source disponible ici).

La partie qui suit est un résumé et une traduction rapide de l'article que vous pourrez trouver à cette adresse.

Penchons-nous un peu sur ce qu'est un TColumn d'une TGrid. Une colonne contient des cellules faites avec un descendant quelconque d'un contrôle stylé, soit pratiquement tout contrôle visuel ! Il est donc relativement simple de créer nos propres types de colonnes. Prenons par exemple une boîte de sélection de couleur (TColorComboBox).

La première chose à faire est de déclarer une nouvelle classe pour notre cellule.

type TColorComboCell = class (TColorComboBox)
end ;

Puis il nous faut une classe pour notre colonne, mais aussi une fonction qui va changer la création par défaut de la cellule :

type TColorComboColumn = class (TColumn)
protected
function CreateCellControl : TStyledControl ; override ;
end ;

implementation

function TColorComboColumn.CreateCellControl:TStyledControl ;
begin
Result:=TColorComboCell.Create(Self) ;
TColorComboBox(Result).onChange:=DoTextChanged ;
end ;

Une fois nos classes créées, il est alors aisé d'ajouter ce nouveau type de colonne à un TGrid.

Grid1.AddObject(TColorComboColumn.Create(Grid1)) ;

Il faudra alors ensuite établir le lien avec nos données comme j'ai pu le montrer au chapitre IV.B.2.h.

Donc, en théorie, il est possible de créer n'importe quel type de colonne mais il y a plusieurs bémols : il faut créer la colonne à la création du programme et, subséquemment, édifier la liaison par la suite.

À moins que vous ne trouviez l'astuce pour ajouter ce type de colonne personnalisées à l'IDE de Delphi, les colonnes personalisées ne sont donc pas aussi pratiques que celles qui nous sont proposées.

VI - Les événements pour aller plus loin

Il est temps de repousser encore les limites du TGrid en s'attaquant à la présentation des cellules. Pas de scoop, les Livebindings ne peuvent pas tout faire, impossible par exemple d'accéder au canvas par leur intermédiaire pour pouvoir « coloriser » nos grilles. La seule réponse à certains besoins restera bien le code par l'intermédiaire des événements des grilles.

VI-A - Un menu contextuel selon la colonne

Tout composant visuel ou presque a une propriété PopupMenu, permettant d'afficher un menu contextuel en utilisant un clic droit. Les deux composants grilles n'échappent pas à cette règle mais aussi les colonnes de ces dernières.

Chaque colonne de la grille peut donc avoir son propre menu contextuel (propriété PopupMenu). Toutefois celui-ci ne s'affiche pas automatiquement, seul celui lié à la grille fonctionne. Bogue ou fonction non implémentée ? Je penche plus vers la seconde hypothèse.

Comment faire alors ? Plusieurs solutions semblent possibles. L'utilisation de l'événement OnCellClick de la grille permet d'accéder facilement à l'objet TColumn, le OnSelectCell à l'index de la colonne.

Reste à résoudre la gestion du menu contextuel par défaut, celui de la grille, que j'ai besoin de mémoriser. Pour ce faire, j'utilise une spécificité des objets FMX, la propriété TagObject, qui sera renseignée à la création de la forme.

OnCreate

procedure TMainForm.FormCreate(Sender: TObject);
begin
TFMXObject(Grid1).TagObject:=Grid1.PopupMenu; // mémoriser popupmenu par défaut
…
end ;

OnCellClick

procedure TMainForm.Grid1CellClick(const Column: TColumn; const Row: Integer);
begin
if Column.PopupMenu=null
then
// menu par défaut mémorisé
Grid1.PopupMenu:=TPopupMenu(TFMXObject(Grid).TagObject)
else Grid1.PopupMenu:=Column.PopupMenu; // menu de la colonne
inherited;
end;

L'astuce est d'utiliser inherited permettant ainsi l'exécution de la méthode héritée et d'obtenir l'affichage du bon menu contextuel.

Cependant, je trouve que l'utilisation de l’événement OnSelectCell a un rendu plus fluide.

onSelectCell

procedure TGridLink.Grid1SelectCell(Sender: TObject; const ACol, ARow: Integer;
var CanSelect: Boolean);
begin
if Assigned(Grid1.Columns[ACol].PopupMenu)
then Grid1.PopupMenu:=Grid1.Columns[ACol].PopupMenu
else Grid1.PopupMenu:=TPopupMenu(TFMXObject(Grid).TagObject);
end;

VI-B - Mettre de la couleur

L’événement OnDrawColumnCell, bien connu de ceux qui utilisent la VCL, permet d'accéder au Canvas, au rectangle de dessin TRectF, à la colonne, à la ligne, etc.

Il est donc assez aisé de changer la couleur de fond d'une cellule ou la couleur du texte selon une condition.

VI-B-1 - Premier exemple : mettre la colonne « Nom » en couleur

Une colonne peut être connue par son index (Column.Index) mais c'est une fausse bonne idée car les colonnes peuvent être déplaçables. Il vaut mieux les nommer de façon correcte et tester sur leur nom (Column.Name).

procedure TForm15.Grid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas;
const Column: TColumn; const Bounds: TRectF; const Row: Integer;
const Value: TValue; const State: TGridDrawStates);
var ARect : TRectF;
ABrush : TBrush;
begin
if SameText(Column.Name,'Nom') then
begin
// initialiser une nouvelle brosse
ABrush:=TBrush.Create(TBrushKind.Solid,TAlphaColors.MoneyGreen);
try
// agrandir le rectangle initial
ARect:=TRectF.Create(Bounds);
InflateRect(ARect,3,3);
// remplir le rectangle
Canvas.FillRect(ARect,0,0,[],1,ABrush);
finally
ABrush.Free;
end;
// réécrire le texte
Canvas.Fill.Color:=TAlphaColors.Black;
Canvas.FillText(Bounds,Value.ToString,false,1,[],TTextAlign.Leading);
end;
end;

VI-B-2 - Deuxième exemple : mettre en couleur les poissons « nobles » (Bar, Sole)

procedure TForm15.Grid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas;
const Column: TColumn; const Bounds: TRectF; const Row: Integer;
const Value: TValue; const State: TGridDrawStates);
var ARect : TRectF;
ABrush : TBrush;
begin
if SameText(Column.Name,'Nom') then
begin
// initialiser une nouvelle brosse
ABrush:=TBrush.Create(TBrushKind.Solid,TAlphaColors.MoneyGreen);
try
// tester les poissons nobles bar ou sole et metter en rose)
if (Value.ToString='Bar') OR (Value.ToString='Sole')
then ABrush.Color:=TAlphaColors.Pink;
// agrandir le rectangle initial
ARect:=TRectF.Create(Bounds);
InflateRect(ARect,3,3);
// remplir le rectangle
Canvas.FillRect(ARect,0,0,[],1,ABrush);
finally
ABrush.Free;
end;
// réécrire le texte
Canvas.Fill.Color:=TAlphaColors.Black;
Canvas.FillText(Bounds,Value.ToString,false,1,[],TTextAlign.Leading);
end;
end;

VI-B-3 - Derniers retranchements : mettre en couleur selon la valeur d'une autre colonne

Ce cas va mettre en exergue quelque chose d'inattendu : TGrid ne stocke pas les valeurs des données mais plutôt un affichage des données !

Comment faire alors pour récupérer la valeur d'une cellule au sein d'une TGrid qui utilise les LiveBindings ? Par exemple, comment mettre la cellule de la colonne « Nom » en orange s'il s'agit de crustacés, soit la catégorie de valeur 2 ?

Je n'ai pu trouver aucun moyen d'accéder à cette valeur via les LiveBindings, il n'est pas possible non plus d'utiliser la source de données, contrairement au TDBGrid de la VCL. Les seules informations trouvée sur Internet n'abordent pas ce cas. Une seule piste est fournie dans la documentation : utiliser l'événement OnSetValue et un tableau dynamique de valeurs (Array of TValue). Sauf que si nous utilisons les LiveBindings l'événement est inaccessible !

Voilà comment résoudre le problème :

- Tout d'abord ajouter sur la fiche un TStringGrid, qui sera invisible à l'affichage.

- Lier le TStringGrid car, contrairement au TGrid, TStringGrid mémorise les valeurs des cellules. Au niveau de l'ordre, mettre la liaison avec la TStringGrid avant celle de la TGrid.

Il n'est pas nécessaire de lier toutes les données, juste celles nécessaires aux tests.

- Il ne restera alors plus qu'à tester la valeur de la cellule de la TStringGrid en fonction de la ligne.

1. procedure TForm15.Grid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas;

2. const Column: TColumn; const Bounds: TRectF; const Row: Integer;

3. const Value: TValue; const State: TGridDrawStates);

4. var ARect : TRectF;

5. ABrush : TBrush;

6. begin

7. if SameText(Column.Index,'Nom') then

8. begin

9. // initialiser une nouvelle brosse

10. ABrush:=TBrush.Create(TBrushKind.Solid,TAlphaColors.MoneyGreen);

11. try

12. // tester aussi sur la valeur (poissons nobles bar ou sole en rose)

13. if (Value.ToString='Bar') OR (Value.ToString='Sole')

14. then ABrush.Color:=TAlphaColors.Pink;

15. // tester en fonction d'une autre cellule

16. if StrToInt(StringGrid1.Cells[0,Row])=2

17. then ABrush.Color:=TAlphaColors.Orangered;

18. // agrandir le rectangle initial

19. ARect:=TRectF.Create(Bounds);

20. InflateRect(ARect,3,3);

21. // remplir le rectangle

22. Canvas.FillRect(ARect,0,0,[],1,ABrush);

23. finally

24. ABrush.Free;

25. end;

26. // réécrire le texte

27. Canvas.Fill.Color:=TAlphaColors.Black;

28. Canvas.FillText(Bounds,Value.ToString,false,1,[],TTextAlign.Leading);

29. end;

30. end;

J'obtiens alors le résultat escompté :

VII - Astuces, le coin du mécano

Au cours de l'élaboration de l'article et du programme exemple associé, j'ai pu découvrir quelques astuces que j'aimerais vous faire partager.

Ces astuces ne se retrouvent pas dans les sources proposées en téléchargement.

VII-A - Hauteur de l'en-tête de grille

Au préalable il faut déclarer l'utilisation de l'unité FMX.Header dans la liste des uses. Il est alors possible de récupérer dans le style ce qui concerne les en-têtes de grille.

uses FMX.Header;

var Header : THeader;

Header := THeader(Grid1.FindStyleResource('header'));
if Assigned(Header) then  Header.Height := 48;

// simplifiable en (mais moins sûre)
// THeader(Grid1.FindStyleResource('header')).Height:=48;

VII-B - Une colonne indicateur d'enregistrement

Je déplorais, au début du tutoriel, la disparition du curseur d'enregistrement que la TDBGrid de la VCL nous proposait. Mes tentatives en ce sens via Livebindings se sont toutes révélées infructueuses. Cependant il est tout à fait possible de le faire par code.

Au préalable j'ajoute une colonne que je nomme Curseur, qui ne sera pas liée aux données.

Ensuite il faut également implémenter l'événement OnAfterScroll de la source de données.

procedure TForm1.CdsAfterScroll(DataSet: TDataSet);
begin
Curseur.Changed; // force le dessin de la colonne
end;

Reste alors à dessiner le triangle au cours de l'événement OnCellDraw de la grille.

VII-B-1 - Dessin d'un triangle dans une cellule

Pour dessiner un triangle dans une cellule j'ai décidé d'écrire une fonction qui me permet non seulement de dessiner un triangle selon une couleur définie mais qui tient également compte de la taille de la cellule et de la direction de pointage.

procedure DrawTriangle(Bounds : TRectF; Color : TAlphaColor;
PointeaDroite : Boolean=True;
Agrandir : Boolean =False);
var Points: array[0..2] of TPointF;
Demi : Single;
APath : TPath;
begin
if Agrandir then Demi:=Bounds.Height/2
else Demi:=Min(Column.Width,Bounds.Height)/2;
if PointeADroite then
begin
Points[0] := PointF(Bounds.Left,Bounds.Top+Bounds.Height/2-Demi);
Points[1] := PointF(Bounds.Left+Bounds.Width,Bounds.Top+(Bounds.Height/2));
Points[2] := PointF(Bounds.Left,Bounds.Top+(Bounds.Height/2)+Demi);
end
else begin
Points[0] := PointF(Bounds.Left,Bounds.Top+Bounds.Height/2);
Points[1] := PointF(Bounds.Left+Bounds.Width,Bounds.Top+(Bounds.Height/2)-Demi);
Points[2] := PointF(Bounds.Left+Bounds.Width,Bounds.Top+(Bounds.Height/2)+Demi);
end;
ABrush:=TBrush.Create(TBrushkind.Solid,Color);
APath:=TPath.Create(Self);
try
APath.Data.MoveTo(Points[0]);
APath.Data.LineTo(Points[1]);
APath.Data.LineTo(Points[2]);
APath.Data.ClosePath;
Canvas.FillPath(APath.Data,1,ABrush);
finally
FreeAndNil(ABrush);
FreeAndNil(APath);
end;
end;

Cela me permet d'obtenir ces différentes possibilités :

D’autres améliorations sont envisageables pour cette fonction comme la possibilité d’indiquer l’ordre de tri dans les entêtes de colonne.

VII-B-2 - Récupérer les couleurs à utiliser

Utiliser FMX implique souvent les styles. Il fallait donc que ma colonne remplisse également le critère de couleur, à savoir : utiliser les mêmes couleurs de fond et de texte (couleur du triangle) que les en-têtes de la grille. Une fonction supplémentaire va me permettre d'obtenir ces indications. Cette fonction me permettra d'obtenir une brosse en cas d'utilisation d'un gradient de remplissage (la plupart des styles proposés de base par Embarcadero en utilisent).

Le cas des styles de grille est assez complexe car le style des en-têtes (header) renvoie à un autre style (headeritemstyle).

Il faut donc accéder à un élément de l'en-tête (ItemHeader) avant de pouvoir accéder aux éléments background et text.

function GetHeaderColors(Sender: TObject; var BackGroundColor,TextColor : TAlphaColor) : TBrush;
var Header : THeader;
HeaderItem : THeaderItem;
aFMXObj : TFMXObject;
begin
// valeurs par défaut
BackGroundColor:=0;
TextColor:=TAlphaColors.Black;
// recherche dans les styles
Header:=THeader((Sender as TGrid).FindStyleResource('header'));
if Assigned(Header) then
begin
// obtention d'un élément
HeaderItem := Header.Items[1];
// accès au style du texte
AFMXObj:=THeaderItem(HeaderItem).FindStyleResource('Text');
TextColor:=TText(AFMXObj).TextSettings.FontColor;
// accès au style du fond
AFMXObj:=THeaderItem(HeaderItem).FindStyleResource('background');
Result:=TRectangle(AFMXObj).Fill;
if Assigned(result) then
begin
if (Result.Kind=TBrushKind.Gradient)
then begin
BackGroundColor:=Result.Gradient.InterpolateColor(50);
// de horizontal à vertical ? (Pas concluant)
Result.Gradient.StartPosition.X:=1;
Result.Gradient.StartPosition.Y:=1;
end
else BackGroundColor:=Result.Color;
end;
end;
end;

J'utilise alors ces deux fonctions ainsi

procedure TForm15.Grid1DrawColumnCell(Sender: TObject; const Canvas: TCanvas;
const Column: TColumn; const Bounds: TRectF; const Row: Integer;
const Value: TValue; const State: TGridDrawStates);
var ARect : TRectF;
ABrush : TBrush;
ABackColor,ATextColor : TAlphaColor;

begin
...
if SameText(Column.Name,'Curseur') then
begin
// Obtention des couleurs
ABrush:=GetHeaderColors(Sender,ABackColor,ATextColor);
// redessiner le fond
ARect:=TRectF.Create(Bounds);
InflateRect(ARect,3,3);
Canvas.ClearRect(ARect,ABackColor);
if Assigned(ABrush) then Canvas.FillRect(ARect,0,0,[],1,ABrush)
else Canvas.FillRect(ARect,0,0,[],1);
// si enregistrement en cours
if Row=cds.RecNo-1 then
// dessin du curseur d'enregistrement
DrawTriangle(Bounds,ATextColor);
end;
end;

Pour obtenir cet écran :

VII-C - Remplacer FillText, les possibilités de TTextLayout

En recherchant dans les différents sources de l'unité FMX.Graphics quel ne fut pas mon étonnement de découvrir, en commentaire, que FillText était considérée comme dépréciée et qu'il fallait se tourner vers un TTextLayout.

FMX.Graphics

{ deprecated, use TTextLayout }
procedure FillText(const ARect: TRectF; const AText: string;
const WordWrap: Boolean; const AOpacity: Single;
const Flags: TFillTextFlags; const ATextAlign: TTextAlign;
const AVTextAlign: TTextAlign = TTextAlign.Center); virtual;

Il ne m'en fallait pas plus pour ronger ce nouvel os, surtout qu'aucune indication supplémentaire ne semblait se trouver dans la documentation.

Pour pouvoir utiliser, comme recommandé, un TTextLayout, la première étape est d'inclure l'unité FMX.TextLayout dans la liste des uses.

Si j'applique ici mes recherches aux cellules d'une grille, ces essais sont valables pour tout composant visuel permettant d'accéder à un canvas.

Remplacer le FillText utilisé par exemple dans les codes source du chapitre VI.B, soit en tout deux malheureuses lignes,

Canvas.Fill.Color:=TAlphaColors.Black;
Canvas.FillText(Bounds,Value.ToString,false,1,[],TTextAlign.Leading);

revient à déclarer en premier lieu une variable TTextLayout puis à écrire les lignes suivantes :

var AtextLayout : TTextLayout;

ATextLayout:=TTextLayoutManager.TextLayoutByCanvas(Canvas.ClassType).Create(Canvas);
ATextLayout.BeginUpdate;
ATextLayout.Text:=Value.ToString;
ATextLayout.Color:=TAlphaColors.Black;
ATextLayout.TopLeft:=Bounds.TopLeft;
ATextLayout.MaxSize := PointF(Bounds.Width, Bounds.Height);
ATextLayout.VerticalAlign:=TTextAlign.Center;
ATextLayout.EndUpdate;
ATextLayout.RenderLayout(Canvas);

Beaucoup de lignes pour le même résultat, l'avantage est bien caché ! Il ne se trouve que dans l'abondance de propriétés d'un TTextLayout, en particulier Font et Color.

Rassurez-vous, la fonction FillText est dépréciée mais cela ne l'empêche nullement d'être utilisable et ce sans erreur de compilation.

VII-C-1 - Grille avec des valeurs de cellules en langues RTL

Une des propriétés de TTextLayout m'a induit en erreur : RightToLeft. Je pensais pouvoir l'utiliser pour écrire de l'arabe ou de l'hébreu simplement en mettant cette propriété à True.

En fait, c'est un peu plus complexe que cela. À ma connaissance FMX ne prend pas nativement en charge ces langues si bien que quelqu'un a développé un ensemble de composants dans ce but : FMXRTL. Loin de ma pensée l'idée de concurrencer cet auteur, je voulais juste utiliser cette propriété pour voir.

Que peut-on constater ? Utiliser la propriété RigthtoLeft ne suffit pas, car cette dernière ne fait qu'écraser l'alignement que l'on a pu indiquer, la langue utilisée pour le contenu de la cellule n'est pas détectée. Pour obtenir quelque chose de conforme à mon attente, j'ai créé une fonction permettant de tester le premier caractère de la chaîne (en prenant toutefois soin de ne pas tenir compte des chiffres et de quelques signes) :

1. function checkRtl (S : string; Exceptions : String = '' ) : TTextAlign;

2. var carray : array of WideChar;

3. i : int64;

4. ws : String;

5. begin

6. for I := 0 to 9 do

7. S:=StringReplace(S,i.ToString,'',[rfReplaceAll]);

8. // supprime autres caractères spéciaux

9. S:=StringReplace(S,'(','',[rfReplaceAll]);

10. S:=StringReplace(S,')','',[rfReplaceAll]);

11. S:=StringReplace(S,'"','',[rfReplaceAll]);

12. S:=StringReplace(S,'''','',[rfReplaceAll]);

13. S:=StringReplace(S,'-','',[rfReplaceAll]);

14. if not E.IsEmpty then

15. begin

16. for I := 1 to Length(Exceptions) do

17. S:=StringReplace(S,Exceptions[i],'',[rfReplaceAll]);

18. end;

20. S:=Trim(S);

21. // arabe + hébreux je n'ai pas inclus le farsi

22. SetLength(carray,$6ff-$590);

23. for I := $590 to $6ff do carray[i-$590]:=Char(I);

24. result:=TTextAlign.Trailing;

25. if S.IsEmpty then exit;

26. if inOpArray(S[1],carray) then result:=TTextAlign.Leading;

27. end;

Vous retrouverez le source au sein de cette discussion.

VII-C-2 - Mixer fontes et couleurs au sein d'un texte

L'exemple avec la propriété RightToLeft est peu représentatif des possibilités de TTextLayout. Il m'est donc venu en tête un exemple plus démonstratif.

ATextLayout:=TTextLayoutManager.TextLayoutByCanvas(Canvas.ClassType).Create(Canvas);
ATextLayout.BeginUpdate;
ATextLayout.Text:=Value.ToString;
ATextLayout.Color:=TAlphaColors.Black;
ATextLayout.TopLeft:=Bounds.TopLeft;
ATextLayout.MaxSize := PointF(Bounds.Width, Bounds.Height);
ATextLayout.VerticalAlign:=TTextAlign.Center;
// Changement de Fonte
AFont:=TFont.Create;
AFont.Family:='Algerian';
AFont.Size:=24;
// 1° caractère Fonte Algerian, taille 24, en vert      ATextLayout.AddAttribute(TTextRange.Create(0,1),TTextAttribute.Create(AFont,TAlphaColors.Green));
// les deux prochains caractères en rouge    ATextLayout.AddAttribute(TTextRange.Create(1,3),TTextAttribute.Create(Grid1.TextSettings.Font,TAlphaColors.Red));
ATextLayout.HorizontalAlign:=TTextAlign.Leading;
ATextLayout.EndUpdate;
ATextLayout.RenderLayout(Canvas);

Le code ci-dessus me permet d'obtenir :

VIII - Conclusions

Au fil de ces pages, j'espère avoir démontré que l'utilisation des LiveBindings avec les grilles, qui de prime abord se révèle assez étrange, peut se révéler relativement facile dès que l'on acquiert certaines notions :

- La différence entre StringGrid et Grid.

- Les tenants et aboutissants de l’utilisation des liaisons manuelles plutôt que rapide.

- L’utilisation de colonnes typées.

Mes premiers remerciements vont aux intervenants du forum, et en particulier à ShaiLeTroll, qui ont pu répondre à certains de mes questionnements. Surtout que, les pauvres, n'avaient, lors de mes questions, qu'une toute petite fenêtre ouverte sur l'objectif final. C'est également grâce à leurs réponses que j'ai pu me faire une idée plus précise de la différence de raisonnement qu'il fallait opérer entre la vision VCL « classique » et celle des LiveBindings.

Mes remerciements vont ensuite à ceux sans qui ce tutoriel ne resterait qu'un brouillon, la « maison d'édition » du club, c'est-à-dire les correcteurs techniques Jean-Luc (Alcatîz) et Gilles (gvasseur58) puis orthographiques et grammaticaux Gérard (F6EEQ) et son mentor Fabien (f-leb).

Retour à l'article

Retour à l'article

Retour à l'article

(1)ShowClearButton fait apparaître une croix à droite de la zone de saisie. Inesthétique dès que la taille de la ligne est importante, il faut également prendre en compte ce bouton pour la largeur de la colonne. En dernier lieu, le fait d’effacer une date (mettre sa valeur à null) est à traiter avec soin en fonction du SGBD.