La création dynamique de composants

Laurent Le Brun

La création dynamique de composants. ♪

Ce cours contient trois parties. Je vous conseille de les lire dans l'ordre. Il vous expliquera comment créer dynamiquement des composants. Il est parfois utile de créer des composants pendant l'exécution pour plusieurs raisons : si on ne sait pas de combien de composants on aura besoin ; si on veut créer une centaine de boutons (et c'est un peu long en mode conception :) ; ou si on veut tout simplement s'amuser :o)

- Titre : La création dynamique de composants

- Auteur : Laurent Le Brun

- Parution : 20 avril 2005

- Mise à jour : 20 avril 2005

- Licence : Copyright ® 2005 Laurent Le Brun. 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. Créer un composant dynamiquement

- II. Gérer des composants créés dynamiquement

- II-A. Première méthode : enregistrer chaque bouton

- II-B. Deuxième méthode : rechercher les boutons

- II-C. Différencier les événements

- III. La FAQ

- Pourquoi Btn.Create(Self) ne « marche » pas ?

- Quelle est la différence entre les propriétés Owner et Parent ?

- Quel composant dois-je choisir pour la propriété Parent ?

- Pourquoi il ne faut pas mettre OnClick := MaProcedure(Sender); pour définir l'événement OnClick du Bouton ?

- Puis-je modifier la propriété Name de mon composant créé dynamiquement ?

- Ça marche comment pour les autres composants ?

- Et pour les fiches ?

I - Créer un composant dynamiquement

On a parfois besoin d'ajouter des composants au cours d'un programme. Bien sûr, on peut parfois les cacher pour les afficher quand on en a besoin ; mais, dans certains cas, on ne sait pas à l'avance combien de composants on aura besoin. C'est pourquoi on crée les composants dynamiquement.

On va prendre l'exemple de la création d'un bouton (TButton). Assuez-vous d'abord que StdCtrls est bien dans la liste des uses, car c'est dans cette unité que les TButton sont déclarés. Ensuite, on va déclarer une variable : on l'appelle Btn (contraction de Button) et c'est un TButton. Maitenant qu'il est déclaré, il faut le créer en utilisant la propriété Create du TButton :

constructor TButton.Create(AOwner: TComponent);

La propriété Owner permet de connaître le propriétaire d'un composant. Lorsqu'un composant en possède un autre, la mémoire du composant possédé est libérée quand la mémoire du propriétaire est libérée. Ainsi, cela signifie que lorsqu'une fiche est détruite, tous les composants de la fiche le sont également. Lors de sa destruction, le propriétaire libère le composant. Dans pratiquement tous les cas, le propriétaire est la fiche (Form1, par défaut). Au lieu de spécifier Form1, on peut utiliser simplement self.

var
Btn: TButton;
begin
Btn := TButton.Create(Self);
end;

Une erreur fréquente est de mettre seulement Btn.Create(Self). En faisant cela, vous provoquez une erreur lors de l'exécution du programme. Si vous l'exécutez maintenant, vous ne verrez pas votre bouton. En fait, il est juste créé en mémoire. Il faut l'afficher. Pour l'afficher, on définit le Parent du composant, c'est-à-dire, le contrôle qui affichera le composant. Dans notre exemple, on choisit encore une fois Form1.

var
Btn: TButton;
begin
Btn := TButton.Create(Self);
Btn.Parent := Self;
end;

Si vous testez le programme, un bouton apparaîtra dans l'angle en haut à gauche, sans rien d'écrit. On peut changer ses propriétés, comme pour un composant habituel.

Astuce : Au lieu de répéter le nom de la variable à chaque ligne, vous pouvez utiliser With, même pour la première ligne !

With TButton.Create(self) do begin
Parent := Self;
Left := 200;
{...}
end;

Cela vous fait l'économie d'une variable !

Maitenant, vous voulez peut-être associer une procédure à l'événement OnClick. Il suffit de faire comme cela :

type
TForm1 = class(TForm)
procedure MaProcedure(Sender: TObject); // A ajouter
{...}

procedure TForm1.MaProcedure(Sender: TObject);
begin
ShowMessage('Bouton cliqué !');
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
With TButton.Create(self) do begin
Parent := Self;
// on peut modifier ici d'autres propriétés
OnClick := MaProcedure;
end;
end;

end.

Notez qu'il est obligatoire de déclarée la procédure dans type, ce qui est facultatif lors d'une procédure/fonction classique.

II - Gérer des composants créés dynamiquement

Créer des composants dynamiquement, c'est bien ; savoir s'en servir, c'est mieux !

C'est pour répondre à cette problématique que j'ai écrit cette 2e page. En effet, nous venons de voir comment créer des composants dynamiquement, mais ils ne sont pas vraiment fonctionnels. Car, avec la méthode que je vous ai donnée, on est confronté à plusieurs problèmes : tous les boutons ont les mêmes propriétés ; ces propriétés ne peuvent plus être changées par la suite ; c'est le même événement qui est exécuté pour chaque bouton.

II-A - Première méthode : enregistrer chaque bouton

Quand j'ai débuté, la première solution a laquelle j'ai pensé, c'est d'utiliser une variable globale (de type TButton). On peut modifier les propriétés de cette variable, ce qui modifiera l'aspect du bouton. Le problème, c'est quand on a créé plusieurs composants : on n'a le pouvoir que sur le dernier. On peut contourner cette limite en créant une variable de type : array of TButton. Ce tableau doit être dynamique (lui aussi !), car on ne sait pas à l'avance le nombre de boutons que l'on créera. Voici un exemple de cette technique :

var
ListeBoutons: array of TButton;
Nombre: integer = 0;

procedure TForm1.Button1Click(Sender: TObject);
begin
inc(Nombre);
SetLength(ListeBoutons, Nombre+1);
ListeBoutons[Nombre] := TButton.Create(self);
with ListeBoutons[Nombre] do
begin
Parent := Self;
Left := Nombre*20;
Width := 20;
Caption := IntToStr(Nombre);
end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
i: Integer;
begin
for i := 1 to Nombre do
ListeBoutons[i].Top := ListeBoutons[i].Top + 10;
end;

Quand l'utilisateur appuie sur Button1, on agrandit le tableau d'un élément (qui servira pour le bouton). Puis, on crée simplement le bouton et on modifie ses propriétés. Ainsi chaque bouton est enregistré dans le tableau. Button2 montre comment modifier les propriétés ultérieurement. Il suffit donc de connaître le numéro du bouton pour pouvoir le contrôler.

II-B - Deuxième méthode : rechercher les boutons

Il existe plusieurs fonctions qui permettent d'accéder à un composant quelconque. Il existe notamment la propriété ComponentCount qui renvoie le nombre de composants qu'il y a sur la fiche. Pour accéder à un composant, on utilise Components.

Remarque issue de l'aide de Delphi : « Pratiquement, Components s'utilise avec ComponentCount pour des traitements itératifs. Néanmoins, gardez à l'esprit que la propriété ComponentCount d'un composant correspond au nombre d'éléments de la liste Components de ce composant. La propriété ComponentCount est supérieure d'une unité à l'indice du dernier élément de Components, car l'indice du premier élément est toujours 0 ».

Pour reprendre le même exemple, pour faire descendre les composants de la fiche, on procède comme suit :

procedure TForm1.Button2Click(Sender: TObject);
var i: Integer;
begin
for i := 0 to pred(ComponentCount) do
if Components[i] is TControl then
with (Components[i] as TControl)do
Top := Top + 10;
end;

Les TComponents n'ont pas de propriété Top, car certains composants n'ont pas à être affichés. On regarde d'abord si le composant est un TControl (il a donc la propriété Top) et on le considère ensuite ainsi pour modifier sa propriété. De la même façon, on peut savoir si le composant est un TButton. C'est astuce permet le contrôle de tous les composants d'une fiche, quels qu'ils soient.

Cependant, vous remarquerez que, dans ce dernier exemple, tous les boutons sont déplacés, même ceux qui n'ont pas été créés dynamiquement. Il nous faut trouver quelque chose pour les distinguer. Ce peut-être leur position dans la fiche, leur propriété Cpation ou n'importe quoi d'autre. Je vous conseille la propriété Tag, car elle est commune à TOUS les composants, et elle n'influe pas sur le programme. Ainsi, les procédures précédentes deviennent :

procedure TForm1.Button1Click(Sender: TObject);
begin
with TButton.Create(self) do begin
Parent := Self;
Left := Random(400);
Top := Random(400);
Tag := 1;
end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var i: Integer;
begin
for i := 0 to pred(ComponentCount) do
if Components[i].Tag = 1 then
(Components[i] as TControl).Top := (Components[i]as TControl).Top + 10;
end;

Ici, tous les composants créés dynamiquement ont leur propriété Tag mise à 1. Cette valeur est bien sûr arbitraire, vous pouvez prendre n'importe laquelle. On aurait pu également utiliser la propriété Name du composant, qui permet, elle, de stocker un string.

D'un point de vue théorique, la propriété Tag ne sert à rien. Il est noté dans l'aide : « La propriété Tag n'a pas de rôle prédéfini. Cette propriété est proposée afin de pouvoir stocker une valeur entière ou elle peut être transtypée sur n'importe quelle valeur 32 bits, comme une référence de composant ou un pointeur ». En fait, puisque chaque composant possède cette propriété, elle peut servir à donner une valeur au composant. Cela permet par exemple de repérer les composants, comme on vient de le faire. Elle peut aussi remplacer des variables entières, relatives à un composant. Avec l'habitude, elle peut donc devenir très intéressante. On peut ainsi regrouper des composants par cette propriété.

II-C - Différencier les événements

Hé, oui ! Si vous avez suivi ce cours, vous aurez remarqué que tous les boutons créés font la même chose. Et là encore, il faut différencier les composants. La méthode reste la même : récupérer la propriété Tag (ou Name, ou autre chose) et agir en conséquence.

procedure TForm1.MaProcedure(Sender: TObject);
begin
case (Sender as TComponent).Tag of
1: {instructions...};
2: {instructions...};
{ autres valeurs ... }
else
{instructions par défaut...};
end;
end;

Ce n'est qu'un exemple… Vous pouvez faire comme bon vous semble. Par exemple, il est possible d'effectuer des calculs en fonction de la valeur Tag et de retourner le résultat. Il est souvent intéressant de numéroter les composants. Pensez à utiliser une variable globale pour cela.

III - La FAQ

J'ai répertorié ici quelques questions que l'on peut se poser, après avoir lu les deux premières pages. J'ai hésité à les incorporer au cours, mais j'ai finalement décidé de les mettre à part pour alléger les pages. De plus, cela permettra d'avoir une réponse plus rapidement, en cas de problème.

Pourquoi Btn.Create(Self) ne « marche » pas ?

Gardez à l'esprit que Btn n'est qu'une variable. On ne peut donc pas le créer, mais plutôt lui assigner une valeur. Ici, la valeur est un peu spéciale : c'est un nouveau bouton qui est créé. Si vous oubliez cette ligne, le compilateur indiquera un avertissement : « La variable 'Btn' n'est peut-être pas initialisée ». Une fois que la ligne est mise, vous pouvez modifier les propriétés de Btn. Mais, surtout pas avant !

Quelle est la différence entre les propriétés Owner et Parent ?

Réponse issue de l'aide de Delphi : Ne confondez pas la propriété Parent déclarée dans TControl avec la propriété Owner déclarée dans TComponent. Le Parent d'un contrôle est toujours un contrôle fenêtré contenant visuellement le contrôle en question et qui est responsable de l'écriture du contrôle dans un flux lorsque la fiche est enregistrée. Le Owner d'un composant est le composant qui a été transmis en paramètre dans le constructeur et qui contrôle quand le composant est libéré.

Quel composant dois-je choisir pour la propriété Parent ?

Réponse issue de l'aide de Delphi : À l'exécution lors de la création d'un nouveau contrôle, affectez une valeur de propriété Parent au nouveau contrôle. C'est généralement une fiche, un volet, une boîte groupe ou tout autre contrôle conçu pour servir de conteneur. Changer le parent d'un contrôle déplace le contrôle à l'écran afin qu'il s'affiche à l'intérieur de son parent. Lorsque le contrôle parent se déplace, l'enfant se déplace avec le parent.

Pourquoi il ne faut pas mettre OnClick := MaProcedure(Sender); pour définir l'événement OnClick du Bouton ?

Le TButton est déjà défini dans l'unité StdCtrls. C'est donc là que se décide de la valeur de Sender. Elle est imposée et ne peut donc pas être modifiée de cette façon. En fait, dans OnClick := MaProcedure(Sender);, Sender renvoie à l'objet qui a appelé la procédure, lors de la création du composant. Or, le Sender de l'événement OnClick renvoie à l'objet qui a appelé la fonction.

Puis-je modifier la propriété Name de mon composant créé dynamiquement ?

OUI ! Changer la propriété Name d'un composant ne change rien. Elle est là uniquement pour se rapporter aux composants, créés en mode conception. Même en mettant Button1 au nom de votre composant créé dynamiquement, vous n'avez pas le droit de modifier ses propriétés par Button1.Top (par exemple). D'ailleurs, deux composants n'ont pas le droit d'avoir le même nom ; ils peuvent cependant ne pas en avoir.

Ça marche comment pour les autres composants ?

De la même façon ! Ce que je vous expliqué peut être adapté à tous les composants (j'ai même essayé avec un TOpenDialog).

Et pour les fiches ?

Ben, c'est pareil ! Juste une petite précision : il ne faut pas oublier d'afficher la fiche avec la procédure Show (ou ShowModal). Si vous mettez Show, pour que la fiche ne reste pas « à l'intérieur » de la fiche principale, il faut mettre la propriété Parent à nil. Si vous essayez de mettre nil au Parent d'un TButton (par exemple), il ne sera pas affiché.