Techniques d’enchaînement de formes

SDI, MDI, Docking

Serge Girard

Dès lors que l’on écrit des applications de taille conséquente, l’interface utilisateur se compose de nombreux écrans différents s’enchaînant ou non.

Trois techniques sont utilisées :

- le SDI qui affiche les différents écrans, chacun disséminés sur l’écran ;

- le MDI permet de positionner tous les écrans à l’intérieur de la fenêtre principale ;

- le Docking permet, en quelque sorte, d’allier les deux précédentes en stockant les fenêtres dans des zones (pages à onglets TTabControl, TPageControl ou panneau TPanel) de la fenêtre principale tout en laissant la possibilité de détacher les formes ainsi stockées.

Les utilisateurs n’apprécient pas outre mesure le SDI, les formes souvent modales bloquant leur navigation.

Le MDI tout en étant plus souple a été classé, par Microsoft, comme technique obsolète. Pourquoi ? Parce que les navigateurs internet proposent des onglets, et que cet interface est maintenant devenu une habitude pour les utilisateurs.

Que proposer alors ? C’est là que la technique de Docking des formes entre en jeu.

Malheureusement cette technique, peu documentée pour Delphi, reste relativement confidentielle. Plus malheureusement encore, le framework FMX ne propose même pas de technique équivalente.

L’objectif de cet article est de vous fournir les informations nécessaires.

Commentez

- Titre : Techniques d’enchaînement de formes

- Auteur : Serge Girard

- Parution : 21 décembre 2018

- Mise à jour : 25 décembre 2018

- Public visé :

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

Sommaire

- I. Framework VCL

- I-A. Utilisation de la technique SDI

- I-A-1. Inconvénients du SDI

- I-B. Utilisation de la technique MDI

- I-B-1. Marche à suivre

- I-B-2. Modifications sur la fiche principale

- I-B-3. Modifications sur la fiche enfant

- I-B-4. Avantages MDI

- I-B-5. Inconvénients

- II. Utilisation du Docking

- II-A. Première approche

- II-B. Utilisation d’un TPanel ou d’un TPageControl comme zone d’ancrage

- II-C. Drag and drop

- II-C-1. Une petite vidéo pour voir le comportement

- II-D. Ce qu’il manque

- III. Le framework FMX

- III-A. Design du nouveau projet

- III-A-1. Fiche principale

- III-A-2. Forme couleur

- III-A-3. Le code de la fenêtre principale

- III-A-4. Code de la fenêtre enfant

- III-A-5. Démonstration vidéo

- III-B. Glisser-Ancrer dans le TTabControl

- III-B-1. Bonus : utilisation du DragDrop

- III-B-2. Résultat final

- III-B-3. Dernière correction

- III-C. Conclusion

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.

SDI

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 SDI, 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.

Design forme principale

(cliquez ici pour aggrandir l'image)

J’ai donc uniquement deux évènements à programmer.

Source Forme Principale

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.

Source Forme Enfant

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.

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://serge-girard.developpez.com/tutoriels/Delphi/Docking/fichiers/Video_SDIdemo.webm

I-B - Utilisation de la technique MDI

Qu’apporte l’utilisation du MDI ? 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.

Il ne faudra pas oublier d’indiquer que le menu doit être fusionné.

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.

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://serge-girard.developpez.com/tutoriels/Delphi/Docking/fichiers/Video_MDIdemo.webm

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 :

Forme principale

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.

(cliquez ici pour aggrandir l'image)

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 ?

Forme enfant

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.

Forme enfant

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.

Forme enfant

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.

Forme enfant

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.

Forme enfant

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

Forme enfant

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.

Forme principale

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

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://serge-girard.developpez.com/tutoriels/Delphi/Docking/fichiers/Video_DockingDemo.webm

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.

FormCreate

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

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://serge-girard.developpez.com/tutoriels/Delphi/Docking/fichiers/Video_FMXDocking_1.webm

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.

Classe TFormColor

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.

Forme principale

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 ;

DragOver

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

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://serge-girard.developpez.com/tutoriels/Delphi/Docking/fichiers/Video_FMXDockingDragDrop.webm

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 !

Animation en format "webm" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://serge-girard.developpez.com/tutoriels/Delphi/Docking/fichiers/videobug.webm

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.

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

(1)Le docking est même accessible à partir de D4, confirmation faite par defluc peu après la parution de l’article.

(2)Savoir n’est pas exactement le terme, je pourrais utiliser une propriété déjà existante : Floating. Il s’agit plutôt de créer une propriété qui me permettra de détecter le changement d’état et l’appliquer.

(3)Un TText est moins sensible au style qu’un TLabel

(4)Au lieu de TabControl1.PageCount-1 il est possible d’utiliser ATabItem.Index, note de der§en