IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Partie I : Composants non visuels

Partie I : Composants non-visuels


précédentsommairesuivant

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 APIs 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'objet
  • La création d'événements
  • L'encapsulation des APIs 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 :

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

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

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

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 :

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

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

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

 
Sélectionnez
  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 coeur 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 :

 
Sélectionnez
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) :

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

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

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

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

 
Sélectionnez
type
  TAcceptDirEvent = procedure(Sender : TObject; Directory : TDirName) of object;

Voici comment se présente maintenant notre interface :

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

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

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

 
Sélectionnez
// 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 :

 
Sélectionnez
// 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 :

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

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

 
Sélectionnez
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
nodefault ou
stored True
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 coeur 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é :

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

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

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


précédentsommairesuivant

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 © 2005 Sébastien Doeraene. 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.