II. Création d'un composant non visuel d'exemple▲
Le meilleur moyen d'apprendre à faire quelque chose, c'est de le faire ! Donc nous allons voir pas à pas comment créer un composant non visuel.
II-A. Choix du composant à développer▲
J'ai décidé de réaliser avec vous un composant du nom de TSelectDirDialog.
Vous devriez connaître le composant TOpenDialog, qui permet de sélectionner un fichier à ouvrir. Notre TSelectDirDialog fera plus ou moins de même, mais permettra de sélectionner un dossier plutôt qu'un fichier.
Bien entendu nous ne dessinerons pas la fenêtre correspondante : nous utiliserons les API de Windows pour utiliser cette boîte classique, tout comme le fait le composant TOpenDialog.
Ce composant permettra donc d'aborder les points suivants :
- les propriétés affichées dans l'inspecteur d'objets ;
- la création d'événements ;
- l'encapsulation des API Windows par un composant ;
- l'installation du composant dans la palette de Delphi ;
- l'association d'une icône au composant.
Voilà un beau programme non ? Alors, allons-y !
II-B. Le paquet de nos composants▲
Dans les deux premières versions de Delphi, les composants étaient tous compilés dans une seule et même librairie appelée CmpLib32.bpl. Cette DLL contenait le code de tous les composants installés et il était difficile d'en décharger.
Delphi 3 a apporté les paquets. Désormais, tous les composants seraient compilés dans des paquets. Cela a permis de simplifier énormément le chargement des composants utiles, ce qui devenait presque indispensable au vu des nombreux composants disponibles sur Internet (notamment l'incontournable JVCL).
Tout au long de ce cours, nous développerons quatre composants. Nous les compilerons tous les quatre dans un seul et même paquet. Nous allons donc commencer par créer ce paquet.
Créez un nouveau paquet, et nommez-le ComposTutoR.bpl. Vous pouvez bien sûr le nommer comme bon vous semble, mais sachez que c'est comme cela qu'il sera dénommé dans la suite de ce cours. Enregistrez-le dans un dossier dédié, par exemple sous le dossier <Delphi>\source\ComposTuto\.
Sélectionnez le menu Projet|Options pour afficher les options du paquet. Dans Description, vous pouvez indiquer une description du paquet, mais surtout vous pouvez spécifier qu'il s'agit d'un paquet de type runtime (Options d'utilisation > Seulement en exécution).
Le R que l'on ajoute à la fin du nom du paquet permet de nous rappeler (à nous et aux développeurs qui utiliseront le paquet) que c'est un paquet de type runtime. Cela veut dire que ce paquet ne peut pas être installé dans l'EDI de Delphi. Le paquet qui sera installé (et qui se nommera ComposTutoD.bpl, avec D pour design) ne contiendra pas de code utile au fonctionnement du composant.
Il est aussi possible de créer des paquets de type runtime et design (des MonPaquetRD.bpl si vous voulez), mais un paquet ne devrait normalement jamais être de ce type : les paquets de type design ne doivent contenir que du code servant à l'intégration dans l'EDI de Delphi, et ne servent donc à rien en dehors.
II-C. Le code de base de tout composant▲
Créez une nouvelle unité, nommée SelDirDlg.pas, dans notre paquet ComposTutoR.bpl. Cette unité contiendra le code de notre composant TSelectDirDialog.
Un composant non visuel descend de TComponent, et éventuellement de l'un de ses descendants. Dans notre cas, il descendra directement de TComponent.
Ainsi, le code minimal d'une unité d'un composant est celui-ci :
unit
SelDirDlg;
interface
uses
Classes;
type
TSelectDirDialog = class
(TComponent)
end
;
implementation
end
.
Notez que nous utilisons l'unité Classes, qui déclare les classes TPersistent et TComponent (entre autres).
II-D. Sur quoi se base le fonctionnement de notre composant ?▲
II-E. Fonctionnement de base▲
Commençons par faire fonctionner notre composant avec ses propriétés de base. Ensuite, nous l'améliorerons avec quelques techniques indispensables à la réalisation de composants dignes de ce nom.
II-E-1. Théorie▲
Tout d'abord, comme nous allons beaucoup utiliser des chaînes de caractères représentant des noms de dossiers, il serait pratique de disposer d'un type prévu pour. Nous allons le déclarer comme le type TFileName qui, je vous le rappelle, désigne une chaîne de caractères représentant un nom de fichier :
type
TDirName = type
string
;
Il faut aussi savoir comment nous comptons utiliser la boîte de dialogue classique de Windows…
L'unité FileCtrl propose une routine SelectDirectory surchargée de deux façons :
function
SelectDirectory(var
Directory: string
;
Options: TSelectDirOpts; HelpCtx: Longint
): Boolean
; overload
;
function
SelectDirectory(const
Caption: string
; const
Root: WideString;
var
Directory: string
; Options: TSelectDirExtOpts = [sdNewUI]; Parent: TWinControl = nil
): Boolean
; overload
;
Ces deux surcharges font apparaître la boîte de dialogue avec diverses informations.
Nous utiliserons la seconde surcharge, car elle est plus complète.
Définissons maintenant quelques propriétés pour notre composant. Nous aurons besoin des divers renseignements demandés en paramètres par la fonction SelectDirectory.
II-E-2. Mise en pratique▲
Nous aurons donc besoin de quatre propriétés :
Utilisation |
Propriété |
Type |
---|---|---|
Titre de la boîte de dialogue |
Caption |
string |
Répertoire racine |
Root |
TDirName |
Répertoire sélectionné |
Directory |
TDirName |
Options de la boîte de dialogue |
Options |
TSelectDirExtOpts |
Nous ajouterons un constructeur pour les initialiser et une méthode Execute qui affichera la boîte de dialogue :
function
TSelectDirDialog.Execute : boolean
;
begin
Result := SelectDirectory(FCaption, FRoot, string
(FDirectory), FOptions);
end
;
Rappelons la déclaration de la fonction SelectDirectory afin de comprendre les différents paramètres passés :
function
SelectDirectory(const
Caption: string
; const
Root: WideString;
var
Directory: string
; Options: TSelectDirExtOpts = [sdNewUI]; Parent: TWinControl = nil
): Boolean
; overload
;
Rien de tout ceci n'est plus compliqué que la création d'une classe implémentant une certaine fonctionnalité. Nous ne nous attarderons pas là-dessus.
Voici le code actuel :
unit
SelDirDlg;
interface
uses
Classes, FileCtrl;
type
TDirName = type
string
;
TSelectDirDialog = class
(TComponent)
private
FCaption : string
;
FRoot : TDirName;
FDirectory : TDirName;
FOptions : TSelectDirExtOpts;
public
constructor
Create(AOwner : TComponent); override
;
function
Execute : boolean
;
property
Caption : string
read
FCaption write
FCaption;
property
Root : TDirName read
FRoot write
FRoot;
property
Directory : TDirName read
FDirectory write
FDirectory;
property
Options : TSelectDirExtOpts read
FOptions write
FOptions;
end
;
implementation
constructor
TSelectDirDialog.Create(AOwner : TComponent);
begin
inherited
;
FCaption := 'Sélectionnez un dossier'
;
FRoot := 'C:\'
;
FDirectory := 'C:\'
;
FOptions := [sdNewUI];
end
;
function
TSelectDirDialog.Execute : boolean
;
begin
Result := SelectDirectory(FCaption, FRoot, string
(FDirectory), FOptions);
end
;
end
.
II-F. Propriétés dans l'inspecteur d'objet▲
Jusqu'à présent, nous n'avons fait qu'écrire une simple classe. Les nouveautés vont maintenant commencer.
Une des qualités essentielles des composants est que leurs propriétés sont éditables en mode conception. Or, bien que la classe TSelectDirDialog soit un composant, ses propriétés n'apparaîtront pas dans l'inspecteur d'objet avec ce code.
Nous pouvons voir ici une des merveilles de la technologie orientée composants de Borland : il suffit de déclarer les propriétés que l'on souhaite voir apparaître dans l'inspecteur comme published (qui se traduit par publié).
published est une directive de visibilité au même titre que public ou private. C'est la visibilité la plus grande : elle a les mêmes propriétés que la visibilité public mais elle permet aussi de conserver les RTTI (RunTime Type Information : Informations de type à l'exécution) qui permettent (notamment) à l'inspecteur d'objet de les voir et de les afficher.
published
property
Caption : string
read
FCaption write
FCaption;
property
Root : TDirName read
FRoot write
FRoot;
property
Directory : TDirName read
FDirectory write
FDirectory;
property
Options : TSelectDirExtOpts read
FOptions write
FOptions;
end
;
II-G. Événements▲
Que seraient les composants sans les événements ? C'est le cœur même de la programmation événementielle. Voyons donc comment ajouter des événements à vos composants.
Toutefois, vous pouvez remarquer que notre composant fonctionne très bien sans événement. Les événements ne sont pas indispensables à la création de composants.
II-G-1. Théorie▲
En Delphi, il est très simple d'ajouter un événement à vos composants. Voici comment procéder.
Premièrement, vous avez besoin d'un type événementiel. En réalité, ce sont des types méthodes.
Un type méthode est déclaré de cette façon :
type
TTypeMethode = procedure
(Param1 : TType1; Param2 : TType2; ...) of
object
;
Les deux mots-clés of object signifient qu'il s'agit d'un type méthode et non d'un type routine. N'oublions pas qu'une méthode est une routine de classe.
Dans le cas d'un type événementiel, un type méthode est requis, et donc ces deux mots-clés le sont aussi.
On peut déclarer des variables de ce type de la même manière que n'importe quel autre.
Pour les utiliser dans la partie gauche d'une affectation, il suffit d'utiliser le nom de la variable à gauche et le nom de la fonction dont on veut enregistrer l'adresse à droite (il est également possible d'affecter la valeur nil qui permettra de tester si la variable est assignée ou non) :
var
VarMethode : TTypeMethode;
...
VarMethode := nil
;
VarMethode := MaMethode;
Pour appeler la méthode associée, il suffit d'utiliser la variable comme s'il s'agissait du nom de la méthode en question :
VarMethode(Param1, Param2, ...);
Si l'on veut tester qu'une méthode a été assignée, on peut utiliser la fonction Assigned qui prend pour unique paramètre la variable méthode et renvoie True si et seulement si elle n'est pas vide (différent de nil).
Finalement, mais cela est moins important et uniquement à titre indicatif, si vous devez comparer deux variables de type méthode pour savoir si elles contiennent la même adresse, vous devrez les transtyper en type TMethod et comparer les champs Code et Data de ce record.
if
(TMethod(VarMethode).Code = TMethod(VarMethode2).Code) and
// C'est la même méthode
(TMethod(VarMethode).Data = TMethod(VarMethode2).Data) then
// C'est le même objet
...
II-G-2. Mise en pratique▲
Nous allons donc ajouter deux événements à notre composant : un qui sera appelé lorsque l'utilisateur aura cliqué sur OK dans la boîte de dialogue, et un autre lorsqu'il aura cliqué sur Annuler.
Le second n'est qu'un événement de notification, donc qui n'a pas besoin de paramètre particulier en dehors du paramètre Sender qui indique le composant appelant l'événement. Nous utiliserons donc pour celui-là le type TNotifyEvent, déclaré comme suit dans l'unité Classes :
type
TNotifyEvent = procedure
(Sender : TObject) of
object
;
Pour le premier, nous aurons besoin d'un type d'événement spécifique puisqu'il devra envoyer comme paramètre le dossier sélectionné dans la boîte de dialogue. Voici la déclaration de ce dernier :
type
TAcceptDirEvent = procedure
(Sender : TObject; Directory : TDirName) of
object
;
Voici comment se présente maintenant notre interface :
interface
uses
Classes, FileCtrl;
type
TDirName = type
string
;
TAcceptDirEvent = procedure
(Sender : TObject; Directory : TDirName) of
object
;
TSelectDirDialog = class
(TComponent)
private
FCaption : string
;
FRoot : TDirName;
FDirectory : TDirName;
FOptions : TSelectDirExtOpts;
FOnAccept : TAcceptDirEvent;
FOnCancel : TNotifyEvent;
public
constructor
Create(AOwner : TComponent); override
;
function
Execute : boolean
;
published
property
Caption : string
read
FCaption write
FCaption;
property
Root : TDirName read
FRoot write
FRoot;
property
Directory : TDirName read
FDirectory write
FDirectory;
property
Options : TSelectDirExtOpts read
FOptions write
FOptions;
property
OnAccept : TAcceptDirEvent read
FOnAccept write
FOnAccept;
property
OnCancel : TNotifyEvent read
FOnCancel write
FOnCancel;
end
;
Il ne reste plus qu'à appeler ces événements lorsque le cas se présente. Rappelons qu'il suffit d'utiliser le nom de la variable comme nom de la méthode à appeler.
function
TSelectDirDialog.Execute : boolean
;
begin
Result := SelectDirectory(FCaption, FRoot, string
(FDirectory), FOptions);
if
Result then
begin
if
Assigned(FOnAccept) then
FOnAccept(Self
, FDirectory);
end
else
begin
if
Assigned(FOnCancel) then
FOnCancel(Self
);
end
;
end
;
II-H. Valeurs par défaut des propriétés▲
Il reste une légère imperfection à notre composant. Et même des développeurs expérimentés dans la réalisation de composants oublient parfois d'ajouter ce détail.
Mais avant de corriger l'imperfection, il faudrait savoir ce qui cloche…
II-H-1. Comment Delphi enregistre-t-il les fiches▲
Vous êtes-vous jamais demandé comment Delphi enregistrait tout ce que vous lui indiquiez lorsque vous êtes en mode conception. Il est assez simple de le savoir en cliquant droit sur une fiche en mode conception et sélectionnez le menu Voir comme texte.
Votre fiche disparaîtra alors pour laisser place à sa forme texte. Voici un exemple avec un bouton et un éditeur :
object
FormMain: TFormMain
Left = 0
Top = 0
Width = 434
Height = 320
Caption = 'Essais'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object
Button1: TButton
Left = 32
Top = 32
Width = 121
Height = 33
Caption = 'Button1'
TabOrder = 0
end
object
Edit1: TEdit
Left = 32
Top = 96
Width = 361
Height = 21
TabOrder = 1
Text = 'Edit1'
end
end
II-H-2. Spécificateurs de stockage▲
Mais qu'y a-t-il donc d'intéressant à découvrir dans ce code ? Eh bien, c'est que toutes les propriétés ne sont pas enregistrées ! Si vous consultez l'inspecteur d'objet pour ces trois types de composants, vous remarquerez qu'il y a beaucoup plus de propriétés que ça.
Ceci s'explique par le fait que Delphi n'enregistre pas les propriétés dont la valeur est celle par défaut. Mais comment sait-il quelle est la valeur par défaut d'une propriété ? C'est ce que nous allons voir.
Dans l'interface d'une classe de composant (et même de persistent, donc descendant de TPersistent), on peut définir les valeurs par défaut d'une propriété au moyen des spécificateurs default ou stored. On peut aussi utiliser le spécificateur nodefault pour supprimer une valeur par défaut héritée. Voici comment utiliser ces spécificateurs.
Pour les propriétés de type scalaire, il suffit d'utiliser le spécificateur default suivi de la valeur par défaut. Voici deux exemples :
// Propriétés scalaires avec valeur par défaut
type
TMonSet = set
of
#0
..#31
;
...
property
MonEntier : integer
read
FMonEntier write
FMonEntier default
1
;
property
MonSet : TMonSet read
FMonSet write
FMonSet default
[#0
, #10
, #13
];
Signalons à cette occasion qu'il est impossible d'utiliser des propriétés publiées de type ensemble dont la taille est plus grande que 4 octets, donc qui contient plus de 32 éléments (d'où la définition TMonSet = set of #0..#31).
Une valeur par défaut ne sert qu'à savoir s'il faut enregistrer la variable ou pas. À la relecture du fichier .dfm, cette valeur par défaut n'est pas utilisée. Vous devez donc prendre garde à bien initialiser la propriété dans le constructeur de l'objet.
Pour les valeurs chaînes, cela se complique un peu. Si la valeur par défaut est une chaîne vide, ne touchez à rien, Delphi utilise cette valeur comme valeur par défaut si rien n'est indiqué.
Si vous voulez utiliser une autre valeur par défaut, vous devrez définir un spécificateur stored. Ce qui suit ce spécificateur est un nom de méthode de la classe en cours qui ne prend aucun paramètre et qui renvoie une valeur de type boolean. Cette valeur doit être True s'il faut enregistrer la valeur et False dans le cas contraire. Voici un exemple :
// Propriétés non scalaires avec valeur par défaut
function
MaChaineStored : boolean
;
property
MaChaine : string
read
FMaChaine write
FMaChaine stored
MaChaineStored;
...
function
TMonObjet.MaChaineStored : boolean
;
begin
Result := MaChaine <> 'C:\Temp\'
;
end
;
Si vous voulez supprimer toute valeur par défaut, vous pouvez utiliser le spécificateur nodefault :
property
Font nodefault
;
Vous pouvez obtenir le même résultat en utilisant stored True, puisque cette dernière spécification indique que la propriété sera enregistrée si True est vrai, donc toujours. De même, vous pouvez utiliser stored False si vous voulez que la propriété ne soit jamais enregistrée.
Code extrait de l'aide de Delphi 6 :
type
TSampleComponent = class
(TComponent)
protected
function
StoreIt: Boolean
;
public
...
published
property
Important: Integer
stored
True
; { toujours stockée }
property
Unimportant: Integer
stored
False
; { jamais stockée }
property
Sometimes: Integer
stored
StoreIt; { dépend de la valeur de la fonction }
end
;
Vous pouvez aussi redéfinir la valeur par défaut d'une propriété héritée très simplement :
property
Visible default
False
;
property
Font stored
FontStored;
property
Color nodefault
;
Voici un petit récapitulatif :
Pour… |
Indiquer… |
---|---|
Enregistrer si la valeur est différente d'une valeur par défaut (type scalaire) |
default ValeurParDefaut |
Toujours enregistrer la valeur |
rien ou |
Ne jamais enregistrer la valeur |
stored False |
Enregistrer la valeur si la valeur de retour d'une fonction est True |
stored NomFonction |
Si toutefois vous n'êtes pas certain de ce que vous faites, vérifiez que tout se passe bien dans le fichier .dfm sous forme texte.
Il ne reste plus qu'à mettre cela en pratique. Avec toute la théorie que nous avons acquise, je crois qu'il n'y a plus besoin de détailler la procédure.
II-I. Une dernière retouche▲
Nous allons apporter une dernière retouche à notre composant. Que se passerait-il en effet si le disque par défaut du système sur lequel tournait l'application n'était pas C:\ ?
On ne sait en effet jamais dans quel contexte sera utilisé le composant, surtout si on compte le distribuer sur Internet. Il n'est donc pas bon de faire des suppositions quant à l'utilisation de ses composants.
En réalité, dans ce cas, il ne se passerait pas grand-chose, car la routine SelectDirectory teste les paramètres pour vérifier qu'ils sont valides, et les ignore sinon.
Pourtant, cela fait peu professionnel. Voyons donc comment remédier à ce désagrément.
Le cœur de la solution réside dans la routine GetWindowsDirectory.
Malheureusement, cette routine fait partie de l'unité Windows, car c'est une API, et utilise donc des paramètres peu confortables (buffer de type PChar).
Déclarons une variable MainDisk dans la partie implémentation de l'unité :
var
MainDisk : TDirName;
Aux quatre endroits où nous avons utilisé C:\, nous allons utiliser cette variable à la place.
Nous déterminerons cette valeur dans la partie initialization de l'unité. Voici comment procéder :
initialization
SetLength(MainDisk, MAX_PATH);
SetLength(MainDisk, GetWindowsDirectory(PChar(MainDisk), MAX_PATH));
MainDisk := IncludeTrailingPathDelimiter(ExtractFileDrive(MainDisk));
end
.
Nous aurons pris soin de rajouter les unités Windows et SysUtils dans les uses.
Après avoir récupéré le dossier de Windows, nous en extrayons le disque et nous nous assurons que le séparateur de dossier (donc le \ sous Windows) est bien ajouté en fin de chaîne.
II-J. Code complet de l'unité SelDirDlg▲
Voici le code complet et définitif de l'unité SelDirDlg :
unit
SelDirDlg;
interface
uses
Windows, SysUtils, Classes, FileCtrl;
type
TDirName = type
string
;
TAcceptDirEvent = procedure
(Sender : TObject; Directory : TDirName) of
object
;
TSelectDirDialog = class
(TComponent)
private
FCaption : string
;
FRoot : TDirName;
FDirectory : TDirName;
FOptions : TSelectDirExtOpts;
FOnAccept : TAcceptDirEvent;
FOnCancel : TNotifyEvent;
// Ces trois méthodes servent à indiquer si les propriétés Caption, Root et Dir
// doivent être enregistrée dans le fichier .dfm
function
CaptionStored : boolean
;
function
RootStored : boolean
;
function
DirStored : boolean
;
public
constructor
Create(AOwner : TComponent); override
;
// Affiche la boîte de dialogue ; renvoie True si l'utilisateur clique sur OK
function
Execute : boolean
;
published
// Titre de la boîte de dialogue
property
Caption : string
read
FCaption write
FCaption stored
CaptionStored;
// Répertoire racine de l'arbre dans la boîte de dialogue
property
Root : TDirName read
FRoot write
FRoot stored
RootStored;
// Répertoire sélectionné (avant et après) dans la boîte de dialogue
property
Directory : TDirName read
FDirectory write
FDirectory stored
DirStored;
// Diverses options pour la boîte de dialogue
property
Options : TSelectDirExtOpts read
FOptions write
FOptions default
[sdNewUI];
// Déclenché lorsque l'utilisateur clique sur OK dans la boîte de dialogue
property
OnAccept : TAcceptDirEvent read
FOnAccept write
FOnAccept;
// Déclenché lorsque l'utilisateur clique sur Annuler dans la boîte de dialogue
property
OnCancel : TNotifyEvent read
FOnCancel write
FOnCancel;
end
;
implementation
var
MainDisk : TDirName;
constructor
TSelectDirDialog.Create(AOwner : TComponent);
begin
inherited
;
FCaption := 'Sélectionnez un dossier'
;
FRoot := MainDisk;
FDirectory := MainDisk;
FOptions := [sdNewUI];
FOnAccept := nil
;
FOnCancel := nil
;
end
;
function
TSelectDirDialog.CaptionStored : boolean
;
begin
Result := FCaption <> 'Sélectionnez un dossier'
;
end
;
function
TSelectDirDialog.RootStored : boolean
;
begin
Result := FRoot <> MainDisk;
end
;
function
TSelectDirDialog.DirStored : boolean
;
begin
Result := FDirectory <> MainDisk;
end
;
function
TSelectDirDialog.Execute : boolean
;
begin
Result := SelectDirectory(FCaption, FRoot, string
(FDirectory), FOptions);
if
Result then
begin
if
Assigned(FOnAccept) then
FOnAccept(Self
, FDirectory);
end
else
begin
if
Assigned(FOnCancel) then
FOnCancel(Self
);
end
;
end
;
initialization
SetLength(MainDisk, MAX_PATH);
SetLength(MainDisk, GetWindowsDirectory(PChar(MainDisk), MAX_PATH));
MainDisk := IncludeTrailingPathDelimiter(ExtractFileDrive(MainDisk));
end
.
Nous allons maintenant tester notre composant.