Techniques d’enchaînement de formes

SDI, MDI, Docking

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 SDISingle Data Interface qui affiche les différents écrans, chacun disséminés sur l’écran ;
  • le MDIMultiple Data Interface permet de positionner tous les écrans à l’intérieur de la fenêtre principale ;
  • le DockingStockage de formulaires 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.

14 commentaires Donner une note  l'article (5)

Article lu   fois.

L'auteur

Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

Image non disponible

Facile à réaliser grâce à une simple instruction Show.

SDI
Sélectionnez
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.

Image non disponible
Design forme principale

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

Source Forme Principale
Sélectionnez
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.

Image non disponible

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
Sélectionnez
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.

 
Sélectionnez
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.

Image non disponible

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.

Image non disponible

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.

Image non disponible

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.

Image non disponible

Il me faut, bien sûr, codifier chacune des options du menu.

 
Sélectionnez
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.

Image non disponible

J’ajoute également un menu qui fusionnera avec celui de la fiche principale.

Image non disponible

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

Image non disponible

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é.

 
Sélectionnez
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.

Image non disponible

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.
    Image non disponible

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 !

 
Sélectionnez
procedure TForm21.AjouterForme1Click(Sender: TObject);
begin
 with TFColorFormDock.Create(Application) do
  begin
    ManualDock(Self);
    Show;
  end;
end;

Image non disponible

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).

Image non disponible

Un peu de code pour articuler tout cela :

Forme principale
Sélectionnez
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;

Image non disponible

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.

Image non disponible

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.

Image non disponible

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
Sélectionnez
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
Sélectionnez
 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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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;

Image non disponible

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.

Image non disponible

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.

Image non disponible

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.

 
Sélectionnez
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.

 
Sélectionnez
 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
Sélectionnez
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.
 
Sélectionnez
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.

Image non disponible

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
Sélectionnez
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;

implementationprocedure 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.

 
Sélectionnez
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
Sélectionnez
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.

 
Sélectionnez
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
Sélectionnez
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.
 
Sélectionnez
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

Image non disponible

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.

 
Sélectionnez
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.

Image non disponible

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


Le docking est même accessible à partir de D4, confirmation faite par defluc peu après la parution de l’article.
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.
Un TText est moins sensible au style qu’un TLabel
Au lieu de TabControl1.PageCount-1 il est possible d’utiliser ATabItem.Index, note de der§en

  

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