I. Présentation▲
Je vais commencer en me cantonnant au strict nécessaire. Pour mes tests je n’ai besoin que d’une seule forme contenant bien évidemment un TTabControl et deux boutons qui vont chacun ajouter une page (et donc un onglet) à ce conteneur.
Ajouter un onglet se fait en une seule instruction ATab:=TabControl1.Add();
Une utilisation simple de cette instruction nous permet bien de créer des onglets.
procedure
TForm1.addTabClick(Sender: TObject);
var
ATab : TTabItem; // un onglet
ForeNumber : Integer
; // prochain numéro
begin
ForeNumber:=TabControl1.TabCount; // nombre d’onglets déjà présents
Atab:=TabControl1.Add(); // création de l’onglet
ATab.Text:='tabitem '
+ForeNumber.ToString; // libellé de l’onglet
TabControl1.ActiveTab:=ATab; // en faire l’onglet actif
end
;
J’obtiens alors ceci (après trois appels à cette procédure) :
L’objectif est d’ajouter un bouton à ces onglets afin de pouvoir les détruire.
Réflexe de programmeur Delphi utilisant la VCL : est-il possible d’atteindre cet objectif via les divers événements disponibles ? Certainement, mais les défis sont nombreux et qui dit défis dit nombreuses lignes de code pour plusieurs événements.
J’avoue avoir fait plusieurs tentatives dans ce sens en m’inspirant, entre autres, de ce qui pouvait se faire en VCL (de nombreuses solutions se trouvent sur internet).
Toutes mes tentatives sans faire appel aux styles se sont soldées par des pseudo-échecs : trop de code, trop de calculs de position, trop de cas particuliers, trop d’événements, etc.
II. Créer un style personnalisé▲
Une particularité du framework FMX tient au fait que les éléments graphiques d’une forme, s’ils peuvent être créés spécifiquement pour un OS particulier via API(1), peuvent être et sont généralement créés à partir de ressources connues sous le terme générique de style.
Même si vous ne posez aucun composant TStyleBook sur votre forme, un style par défaut est utilisé.
Cerise sur le gâteau, ces styles, comme un composant, peuvent être reproduits ou dérivés. C’est cette technique que je vais utiliser en créant un style personnalisé.
II-A. Mode opératoire▲
Tout d’abord, en mode conception, il faut ajouter au moins un onglet qui pourra être détruit plus tard. Cela peut être fait en utilisant le menu contextuel du composant TTabControl (clic droit, « Ajouter TTabItem »).
Une fois l’onglet créé, un nouveau clic droit sur ce dernier affiche un nouveau menu contextuel dans lequel je choisis l’option « Modifier un style personnalisé... », ce qui ouvre la fenêtre de conception de style.
Première action incontournable, changer le nom de style proposé par défaut : TabItem1Style1 est certainement moins explicite que, pour mon exemple, CloseTabItem.
L’élément bottom de la structure ne nous servant à rien, il est supprimé.
Garder l’élément bottom obligerait à ajouter un nouveau bouton, créant ainsi une redondance qu’il serait nécessaire de gérer.
Seconde étape, il faut glisser et déposer dans la structure un TButton de la palette. Par convention, ce bouton sera aligné à droite. Là encore, renseigner la propriété StyleName sera un plus non négligeable si, par la suite, je veux changer de glyphe en jouant sur les propriétés images et imageindex. Par défaut, j’ai décidé de fournir ce glyphe via la propriété StyleLookup comme étant un stoptoolbuttonbordered.
Une fois ces actions effectuées, j’applique les modifications et ferme le concepteur de style.
II-B. Appliquer le style▲
De retour au design de la forme, on remarque tout de suite qu’un composant TStyleBook a été créé. Le simple fait de changer la propriété StyleLookup du TTabItem, renseignée par défaut en TabItem1Style1, par le nom du style que je viens de créer (CloseTabItem), fait apparaître le bouton ajouté.
II-C. Coder l’événement▲
Néanmoins cela ne suffit pas : pour l’instant le bouton n’est relié à aucun événement et il est évidemment hors de question d’utiliser l’événement OnClick de l’onglet. Par rapport à la création d’un onglet présenté au chapitre I, la première chose à faire est, bien sûr, d’indiquer quel style appliquer en renseignant, comme lors de la conception, la propriété StyleLookup :
ATab.StyleLookup:='CloseTabItem'
;
Ensuite il faut assigner au bouton une procédure quelque peu particulière, un TNotifyEvent :
TButton(ATab.FindStyleResource('btnTabItem'
)).OnClick:=CloseTab;
mais aussi renseigner quel objet il faudra détruire (indication qui sera stockée dans une propriété du bouton créé, le très pratique : TagObject).
TButton(ATab.FindStyleResource('btnTabItem'
)).TagObject:=ATab;
Il n’y a alors plus qu’à coder cette procédure :
procedure
TForm1.CloseTab(sender: TObject);
begin
TTask.Run(
procedure
begin
TThread.Synchronize(nil
,
procedure
begin
TButton(Sender).TagObject.DisposeOf;
end
);
end
);
end
;
Pourquoi un thread pour supprimer l’onglet ? Parce que le bouton sera supprimé (instruction DisposeOf) en même temps que l’onglet et que ça, ce n’est pas recommandé !
procedure
TForm1.BtnStyleClick(Sender: TObject);
var
ATab : TTabItem;
ForeNumber : Integer
;
begin
ForeNumber:=TabControl1.TabCount;
ATab:=TabControl1.Add();
ATab.Text:='tabitem '
+ForeNumber.ToString;
Atab.StyleLookup:='CloseTabItem'
;
// le bouton n’apparaît que si l’onglet est positionné
// en haut ou en bas
case
TabControl1.EffectiveTabPosition of
TTabPosition.Bottom,TTabPosition.Top : begin
TButton(ATab.FindStyleResource('btnTabItem'
)).OnClick:=CloseTab;
TButton(ATab.FindStyleResource('btnTabItem'
)).TagObject:=ATab;
end
;
end
;
TabControl1.ActiveTab:=ATab;
end
;
II-D. Piège à éviter▲
Tout fonctionne sur le poste de développement, un poste Windows 10, mais au déploiement vers d’autres systèmes d’exploitation (version de Windows antérieure, Mac, Android), il n’y a plus de bouton, de quoi devenir dingue ! Il ne s’agit pas d’une faute de programmation, mais d’un manque d’information concernant les styles. Depuis la version Seattle, les styles pour les différents systèmes d’exploitation sont, en quelque sorte, stockés dans le même conteneur, une sorte de super TStyleBook, qui contient une collection de styles. C’est là que ça se corse, tout à mon ajout, je n’ai pas fait attention au fait que je travaillais sur la collection Windows 10 Desktop (cela se fait naturellement).
Une autre manipulation aurait pu, elle aussi, me mettre sur la voie : changer de style.
Deux solutions sont envisageables pour pallier cette « erreur » :
- copier les éléments créés dans la collection par défaut et toutes les autres collections nécessaires ;
- supprimer la collection par défaut, puis renommer la collection Windows 10. En fait, quand j’écris « renommer » il ne s’agit pas tant d’écrire « défaut » que d’effacer le nom de la collection.
Même si la première solution est assez simple (quelques copier-coller), il est encore plus facile d’utiliser la seconde.
Utilisez la première méthode si vous devez personnaliser le bouton pour certains OSSystème d'exploitation.
III. Ajouter le bouton à l’exécution▲
Une fois la création d’un style personnalisé résolue, il m’a été facile de distinguer cette autre solution plus longue de quelques lignes au niveau code, mais intervenant moins sur les styles ou, pour être plus exact, sans passer par un style personnalisé.
Maintenant que je sais où ajouter le bouton grâce à un style personnalisé, il faut juste que je puisse accéder au TLayout nommé top :
ATab.FindStyleResource('Top'
)
Une fois cet accès obtenu, créer et ajouter un TButton ne relève plus que de quelques connaissances en création d’un objet à l’exécution et des propriétés qu’il faut renseigner pour ledit objet :
procedure
TForm1.BtnAddButtonClick(Sender: TObject);
var
ATab : TTabItem;
AButton : TButton;
ARectangle : TFMXObject;
ForeNumber : Integer
;
begin
ForeNumber:=TabControl1.TabCount;
ATab:=TabControl1.Add();
ATab.Text:='tabitem '
+ForeNumber.ToString;
// Création d’un bouton
AButton:=TButton.Create(Self
);
// Propriétés à renseigner
AButton.StyleLookup:='stoptoolbuttonbordered'
;
AButton.Align:=TAlignLayout.Right;
AButton.Margins.Top:=2
;
AButton.Margins.Bottom:=2
;
AButton.Width:=TLayout(ATab.FindStyleResource('Top'
).Owner).Height-4
;
AButton.TagObject:=ATab;
AButton.TabStop:=False
;
// Évènement
AButton.OnClick:=CloseTab;
// Ajout du bouton
case
TabControl1.EffectiveTabPosition of
TTabPosition.Bottom : ATab.FindStyleResource('Bottom'
).AddObject(AButton);
TTabPosition.Top : ATab.FindStyleResource('Top'
).AddObject(AButton);
end
;
TabControl1.ActiveTab:=ATab;
end
;
D’aucuns préféreront peut-être cette méthode. Le seul inconvénient que j’y vois est que l’accès au bouton est impossible par la suite : par exemple, il n’est pas possible de changer le glyphe hors de la procédure.
IV. Conclusion▲
Pour ce papier, j’ai voulu rester simple, mais j’aimerais signaler quelques points d’intérêt. Une fois le principe connu, vous découvrirez qu’il est facile d’ajouter d’autres fonctionnalités comme la coloration de l’onglet (en ajoutant un TRectangle) ou l’utilisation de glyphes plus sophistiqués (pensez aux possibilités du MultiresBitmap d’une image). Vous pourriez aussi vouloir inclure vos propres glyphes en créant des éléments de style (TButtonStyleObject) dans votre style personnalisé.
Si je devais tirer une leçon de cet article en quelques lignes, je soulignerais que les styles (personnalisés ou non) ne sont pas à négliger en FMX. Ils forment un outil puissant qu’il faut apprendre à apprivoiser et qui, à lui seul, mériterait de nombreux tutoriels.
Mes remerciements à l’équipe rédactionnelle pour ses retouches techniques (gvasseur58, Alcatîz) et grammaticales (jacques_jean)
Les sources du programme de démonstration, réalisés avec une version Delphi Rio, sont téléchargeables ici