IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

LiveBindings de A à … Écrire un composant observable II

Lors du tutoriel précédent (LiveBindings de A à … Écrire un composant observable), je m’étais arrêté à une liaison unidirectionnelle ou, du moins, était-ce mon objectif.

Le fait de tester un composant qui soit aussi bien VCL que FMX rendait la chose un peu plus ardue.

Cette fois, je reprends l’ouvrage avec un objectif plus simple : une simple diode qui sera déclinée par la suite, mais surtout uniquement pour la plateforme FMX.

Commentez Donner une note à l´article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Objectif : créer un composant TDiode pour FMX

En soi dessiner une diode sur une form consiste simplement à poser un TCircle. S’il ne s’agissait que de cela, vous pourriez me dire qu’il n’est pas nécessaire d’en faire un composant.

C’est totalement vrai, même si quelques propriétés sont cachées (vous en découvrirez bientôt quelques-unes), il n’est pas impossible d’y accéder par code.

De même, s’il ne s’agissait que de jouer sur la couleur de remplissage, et dans le cas d’une diode cela se résume souvent à ça, une fois de plus un peu de code suffirait.

Pourquoi alors m’embarquer dans cette galère ? D’autres membres du forum DVP ont déjà proposé leurs solutions, à l’exemple de Gouyon et ses composants d’affichages et, bien sûr des sociétés tierces connues peuvent aussi le proposer.

Quelques raisons à cela :

  • Le composant de base est simple et permet d’extraire les principes fondamentaux pour évoluer dans l’écriture de composants.
  • Plusieurs propriétés ne sont pas utilisables par le concepteur de liens. Pour illustrer ce propos, l’image suivante montre que l’on peut lier la propriété Fill.Color d’un TCircle mais aucunement la couleur du gradient qui offre un rendu plus « 3D ». De même, il faut que la liaison soit faite sur une donnée de type TAlphaColor.
  • Souvent, les développeurs ne pensent pas à rendre le composant utilisable par LiveBindings, à l’exemple du VCL.TTrackBar et du tutoriel d’Embarcadero qui est à la base de cette série.

Les images qui suivront ont été prises en utilisant la dernière version de Delphi à ce jour soit Delphi 11.1 (Alexandria). Néanmoins la démarche peut très bien s’appliquer à des versions moins récentes comme la version Community (10.4.2). Un seul bémol, certaines versions présentent quelques défauts (bogues) que j’essaierai de signaler.

Image non disponible

II. Création du paquet

Un développeur Delphi confirmé passera, très certainement, directement par le menu Fichier/Nouveau/Autres  :

Et commencera à écrire la ou les unités nécessaires.

Image non disponible

Un débutant préférera certainement utiliser le wizard (Composant/Nouveau composant) et suivre les quatre étapes :

Image non disponible
Image non disponible
Image non disponible
Image non disponible

avec l’avantage d’obtenir une première unité.

Unité de départ
Sélectionnez
unit Lug.FMX.Diode;

interface

uses
  System.SysUtils, System.Classes, FMX.Types, FMX.Controls, FMX.Objects;

type
  TDiode = class(TCircle)
  private
    { Déclarations privées }
  protected
    { Déclarations protégées }
  public
    { Déclarations publiques }
  published
    { Déclarations publiées }
  end;

procedure Register;

implementation

procedure Register;
begin
RegisterComponents('govel', [TDiode]);
end;

end.

II-A. Préliminaires

Il est temps de faire une parenthèse sur les noms de variables, propriétés et autres à utiliser.

Si pour un composant « maison » à usage unique les noms n’ont que peu d’importance, il n’en va pas de même lorsque l’objectif est de réutiliser ce dernier dans plusieurs programmes ou dans le cas d’un partage à large spectre (pour un public plus international), histoire de passer à la postérité ;-).

Une autre raison de bien choisir les noms de propriétés est que celles-ci seront proposées par ordre alphabétique dans l’éditeur de propriété.

Pas de stress, vous pouvez toujours par la suite utiliser le Refactoring ou les fonctions Rechercher/Remplacer.

De fait le choix des noms commence dès celui de l’unité. Dorénavant les unités sont souvent préfixées du type de bibliothèque : VCL, FMX donc il serait bien d’appliquer ce principe. L’équipe d’Embarcadero estime que ce n’est pas suffisant comme distinction et suggère (« fortement » ?)(1) que tout composant tiers ait un autre préfixe que ces deux-ci, histoire que le nom d’unité soit bien distingué de celui d’une unité « native ».

Ma future unité se nommera donc Lug.FMX.Diode.

Ici un peu de mon vernis de culture Celte entre en jeu. Lug ? Lug est un dieu celte, un polytechnicien (Salmidanach), à la fois dieu de la poésie, de la musique, du commerce, de la forge, etc. Il excelle dans toute forme d’art. « Le Lugus » (nom romanisé) gaulois est à la fois un dieu « lieur » par la magie, ainsi qu'un dieu « lié » avec des chaînes. Ce nom me semblait donc tout indiqué.

Le nom de la palette : govel ? Traduction de forge en breton. Pas mal quand on sait le nombre de fois où j’ai remis au feu et à l’enclume mes composants.

II-B. Recensement des propriétés à proposer

C’est une étape importante, même si par la suite il se peut que vous vous aperceviez qu’il vous en faut d’autres, cela vous donnera quand même un bon point de départ.

Comment se décider ? J’avoue que ce n’est pas si évident que cela. Si l’objectif n’est qu’un changement de couleur comme montré dans la première image, il me faut évidemment une propriété en rapport avec la couleur. En définissant le gradient, je me suis aperçu que :

  • je changeai le type de gradient (type radial) ;
  • je nécessitai deux couleurs (celle de début et celle de fin qui simule un éclat plus brillant) ;
  • un coup d’œil au composant de Gouyon m’a convaincu d’ajouter une couleur supplémentaire pour « éteindre » la diode.

Donc déjà trois propriétés.

Le fait d’utiliser un gradient de type radial permet également de déplacer le centre de celui-ci. Je vais en faire d’ores et déjà une propriété supplémentaire sans toutefois la rendre accessible, ce qui me permettra d’exposer par la suite un premier dérivé de mon composant.

Plusieurs versions de Delphi (X8 à 10.4.1) contiennent un bogue en ce qui concerne les fonctions de transformation radiale (Gradient.RadialTransform) ; vous pourriez donc avoir un effet un peu différent de celui souhaité avec ces versions.

Nommer ces propriétés va aussi être un peu « casse-tête ». Il est en effet assez agaçant d’aller les retrouver un peu partout dans l’inspecteur d’objet. Mon conseil : essayez de nommer toutes les propriétés d’un même groupe avec un même préfixe. Par exemple, pour ce qui est des couleurs, je vais utiliser le préfixe color, pour l’éclat le préfixe bright.

Pourquoi de l’anglais pour mes préfixes ? Pour les proposer à un plus large public, mais aussi parce que c’est le cas de toutes les propriétés du composant de base.

Pas de stress, le droit à l’erreur existe. Vous pourrez très bien reprendre les noms par la suite grâce à l’outil de refactoring ou l’utilisation de Rechercher/Remplacer proposé par l’éditeur de texte.

Pour récapituler :

  • propriétés ColorOn, ColorOff de type TAlphaColor ;
  • propriétés BrightColor de type TAlphaColor et BrightPosition de type TPosition ;
  • une propriété OnOff de type Boolean pour indiquer si la diode sera allumée ou non.
 
Sélectionnez
type
  TDiode = class(TCircle)
  private
    { Déclarations privées }
  protected
    { Déclarations protégées }
  public
    { Déclarations publiques }
    property BrightPosition : TPosition;
  published
    { Déclarations publiées }
    property OnOff : Boolean ;
    property ColorOn : TAlphaColor;
    property ColorOff : TAlphaColor;
    property BrightColor : TAlphaColor;
  end;

Ensuite quelques autres prérequis vont être nécessaires : un constructeur et, comme dans celui-ci l’objet BrightPosition sera créé, un destructeur.

 
Sélectionnez
public 
  { Déclarations publiques }
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;

À cela s’ajoute la nécessité d’une procédure de dessin.

 
Sélectionnez
function Paint: Boolean; reintroduce;

Comme indiqué lors du premier tutoriel de cette série, il faudra aussi ajouter trois éléments,

deux procédures et une fonction pour la prise en charge des LiveBindings.

private
Sélectionnez
1.
procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
protected
Sélectionnez
1.
2.
function CanObserve(const ID: Integer): Boolean; override;  
procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override;

Ces trois éléments nécessitent l’ajout de l’unité System.Classes.

Une fois tout ajouté, Ctrl+Shift+C permettra d’obtenir les déclarations complètes nécessaires.

 
Sélectionnez
  TDiode = class(TCircle)
  private
    { Déclarations privées }
    FOnOff : Boolean;
    FColorOn : TAlphaColor;
    FColorOff : TAlphaColor;
    FBrightPos: TPosition;
    FBrightColor: TAlphaColor;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
    procedure SetColorOff(const Value: TAlphaColor);
    procedure SetOnOff(const Value: Boolean);
    procedure SetColorOn(const Value: TAlphaColor);
    procedure SetBrightPosition(const Value: TPosition);
    procedure SetBrightColor(const Value: TAlphaColor);
 protected
    { Déclarations protégées }
    function CanObserve(const ID: Integer): Boolean; override;  
    procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override;
  public
    { Déclarations publiques }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function Paint: Boolean; reintroduce;
    property BrightPosition : TPosition read FBrightPos write SetBrightPosition;
  published
    { Déclarations publiées }
    property OnOff : Boolean read FOnOff write SetOnOff;
    property ColorOn : TAlphaColor read FColorOn write SetColorOn;
    property ColorOff : TAlphaColor read FColorOff write SetColorOff;
    property BrightColor : TAlphaColor read FBrightColor write SetBrightColor;
  end;

II-C. Codage

Il faut tout d’abord proposer des propriétés par défaut si nous sommes dans l’IDE, et pour cela la procédure Create est tout indiquée.

Constructor
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
constructor TDiode.Create(AOwner: TComponent);
  begin
    if not(csloading in ComponentState) then
    begin
      inherited;
      FOnOff:=True;
      FColorOff:=TAlphaColors.Lightgray;
      FColorOn:=TAlphaColors.Red;
      FBrightPos:=TPosition.Create(PointF(0.5,0.5));

      FBrightColor:=TAlphaColors.Antiquewhite;
      Fill.DefaultColor:=FColorOn;
      Fill.Color:=FColorOn;
      Fill.Gradient.Color:=FColorOn;
      Fill.Gradient.Color1:=FBrightColor;
      Fill.Kind:=TBrushKind.Gradient;
      Fill.Gradient.Style:=TGradientStyle.Radial;
      Fill.Gradient.RadialTransform.RotationCenter.X :=FBrightPos.X;
      Fill.Gradient.RadialTransform.RotationCenter.Y :=FBrightPos.Y;
      Paint;
    end;
  end;

N’oublions pas : à la ligne 9 je fais une création d’un objet de type TPosition ; il faudra donc le détruire.

Destructor
Sélectionnez
  destructor TDiode.Destroy;
  begin
    FBrightPos.Free;  // libération 
    inherited;
  end;

Passons au dessin. La fonction Paint, dont la subtilité la plus importante se situe dans la déclaration avec, à sa suite, le mot clé reintroduce.

Paint
Sélectionnez
function TDiode.Paint: Boolean;
  begin
  if FonOff then Fill.Gradient.Color:=FColorOn
            else Fill.Gradient.Color:=FColorOff;
  Fill.Gradient.Color1:=FBrightColor;
  Fill.Gradient.RadialTransform.RotationCenter.X:=FBrightPos.X;
  Fill.Gradient.RadialTransform.RotationCenter.Y:=FBrightPos.Y;
  Result:=True; // indiquera qu’il faut rafraîchir l’affichage
  end;

Pour les autres propriétés, il faudra si nécessaire détecter le changement de valeur et forcer le dessin.

 
Sélectionnez
  procedure TDiode.SetColorOff(const Value: TAlphaColor);
  begin
    FColorOff := Value;
  end;

  procedure TDiode.SetColorOn(const Value: TAlphaColor);
  begin
  if FColorOn<>Value then
   begin
    Fill.Color:=Value;
    FColorOn := Value;
    Paint;
   end;
  end;

  procedure TDiode.SetBrightPosition(const Value: TPosition);
  begin
   if (Value<>FBrightPos) then
       FbrightPos.Assign(Value); // attention, assign, pas := 
  end;

  procedure TDiode.SetBrightColor(const Value: TAlphaColor);
  begin
    if (Value<>FBrightColor) then
     begin
      FBrightColor:=Value;
      Fill.Gradient.Change; // force le calcul du gradient
      Paint;
     end;
  end;

  procedure TDiode.SetOnOff(const Value: Boolean);
  begin
   if FonOff<>Value then
    begin
     FOnOff := Value;
     Paint;  // force le dessin
    end ; 
  end;

II-D. Ajout de la partie LiveBindings

 
Sélectionnez
function TDiode.CanObserve(const ID: Integer): Boolean;
begin
  case ID of
      TObserverMapping.EditLinkID, TObserverMapping.ControlValueID:
        Result := True;
    else
      Result := False;
    end;
end;

procedure TDiode.ObserverAdded(const ID: Integer; const Observer: IObserver);
begin
   if ID = TObserverMapping.EditLinkID then
      Observer.OnObserverToggle := ObserverToggle;
end;

procedure TDiode.ObserverToggle(const AObserver: IObserver;
    const Value: Boolean);
  var
    LEditLinkObserver: IEditLinkObserver;
  begin
    if Value then
    begin
      if Supports(AObserver, IEditLinkObserver, LEditLinkObserver) then
        Enabled := not LEditLinkObserver.IsReadOnly;
    end
    else
      Enabled := True;
end;

Reste alors à indiquer quelle propriété nous voulons voir apparaître lorsque nous utiliserons le concepteur de lien visuel.

 
Sélectionnez
1.
2.
  [ObservableMembers('ColorOn',false)]
  TDiode = class(TCircle)

À indiquer juste au-dessus de la déclaration de classe.

Mais il faut surtout faire en sorte que les propriétés soient prises en compte avec le lieur et pour cela enregistrer celles-ci au niveau du lieur de l’unité Data.Bind.Components.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
initialization

Data.Bind.Components.RegisterObservableMember
  (TArray<TClass>.create(TDiode), 'ColorOn', 'FMX');

Data.Bind.Components.RegisterObservableMember
  (TArray<TClass>.create(TDiode), 'OnOff', 'FMX');


Qu’il faudra bien sûr désinscrire par la suite, à la libération du composant.

 
Sélectionnez
1.
2.
3.
4.
finalization

Data.Bind.Components.UnregisterObservableMember
  (TArray<TClass>.create(TDiode));

III. Installation et tests

Ceux qui ont opté pour l’utilisation du wizard peuvent d’ores et déjà envisager une installation du composant. Pour les autres, une étape supplémentaire est nécessaire : la rédaction de l’unité de design. Elle permet d’enregistrer le composant grâce à sa procédure register.

 
Sélectionnez
unit Lug.FMX.DiodeD;

interface
uses  System.Classes, Lug.FMX.Diode;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Govel', [TDiode]);
end;

end.

Pourquoi ai-je séparé en deux unités, contrairement à ce que le wizard génère ?

En priorité, pour séparer la partie design de la partie runtime, ainsi pourrai-je ne distribuer que la bibliothèque du runtime (bpl) si je le souhaite.

Enfin, il n’est pas exclu que j’ai besoin de créer des boîtes de dialogues pour saisir des valeurs de propriétés, ce qui nécessite l’inclusion d’unités uniquement accessibles en design (DesignIDE).

Lug.FMX.Diode sera renommée Lug.FMX.DiodeR. Le suffixe R, pour runtime, rendra le nom plus explicite.

Image non disponible

III-A. Installer le composant

Compiler ou construire un composant se fait de la même manière que pour un programme, soit en utilisant les options du menu Projet soit en utilisant les raccourcis Ctrl+F9 et Maj+F9.

Par contre, ne vous avisez pas, par habitude, d’utiliser le menu Exécuter car un paquet n’est pas un programme. C’est un peu pour cette raison que je préfère utiliser le menu contextuel de la fenêtre Projet qui a l’avantage de proposer ces options, en plus celle d’installation.

Notez qu’il y a aussi une possibilité de désinstaller le composant.

Une première compilation vous proposera certainement d’ajouter certaines unités à votre package, acceptez.

Image non disponible

Une installation réussie sera validée par un écran d’information recensant les composants installés.

Image non disponible

III-B. Programme de test

Créez un nouveau programme FMX et posez sur votre fiche vierge le composant Diode de la palette. Pour « simuler » des données, ajoutez un TPrototypeBindSource contenant deux champs : un champ booléen pour tester la propriété OnOff, un champ de type TAlphaColor pour la propriété ColorOn. Un BindNavigator, dont on enlèvera les boutons non nécessaires, permettra de naviguer au sein de l’ensemble de données créé.

Tant qu’à faire, limitez le nombre d’enregistrements créés.
Pour cela modifiez la propriété RecordCount du TProtypeBindSource selon votre convenance (une vingtaine devrait suffire).

Image non disponible

Constatations :

  • Les propriétés publiées sont bien visibles dans l’inspecteur d’objets.
  • Une fois liée, notre diode réagit tout de suite et ce sans aucune ligne de code, dois-je, encore, le souligner ?
  • Les propriétés OnOff et ColorOff n’apparaissent pas dans le lieur. Il faudra les sélectionner en utilisant le bouton . Faire en sorte que les propriétés autres que celle indiquée au-dessus de la déclaration de type du composant, reste encore, pour moi, un point à élucider.

    Image non disponible

  • Le lien entre ColorOn et le champ AlphaColors1 est de type bidirectionnel - ça, c’est absurde. Bien sûr il est toujours possible de modifier la propriété Direction du lien en LinkDataToControl, mais il y a certainement mieux à faire. En effet, si l’on regarde le lien de la propriété OnOff, la flèche va bien uniquement du champ vers la propriété.

III-C. Corrections nécessaires sur les liaisons

Pourquoi la liaison « principale » est-elle bidirectionnelle alors que les liaisons à d’autres propriétés ne le sont pas ? C’est ce qui m’a fait mettre au feu plusieurs fois mon ébauche.

Une étude des liaisons fournit une première piste.

Image non disponible

Il est facile de remarquer que la première liaison, celle sur la propriété « principale » définie lors du recensement, crée un lien de type ControlToField alors que la seconde est de type PropertyToField.

III-C-1. Première solution

Une première solution consisterait, bien que cela aille à l’encontre du tutoriel Embarcadero, à ne pas recenser la propriété.

Image non disponible

Effectivement, après essai, si l’on doit créer des liens il faut choisir la propriété et le lien établi sera bien alors de type PropertyToField. Seulement cette solution laisse quand même un goût amer, la nécessité de cette opération de choix de propriété est pour le moins gênante ! Un futur utilisateur de ce composant ne le comprendrait pas.

III-C-2. Seconde solution

Après avoir vainement cherché dans la documentation, il m’a fallu fouiller dans les sources.

Embarcadero propose ce type de liaison pour un TLabel ; il me fallait retrouver l’endroit où ce composant, ou plutôt la propriété text du composant, était recensé. Pas facile comme recherche ! Avec de la persévérance, j’ai fini par trouver l’unité contenant la plupart des recensements, à savoir Fmx.Bind.Editors, suite à quoi il fut alors facile de retrouver le TLabel et la propriété Text et découvrir que la procédure utilisée se nommait en fait : RegisterValuePropertyName.

Les parties initialization et finalization sont donc à remplacer comme suit :

 
Sélectionnez
initialization

//Data.Bind.Components.RegisterObservableMember       
// (TArray<TClass>.create(TDiode), 'ColorOn', 'FMX');
Data.Bind.Components.RegisterValuePropertyName
  (TArray<TClass>.create(TDiode), 'ColorOn', 'FMX');

finalization

// Data.Bind.Components.UnRegisterObservableMember
//   (TArray<TClass>.create(TDiode));
Data.Bind.Components.UnregisterValuePropertyName
  (TArray<TClass>.create(TDiode));

IV. Extension : rendre la propriété BrightPos accessible

La position de l’éclat de lumière, propriété BrightPosition, n’est pas publiée. Bien qu’il soit possible d’y accéder par code.

 
Sélectionnez
Diode1.BrightPosition.x:=0.7;
Diode1.BrightPosition.y:=0.3;

Cette méthode n’est pas forcément agréable, il serait plus aisé de publier cette propriété pour y accéder au moment du design.

J’ai plutôt décidé de faire un nouveau composant dérivé de TDiode.

 
Sélectionnez
 [ObservableMembers('ColorOn',false)]
 TDiodeEx = class(TDiode)
  published
    { Déclarations publiées }
    property BrightPosition : TPosition read FBrightPos write SetBrightPosition;
 end;

Cependant, les valeurs X et Y sont à limiter entre 0 et 1. Fallait-il donc créer pour cela un nouvel éditeur de propriété ? Nous allons voir que, heureusement, ce ne sera pas nécessaire.

IV-A. Les valeurs X et Y de la propriété BrightPos

Un objet TPosition fournit la possibilité d’ajouter un évènement OnChange (voir documentation). Je vais donc lui assigner une procédure qui prendra en charge les déplacements du centre du gradient pendant le design. Cette procédure sera indiquée dans la partie protected.

 
Sélectionnez
  procedure TDiode.ChangeBrightPos(sender: TObject);
  begin
  if (not FInPaintTo) and (not IsUpdating) then
     begin
      if FBrightPos.X>1 then FBrightPos.X:=1;
      if FBrightPos.X<0 then FBrightPos.X:=0;
      if FBrightPos.Y>1 then FBrightPos.Y:=1;
      if FBrightPos.Y<0 then FBrightPos.Y:=0;
      Paint;
     end;
  end;

J’ai préféré mettre cette procédure dans la classe TDiode plutôt que d’avoir à surcharger certaines procédures existantes.

Nouvelle Classe TDiode
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.
31.
32.
33.
  [ObservableMembers('ColorOn',false)]
  TDiode = class(TCircle)
  private
    { Déclarations privées }
    FOnOff : Boolean;
    FColorOn : TAlphaColor;
    FColorOff : TAlphaColor;
    FBrightPos: TPosition;
    FBrightColor: TAlphaColor;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
    procedure SetColorOff(const Value: TAlphaColor);
    procedure SetOnOff(const Value: Boolean);
    procedure SetColorOn(const Value: TAlphaColor);
    procedure SetBrightPosition(const Value: TPosition);
    procedure SetBrightColor(const Value: TAlphaColor);
 protected
    { Déclarations protégées }
    function CanObserve(const ID: Integer): Boolean; override;  { declaration is in System.Classes }
    procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override; { declaration is in System.Classes }
    procedure ChangeBrightPos(sender : TObject);
  public
    { Déclarations publiques }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function Paint: Boolean; reintroduce;
    property BrightPosition : TPosition read FBrightPos write SetBrightPosition;
  published
    { Déclarations publiées }
    property OnOff : Boolean read FOnOff write SetOnOff;
    property ColorOn : TAlphaColor read FColorOn write SetColorOn;
    property ColorOff : TAlphaColor read FColorOff write SetColorOff;
    property BrightColor : TAlphaColor read FBrightColor write SetBrightColor;
  end;

L’assignation de l’évènement se fera dans le constructeur de la diode.

Constructor
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
  constructor TDiode.Create(AOwner: TComponent);
  begin
    if not(csloading in ComponentState) then
    begin
      inherited;
      FOnOff:=True;
      FColorOff:=TAlphaColors.Lightgray;
      FColorOn:=TAlphaColors.Red;
      FBrightPos:=TPosition.Create(PointF(0.5,0.5));
      FBrightPos.OnChange:=ChangeBrightPos;
      FBrightColor:=TAlphaColors.Antiquewhite;
      Fill.DefaultColor:=FColorOn;
      Fill.Color:=FColorOn;
      Fill.Gradient.Color:=FColorOn;
      Fill.Gradient.Color1:=FBrightColor;
      Fill.Kind:=TBrushKind.Gradient;
      Fill.Gradient.Style:=TGradientStyle.Radial;
      Fill.Gradient.RadialTransform.RotationCenter.X :=FBrightPos.X;
      Fill.Gradient.RadialTransform.RotationCenter.Y :=FBrightPos.Y;
      Paint;
    end;
  end;

Avant d’installer, n’oublions pas de modifier l’unité « design » Lug.FMX.DiodeD pour déclarer ce nouveau composant et l’inclure dans la palette.

 
Sélectionnez
  RegisterComponents('Govel', [Tdiode,TdiodeEx]) ;

IV-B. Test final

Après l’installation, nous avons maintenant deux composants Diode et DiodeEx dans la palette Govel.
Positionnons quelques éléments supplémentaires sur la forme de départ :

  • Remplaçons la diode par le nouveau composant DiodeEx.
  • Un ColorListbox pour donner une couleur à l’éclat.
  • Ajoutons deux TrackBars qui déplaceront la lumière intérieure. C’est plus un gadget qu’autre chose mais cela permettra de montrer l’utilisation des CustomFormats.
  • Un CheckBox pour activer ou désactiver la diode, qui écrasera la valeur pouvant être contenue dans l’ensemble de données prototype.

    Image non disponible

  • Puis établissons quelques liaisons.

Image non disponible

Une fois ces éléments indiqués, le programme est fonctionnel et toujours sans aucune ligne de code.

Je reviens sur les liaisons avec les TrackBars. La valeur d’un trackbar sera comprise entre 0 et 100. Pour transformer la valeur, il faut donc diviser par 100 ; par conséquent, la propriété CustomFormat contiendra la formule %s/100 et la propriété CustomParse sera exprimée ainsi : %s*100.

Bogue de la version Rio (10.3.x) : le changement de position du centre du gradient ne fonctionnera pas, sauf si vous indiquez l’utilisation de GPUCanvas. Par exemple, dans la partie initialization de la forme principale, écrivez GlobalUseGPUCanvas :=true;.

V. Un composant plus orienté données de la vie entreprise

Côté informatique de gestion, une diode pourrait servir à fournir une indication visuelle à l’utilisateur, par exemple distinguer sur une fiche client les bons en vert, les litigieux en orange et les mauvais en rouge. Par contre, il faudrait quelque chose de plus facile que le stockage de valeurs de couleurs (TAlphacolors) dans la base de données. L’idéal serait donc d’avoir un tableau d’équivalence entre le contenu d’un champ (toujours pour le même exemple V, O ou R) et une couleur. Ce qui, codifié, pourrait être un objet de ce type :

Élément
Sélectionnez
  /// Définition des équivalences
  /// une valeur = une couleur
 TDiodeDataColor = Class
  private
    FDataValue: String;
    FDiodeColor: TAlphaColor;
    procedure SetDataValue(const Value: String);
    procedure SetDiodeColor(const Value: TAlphaColor);
  published
    constructor Create(const DataValue : String; const DiodeColor : TAlphaColor);
    property DataValue: String read FDataValue write SetDataValue;
    property DiodeColor: TAlphaColor read FDiodeColor write SetDiodeColor;
  end;
...

implementation

{ TDiodeDataColor }
constructor TDiodeDataColor.Create(const DataValue : String; const DiodeColor: TAlphaColor);
begin
  FDataValue := DataValue;
  FDiodeColor := DiodeColor;
end;

procedure TDiodeDataColor.SetDataValue(const Value: String);
begin
FDataValue:=Value;
end;

procedure TDiodeDataColor.SetDiodeColor(const Value: TAlphaColor);
begin
FDiodeColor:=Value;
end;

Pour faire bonne mesure, j’ai ajouté un constructeur afin de rendre une codification plus simple lors d’ajout d’un couple valeur/couleur.

Toutefois, comment inclure cela au niveau d’un composant et de ses propriétés, pour une saisie facilitée au cours du design ? L’astuce est d'utiliser un éditeur de propriété. Heureusement, il en existe un qui convient parfaitement, moyennant quelques ajustements : l’éditeur de collections.

Première étape, transformer la classe en classe de type élément de collection (TCollectionItem).

CollectionItem
Sélectionnez
 TDiodeDataColor = Class(TCollectionItem)

Ensuite, ajoutons une nouvelle classe de type TCollection.

Collection
Sélectionnez
  TItemCollection<T: TDiodeDataColor> = class(TCollection)
  private
    FOwner: TComponent;
    FCollString: string;
  public
    constructor create(CollOwner: TComponent);
    function GetOwner: TPersistent; override;
    procedure Update(Item: TCollectionItem); override;
  end;
...

implementation

{ TItemCollection<T> }
constructor TItemCollection<T>.create(CollOwner: TComponent);
begin
  inherited create(T);
  FOwner := CollOwner;
end;

function TItemCollection<T>.GetOwner: TPersistent;
begin
  Result := FOwner;
end;

procedure TItemCollection<T>.Update(Item: TCollectionItem);
var
  str: string;
  I: Integer;
begin
  inherited;
  str := '';
  for I := 0 to Count - 1 do
  begin
    str := str + Format('%#x',[(Items[I] as TDiodeDataColor).FDiodeColor]);
    if I < Count - 1 then
      str := str + '-';
  end;
  FCollString := str;
end;

Cette partie est plus corsée et j’avoue être allé chercher le modèle du code dans les sources d’Embarcadero. À remarquer, le constructeur un peu particulier et surtout la nécessité de la procédure Update.

V-A. Le composant DiodeDB

Il est désormais possible de créer le nouveau composant, à dériver de TDiode ou TDiodeEx.

Au préalable, je définis un nouveau type (TDefinedColors), afin de l’utiliser.

DiodeDB
Sélectionnez
 TDefinedColors = Class(TItemCollection<TDiodeDataColor>);

 [ObservableMembers('DataValue',false)]
 TDiodeData = class(TDiodeEx)
  private
    FDataValue: String;
    FColors : TDefinedColors;
    function ColorsStored: Boolean;
    procedure SetColors(const Value: TDefinedColors);
    function FindColor(const Value: String) : TAlphaColor;
    procedure SetDataValue(const Value: String);
  published
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    property DataValue : String read FDataValue write SetDataValue;
    function Paint: Boolean; reintroduce;
  published
    { Déclarations publiées }
   property Colors : TDefinedColors read FColors write SetColors
      stored ColorsStored;
 end;
...
implementation
...
{ TDiodeData }

function TDiodeData.ColorsStored: Boolean;
begin
  result:=True;
end;

constructor TDiodeData.Create(AOwner: TComponent);
begin
    inherited;
    if (not(csloading in ComponentState)) OR (NOT Assigned(FColors)) then
        FColors:=TDefinedColors.create(Self);
end;

destructor TDiodeData.Destroy;
begin
    FreeAndNil(FColors);
    inherited;
end;

function TDiodeData.FindColor(const Value: String) : TAlphaColor;
var  I: Word;
begin
  for I := 0 to FColors.Count - 1 do
    if TDiodeDataColor(FColors.Items[I]).DataValue= Value then
    begin
      Exit(TDiodeDataColor(FColors.Items[I]).DiodeColor);
    end;
  Result := FColorOff;
end;

function TDiodeData.Paint: Boolean;
var ApplyColor : TAlphacolor;
begin
ApplyColor:=FindColor(FDataValue);
FOnOff:=ApplyColor<>FColorOff;
if FonOff then Fill.Gradient.Color:=ApplyColor
            else Fill.Gradient.Color:=FColorOff;
  Fill.Gradient.Color1:=FBrightColor;
  Fill.Gradient.RadialTransform.RotationCenter.X:=FBrightPos.X;
  Fill.Gradient.RadialTransform.RotationCenter.Y:=FBrightPos.Y;
  Result:=True;
end;

procedure TDiodeData.SetColors(const Value: TDefinedColors);
begin
  FColors.Assign(Value);
end;

procedure TDiodeData.SetDataValue(const Value: String);
begin
  FDataValue := Value;
  Paint;
end;

Par rapport aux composants déjà écrits, vous remarquerez certaines nouveautés, à commencer par la déclaration de la propriété publiée Colors qui est suivie du mot clé stored ainsi que du nom d’une fonction (ColorsStored).

Important car cela va indiquer, via la fonction retournant true, que la collection va être stockée dans le dfm.

Cette nouvelle propriété nécessite que le constructeur et le destructeur soient surchargés pour réserver et libérer la mémoire.

Une réintroduction de la méthode Paint sera également nécessaire.

Enfin, il faut retrouver la couleur à utiliser en fonction de la valeur du champ, ce qui sera chose faite avec une fonction de classe : FindColor.

V-B. Test rapide

Après ajout à la palette et installation de ce nouveau composant, un test rapide peut se réaliser en positionnant sur une forme le nouveau composant et un combobox, puis en créant deux liens.

Image non disponible

Le combobox contiendra les éléments (R,O,V).
L’éditeur de collection fera la jonction entre ces éléments et la couleur souhaitée.

Ici, toute l’astuce est dans l’utilisation d’un TProtypeBindSource (d’une seule ligne) pour faire la liaison entre la boite de choix et le composant DiodeDB. C’est d’ailleurs la seule solution pour lier les propriétés de deux composants.

Seul bémol, il faudra exécuter le programme pour vérifier que les liaisons et le composant fonctionnent.

VI. Touche finale

Il est désormais temps de peaufiner nos composants en ajoutant des icônes à notre palette car, pour l’instant, les glyphes qui les représentent sont loin d’être significatifs.

Image non disponible

Bien évidemment la première tâche va être de composer des images. À l’ancienne, il nous faut, en théorie, des images de type bitmap de taille 16x16, 24x24 et 32x32. Une fois les diverses images créées, il faut les incorporer au sein d’un fichier ressource.


Le plus simple, à partir du menu principal, sélectionnez Projet/Ressources et images ...

et ajoutez les images créées pour chaque composant en respectant un principe : le nom de la ressource doit être composé selon le format <Nom du composant>_<taille>.

Image non disponible

Mais, depuis la version Tokyo (10.2.2), à la différence des versions plus anciennes, il n’y a pas que des images de type bitmap qui peuvent être utilisées (on pourra aussi utiliser des png) mais il faudra aussi prendre en compte les thèmes clair ou foncé de l’IDE (voir ce billet).

J’ai donc préféré créer des images png.

Image non disponible

Mais, selon le billet, un mix de bitmaps (32ppm) et de png serait idéal si vous vous souciez de rétrocompatibilité ou de vitesse de chargement de l’IDE.

Pour créer ces images, j’ai utilisé un programme concocté lors de mes études sur le composant FMX.Graphics.Tpath, que vous pourrez retrouver soit dans la section source du forum soit dans un de mes dépôts GitHub https://github.com/Serge-Girard/TPath.

Pour appliquer ces images, vous devrez :

  1. Reconstruire (pas simplement compiler) le paquet ;
  2. Redémarrer l’IDE.

Image non disponible

VII. Mises en garde et problèmes

Vous retrouverez les sources de ces composants (version Delphi 11 Alexandria) dans ce fichier ou dans un de mes dépôts GitHub

Sur une cible Android, on découvrira que le gradient laisse un petit pixel en blanc.

Image non disponible

Ce problème peut être réglé en utilisant SKIA4Delphi comme moteur graphique.

Sur une cible Linux, le déplacement du centre du gradient se révèle impossible. Il s’agit certainement d’un « oubli » de Delphi.

Je n’ai pu, faute de matériel, tester les cibles macOS et iOS.

VIII. Conclusion

Avec ce parcours, j’espère vous avoir démontré qu’il n’était pas si compliqué que cela de créer des composants réactifs aux liaisons. S’il y a quelque chose à retenir c’est que, pour les rendre réactifs :

  • Il faut ajouter trois méthodes à la classe : la fonction CanObserve et les procédures ObserverToggle et ObserverAdded.
  • Il faut recenser les liaisons dans la partie initialization, sans oublier de les libérer par la suite dans la section finalization.
  • Il y a deux méthodes de recensement, Data.Bind.Components.RegisterObservableMember qui créera un lien bidirectionnel et Data.Bind.Components.RegisterValuePropertyName, plus pratique si le lien n’est qu’en sens unique, d’une donnée vers une propriété.

Je remercie l’équipe rédactionnelle pour leur participation à la publication de ce tutoriel.

Un merci spécial à Jean-Luc (Alcatîz) qui s’est lancé sur la pointe des pieds vers FMX et ses concepts ‘étranges’ loin d’un Pascal plus traditionnel, et bien évidemment à f-leb qui a passé au crible mon texte pour y trouver les fautes grammaticales.

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

En complément sur Developpez.com


Cette recommandation a souvent fait, lors de conférences, grincer les dents de concepteurs de composants renommés, dont Ray Konopka.

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 © 2022 Serge Girard. 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.