I. Framework VCL▲
Pour faire la démonstration des différentes techniques, un programme simple : une forme principale qui, via une option de menu, va afficher d’autres fiches elles aussi très simples, et leur titre (Form.Caption) servant à les distinguer, ainsi qu'un changement de couleur de fond (couleur aléatoire).
I-A. Utilisation de la technique SDI▲
La plus classique des approches, chaque fenêtre est créée et affichée avec plus ou moins de bonheur sur l’écran.
Facile à réaliser grâce à une simple instruction Show.
procedure
TFMainForm.NewWindow1Click(Sender: TObject);
begin
TFColorFormNormal.Create(Application).Show;
end
;
Revenons en arrière, comment en suis-je arrivé là ?
J’ai tout d’abord demandé à créer un projet VCL de type SDISingle Document Interface, la première unité, principale, ne contient qu’un menu avec une option me permettant de créer des fenêtres de différentes couleurs et une autre pour quitter le programme.
J’ai donc uniquement deux évènements à programmer.
unit
FormePrincipaleSDI;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.Menus, FormeEnfantSDI;
type
TFMainForm = class
(TForm)
MainMenu1: TMainMenu;
Fermer1: TMenuItem;
Window1: TMenuItem;
NewWindow1: TMenuItem;
procedure
Fermer1Click(Sender: TObject);
procedure
NewWindow1Click(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
FMainForm: TFMainForm;
implementation
{$R *.dfm}
procedure
TFMainForm.Fermer1Click(Sender: TObject);
begin
Close;
end
;
procedure
TFMainForm.NewWindow1Click(Sender: TObject);
begin
TFColorFormNormal.Create(Application).Show;
end
;
end
.
Les formes que je vais afficher, dont une seule que je vais « colorier » de façon aléatoire qui sera également très simple : elle ne contient qu’un bouton pour pouvoir fermer la fenêtre.
Toute la « magie » s’effectue au moment de la création de cette forme, grâce au générateur de nombre aléatoire (random). J’en profite également pour personnaliser le titre de la fenêtre.
unit
FormeEnfantSDI;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TFColorFormNormal = class
(TForm)
btnClose: TButton;
procedure
FormCreate(Sender: TObject);
procedure
btnCloseClick(Sender: TObject);
procedure
FormClose(Sender: TObject; var
Action: TCloseAction);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
FColorFormNormal: TFColorFormNormal;
implementation
{$R *.dfm}
procedure
TFColorFormNormal.btnCloseClick(Sender: TObject);
begin
Close;
end
;
procedure
TFColorFormNormal.FormClose(Sender: TObject; var
Action: TCloseAction);
/// destruction à la fermeture
begin
Action:=caFree;
end
;
procedure
TFColorFormNormal.FormCreate(Sender: TObject);
/// couleur aléatoire
/// titre
begin
Color:=Random($FFFFFF
);
Caption:=Format('Fiche Enfant %.6x'
,[Color]);
end
;
end
.
Seuls points à ne pas oublier :
- la libération de la forme à sa clôture (Action:=cafree ;dans l’évènement OnClose).
Ne pas créer cette forme au démarrage du programme, soit en gérant les options,
soit en modifiant directement le code source du projet.
program
ProjetSDI;
uses
Vcl.Forms,
FormePrincipaleSDI in
'FormePrincipaleSDI.pas'
{FMainForm}
,
FormeEnfantSDI in
'FormeEnfantSDI.pas'
{FColorFormNormal}
;
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True
;
Application.CreateForm(TFMainForm, FmainForm);
// il ne faut pas qu’une forme enfant soit créée au démarrage
// Application.CreateForm(TFColorFormNormal, FColorFormNormal);
Application.Run;
end
.
I-A-1. Inconvénients du SDI▲
Les différentes fiches peuvent se superposer, voire être cachées, l’utilisateur final aura donc des difficultés à naviguer entre ses fiches ouvertes. La petite vidéo suivante montre ce qui peut arriver.
I-B. Utilisation de la technique MDI▲
Qu’apporte l’utilisation du MDIMultiple Document Interface ? Toutes les fiches vont se retrouver à l’intérieur de la forme principale et un menu particulier va me permettre de retrouver mes différentes fiches enfants et même de les organiser.
On a tendance à penser que MDI veut dire une seule et même fiche affichée plusieurs fois et c’est ce que mon exemple ou l’expert proposé pour créer un projet VCL MDI laissent à penser. Pas du tout, vous pouvez très bien ajouter des formes aussi différentes qu’une fiche client, une fiche article ou une fiche commande.
Par document entendez donc bien : une forme quelque soit son design.
I-B-1. Marche à suivre▲
Il est bien sûr possible d’utiliser l’expert Delphi pour créer un projet MDI complet, mais il est également possible de modifier le projet précédent, c’est d’ailleurs cette approche que je vais prendre pour souligner les différences.
I-B-2. Modifications sur la fiche principale▲
Tout d’abord changer le type de forme la propriété FormStyle, de fsNormal dans mon programme SDI sera changée en fsMDIForm.
Ensuite je modifie le menu pour ajouter les fonctionnalités possibles de gestion des fenêtres enfants telles que : l’organisation en cascade ou en mosaïque, la possibilité de tout réduire en icônes, etc.
Je change également la propriété GroupIndex de l’option « Quitter » du menu de 0 à 99, et ce afin de pouvoir fusionner correctement ce menu avec le menu de la fiche enfant active.
Une ultime modification de propriété de la forme principale (WindowsMenu) permettra de recenser les différentes fenêtres créées dans l’option du menu principal indiquée.
Il me faut, bien sûr, codifier chacune des options du menu.
unit
FormePrincipaleMDI;
interface
uses
Winapi.Windows, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Forms,
Vcl.Controls, Vcl.Menus, Vcl.StdCtrls, Vcl.Dialogs, Vcl.Buttons, Winapi.Messages,
Vcl.ExtCtrls, Vcl.ComCtrls, Vcl.StdActns, Vcl.ActnList, Vcl.ToolWin,
Vcl.ImgList, FormeEnfantMDI;
type
TMainForm = class
(TForm)
MainMenu1: TMainMenu;
Window1: TMenuItem;
WindowCascadeItem: TMenuItem;
WindowTileItem: TMenuItem;
WindowArrangeItem: TMenuItem;
WindowMinimizeItem: TMenuItem;
Quitter1: TMenuItem;
N1: TMenuItem;
NouvelleFentre1: TMenuItem;
N2: TMenuItem;
ToutFermer1: TMenuItem;
procedure
NouvelleFentre1Click(Sender: TObject);
procedure
Quitter1Click(Sender: TObject);
procedure
WindowCascadeItemClick(Sender: TObject);
procedure
WindowTileItemClick(Sender: TObject);
procedure
WindowMinimizeItemClick(Sender: TObject);
procedure
WindowArrangeItemClick(Sender: TObject);
procedure
ToutFermer1Click(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure
TMainForm.NouvelleFentre1Click(Sender: TObject);
begin
TFColorFormMDiChild.Create(Application).Show;
end
;
procedure
TMainForm.ToutFermer1Click(Sender: TObject);
var
i : integer
;
begin
for
i:= MdiChildCount-1
downto
0
do
MdiChildren[i].Close;
end
;
procedure
TMainForm.Quitter1Click(Sender: TObject);
begin
Close;
end
;
procedure
TMainForm.WindowArrangeItemClick(Sender: TObject);
begin
ArrangeIcons;
end
;
procedure
TMainForm.WindowCascadeItemClick(Sender: TObject);
begin
Cascade;
end
;
procedure
TMainForm.WindowMinimizeItemClick(Sender: TObject);
var
i : integer
;
begin
for
i:= MdiChildCount-1
downto
0
do
MdiChildren[i].WindowState:=wsMinimized;
end
;
procedure
TMainForm.WindowTileItemClick(Sender: TObject);
begin
TileMode:=tbHorizontal;
Tile;
end
;
end
.
I-B-3. Modifications sur la fiche enfant▲
Comme pour la fiche principale, il faut changer le type de forme : la propriété FormStyle est à passer à fsMDIChild.
J’ajoute également un menu qui fusionnera avec celui de la fiche principale.
Un peu de code supplémentaire me permettra de changer l’intitulé du menu. L’élément du menu permettant de fermer la forme utilisera le même évènement que le bouton btnClose déjà codifié.
unit
FormeEnfantMDI;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Menus;
type
TFColorFormMDIChild = class
(TForm)
btnClose: TButton;
MainMenu1: TMainMenu;
mnuchild1: TMenuItem;
Fermer1: TMenuItem;
procedure
FormCreate(Sender: TObject);
procedure
btnCloseClick(Sender: TObject);
procedure
FormClose(Sender: TObject; var
Action: TCloseAction);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end
;
var
FColorFormMDIChild: TFColorFormMDIChild;
implementation
{$R *.dfm}
procedure
TFColorFormMDIChild.btnCloseClick(Sender: TObject);
begin
Close; // fermeture de la fiche
end
;
procedure
TFColorFormMDIChild.FormClose(Sender: TObject; var
Action: TCloseAction);
begin
Action:=caFree; // libération de la fiche
end
;
procedure
TFColorFormMDIChild.FormCreate(Sender: TObject);
begin
Color:=Random($FFFFFF
); // couleur aléatoire
mnuChild1.Caption:=Format('Fenêtre %.6x'
,[Color]); // intitulé menu
Caption:=Format('Fiche Enfant %.6x'
,[Color]); // intitulé forme
end
;
end
.
I-B-4. Avantages MDI▲
Sans conteste, le menu qui permet de naviguer entre les différentes formes entreposées dans la forme principale.
Mais aussi les actions standards que nous pouvons appliquer sur toutes les fiches enfants pour réarranger les différentes fenêtres (Cascade, Mosaïque, etc.)
Les possibilités de fusion de menu entre la forme enfant active, elles non plus, ne sont pas négligeables.
Tout cela est démontré dans cette petite vidéo.
I-B-5. Inconvénients▲
Les utilisateurs ne sont pas habitués à ce type de navigation entre fenêtres, c’est d’ailleurs ce qui a poussé Microsoft à déclarer cette technique obsolète.
Autre inconvénient, il est impossible de mettre des fenêtres hors de la fenêtre principale.
II. Utilisation du Docking▲
J’en arrive à cette technique. Elle est devenue possible dès Delphi 5 (1)(confirmé par tourlourou) avec l’apparition des zones dites d’ancrage.
II-A. Première approche▲
Tous les composants qui sont à base de TScrollingWinControl, ce qui, a minima, comprend les composants TForm, TPanel et TPageControl permettent l’ancrage de contrôles (dans le cas qui m’intéresse : de formes) .
Comment les reconnaître ? Ils ont tous une propriété DockSite. C’est cette propriété qui va me permettre de changer l’endroit d’affichage de mes formes.
Je repars de mon programme SDI. Cette fois je vais activer les deux propriétés qui concernent l’opération d’ancrage :
- DockSite qui permet effectivement l’ancrage sensu-stricto ;
- UseDockManager pour indiquer d’utiliser le gestionnaire d’ancrage.
Utiliser le gestionnaire d’ancrage permet de gérer la disposition et le dessin autour des contrôles ancrés.
Il faut donc utiliser ces deux propriétés et, bien sûr, ajouter un peu de code !
procedure
TForm21.AjouterForme1Click(Sender: TObject);
begin
with
TFColorFormDock.Create(Application) do
begin
ManualDock(Self
);
Show;
end
;
end
;
Je me rapproche un peu de ce qui se passe avec la technique MDI : les fenêtres sont stockées dans la forme principale et arrangées automatiquement. Toutefois c’est loin d’être l’idéal !
II-B. Utilisation d’un TPanel ou d’un TPageControl comme zone d’ancrage▲
Cette fois, pour ne pas trop m’étendre je vais, sur le même programme, utiliser les deux composants comme zones d’ancrage. Des options de menu me permettront de créer des fenêtres enfants soit dans l’un de ces deux composants, soit en dehors de la forme principale (comme le programme SDI de départ).
Un peu de code pour articuler tout cela :
procedure
TFormPrincipaleDock.NotDocked1Click(Sender: TObject);
/// fenêtre ‘volante’
begin
with
TFColorFormDock.Create(Application) do
begin
Show;
end
;
end
;
procedure
TFormPrincipaleDock.DockinPagecontrolClick(Sender: TObject);
/// ancrer la fenêtre dans le pagecontrol
begin
with
TFColorFormDock.Create(Application) do
begin
ManualDock(PageControl1);
Show;
// se retrouver sur la dernière page créée
PageControl1.ActivePageIndex:=PageControl1.PageCount-1
;
end
;
end
;
procedure
TFormPrincipaleDock.DockinPanel1Click(Sender: TObject);
/// ancrer la fenêtre dans le panel
begin
with
TFColorFormDock.Create(Application) do
begin
ManualDock(Panel1);
Show;
end
;
end
;
Constatations à l’exécution : le TPanel se comporte comme le TForm en ce qui concerne le positionnement des fenêtres. Le TPageControl, lui, rappelle beaucoup les premiers temps de nos navigateurs internet. La seule chose qui manque serait un bouton dans l’onglet, mais ce n’est pas non plus implémenté de base, dans ce composant.
II-C. Drag and drop ▲
Il ne me reste plus qu’à améliorer ce programme : permettre de détacher une fenêtre de la forme principale ou, a contrario, accepter une fenêtre « libre » à l’intérieur d’une zone de stockage.
En préambule, il faut activer la fonctionnalité en indiquant que je ferai, sur ces deux composants des opérations de glisser-ancrer (dkDock) au lieu du glisser-déposer (dkDrag) proposé par défaut.
Je passe maintenant à la forme enfant et rajoute un bouton qui me permettra d’effectuer l’opération de détachement de la fenêtre principale.
Cette fois la partie principale va se jouer dans le code. Si pour l’opération d’ancrage rien ne sera à coder, le détachement se fera en utilisant la procédure ManualFloat.
Premier problème auquel il faut pallier : l’argument de ManualFloat est un TRect , zone de réception de notre contrôle (fenêtre), qu’indiquer comme positions et taille ?
procedure
TFColorFormDock.BtnUndockClick(Sender: TObject);
begin
Manualfloat(Rect(?,?,?,?));
end
;
Je vais créer plusieurs propriétés (FUnDockedWidth, FUnDockedTop, FUnDockedHeight, FUnDockedLeft) pour mémoriser les dernières positions et taille de la fenêtre afin de pouvoir les restituer en cas de détachement. Et je vais également créer une propriété qui me permettra de savoir(2) si la fenêtre est ancrée ou non (Docked), et elle me permettra de cacher ou non le bouton ajouté en fonction de son état.
TFColorFormDock = class
(TForm)
btnClose: TButton;
BtnUndock: TSpeedButton;
procedure
FormCreate(Sender: TObject);
procedure
btnCloseClick(Sender: TObject);
procedure
FormClose(Sender: TObject; var
Action: TCloseAction);
procedure
BtnUndockClick(Sender: TObject);
procedure
FormResize(Sender: TObject);
private
{ Déclarations privées }
FDocked: boolean
;
FUnDockedWidth: Integer
;
FUnDockedTop: Integer
;
FUnDockedHeight: Integer
;
FUnDockedLeft: Integer
;
procedure
SetDocked(const
Value: boolean
);
public
{ Déclarations publiques }
property
docked : boolean
read
FDocked write
SetDocked;
property
TopDock : Integer
read
FUnDockedTop;
property
LeftDock : Integer
read
FUnDockedLeft;
property
DockWidth : Integer
read
FUnDockedWidth;
property
DockHeight : Integer
read
FUnDockedHeight;
procedure
BeginDrag(var
msg : TMessage); message
WM_NCLBUTTONDOWN;
end
;
Reste à obtenir les valeurs ! Pour la position j’ai besoin de capturer le message WM_NCLBUTTONDOWN, détectant un clic sur la barre de titre c’est le signal qui indique qu’une opération de glisser-ancrer débute et donc, la nécessité de mémoriser la position du coin en haut et à gauche de la fenêtre.
procedure
TFColorFormDock.BeginDrag(var
msg: TMessage);
begin
inherited
;
FUnDockedLeft:=Left;
FUnDockedTop:= Top;
FUnDockedWidth:=Width;
FUnDockedHeight:=Height;
end
;
J’en profite également pour mémoriser largeur et hauteur de la fenêtre.
Le fait d’avoir créé une propriété Docked me permet de détecter son changement d’état et de rendre le bouton visible ou non.
procedure
TFColorFormDock.SetDocked(const
Value: boolean
);
begin
FDocked := Value;
BtnUndock.Visible:=Value;
end
;
Fort de ces données je suis alors capable de fournir les informations nécessaires à la procédure ManualFloat.
procedure
TFColorFormDock.BtnUndockClick(Sender: TObject);
begin
Docked:=False
;
Manualfloat(Rect(FUnDockedLeft,FUnDockedTop,FUnDockedLeft+FUnDockedWidth,FUnDockedTop+FUnDockedHeight));
end
;
Il ne reste plus qu’à initialiser les différentes propriétés à la création de la fenêtre
procedure
TFColorFormDock.FormCreate(Sender: TObject);
begin
Color:=Random($FFFFFF
);
Caption:=Format('Fiche Enfant %.6x'
,[Color]);
FUnDockedLeft:=Left;
FUnDockedTop:=Top;
FUnDockedHeight:=Height;
FUnDockedWidth:=Width;
BtnUndock.Visible:=FDocked;
end
;
et à modifier légèrement le code source de la forme principale, de façon à initialiser la propriété Docked à l’ordre de création de la fenêtre.
procedure
TFormPrincipaleDock.NotDocked1Click(Sender: TObject);
begin
with
TFColorFormDock.Create(Application) do
begin
Docked:=False
;
Show;
end
;
end
;
procedure
TFormPrincipaleDock.Panel1DockDrop(Sender: TObject;
Source: TDragDockObject; X, Y: Integer
);
/// Si une forme couleur est ancrée, modifier la propriété docked
begin
if
Source.Control.ClassNameIs('TFColorFormDock'
) then
TFColorFormDock(Source.Control).docked:=True
;
end
;
procedure
TFormPrincipaleDock.DockinPagecontrolClick(Sender: TObject);
begin
with
TFColorFormDock.Create(Application) do
begin
Docked:=True
;
ManualDock(PageControl1);
Show;
PageControl1.ActivePageIndex:=PageControl1.PageCount-1
;
end
;
end
;
procedure
TFormPrincipaleDock.DockinPanel1Click(Sender: TObject);
begin
with
TFColorFormDock.Create(Application) do
begin
Docked:=True
;
ManualDock(Panel1);
Show;
end
;
II-C-1. Une petite vidéo pour voir le comportement▲
II-D. Ce qu’il manque▲
Cela n’entre pas dans le cadre de l’objectif visé par ce tutoriel, mais des boutons sur les onglets du TPageControl seraient un plus appréciés des utilisateurs. De même, les fonctionnalités trouvées avec la technique MDI, je pense en particulier à la liste des fenêtres et à la fusion des menus, seraient, peut-être, de bons suppléments.
III. Le framework FMX▲
Mauvaise surprise, l’ancrage (la propriété DockSite) n’existe pas dans ce framework ! Tout ce que j’ai exposé serait à jeter aux orties ? Heureusement, non. Par contre, ce ne sera bien évidemment pas fait de la même manière.
III-A. Design du nouveau projet▲
Bien sûr, le design va être différent de celui d’une application VCL. Même si la cible visée en dernier lieu sera nécessairement une application de bureau à cause des fonctionnalités de glisser-déposer que je présenterai au chapitre III.B, construire une application mobile avec plusieurs formes différentes, stockées dans un ensemble d’onglets, est tout à fait envisageable (voire conseillée).
III-A-1. Fiche principale▲
Je remplace le menu par une barre d’outils. L’ensemble d’onglets VCL.TPageControl est remplacé par un FMX.TTabControl.
Pas de grands changements donc.
III-A-2. Forme couleur▲
La forme enfant va poser quelques petits soucis.
En premier lieu une forme FMX n’a pas de couleur de fond stricto sensu, c’est la feuille de style et sa propriété Background qui fourni la couleur. Pour pallier cet inconvénient je rajoute un TRectangle qui occupera toute la fenêtre. Un bouton permettant de fermer la fenêtre ainsi qu’un texte (TText mais ce pourrait être un TLabel(3)) qui contiendra la valeur de la couleur compléteront l’ensemble.
Vous remarquerez que j’ai inclus ce rectangle à l’intérieur d’un TLayout. Une habitude prise afin de regrouper tous les composants de la fiche. Dans ce cas particulier, ce TLayout n’est pas nécessaire le rectangle contenant déjà tous les composants.
En ce qui concerne le design, l’affaire est réglée, il faut maintenant codifier.
III-A-3. Le code de la fenêtre principale▲
Je vais y introduire un dictionnaire (Liste : TDictionary<String
,TForm>;) qui va me permettre de mémoriser les formes enfants créées.
Nécessite la déclaration de l’unité System.Generics.Collections dans la liste des unités utilisées
Serait-ce les prémices d’un palliatif au WindowsMenu d’une forme MDI ?
Oui, en partie, mais non, ce n’est pas l’objectif principal qui est de pouvoir gérer la fermeture de la fenêtre enfant. Il faudra donc que cette liste soit publique.
La création d’une nouvelle forme enfant se fait lors de l’utilisation du bouton.
procedure
TFormPrincipale.btnnouveauClick(Sender: TObject);
var
F : TFormColor;
ATabItem : TTabItem;
begin
F:=TFormColor.Create(Self
);
ATabItem:=TabControl1.Add();
ATabItem.Text:=F.Caption;
// Ancrage
ATabItem.AddObject(F.ChildLayout);
// ajout à la liste
Liste.Add(Format('Tab%d%8x'
,[TabControl1.TabCount-1
,F.RectColor]),F);
// onglet actif=nouvelle fenêtre
TabControl1.TabIndex:=TabControl1.TabCount-1
;
end
;
Deux instructions sont à dégager du lot :
ATabItem.AddObject(F.ChildLayout);ce qui est un équivalent de l’instruction ManualDock présentée pour le programme VCL ;
Liste.Add(Format('Tab%d%8x'
,[TabControl1.TabCount-1
,F.RectColor]),F);(4).
c’est plus la clé du dictionnaire qu’autre chose qu’il y a à expliquer. Il me fallait une clé unique, je l’ai donc composée en concaténant l’index de l’onglet créé et la couleur du rectangle (mémorisé dans une propriété de la forme enfant).
III-A-4. Code de la fenêtre enfant▲
Comme indiqué à la fin du chapitre précédent, j’ajoute une propriété couleur qui me sera nécessaire en lecture.
public
property
RectColor : TAlphaColor read
FColor;
Tout d’abord, pour le code de création de la fenêtre, peu de différence par rapport à celui d’une même fenêtre VCL.
procedure
TFormColor.FormCreate(Sender: TObject);
begin
FColor:=TAlphaColors.Alpha OR
Cardinal
(Random($FFFFFF
));
Caption:=Format('Couleur %8x'
,[FColor]);
Text1.Text:=Format('Couleur : %8x'
,[FColor]);
Rectangle1.Fill.Color:=FColor;
end
;
Notez quand même le « calcul » de la couleur, une fusion entre la couche alpha ($FF000000) et un nombre aléatoire.
C’est plus le code de clôture sur lequel il faut se pencher. En effet, avant de détruire la fenêtre, il y a deux actions à accomplir dans la fenêtre principale :
- le changement d’onglet ;
- la destruction de l’entrée dans le dictionnaire.
procedure
TFormColor.FormClose(Sender: TObject; var
Action: TCloseAction);
var
ti : integer
; // onglet en cours
begin
with
FormPrincipale do
begin
Ti:=TabControl1.TabIndex;
TabControl1.Next(); // changement d’onglet avec transition
Liste.Remove(Format('Tab%d%8x'
,[ti,FColor])); //suppression dans le dictionnaire
TabControl1.Delete(ti); // suppression de l’onglet
end
;
Action:=TCloseAction.caFree;
end
;
III-A-5. Démonstration vidéo ▲
III-B. Glisser-Ancrer dans le TTabControl▲
Il est temps d’ajouter quelques finitions pour un programme à destination des postes de bureau ou assimilés. L’objectif : pouvoir détacher une fenêtre et la réancrer au besoin.
Avant de se lancer dans la codification, j’effectue un premier ajustement au niveau du design de la forme enfant et j’ajoute un bouton, visible uniquement si la fenêtre est ancrée, qui déclenchera le détachement de la fenêtre.
Il va falloir que je gère l’affichage de ce bouton visible si la fenêtre est ancrée, dans le cas inverse je changerai l’icône ou même, si l’opération de glisser-déposer est possible, je le rendrai invisible.
Pour faire cette gestion, j’utiliserai, encore une fois, une propriété que j’ai nommée Docked.
type
TFormColor = class
(TForm)
Fermer: TButton;
btnDetache: TSpeedButton;
ChildLayout: TLayout;
Text1: TText;
Rectangle1: TRectangle;
procedure
FermerClick(Sender: TObject);
procedure
FormClose(Sender: TObject; var
Action: TCloseAction);
procedure
btnDetacheClick(Sender: TObject);
procedure
FormCreate(Sender: TObject);
procedure
FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single
);
private
FColor: TAlphaColor;
FDocked: Boolean
;
procedure
Dockme(const
Value: Boolean
);
{ Déclarations privées }
public
{ Déclarations publiques }
property
RectColor : TAlphaColor read
FColor;
property
Docked : Boolean
read
FDocked write
Dockme;
end
;
var
FormColor: TFormColor;
implementation
…
procedure
TFormColor.Dockme(const
Value: Boolean
);
begin
FDocked := Value;
if
value then
btnDetache.StyleLookup:='pagecurltoolbutton'
else
btnDetache.StyleLookup:='rewindtoolbutton'
;
end
;
Le fait d’utiliser une propriété en écriture me permet de jouer sur l’affichage du bouton en cas de changement de valeur.
Cette propriété est bienvenue, car le bouton ayant maintenant deux fonctions (ancrer ou détacher) elle me permet de savoir quelle action doit être effectuée.
procedure
TFormColor.btnDetacheClick(Sender: TObject);
var
ti : Integer
;
begin
if
Docked then
begin
with
FormPrincipale do
begin
ti:=TabControl1.TabIndex;
ChildLayout.Parent:=Self
;
Liste.Remove(Format('Tab%d%8x'
,[ti,FColor])); //
TabControl1.Next();
TabControl1.Delete(ti);
end
;
Docked:=False
;
end
else
begin
FormPrincipale.AddPage(Self
);
Docked:=True
;
end
;
end
;
Pour faciliter la codification, j’ai créé une procédure AddPage dans l’unité de la forme principale. Cette procédure contient la majeure partie du code lors de l’utilisation du bouton d’ajout d’une nouvelle fenêtre.
procedure
TFormPrincipale.AddPage(AForm : TForm);
var
ATabItem : TTabItem;
F : TFormColor;
begin
F:=TFormColor(AForm);
F.Docked:=True
;
ATabItem:=TabControl1.Add();
ATabItem.Text:=F.Caption;
ATabItem.AddObject(F.ChildLayout);
// ajout à la liste
Liste.Add(Format('Tab%d%8x'
,[TabControl1.TabCount-1
,F.RectColor]),F);
// onglet actif=nouvelle fenêtre
TabControl1.TabIndex:=TabControl1.TabCount-1
;
end
;
procedure
TFormPrincipale.btnnouveauClick(Sender: TObject);
var
F : TFormColor;
ATabItem : TTabItem;
begin
F:=TFormColor.Create(Self
);
AddPage(F);
end
;
III-B-1. Bonus : utilisation du DragDrop▲
Le tour de l'application ne serait pas complet si, comme pour la présentation du Docking en VCL, il n’était pas possible de glisser-déposer une fenêtre détachée directement dans la fenêtre principale (ou plutôt la page d’onglet de celle-ci).
Le dragdrop avec le framework FMX à évolué entre les différentes versions, celle que je présente ici correspond aux versions 10.2
Première étape : détecter quand l’opération va débuter et déclencher le service.
Contrairement à ce qui se passe en VCL où l’évènement déclencheur était une pression continue du bouton de la souris dans la barre de titre de la fenêtre, en FMX rien n’est directement proposé.
Je ne suis pas sûr non plus que l’interception des messages de fenêtre comme j’ai pu le faire chapitre II.C (WM_NCLBUTTONDOWN) soit portable sur un système d’exploitation autre que Windows. Je vais donc utiliser l’évènement OnMouseDown de la forme enfant.
procedure
TFormColor.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single
);
var
ABitmap : TBitmap;
DDService: IFMXDragDropService;
D: TDragObject;
begin
if
Docked then
Exit;
ABitmap:=TBitmap.Create;
try
ABitmap.SetSize(40
,40
);
ABitmap.Clear(FColor);
D.Source:=Self
;
if
TPlatformServices.Current.SupportsPlatformService(IFMXDragDropService,
IInterface(DDService))
then
DDService.BeginDragDrop(FormPrincipale, D, FMX.Graphics.TBitmap(ABitmap));
finally
ABitmap.Free;
end
;
end
;
Pourquoi il y a création d’une image ? Troisième paramètre de l’instruction BeginDragDrop, cette image permet de visualiser le déplacement.
Qu’est-ce que cette histoire de TPlatformServices ? Cela demande des explications qui débordent largement du cadre du tutoriel. Je préfère vous renvoyer à la documentation Embarcadero DocWiki.
Une fois le service déclenché, il faut bien sûr une cible, le TTabControl de la forme principale.
Deux évènements particuliers sont à déclarer :
- DragOver qui permet d’indiquer si le contrôle accepte ou non l’objet qui glisse dessus ;
procedure
TFormPrincipale.TabControl1DragOver(Sender: TObject;
const
Data: TDragObject; const
Point: TPointF; var
Operation: TDragOperation);
begin
if
(Data.Source<>Nil
) AND
data.Source.ClassNameIs('TFormColor'
)
then
Operation:=TDragOperation.Move
else
Operation:=TDragOperation.None;
end
;
- DragDrop qui va procéder à l’ancrage de l’objet dans le contrôle.
procedure
TFormPrincipale.TabControl1DragDrop(Sender: TObject;
const
Data: TDragObject; const
Point: TPointF);
begin
if
(Data.Source<>Nil
) AND
data.Source.ClassNameIs('TFormColor'
)
then
AddPage(TFormColor(Data.Source));
end
;
III-B-2. Résultat final▲
Attention, un bogue de la version Tokyo 10.2.3, empêche l’opération de glisser-déposer. Cette vidéo a été faite à partir d’un programme compilé avec la version Rio 10.3.
III-B-3. Dernière correction▲
Un test approfondi de ce dernier programme laisse encore apparaître un bogue. Lorsqu’une forme enfant est ancrée, l’utilisation du bouton de fermeture de ladite fenêtre semble désactiver les boutons de la barre de titre de la fenêtre principale !
La raison ? L’action de clôture détruit l’objet qui envoie l’évènement, ce qu’il ne faut bien évidemment pas faire.
La solution : exécuter l’action dans un thread.
procedure
TFormColor.FormClose(Sender: TObject; var
Action: TCloseAction);
var
ti : integer
;
begin
if
FDocked then
with
FormPrincipale do
begin
Ti:=TabControl1.TabIndex;
TabControl1.Next;
Liste.Remove(Format('Tab%d%8x'
,[ti,FColor])); //
TTask.Run(
procedure
begin
TThread.Synchronize(nil
,
procedure
begin
TabControl1.Delete(ti);
end
);
end
);
end
;
Action:=TCloseAction.caFree;
end
;
Vous devrez déclarer l’utilisation de la bibliothèque System.Threading.
III-C. Conclusion▲
Bien évidemment, je voulais surtout mettre en avant les techniques d’ancrage de formes, aussi bien pour les frameworks VCL que FMX. Le tour d’horizon effectué vous permettra certainement de mieux vous familiariser avec l’enchaînement de formes et peut-être améliorer vos interfaces utilisateurs.
Un avantage à l’utilisation de plusieurs formes que l’on peut ancrer et qui ne ressort pas dans ce tutoriel, vous le trouverez lorsque vous utiliserez des formes plus complexes que celles utilisées ici et utilisant les LiveBindings. Au lieu de mettre vos différents écrans directement dans des pages à onglet, ce qui complexifie la codification, chaque écran est bien séparé, rendant par la suite la maintenance beaucoup plus aisée.
Rendons à César, en l’occurence Alister Christie, ce qui lui appartient, l’idée des formes remplies d’une couleur aléatoire pour démontrer le docking est totalement inspirée des ses deux vidéos trouvées à cette adresse.
Tous mes remerciements à mon « Candide » Nabil qui a été l’initiateur, mais a aussi essuyé les plâtres de cet article, les correcteurs techniques Grégory et les correcteurs orthographiques et grammaticaux Jacques_Jean de l’équipe de rédaction.
Les différents programmes sources se trouvent ici.