LiveBindings de A à … Écrire un composant observable I

Au cours de mon introduction aux LiveBindings j'avais utilisé un VCL.TTrackBar et en particulier sa propriété Position. Nous y avions découvert que ce composant n'était pas observable et je vous avais alors proposé un contournement pour résoudre ce problème : l’utilisation d’un lien non géré et l’instruction Notify.

L'objectif de ce tutoriel est d'apprendre à rendre une propriété d'un composant observable et même d’ajouter d’autres propriétés que nous pourrons lier.

5 commentaires Donner une note  l'article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

L'objectif premier de ce tutoriel n'est pas de construire des composants de A à Z, ce qui est parfaitement décrit dans d'autres tutoriels comme ceux rassemblés ici par Sébastien Doeraene. Il s'agit plutôt d'améliorer un composant existant en héritant de celui-ci comme décrit dans la partie II de ce tutoriel.

La tâche est facilitée par l'expert de création de composant, invocable de deux manières :

Fichier/Nouveau/Autre…

Image non disponible

ou Outils/Composants/Nouveau composant…

Image non disponible

Nous devons ensuite indiquer quel sera le framework cible :

Image non disponible
Étape 1

Il est à noter que si un framework est déjà chargé, autrement dit si un projet est déjà ouvert, cette information n’est pas demandée. Le composant créé sera du même environnement que le projet.

Pour ajouter l'observateur, nous ne pouvons (devons) pas modifier le composant d'origine : il faut donc créer une nouvelle classe, descendante de ce composant.

Image non disponible
Étape 2

La dernière étape permet de fournir à l’expert les informations nécessaires à la construction du code source, à savoir :

  • le nom du composant (classe) ;
  • la palette dans laquelle il va se trouver ;
  • le nom de l’unité source à générer.
Image non disponible
Étape 3

À propos du nom de l’unité : il est tout à fait possible de nommer cette dernière en la préfixant par le framework (exemples : VCL.mounite.pas ou FMX.monunite.pas), et ce, à la manière très pratique utilisée par Embarcadero.

Tant que cela reste un composant interne à votre organisation, cela ne pose aucun problème. Toutefois, Embarcadero ne recommande pas, voire déconseille tout à fait, l’utilisation de ces préfixes pour des composants que vous pourriez distribuer.

II. Rendre un composant actif

Comme un tutoriel existe déjà dans la documentation d’Embarcadero (création de composant activé), nous allons simplement étudier le déroulement de cette création d’un composant activé.

En résumé, par rapport à une création classique (d'avant les Livebindings), deux exigences doivent être remplies :

  • le contrôle doit implémenter le support des observateurs, car LiveBindings dépend des observateurs pour l'abonnement aux notifications des contrôles ;
  • vous devez recenser le nom de la valeur du contrôle. Ce dernier est utilisé par les composants LiveBindings pour générer des expressions qui obtiennent et définissent la valeur du contrôle.

II-A. Le nouveau composant TObservableTrackBar

Je vais dériver le composant d'origine TTrackBar afin d’ajouter l'observateur. Nous devons donc créer une nouvelle classe, descendante de ce composant.

Image non disponible
Étape 4

II-A-1. Ajouter un attribut

Image non disponible
Étape 5

La première étape est d'ajouter un attribut ObservableMember (ligne 10) juste avant la déclaration de cette nouvelle classe. Cet attribut sera utilisé par LiveBindings pour générer les expressions.

Partie interface
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
unit VCL.TrackBarObservable;

interface

uses
  Vcl.ComCtrls, System.Classes, WinApi.Messages, WinApi.CommCtrl, Vcl.Controls;

type
 
  [ObservableMember('Position')] // Attribut, identifie le nom de la valeur à observer 
  TObservableTrackBar = class(TTrackBar)
  private
    procedure CNHScroll(var Message: TWMHScroll); message CN_HSCROLL;
    procedure CNVScroll(var Message: TWMVScroll); message CN_VSCROLL;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
  protected
    // déclarées dans System.Classes elles doivent être écrasées (override)
    function CanObserve(const ID: Integer): Boolean; override;                       
    procedure ObserverAdded(const ID:  Integer; const Observer: IObserver);  override;
  end;

II-A-2. Rendre observable

La seconde étape consiste à indiquer que le composant peut être observé. C'est le rôle de la fonction CanObserve de la ligne 18 dans la partie interface. Sans elle, la modification du composant n'a pas lieu d'être.

CanObserve
Sélectionnez
function CanObserve(const ID: Integer): Boolean;
begin
 case ID of //est-ce que la destination et la source sont compatibles ?
    TObserverMapping.EditLinkID,
    TObserverMapping.ControlValueID:
      Result := True;
  else
    Result := False;
  end;
end;

II-A-3. Ajouter un observateur

La troisième étape ajoute la procédure ObserverAdded et, pour la méthode d'articulation associée, ObserverToggle. Ces deux méthodes sont en fait écrites pour le concepteur visuel de LiveBindings.

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

procedure TObservableTrackBar.ObserverToggle(const AObserver: IObserver; const Value: Boolean);
var
  LEditLinkObserver: IEditLinkObserver;
begin
  if Value then
  begin
// désactive le composant si le champ associé est en écriture seule
    if Supports(AObserver, IEditLinkObserver, LEditLinkObserver) then
      Enabled := not LEditLinkObserver.IsReadOnly;
  end else
    Enabled := True;
end;

II-A-4. À quoi servent ces deux dernières étapes ?

Pour nos programmes qui vont utiliser ce composant, la réponse est catégorique : les deux dernières étapes ne serviront à rien ! Nous pouvons même nous passer de ces deux procédures puisque, contrairement à la procédure CanObserve qui est indispensable, notre composant fonctionnera quand même sans elles ! Ce n'est qu'au niveau du concepteur visuel de liaisons, donc dans l'EDI, que ces méthodes sont utilisées.

Pour vous en convaincre, commentez ces deux dernières méthodes puis réinstallez le composant. A priori, le comportement est identique !

II-A-4-a. Comment cela fonctionne ?

Vous avez certainement remarqué que les méthodes CanObserve et ObserverAdded ont toutes deux un argument ID. C'est cet identifiant qui permet d'obtenir les informations de type et dans le cas d’ObserverAdded, de faire le lien avec la méthode ObserverToggle.

Pour imager davantage : c'est ce qui fait que le concepteur visuel accepte ou refuse une liaison entre deux composants, voire entre deux propriétés de ceux-ci et détermine le type de flèche qui en résulte (bidirectionnelle ou non).

II-A-5. Notifier les changements

La dernière étape créera les méthodes (lignes 13 et 14 de la partie interface) qui vont surveiller les valeurs, en l’occurrence la position du curseur du TrackBar, de façon à alerter l'observateur. Comme le TrackBar peut être horizontal ou vertical, il nous en faut une par mode.

Ces méthodes écrasent celles de la classe parente. Ce qui est à retenir, c'est que la méthode TLinkObservers.ControlChanged permet d'indiquer qu'il y a eu un changement.

surveillance
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
// le code est identique pour ces deux procédures
var
  LPosition: Integer;
begin
  LPosition := Position; // récupère la position précédente
  inherited; // appel de la méthode parente
  if LPosition <> Position then  // vérifie s’il y a eu déplacement
     TlinkObservers.ControlChanged(Self); // notifie le changement
end ;

II-B. Recenser le composant

Pour pouvoir utiliser ce composant, il faut bien sûr l'installer ! C'est l'objectif de la procédure register qui permet d'ajouter le composant à la palette. Mais il faut aussi recenser ce dernier pour le concepteur visuel. La propriété du nom de valeur du composant (dans notre cas « Position ») doit être signalée en utilisant la méthode RegisterObservableMember.

Recensement composant
Sélectionnez
unit VCL.TrackBarObservableReg;

interface

procedure Register;

implementation

uses
  System.Classes, VCL.TrackbarObservable, Data.Bind.Components;


procedure Register;
begin
  RegisterComponents('LiveBindings Samples', [TObservableTrackBar]);
end;

initialization 

Data.Bind.Components.RegisterObservableMember(TArray<TClass>.Create(TObservableTrackBar), 'Position', 'DFM');

finalization 

Data.Bind.Components.UnregisterObservableMember(TArray<TClass>.Create(TObservableTrackBar));

end.

Reste à installer le composant : ce sera chose faite en créant un nouveau paquet ou en ajoutant les sources à un paquet déjà existant.

Image non disponible

Vous remarquerez que j’indique les sources et non le source. En effet, le tutoriel de la documentation en présente deux et non un seul, la partie recensement du composant étant séparée de la partie source de la nouvelle classe.
Or, si vous utilisez l’expert, vous n’obtenez qu’un seul source fusionnant à la fois la classe et le recensement !
Les deux modes fonctionnent, mais séparer le recensement du reste est ce que je recommanderais.

Une fois le paquet installé, un programme test nous permettra de vérifier :

1 . que le concepteur visuel est fonctionnel ;

Image non disponible

2 . que le composant est observé pendant le design (cf. mon introduction aux LiveBindings).

Image non disponible
en vert les propriétés où le lien est possible

Pour cette partie, je ne fournirai pas les sources. Je préfère vous inciter à suivre les étapes de la documentation à la lumière de ce chapitre.

Je vous incite même à afficher cette documentation dans votre EDI.

Recherchez « Observable » puis sélectionnez le tutoriel.

Vous pourrez alors facilement faire des opérations de copier-coller sans sortir de l’EDI.

Image non disponible

III. Application à un autre composant

Remâcher le tutoriel de la documentation, c'est bien pour en comprendre le principe ; essayer d'appliquer ces principes à un autre composant va certainement être plus formateur.

Comme composant d’entraînement, je vais utiliser le TImage. Il est facile de lier la propriété Picture à une image contenue dans une colonne de type binaire (blob) d’une table. En revanche, il n'y a pas de propriété contenant le chemin vers une image et qui permettrait de charger celle-ci. Or, par souci d’économie de l’espace occupé dans les tables, sauf cas particulier, je préfère indiquer le chemin et le nom de l'image dans mes bases de données.

Le défi est donc posé : ajouter une propriété qui contiendra le nom de fichier et faire en sorte que le moteur des LiveBindings la gère.

III-A. Créer le composant

La création du composant s’opère à partir de l’expert :

Image non disponible
1. Choix du framework
Image non disponible
2. Sélection de l’ancêtre
Image non disponible
3. Nommer
Image non disponible
4. Choix de génération

J’obtiens un premier source qu’il me sera ensuite possible de scinder en deux unités distinctes, une pour la classe et une pour le recensement.

Cette scission n’est pas une obligation, mais gardez cette possibilité en mémoire, car elle nous sera très utile si notre choix se porte sur la création d’un composant mixte (VCL et FMX).

Je suivrai les mêmes étapes que dans le premier chapitre : il me faut dériver la classe TImage, y ajouter la propriété, la mettre en attribut et rendre celle-ci observable.

interface
Sélectionnez
unit ImageFileVCL;

interface

uses
  Vcl.ComCtrls, System.Classes, WinApi.Messages, WinApi.CommCtrl,
  Vcl.Controls, VCL.ExtCtrls, System.SysUtils;
type
  [ObservableMember('NomFichierImage')]                            
  TFileImageVCL = class(TImage)
  private
    FNomFichierImage : String;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
  protected
    function CanObserve(const ID: Integer): Boolean; override;
    procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override;
    procedure DrawImage(Value : String);
  published
    property NomFichierImage : string read FNomFichierImage write DrawImage;
  end;

La principale différence avec le chapitre II.A.1 est donc l’ajout d’une propriété (NomFichierImage) et le traitement de celle-ci lorsqu'elle est changée (procédure DrawImage).

DrawImage
Sélectionnez
procedure TFileImageVCL.DrawImage(Value : String);
var OldValue : String;
begin
if FNomFichierImage<>Value then
 begin
  FNomFichierImage:=Value; // modifie la valeur de la propriété
  try 
    Picture.LoadFromFile(Value); // charge l'image 
  except
    Picture:=nil; // efface l'image si le fichier n'existe pas
  end;

  TlinkObservers.ControlChanged(Self); // notification du changement
 end;
end;

Il s’agit à présent de rendre observable la propriété et d’ajouter un observateur. La méthode est la même que lors des étapes 2 et 3 du chapitre précédent. Je résume ce qu’il faut faire :

  • ajouter un attribut [ObservableMember('xxxx')] II.A.1 ;
  • rendre observable par l’ajout de la fonction CanObserve II.A.2 ;
  • ajouter l’observateur pour le concepteur visuel par l’intermédiaire des procédures ObserverAdded et ObserverToggle II.A.3.

III-B. Recenser le composant

Si nous nous en tenons au chapitre II.B, l'écriture du recensement du composant est simple.

Recensement TFileImage
Sélectionnez
unit RegImageFile;

interface
uses System.Classes, System.SysUtils, VCL.Controls, VCL.Graphics,
     Data.Bind.Components,
     System.Bindings.Outputs, System.rtti, System.TypInfo,
     ImageFileVCL;

procedure Register;

implementation

procedure Register;
begin
  GroupDescendentsWith(TFileNameImageVCL, Vcl.Controls.TControl);
  RegisterComponents('Tutoriels', [TFileNameImageVCL]);
end;

initialization
 Data.Bind.Components.RegisterObservableMember(TArray<TClass>.Create(TFileNameImageVCL), 'NomFichierImage','DFM');

finalization
  Data.Bind.Components.UnregisterObservableMember(TArray<TClass>.Create(TFileNameImageVCL));
end.

III-C. Programme test

Après avoir installé le composant dans notre palette(1), il est aisé de créer un petit programme test.

Trop impatient, j'ai mis la charrue avant les bœufs ! Le bon usage aurait été de tester le composant sans que celui-ci soit installé afin de faire un premier essai en le créant à l'exécution (runtime) et, en cas de satisfaction, installer ensuite le package. Toutefois, il est assez délicat de créer des liens directement à l’exécution et tester les Livebindings sans installation, sans parler du fait que l’on ne pourrait y voir les impacts sur l’EDI.

L'écran comprend quatre composants visuels : notre nouveau composant TFileImage, un bouton, une zone de saisie et un simple libellé. En composants non visuels, nous avons un TOpenPictureDialog que nous utiliserons en relation avec le bouton pour charger une image, un TPrototypeBindSource qui simulera notre accès à une base de données et un TBindingsList afin d’établir les différentes liaisons, comme vous pouvez le voir sur l'image suivante.

Image non disponible

Le seul code à écrire est celui concernant le comportement du bouton.

 
Sélectionnez
procedure TForm8.Button1Click(Sender: TObject);
begin
if OpenPictureDialog1.Execute then
  FileImage1.FichierImage:=OpenPictureDialog1.FileName;
end;

Ce code sera avantageusement remplacé par :

 
Sélectionnez
if FileOpenDialog1.execute  then
  begin
   PrototypeBindSource1.Edit;
PrototypeBindSource1.InternalAdapter.FindField('NomImage').SetTValue(OpenPictureDialog1.FileName);
   PrototypeBindSource1.Post;
  end;

Ce code « joue » directement sur les données et ainsi s’applique à tous les liens.

À l'exécution, le programme est conforme à notre attente : la saisie dans la zone de texte, une fois le changement de zone effectué, charge l'image et modifie le libellé.

Image non disponible

J'ai utilisé un style (Glossy) pour avoir un rendu plus sympathique.

Cela pourrait donc nous suffire, toutefois il reste quelques difficultés à résoudre :

  • comme le montre l'image suivante, nous ne pouvons pas lier directement l'image à une zone de saisie (Edit2). Nous avons cependant la solution de passer par un TProtypeBindSource. Le champ NomImage est lié à la propriété FichierImage de mon composant, mais aussi à ma zone de saisie Edit1 ;
Image non disponible

Gardez cette technique de l'utilisation d'un PrototypeBindSource en mémoire. En effet, passer par cet intermédiaire est un moyen facile de lier les propriétés de deux composants.

  • la nouvelle propriété FichierImage du composant ne nous permet pas de faire une recherche sur le disque.
Image non disponible

Pour corriger immédiatement ce dernier point, il faut changer le type de FNomFichierImage en TFileName, ce qui entraîne la correction de la propriété FichierImage et donc de la procédure DrawImage.

Corrections
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.
uses
  Vcl.ComCtrls, System.Classes, WinApi.Messages, WinApi.CommCtrl,
  Vcl.Controls, VCL.ExtCtrls, System.SysUtils;
type
  [ObservableMember('NomFichierImage')]
  TFileNameImageVCL = class(TImage)
  private
    { Déclarations privées }
    FNomFichierImage : TFileName;
    FUseEvalShortcuts: Boolean;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
  protected
    { Déclarations protégées }
    procedure DrawImage(Value : TFileName); overload;
//    procedure DrawImage(Value : String); overload;
    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;
  published
    { Déclarations publiées }
    property NomFichierImage : TFileName read FNomFichierImage write DrawImage;
    property UseEvalShortcuts: Boolean read FUseEvalShortcuts;
  end;
Image non disponible

III-C-1. Test en situation (liaison avec une table)

Le test précédent n’impliquait pas de données stricto sensu. Il est par conséquent temps de faire un test un peu plus poussé. Pour cela, je vais écrire un nouveau programme qui va utiliser une table en mémoire que je remplirai grâce à un petit jeu d’essais.

Image non disponible
Programme test, design

Premier écueil, dès la tentative de mise en place du lien, j’obtiens l’erreur suivante :

Image non disponible
Erreur de conversion

Pourtant ce composant semblait fonctionner !

Que s’est-il passé ? Le type de lien est en cause, car ce n’est pas un lien de type TLinkPropertyToField, mais un TLinkControlToField qui a été créé par le concepteur visuel.

Je vous rappelle que les LiveBindings utilisent les RTTI. Malheureusement, aucun mécanisme ne permet d’indiquer qu’une chaîne de caractères fait référence à un fichier image !

Pour résoudre ce problème, il me faut ajouter ce mécanisme, à savoir un convertisseur.

Je n’avais pas encore parlé de cette nouveauté dans ma série sur les LiveBindings !

Plus d’informations dans la documentation Wiki Embarcadero.

Où placer ce code ? Comme nous avons déjà eu affaire à ce genre de processus avec RegisterObservableMember, il semble évident de placer cette fonction dans une section initialization tout comme il ne faudra pas oublier de dé-recenser celle-ci dans la partie finalization. Par contre, ce n’est pas dans le fichier de recensement du composant (qui ne fait qu’ajouter notre composant à l’EDI Delphi), mais dans l’unité de notre classe que cette partie doit être incluse.

Convertisseur
Sélectionnez
Type 
// …
const Convertisseur : String = 'NomFichierVersTPicture';
      CetteUnite : String = 'ImageFileVCL';
      Description : String = 'Récupère l''image';

procedure RegisterConverter;
procedure UnRegisterConverter;


implementation

//…


procedure RegisterConverter;
begin
if not TValueRefConverterFactory.HasConverter(convertisseur)
 then begin
  TValueRefConverterFactory.RegisterConversion(TypeInfo(String),TypeInfo(TPicture),
    TConverterDescription.Create(
      procedure(const InValue: TValue; var OutValue: TValue)
      var LOutObject : TObject;
      begin
          LOutObject := OutValue.AsObject;
          Assert(LOutObject <> nil);
          Assert(LOutObject is TPicture);
            try
              TPicture(LOutObject).LoadFromFile(InValue.ToString)
            except
              TPicture(LOutObject).Graphic := nil;
            end;
      end,
      Convertisseur,  // id du convertisseur 'NomFichierVersTPicture'
      Convertisseur,  // nom du convertisseur 'NomFichierVersTPicture'
      CetteUnite,     // nom de l’unité 'ImageFileVCL' 
      True,
      Description,   // description 'Récupère l'image'  
      VCL.Controls.TControl // VCL
      )
    );
 end;
end;

procedure UnRegisterconverter;
begin
 TValueRefConverterFactory.UnRegisterConversion(Convertisseur);
end;

initialization
  registerconverter;
finalization
  unregisterconverter;

Question de style, plutôt que de coder directement le convertisseur au sein de la partie initialisation, j’ai préféré créer deux procédures : RegisterConverter, UnRegisterConverter. Les noms de celles-ci étant purement conventionnels.

J’ai également ajouté plusieurs constantes alors que j’aurais pu inscrire directement ces valeurs. En fait, j’ai préféré rester au plus près de ce qui se trouve dans les sources Delphi.

Quels impacts ? Le premier est bien évidemment que le composant fonctionne, du moins pour les fichiers de type BMP ; un (petit) second se trouve dans l’IDE lorsque l’on regarde la liste des convertisseurs proposés par le TBindingsList.

Image non disponible

Cette image montre l’utilité de fournir les informations, même les optionnelles que sont les paramètres ADisplayName, AUnitName, ADescription et AFrameworkClass du constructeur.

Si vous êtes curieux, utilisez des instructions comme ShowMessage, l’une dans la procédure DrawImage et l’autre dans le convertisseur.
Comme moi, vous constaterez que DrawImage est utilisé pour les liens de propriété à propriété (via le TProtypeBindSource) alors que le convertisseur est utilisé dans le second cas (liaison avec une source de données).

Enfin, étrangement, seuls les fichiers de type BMP sont affichés ! Pour afficher tout type de fichier image (JPEG, PNG ou GIF), il faudra ajouter Vcl.Imaging.JPEG, VCL.Imaging.PngImage, VCL.Imaging.GIF à la liste des unités à utiliser.

Image non disponible

Les sources de cette première partie se trouvent dans l’archive téléchargeable à cette adresse.

Attention, il vous faudra certainement indiquer ou modifier le chemin du composant dans les options du projet.

III-D. Rédaction du même composant pour FireMonkey

Ce qu'il y a d’intéressant avec l’exemple choisi, c'est que le composant n'existant ni en VCL ni en FMX, nous allons pouvoir aborder deux thèmes nouveaux : les différences entre les deux frameworks et les techniques pour mixer les deux en un seul package.

III-D-1. Écriture du composant pour FiremonKey

Le point de départ est le même que pour le composant en VCL, mais cette fois-ci, j’indique l’utilisation du framework FMX.

Image non disponible
Choix du framework

Je sélectionne ensuite le composant ancêtre TImage, comme pour la VCL, mais cette fois, il est contenu dans l'unité FMX.Objects.

Image non disponible
Choix de l’ancêtre
Généré par l'expert
Sélectionnez
unit FMX.FileNameImage;

interface

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

type
  TFileNameImage = class(TImage)
  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('Tutoriels', [TFileNameImage]);
end;

end.

À quelles différences suis-je confronté ? La première, assez évidente, est dans la liste des uses. Elle est logique puisqu’il s’agit du framework FMX.

En revanche, tout ce qui concerne les LiveBindings, c’est-à-dire les procédures ou fonctions (ObserverToggle, ObserverAdded, CanObserve), sera strictement identique à ce qui a été déjà écrit pour le composant VCL.

Après m’être occupé du processus de recensement et avoir déclaré les différentes procédures et fonctions (simples copier/coller à partir de mon fichier source VCL), j’obtiens :

 
Sélectionnez
unit FMX.FileNameImage;

interface

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

type
  [ComponentPlatforms(pidWin32 or pidWin64 or pidAndroid)] 
  [ObservableMember('NomFichierImage')]
  TFileNameImage = class(TImage)
  private
    { Déclarations privées }
     FNomFichierImage : TFileName;
     procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
  protected
    function CanObserve(const ID: Integer): Boolean; override;
    procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override;
    procedure DrawImage(Value : TFileName);
  published
    property FichierImage : TFileName read FNomFichierImage write DrawImage;
  end;


procedure RegisterConverter;
procedure UnRegisterConverter;

implementation


{ TFileNameImage }

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

procedure TFileNameImage.DrawImage(Value: TFileName);
begin
//
end;

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

procedure TFileNameImage.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;

procedure RegisterConverter;
begin
if not TValueRefConverterFactory.HasConverter('NomFichierImageToStringFMX')
 then begin
  TValueRefConverterFactory.RegisterConversion(TypeInfo(String),
    TypeInfo(TBitmap),
    TConverterDescription.Create(
      procedure(const InValue: TValue; var OutValue: TValue)
      var LOutObject : TObject;
      begin
          LOutObject := OutValue.AsObject;
          Assert(LOutObject <> nil);
          Assert(LOutObject is TBitmap);
            try
              TBitmap(LOutObject).LoadFromFile(InValue.ToString)
            except
              TBitmap(LOutObject).Clear(0);
             end;
      end,
      'NomFichierImageToStringFMX',
      'NomFichierImageToStringFMX',
      EmptyStr,
      True,
      EmptyStr,
      nil      // pas d'indication de framework
      )
    );
 end;
end;

procedure UnRegisterconverter;
begin
 TValueRefConverterFactory.UnRegisterConversion('NomFichierImageToStringFMX');
end;

initialization
  registerconverter;
finalization
  unregisterconverter;
end.

Reste à prendre en compte le changement de valeur de la propriété et charger l’image, ce qui est du ressort de la procédure DrawImage.

 
Sélectionnez
procedure TFileNameImage.DrawImage(Value: TFileName);
begin
if FNomFichierImage<>Value then
 begin
   FNomFichierImage:=Value;
  if FileExists(Value)
    then Bitmap.LoadFromFile(Value)
    else Bitmap:=nil;
  TLinkObservers.ControlChanged(Self);
 end;
end;

Une seule différence apparaît : ce n’est plus la propriété Picture comme en VCL qui est utilisée, mais Bitmap.

Je pourrais m’arrêter là, mais ce serait ignorer les spécificités de chaque cible :

  • les noms de fichiers Windows sont insensibles à la casse ;
  • le stockage des images Android doit se faire dans un emplacement spécifique.

C’est à ce moment que commencent à entrer en scène les directives de compilation et plus particulièrement les conditions prédéfinies.

Pour ce qui est de la sensibilité à la casse, je peux soit introduire une variable booléenne, soit utiliser plus directement les directives de compilation, la première méthode étant peut-être plus lisible que la seconde.

Comparez :

Première méthode
Sélectionnez
procedure TFileNameImage.DrawImage(Value: TFileName);
var Changed: Boolean;
begin
{$IFDEF MSWINDOWS}
Changed:=not SameText(FNomFichierImage,Value);
{$ELSE}
Changed:= FNomFichierImage<>Value;
{$ENDIF}
if Changed then
...
Seconde méthode
Sélectionnez
procedure TFileNameImage.DrawImage(Value: TFileName);
begin
if 
{$IFDEF MSWINDOWS}
 not SameText(FNomFichierImage,Value)
{$ELSE}
  FNomFichierImage<>Value
{$ENDIF}
then begin
...

Pour ce qui est des emplacements de fichiers, ils vont beaucoup dépendre des conventions que vous voudrez mettre en place. Pour Android, les images seront certainement stockées dans un répertoire spécifique /data/data/<ID application>/files que l’on obtient facilement grâce à System.IOUtils.TPath.GetDocumentsPath. Pour Windows, les possibilités sont plus variées avec l’utilisation, au choix, d’un chemin complet dans la propriété, ou d’un lieu de stockage particulier, en bref d’un répertoire accessible. !

J’ai choisi un compromis : TPath.GetDocumentsPath pour Android alors que pour Windows il s’agira d’un chemin complet.

Pour prendre en compte ces conventions, je déclare la nouvelle variable locale FullPathName.

Code final
Sélectionnez
procedure TFileNameImage.DrawImage(Value: TFileName);
var Changed : Boolean;
    FullPathName : TFileName;
begin
{$IFDEF MSWINDOWS}
Changed:=Not SameString(FNomFichierImage,Value);
{$ELSE}
Changed:=FNomFichierImage<>Value;
{$ENDIF}
if FNomFichierImage<>Value then
 begin
  {$IFDEF MSWINDOWS}
    FullPathName:=Value;
  {$ELSE}
 FullPathName:=System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetSharedPicturesPath,ExtractFilePath(Value));
  {$ENDIF}
  FNomFichierImage:=Value;
  if FileExists(FullPathName)
    then Bitmap.LoadFromFile(FullPathName)
    else Bitmap:=nil;
  TLinkObservers.ControlChanged(Self);
 end;

Vous remarquerez que je ne tiens pas compte d’OSX ni d’iOS. C’est plus ou moins voulu : d’une part je n’ai aucun matériel pour tester ces deux environnements, d’autre part cela va me permettre d’introduire la notion de cibles de mon package FMX et ainsi de restreindre mon composant aux deux environnements testés.

Pour limiter mon composant aux seules plates-formes prévues, j’ajoute l’instruction tout simplement [ComponentPlatformsAttribute(pidWin32 or pidWin64 or pidAndroid)]devant ma déclaration de classe. Passer la souris sur le composant dans la palette m’indiquera alors les plates-formes supportées (cf. documentation).

Image non disponible

III-D-2. Recensement

Un dernier coup de collier est nécessaire avant de pouvoir utiliser notre nouveau composant : il faut écrire l’unité qui servira à recenser le composant, avec les quelques lignes que j’ai supprimées du source proposé par l’expert, en prévision du chapitre suivant III.D.5.

Recensement du composant
Sélectionnez
unit FMX.FileNameImageReg;

interface

procedure register;

implementation

uses System.Classes, FMX.FileNameImage, Data.Bind.Components;

procedure register;
begin
  RegisterComponents('Tutoriels', [TFileImage]);
end;

initialization
Data.Bind.Components.RegisterObservableMember(TArray<TClass>.Create(TFileImage), 'NomFichierImage', 'FMX');

finalization
Data.Bind.Components.UnregisterObservableMember(TArray<TClass>.Create(TFileImage));
end.

La seule différence notable avec le même composant VCL créé précédemment est la constante « FMX » au lieu de « DFM » en ce qui concerne la liaison.

III-D-3. Création du package

Ayant demandé de créer un source lors du déroulement de l’expert, mes sources ne sont pour l’instant pas installables en l’état : il me faut à présent créer un package.

Il faut choisir Fichier/Nouveau/Autres/Package depuis le menu principal pour cette création.

J’y ajoute mes deux sources et les unités requises.

Image non disponible

Vous ne savez pas vraiment quelles unités sont requises ?
Pas de problème ! Faites une première compilation, qui se révélera pleine d’erreurs d’inattention, mais qui ajoutera les unités requises.

Une fois tout réglé, il suffit d’utiliser l’option d’installation du paquet.

Image non disponible

À force de faire des essais, il est possible que l’installation échoue.

Si vous êtes confronté à ce problème, désinstallez le package en passant par les options de menu Outils/Composants/Installer des packages.

Image non disponible

Refaites alors une tentative.

III-D-4. Utilisation

Je laisse libre cours à votre imagination pour l’utilisation du composant. Voici cependant ci-dessous quelques captures réalisées sur mon téléphone Android :

Image non disponible

III-D-5. Deux cadres, un seul code source ?

La question se pose de savoir s’il est possible d’écrire un seul code pour les deux frameworks.

Est-ce possible ? Oui, et l'objet de ce chapitre est d'expliquer comment.

Est-ce judicieux ? La réponse est moins facile à donner. Comme vous le constaterez, il y aura quand même deux packages (.bpl), même si l'un est réduit à sa plus simple expression. De plus, le code source du composant devra être truffé de directives de compilation, voire, dans le cas de composants plus complexes, d'un nombre de déclarations et fonctions supplémentaires (pour les anglophones, je suggère de suivrecette vidéoafin de vous faire une idée du problème).

III-D-5-a. VCL vs FireMonkey

Je ne vais pas entrer dans les détails, mais plutôt proposer une approche générale en quelques tableaux (extraits de cette présentation CodeRage VI en 2011).

Déjà, il y a beaucoup de points communs entre les contrôles de la VCL et ceux de FireMonkey.

Points communs

Les deux héritent de TComponent.

Les deux ont une méthode Paint.

Les deux cadres ont des TCanvas.

Les deux cadres ont des TBitmap.

Les deux ont des méthodes similaires :

  • MouseDown, MouseUp ;
  • SetBounds.

Les deux ont des propriétés identiques ou très proches :

  • Color ;
  • Visible ;
  • TabOrder

Mais les deux comportent aussi des différences qu'il faudra bien prendre en compte.

Différences

VCL

FMX

Différentes unités

VCL.Controls , VCL.Graphics

FMX.Types
pour les couleurs (TAlphaColors) System.UITypes

 

TCustomControl

TControl

Types de coordonnées

Integer

Single

Forcer le dessin

 

Invalidate inexistant

Focalisation

Focused

IsFocused

 

Message WM_VISIBLECHANGED

Fonction SetVisible

 

Fonction MouseWheel

Fonctions
DoMouseWheelUp
DoMouseWheelDown

TCanvas est très différent

TBitmap est différent

Différences de TCanvas

VCL

vs

FMX

Les coordonnées

Integer

devient

Single

Les couleurs

TColor

devient

TAlphaColor

La brosse

Brush

devient

Fill

Le crayon

Pen

devient

Stroke

Pour le dessin

TBrush, TPen

sont fusionnés en

TBrush

III-D-5-b. Modifications à apporter

Si nous comparons les deux sources VCL.FileImage.pas et FMX.FileImage.pas, nous remarquons qu’ils comportent de nombreuses similitudes.

Les différences se situent au niveau des unités à inclure et dans la procédure DrawImage où la propriété qui va charger l’image est Picture pour un composant VCL et Image pour FMX.

Il est donc envisageable de se poser la question : pourquoi ne pas les fusionner et n’en faire qu’un seul ? Avantage ciblé ? Que toute modification (par exemple l’ajout d’une propriété) soit répercutée sur les deux composants.

Comment réussir à mixer les deux frameworks ? La réponse réside, une fois de plus, dans les directives de compilation, mais cette fois il va s’agir d’introduire une définition personnelle.

Pour fournir un exemple sans écraser les composants déjà écrits, je vais tout d’abord copier et renommer le fichier FileImageVCL.pas en FileImage.pas, mais aussi changer le nom de la classe.

FileImage
Sélectionnez
unit ImageFile;

interface

uses
  System.SysUtils, System.Classes, System.IOUtils,
  Vcl.Controls, Vcl.ExtCtrls,
  Data.Bind.Components;

type
  [ObservableMember('NomFichierImage')]
  TFileImage = class(TImage)
  private
    { Déclarations privées }
    FNomFichierImage : TFileName;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
  protected
    { Déclarations protégées }
    procedure DrawImage(Value : TFileName);
    function CanObserve(const ID: Integer): Boolean; override;
    procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override;
  public
    { Déclarations publiques }
  published
    { Déclarations publiées }
    property NomFichierImage : TFileName read FNomFichierImage write DrawImage;
  end;


implementation


{ TFileImage }

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

procedure TFileImage.DrawImage(Value: TFileName);
var FullPathName : TFileName;
begin
if not SameText(FNomFichierImage,Value) then
  begin
  FNomFichierImage:=Value;
  if FileExists(FullPathName)
    then Picture.LoadFromFile(Value)
    else Picture:=nil;
  TLinkObservers.ControlChanged(Self);
 end;
end;

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

procedure TFileImage.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;

end.

Mon groupe de projets va être constitué ainsi :

Image non disponible

Le fichier FileImageFMX.pas se réduira à quelques lignes :

FileImageFMX
Sélectionnez
unit ImageFileFMX;
{$DEFINE FMX}
{$I ImageFile.pas}

Je vais ensuite modifier le fichier FileImage.pas de façon à prendre en compte ma définition.

Première ligne : il faut effacer le nom de l’unité s’il s’agit du composant FMX.

Dans les uses, il s’agit de préciser les unités à inclure.

Les dernières modifications concernent la procédure DrawImage.

 
Sélectionnez
{$IFNDEF FMX} unit ImageFile; {$ENDIF}

interface

uses
  System.SysUtils, System.Classes, System.IOUtils,
  {$IFDEF FMX}
    FMX.Types, FMX.Controls, FMX.Objects,
  {$ELSE}
    Vcl.Controls, Vcl.ExtCtrls,
  {$ENDIF}
  Data.Bind.Components;

type
  [ObservableMember('NomFichierImage')]
  TFileImage = class(TImage)
  private
    { Déclarations privées }
    FNomFichierImage : TFileName;
    procedure ObserverToggle(const AObserver: IObserver; const Value: Boolean);
  protected
    { Déclarations protégées }
    procedure DrawImage(Value : TFileName);
    function CanObserve(const ID: Integer): Boolean; override;
    procedure ObserverAdded(const ID: Integer; const Observer: IObserver); override;
  public
    { Déclarations publiques }
  published
    { Déclarations publiées }
    property NomFichierImage : TFileName read FNomFichierImage write DrawImage;
  end;


implementation


{ TFileImage }

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

procedure TFileImage.DrawImage(Value: TFileName);
var FullPathName : TFileName;
begin
{$IFDEF FMX}
if FNomFichierImage<>Value then
 begin
  {$IFDEF ANDROID}
    FullPathName:=System.IOUtils.TPath.Combine(System.IOUtils.TPath.GetDocumentsPath,Value);
  {$ELSE}
    FullPathName:=Value;
  {$ENDIF}
{$ELSE}
if not SameText(FNomFichierImage,Value) then
  begin
{$ENDIF}
  FNomFichierImage:=Value;
  if FileExists(FullPathName)
    then {$IFDEF FMX}Bitmap.LoadFromFile(FullPathName)
         {$ELSE}     Picture.LoadFromFile(Value) {$ENDIF}
    else {$IFDEF FMX}Bitmap:=nil;
         {$ELSE}     Picture:=nil; {$ENDIF}
  TLinkObservers.ControlChanged(Self);
 end;
end;

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

procedure TFileImage.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;

La partie convertisseur reste peut-être celle qui est la plus délicate, mais pas plus que la procédure DrawImage. Il suffit de faire attention aux directives de compilation à indiquer.

 
Sélectionnez
const Convertisseur : String {$IFDEF FMX}= 'NomFichierImageToStringFMX';
                             {$ELSE}= 'NomFichierImageToString';
                             {$ENDIF}
      CetteUnite : String = 'ImageFile';
      Description : String = 'Affiche une image à partir d''un nom de fichier';

procedure RegisterConverter;
procedure UnRegisterConverter;


procedure RegisterConverter;
begin
if not TValueRefConverterFactory.HasConverter(convertisseur)
 then begin
  TValueRefConverterFactory.RegisterConversion(TypeInfo(String),
    {$IFDEF FMX} TypeInfo(TBitmap){$ELSE} TypeInfo(TPicture){$ENDIF},
    TConverterDescription.Create(
      procedure(const InValue: TValue; var OutValue: TValue)
      var LOutObject : TObject;
      begin
          LOutObject := OutValue.AsObject;
          Assert(LOutObject <> nil);
          {$IFDEF FMX}
            Assert(LOutObject is TBitmap);
            try
              TBitmap(LOutObject).LoadFromFile(InValue.ToString)
            except
              TBitmap(LOutObject).Clear(0);
             end;
          {$ELSE}
            Assert(LOutObject is TPicture);
            try
              TPicture(LOutObject).LoadFromFile(InValue.ToString)
            except
              TPicture(LOutObject).Graphic := nil;
            end;
          {$ENDIF}
      end,
      Convertisseur,
      Convertisseur,
      CetteUnite,
      True,
      Description,
{      nil      // pas d'indication de framework}
    {$IFDEF FMX} FMX.Controls.TControl{$ELSE} VCL.Controls.TControl{$ENDIF}
      )
    );
 end;
end;

Une autre solution aurait été d’écrire deux procédures d’enregistrement différentes.

Alternative
Sélectionnez
const ConvertisseurFMX : String = 'NomFichierImageToStringFMX';
      ConvertisseurVCL : String= 'NomFichierImageToString';
      CetteUnite : String = 'ImageFile';
      Description : String = 'Affiche une image à partir d''un nom de fichier';

procedure RegisterConverterVCL;
procedure RegisterConverterFMX ;
procedure UnRegisterConverter;

Reste à enregistrer les composants. Comme l’indique la structure de mon groupe de projet, j’ai préféré créer deux unités distinctes.

Pour la VCL, j’obtiens :

RegImageFileVCL
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.
unit RegImageFileVCL;

interface
uses System.Classes, VCL.Controls,
     Data.Bind.Components,
     ImageFile;

procedure Register;

implementation

procedure Register;
begin
  GroupDescendentsWith(TFileImage, Vcl.Controls.TControl);
  RegisterComponents('Tutoriels', [TFileImage]);
end;

initialization
 Data.Bind.Components.RegisterObservableMember(TArray<TClass>.Create(TFileImage), 'NomFichierImage', 'DFM');

finalization
 Data.Bind.Components.UnregisterObservableMember(TArray<TClass>.Create(TFileImage));
end.

Pour FMX, je propose :

RegImageFileFMX
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.
unit RegImageFileFMX;

interface

procedure Register;

implementation

uses System.Classes, Data.Bind.Components,
     FMX.Controls,
     ImageFileFMX;

procedure Register;
begin
  GroupDescendentsWith(TFileImage, Fmx.Controls.TControl);
  RegisterComponents('Tutoriels', [TFileImage]);
end;

initialization
Data.Bind.Components.RegisterObservableMember(TArray<TClass>.Create(TFileImage), 'NomFichierImage', 'FMX');

finalization
 Data.Bind.Components.UnregisterObservableMember(TArray<TClass>.Create(TFileImage));
end.

Remarquez bien la première ligne de la procédure Register : la fonction GroupDescendentsWith est indispensable pour distinguer les frameworks !

Une fois les deux packages installés, l’objectif est atteint !

Image non disponible

Vous retrouverez les sources ainsi que les programme tests de ce chapitre en téléchargement Image non disponibleici.

Même si cela fonctionne, deux points restent obscurs :

- un projet FMX devra contenir, dans la liste des unités utilisées (uses), les deux unités ImageFile et ImageFileFMX (cela se fait automatiquement) dans l’ordre, sous peine d’obtenir une erreur de compilation ;

[dcc32 Erreur fatale] UTestImageFMX.pas(11): F2613 Unité 'ImageFile' non trouvée.

- la compilation d’un projet FMX soulèvera quelques erreurs d’avertissement que des heures passées à rechercher n’ont pas résolues.

[dcc32 Conseil] H2161 Warning: Duplicate resource: Type 12 (CURSOR GROUP), ID 32763; File embarcadero\studio\20.0\lib\Win32\release\controls.res resource kept; file embarcadero\studio\20.0\lib\Win32\release\FMX.Controls.Win.res resource discarded.

IV. Conclusion

À l’issue de ce tutoriel, nous savons comment créer un composant qui peut être lié par l’intermédiaire des LiveBindings. Cette première partie nous a permis de nous familiariser avec les méthodes supplémentaires (ObserverToggle, ObserverAdded, CanObserve) nécessaires au fonctionnement du concepteur visuel de liaisons de l’EDI.

En guise d’exercice, je peux vous suggérer une amélioration possible pour le composant FMX. Le composant TImage du framework FMX offre la possibilité de charger différentes images dans sa propriété MultiresBitmap. Il doit donc être possible d’utiliser cette collection d’images pour afficher une image par défaut.

Comme piste de travail, je vous propose le code suivant :

Image par défaut
Sélectionnez
uses FMX.MultiresBitmap, FMX.PlatForm;

procedure TForm22.Button1Click(Sender: TObject);
var
  ScreenSvc: IFMXScreenService;
  SCale: Single;
begin
  if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService,
    IInterface(ScreenSvc)) then
  begin
   Scale:=ScreenSvc.GetScreenScale;
  end;
  try
    Image1.Bitmap := Image1.MultiresBitmap.ItemByScale(Scale, false, true).Bitmap;
  finally
  end;
end;

Le but de l’exercice est d’intégrer ce code au composant. À vous de jouer !

D’autres améliorations sont possibles comme la faculté de charger l’image via FTP, la possibilité d’indiquer comment négocier avec les répertoires des images, etc.

À vos claviers !

Je remercie les bêta-testeurs de mon composant pprem, nabil74, nicolas.bbs et, bien-sûr, l’équipe des correcteurs techniques gvasseur58 et orthographiques escartefigue de cet article (ma maison d’édition en quelque sorte).

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


Pour le faire, il faut créer un package ou ajouter nos sources à un package déjà existant.

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2017 Serge Girard. Aucune reproduction, même partielle, ne peut être faite de ce site 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.