Grilles et Livebindings

Les Livebindings de A à Z

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.

2 commentaires Donner une note  l'article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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
Sélectionnez
 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.

Image non disponible

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 ».

Image non disponible

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

Image non disponible

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

Image non disponible

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.

Image non disponible

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.

Image non disponible

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 !

Image non disponible

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 !

Image non disponible

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.

Image non disponible

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.

Image non disponible
Image non disponible
Image non disponible

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

Image non disponible

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 !

Image non disponible

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)'.

Image non disponible

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.

Image non disponible

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.

Image non disponible

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 !

 
Sélectionnez
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.

 
Sélectionnez
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.

Image non disponible

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

Image non disponible

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.

Image non disponible

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é.

Image non disponible

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.

Image non disponible

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
Sélectionnez
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 ».

 
Sélectionnez
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.

 
Sélectionnez
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.

Image non disponible

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 :

 
Sélectionnez
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.

Image non disponible

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

Image non disponible

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 !

Image non disponible

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.

Image non disponible

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.

Image non disponible

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.

Image non disponible

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.

Image non disponible

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é.

Image non disponible

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.

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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'IDEInterface de développement 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
Sélectionnez
procedure TMainForm.FormCreate(Sender: TObject);
begin
TFMXObject(Grid1).TagObject:=Grid1.PopupMenu; // mémoriser popupmenu par défautend ;
OnCellClick
Sélectionnez
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
Sélectionnez
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).

 
Sélectionnez
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)

 
Sélectionnez
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.

    Image non disponible

  • 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.

Image non disponible
Image non disponible

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.
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
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.Index,'Nom') then
    begin
     // initialiser une nouvelle brosse
     ABrush:=TBrush.Create(TBrushKind.Solid,TAlphaColors.MoneyGreen);
     try
       // tester aussi sur la valeur (poissons nobles bar ou sole en rose)
       if (Value.ToString='Bar') OR (Value.ToString='Sole')
         then ABrush.Color:=TAlphaColors.Pink;
       // tester en fonction d'une autre cellule
       if StrToInt(StringGrid1.Cells[0,Row])=2
         then ABrush.Color:=TAlphaColors.Orangered;
       // 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;

J'obtiens alors le résultat escompté :

Image non disponible

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.

 
Sélectionnez
 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.

Image non disponible

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

 
Sélectionnez
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.

 
Sélectionnez
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 :

Image non disponible
Image non disponible

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).

Image non disponible

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.

 
Sélectionnez
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

 
Sélectionnez
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 :

Image non disponible

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
Sélectionnez
{ 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,

 
Sélectionnez
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 :

 
Sélectionnez
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.

Image non disponible

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) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
function checkRtl (S : string; Exceptions : String = '' ) : TTextAlign;
var carray : array of WideChar;
    i : int64;
    ws : String;
begin
   for I := 0 to 9 do
      S:=StringReplace(S,i.ToString,'',[rfReplaceAll]);
  // supprime autres caractères spéciaux
    S:=StringReplace(S,'(','',[rfReplaceAll]);
    S:=StringReplace(S,')','',[rfReplaceAll]);
    S:=StringReplace(S,'"','',[rfReplaceAll]);
    S:=StringReplace(S,'''','',[rfReplaceAll]);
    S:=StringReplace(S,'-','',[rfReplaceAll]);
   if not E.IsEmpty then
     begin
        for I := 1 to Length(Exceptions) do
                S:=StringReplace(S,Exceptions[i],'',[rfReplaceAll]);
     end;

   S:=Trim(S);
   // arabe + hébreux je n'ai pas inclus le farsi
   SetLength(carray,$6ff-$590);
   for I := $590 to $6ff do carray[i-$590]:=Char(I);
   result:=TTextAlign.Trailing;
   if S.IsEmpty then exit;
   if inOpArray(S[1],carray) then result:=TTextAlign.Leading;
end;

Vous retrouverez le source au sein de cette discussion1.

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.

 
Sélectionnez
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;
//  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 :

Image non disponible

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).

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   


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.

  

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 ni 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.