Fiche secondaire


Il est dans les bonnes pratiques de développement sous Delphi de ne pas instancier toutes les fiches de l'application au démarrage (ce que propose cependant Delphi par défaut). En effet, l'application se contentera généralement d'une fiche principale et peut-être d'un DataModule, mais toutes les autres fiches sont instanciées dynamiquement.

Dans cet article nous allons étudier plusieurs méthodes pour permettre l'accès depuis une fiche secondaire à la fiche principale, ou plus généralement à tout autre fiche de l'application.

La méthode directe


Pour rappel, par défaut, les fiches nouvellement créées sont instanciées automatiquement au lancement de l'application car Delphi insère une ligne Application.CreateForm(TForm2, Form2); dans le fichier DPR. Cela permet d'avoir accès à cette fiche via la variable globale Form2.

Si je veux faire référence à la fiche principale (Form1 pour l'exemple), il suffit d'ajouter une référence à Unit1 dans la partie Implementation de Unit2 et d'y faire référence.

 // Unit2.pas
implementation
  
uses
  Unit1;
  
procedure TForm2.Button1Click(Sender: TObject);
begin
  Form1.Edit1.Text := Edit1.Text; // copie de la zone Edit1 de Form2 dans la zone Edit1 de Form1
end;

NB: quand je fais référence à Edit1 depuis la fiche Form2, je ne préfixe pas son nom de Form2. Ce serait une erreur de le faire car Form2 est une variable qui pointe sur une instance particulière de TForm2, si je crée deux instances de cette fiche, la variable ne pourra au mieux en désigner qu'une des deux. Si je veux absoluement utiliser un préfixe, il faut utiliser tout simplement Self (moi-même).

La méthode dynamique


La méthode précédente fonctionne également si la fiche Form2 est crée dynamiquement.

 // Unit1.pas
procedure TForm1.Button1Click(Sender: TObject);
begin
  TForm2.Create(Self);
end;

Notez qu'en créant une instance de TForm2 de la sorte, il n'existe aucun moyen d'y accéder directement (cela reste possible via Screen.Forms)...et si cela avait été le cas pour TForm1 ? n'est-il finalement pas un peu risqué de faire référence à Form1.Edit1 ?

Si Form1 est la fiche principale de votre application et que c'est un singleton (une seule instance dans l'application), non...mais de façon générale, ce n'est pas forcément une bonne pratique.

Alors comment faire ?

Les helpers à la rescousse


Depuis quelques versions maintenant, Delphi propose des class helper qui permettent d'enrichir une classe existante de nouvelles méthodes. Nous allons exploiter cette particularité et le fait que le constructor de TForm2 reçoive en paramètre le "Self" de TForm1 pour retrouver son instance de façon détournée.

 // Unit2.pas
implementation

uses  
  Unit1;
  
type
  TForm2Helper = class helper for TForm2
    function Form1: TForm1;
  end;
   
function TForm2Helper.Form1: TForm1;
begin
  Result := Owner as TForm1;
end;
 
procedure TForm2.Button1Click(Sender: TObject);
begin
  Form1.Edit1.Text := Edit1.Text; // copie de la zone Edit1 de Form2 dans la zone Edit1 de Form1
end;

Le code semble ici similaire à ce que j'ai donné plus haut, mais en fait, en ajoutant TForm2Helper, Form1 dans Button1Click n'est plus une référence directe à la variable de Unit1 mais un appel à la méthode TForm2Helper.Form1. Evidemment si le propriétaire de l'instance de TForm2 n'est pas un TForm1, le code déclenchera une exception...mais cela peut très bien être un prérequis dont on s'assurera par une assertion.

function TForm2Helper.Form1: TForm1;
begin
  Assert(Owner is TForm1);
  Result := TForm1(Owner);
end;

Découplage via des Interfaces


Tout cela fonctionne fort bien, mais que se passe-t-il si Form2 peut être invoquée depuis différentes formes parentes ? Sauf à leur donner un ancêtre commun cela devient compliqué. De plus, il pourrait être intéressant de pouvoir utiliser notre fiche Form2 dans différents projets (une fenêtre de saisie de mot de passe par exemple).

Pour répondre à tout cela, les Interfaces sont une solution intéressante.

 // Unit2.pas
unit Unit2;

type
  IForm2Interface = interface
    procedure SetForm2Text(const Str: string);
  end;
  
  TForm2 = class(TForm)
    Edit1: TEdit;
    Button1: TButton;
    procedure Button1Click(Sender: TObject)
  private
    FIntf: IForm2Interface;
    constructor Create(AOwner: TComponent; AIntf: IForm2Interface);
  end;
  
 implementation
 
 {$R *.DFM}
 
 constructor TForm2.Create(AOwner: TComponent; AIntf: IForm2Interface);
 begin  
   inherited Create(AOwner);
   FIntf := AIntf;
 end;
 
 procedure TForm2.Button1Click(Sender: TObject);
 begin
   FIntf.SetForm2Text(Edit1.Text);
 end;

Notez, qu'ici Form2 ignore tout de la fiche Form1 ! Il ne reste plus qu'à l'utiliser dans Form1

 // Unit1.pas
unit Unit1;

interface

uses
  Unit2;
  
type
  TForm1 = class(TForm, IForm2Interface)
    Edit1: TEdit;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
  // IForm2Interface
    procedure SetForm2Text(const Str: string);
  end;

implementation

{$R *.dfm}

procedure Tform1.Button1Click(Sender: TObject);
begin
  TForm2.Create(Self, Self);
 end;
 
procedure TForm1.SetForm2TExt(const Str: string);
begin
  Edit1.Text := Str;
end;

Et voilà, le tour est joué !

Il est également possible de passer par un Owner as IForm2Interface, (ou même via supports), mais dans ce cas, il ne faudra pas oublier d'attribuer un GUID à l'interface (Ctrl+Shift+G).
Date de dernière modification : 19/06/2017