I. Les bases du modèle

Le modèle COM est la norme Microsoft pour le développement de composants réutilisables et communicants. Sa grande particularité est qu'il n'est pas destiné à un langage précis, ni à une plate-forme précise, car il définit un standard binaire. Le modèle peut aussi bien être appliqué avec un langage Pascal, C, C++, Visual Basic ou Java pour ne citer que ces langages.

Comme il est destiné à la conception de composants, une grande partie de son fondement repose sur la technologie objet sans pour autant le destiné à un langage orienté objet. Bien entendu, un langage orienté objet rends son implémentation plus aisée.

Étant un modèle de programmation, son implémentation ne nécessite pas de routine spécifique. Les instructions de base des langages suffisent. Néanmoins, Microsoft fourni une petite API pour faciliter son usage.

Devant l'importance de ce modèle sur la plate-forme Windows, certains éditeurs de langage, tel que Inprise ont pris l'initiative de modifier leur langage afin de faciliter son intégration.

Ce que le modèle apporte

Le modèle COM apporte énormément de possibilité dans bien des domaines de programmation. Un bon exemple est l'interface moderne conçue sur ce modèle des systèmes Windows 95, 98 et NT 4.0. Non pas que COM ait défini la charte graphique de l'interface, mais par l'ouverture de celle-ci. La personnalisation est si fine que deux postes, disposant du même système, peuvent être très différents. Sans une forte interopérabilité entre les différents composants du système, il serait impossible d'effectuer du glisser/déplacer de fichiers, de créer des raccourcis sur le bureau, de changer l'aspect du curseur, etc.

Il est donc difficile de lister tous les apports de ce modèle, néanmoins, voici une liste non exhaustive :

  • Le modèle établi un standard binaire d'écriture de composant.
  • Le modèle normalise la communication entre objets.
  • Le modèle normalise les développements sans avoir à changer de langage.
  • Le modèle automatise la gestion de la mémoire.
  • Le modèle rend l'utilisation des DLL plus souple.
  • Le modèle permet le développement d'applications à objets distribués.

I-A. Le concept d'interface

L'interface est une description des caractéristiques supportées par un objet COM. Elle définit les méthodes et les propriétés de l'objet accessibles si on connaît l'interface. Dans le modèle COM, on ne manipule un objet que via des interfaces.

Une interface doit être considérée comme un prototypage de routine transposé aux composants. En effet, le prototypage de routine permet au compilateur de faire un contrôle de type avant l'appel de la routine et au développeur d'appeler la routine. Une interface permet principalement au développeur de savoir comment manipuler un composant.

Une interface n'existe pas, c'est une description. Ce qui existe réellement, c'est le code de l'objet qui implémente l'interface.
C'est pour cette raison que l'on représente graphiquement un objet COM de cette manière :

Imaginons un objet COM proposant les interfaces IA et IB, constitué de deux instances A et B. Un composant externe n'a pas connaissance des objets A et B. Il doit posséder les définitions des interfaces IA et IB pour communiquer avec le composant.

I-B. La déclaration en langage Pascal

Dans un langage objet, la manière la plus simple de définir une interface est d'utiliser la notion de classe abstraite pure.
Il s'agit d'une classe ne comportant que des méthodes abstraites :

 
Sélectionnez
type
  TZipper = class
  public
    procedure ZipFile; virtual; abstract;
    procedure UnZipFile; virtual; abstract;
  end;

Cette déclaration vous assure que toutes les classes dérivées de la classe TZipper comporteront les méthodes ZipFile et UnZipFile . On parle alors de contrat.

I-C. Le mot clé interface

Afin que vous ne soyez pas tenté de définir des méthodes non abstraites ou des données membres dans une classe abstraite pure, Inprise à introduit le mot clé interface . L'avantage de l'utilisation de ce mot clé par rapport à l'utilisation d'une classe abstraite est qu'il n'est pas possible de déclarer de données membres au sein d'une interface, et que toutes les méthodes sont obligatoirement publiques et abstraites. De plus, ce mot clé facilite l'association d'un GUID à l'interface pour l'identifier de manière unique. Consultez le paragraphe portant sur l'identification pour plus de détails.

Voici la même classe TZipper définie par le mot clé interface.

 
Sélectionnez
type
  IZipper = interface
    procedure Zip;
    procedure UnZip;
  end;

Voici quelques règles introduites avec ce mot clé :

  • Dans une interface, toutes les méthodes sont publiques et par conséquent, l'utilisation des opérateurs de portés des classes n'a pas de sens.
  • Par convention dans les sources de Delphi, le nom d'une interface débute par « I » et non par « T » comme pour les classes.
  • De même que les classes abstraites, une interface peut ne pas être instanciée et sera ignorée de toute façon par le compilateur. Mais à la différence de celle-ci, une interface ne peut pas être partiellement implémentée par une classe dans l'espoir qu'une classe dérivée implémente le reste. Une classe implémentant une interface doit implémenter toutes les entrées de l'interface.

I-D. Les propriétés

Une interface peut proposer une propriété à l'aide du mot clé property . Mais comme elle ne peut déclarer de données membres, les accès en lecture et en écriture se font obligatoirement à l'aide de méthodes.

 
Sélectionnez
type
 IZipper = interface
  procedure Set_Filename(f : string);
  function Get_Filename : string;
  property Filename : string read Get_Filename write Set_Filename;
 end;

Note
Par convention, les méthodes d'accès en lecture et en écriture pour une propriété doivent être préfixées par Get_ et Set_ pour un contrôle ActiveX.

I-E. La portabilité d'une interface

Le mot clé interface n'est reconnu que par le langage Pascal. Comment peut-on dans ce cas déclarer une interface pour d'autres langages ?

La réponse est l'utilisation de l'IDL. Il s'agit d'un langage de définition d'interfaces. Ce langage est transparent dans Delphi, et vous ne le rencontrerez que très rarement.

Mais l'approche standard est de définir un fichier IDL, puis d'utiliser un utilitaire tel que MIDL de Microsoft pour générer des classes C++.

Voici l'équivalent de l'interface IZipper, tel que déclarée ci-dessus, en langage IDL :

 
Sélectionnez
[
 uuid(F2737788-5902-11D2-8980-00600844A34A),
 version(1.0),
 helpstring("Interface pour Zipper Objet")
]
 interface IZipper: IUnknown
{
 [propget, id(0x00000002)]
 void _stdcall Filename([out, retval] long * Value );
 [propput, id(0x00000002)]
 void _stdcall Filename([in] long Value );
};

Delphi, quant à lui, dispose d'un éditeur de bibliothèque de types pour définir visuellement une interface. Cet utilitaire dispose d'une page nommée IDL permettant à tout moment de consulter l'équivalent de l'interface Pascal en langage IDL. Vous avez alors la possibilité de sauver ce texte source afin qu'il soit utilisé pour d'autres langages.

Cette opération est aussi réalisable avec l'utilitaire OLEView de Microsoft qui lit directement les bibliothèques de types et permet une exportation des interfaces en langage IDL. Inversement, à partir d'une source IDL, vous pouvez obtenir une bibliothèque de types en compilant le source à l'aide de l'exécutable MIDL.

La portabilité d'une interface est liée au standard défini par le langage IDL.

I-F. La CoClasse

Le terme de coClasse est employé pour désigner une classe qui implémente une ou plusieurs interfaces. Le moyen utilisé en langage Pascal s'apparente à celui de l'héritage multiple qui existe dans d'autres langages orientés objet :

 
Sélectionnez
type
  IZipHuffman = interface
    procedure ZipFile; safecall;
  end;

  TZipper = class(TInterfacedObject, IZipHuffman)
    // interface IZipHuffman
procedure ZipFile; safecall;
end;

Une coClasse doit suivre les règles suivantes :

  • Elle doit obligatoirement implémenter toutes les entrées d'une ou des interfaces qu'elle supporte.
  • Elle doit obligatoirement dériver d'une classe de base et celle-ci doit être la première listée.

La première règle est une question de bon sens. Si la coClasse n'implémente pas toutes les entrées des interfaces qu'elle doit implémenter, alors que produira l'appel à l'une des entrées non définies ?

La deuxième règle semble être un choix de Inprise pour simplifier l'analyse du code source. En effet, sans cette règle, le compilateur doit s'assurer que, parmi la liste des noms fournis dans la déclaration d'une classe, il n'y ait qu'une classe et que les autres soient bien des interfaces. Vous pouvez dériver de n'importe quelle classe, mais pour faciliter l'implémentation de COM, il est préférable de dériver au moins de la classe TInterfacedObject, car elle implémente l'interface IUnknown, nécessaire à tous les objets COM. Consultez le paragraphe qui suit pour plus d'informations.

Le langage Pascal n'autorise pas l'héritage multiple, mais en revanche, il autorise l'implémentation d'interfaces multiples. Voici une coClasse de l'interface IZipHuffman. Une coClasse qui implémente les interfaces IZipHuffman et IFile se déclare de cette manière :

 
Sélectionnez
type
  IZipHuffman = interface
    procedure ZipFile; safecall;
  end;

  IFile = interface
    function GetFilename : string; safecall;
    procedure SetFilename(f : string); safecall;
  end;

  TZipper = class(TInterfacedObject, IZipHuffman, IFIle)
    // interface IZipHuffman
    procedure ZipFile; safecall;
  
    // interface IFile
    function GetFilename : string; safecall;
    procedure SetFilename(f : string); safecall;
end;

L'interface IUnknown

Chaque développeur est libre de définir ses propres interfaces. Dans ce cas, comment se déplacer dans la mémoire d'un objet pour utiliser telle ou telle interface ? Comment le programme fait-il pour exécuter la méthode correspondant à une entrée d'une interface ?

À ces deux questions, voici leur réponse :

  • En ce qui concerne les méthodes, il y a un fort couplage entre l'interface et la table des méthodes virtuelles d'un objet dans Delphi.
  • Pour les interfaces, tous les objets COM doivent supporter l'interface IUnknown comme interface de base. Par conséquent, l'affectation d'un objet COM à une variable de type IUnknown fait toujours pointer la variable sur cette interface.

Ne connaissant pas un objet, il est toujours possible de demander son interface IUnknown. L'avantage est que cette interface décrit trois méthodes, dont une nommée QueryInterface permettant l'accès à d'autres interfaces supportées par l'objet.

Voici la définition de l'interface IUnknown :

 
Sélectionnez
IUnknown = interface
  ['{00000000-0000-0000-C000-000000000046}']
  function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
  function _AddRef: Integer; stdcall;
  function _Release: Integer; stdcall;
end;

Toutes les interfaces que vous définirez avec le mot clé interface héritent implicitement de l'interface IUnknown. Chaque objet COM doit implémenter les trois méthodes essentielles au bon fonctionnement du modèle :

  • La méthode QueryInterface permet d'interroger l'objet sur l'implémentation ou non d'une interface dans celui-ci. Il en est de la responsabilité de chaque programme d'implémenter la manière dont une interface est retrouvée.
  • La méthode _AddRef doit être appelée systématiquement lorsque l'objet COM est créé. Généralement, en Pascal, vous n'aurez pas à faire cet appel, puisque le constructeur des objets le fait. Le but de cette méthode est d'incrémenter de 1 le compteur de référence de l'objet.
  • La méthode _Release doit être appelée lorsque l'objet n'est plus utilisé. Elle a pour but de décrémenter le compteur de référence de l'objet, et si celui-ci est nul, le destructeur de l'objet doit être appelé afin de libérer la mémoire de l'objet.

Ces trois méthodes doivent obligatoirement être définies pour chaque objet COM.

Afin de ne pas avoir à le faire systématiquement, les différentes classes de base de Delphi, compatibles COM, implémentent cette interface. C'est le cas des classes TInterfacedObject, TComObject, TTypedComObject, TAutoObject, etc.

Note Généralement, vous n'aurez pas à gérer le compteur de référence d'un objet COM, Delphi s'en charge pour vous.

I-G. L'interface au niveau binaire

Dans le langage Pascal, une variable de type interface est en fait un pointeur sur une zone mémoire de l'objet qui l'implémente. Cette zone mémoire contient l'adresse de la table des méthodes virtuelles de l'objet. C'est pour ces raisons que l'on dit qu'une interface est un double pointeur.

Les différentes entrées d'une interface correspondent à un déplacement dans la table virtuelle de l'objet.

I-H. La recherche d'interface

Voici l'exemple qui montre l'utilisation de l'entrée QueryInterface afin de trouver une interface particulière dans un objet. Cet exemple montre une routine, TestZip, qui reçoit en paramètre un objet COM. Elle interroge l'objet reçu afin de savoir s'il supporte l'interface IZipper. Si c'est le cas, elle appelle la méthode ZipName.

 
Sélectionnez
type
 IZipper = interface
 ['{0A327261-2DE2-11D2-88E2-00600844A34A}']
 procedure ZipName; safecall;
end;


TMp3 = class(TInterfacedObject, IZipper)
 procedure ZipName; safecall;
end;

TVideo = class(TInterfacedObject)

end;

procedure TestZip(o : TObject);

var ik : IUnknown;
    z : IZipper;

begin
 if o.GetInterface(IUnknown, ik) then
 if ik.queryInterface(IZipper, z)=S_OK then z.ZipName;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
 TestZip(TMp3.Create); // un message s'affichera
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
 TestZip(TVideo.Create); // aucun message sera visible
end;

{ TMp3 }
procedure TMp3.ZipName;
begin
 ShowMessage('ZipperName');
end;

I-I. Le compteur de référence

L'utilisation d'un objet COM en tant qu'objet du langage Pascal, ne change en rien aux règles prédéfinies par le langage Pascal objet. Une variable de type class étant une référence, donc un pointeur, tout objet Pascal créé doit être manuellement détruit. Certains objets sont codés pour libérer leurs composants. Par exemple, la fermeture d'une fiche entraîne automatiquement la libération de tous les objets attribués à la fiche, car du code spécifique a été prévu pour.

En revanche, tout se complique avec l'utilisation des interfaces !

En effet, lorsqu'on manipule un objet COM via une interface, ce sont les règles du modèle COM qui rentrent en vigueur. Chaque objet COM défini par l'implémentation de l'interface IUnknown, un mécanisme de compteur de référence d'utilisation du composant. Ceci est très important, car c'est le seul mécanisme qui permet de détruire un objet de la mémoire lorsque l'objet est distribué.

Par conséquent, si le code suivant crée un objet de type TTest, quelque soit la nature de cet objet, l'instance créée ne sera libérée de la mémoire qu'à la fin d'exécution du processus.

 
Sélectionnez
procedure TForm1.Button1Click(Sender: TObject);
var  T : TTest;
begin
 T:=TTest.Create;
end;

Si TTest était un composant Edit, l'instance créée pourrait être montrée, et dans ce cas, l'instance reste après l'appel du gestionnaire d'événement.

Si au lieu d'utiliser une variable de type référence sur la classe, on utilise une variable d'une interface supportée par l'objet TTest, il en va autrement :

 
Sélectionnez
procedure TForm1.Button1Click(Sender: TObject);
var i : ITest;
begin
 i:=TTest.Create;
end;

En effet, dans ce cas, l'instance de la classe TTest créée, est immédiatement libérée à la sortie du gestionnaire d'événement. La raison est que l'objet COM étant utilisé par une interface, les méthodes _AddRef et _Release sont utilisées. Par conséquent, l'affectation de l'objet COM à la variable i à pour conséquence d'incrémenter le compteur de référence, le positionnant ainsi à 1. Lorsque le gestionnaire d'événement se termine, les variables locales sont libérées. La variable i ne pointe alors plus sur l'objet créé. La méthode _Release est appelée et comme le compteur de référence passe à 0, l'objet COM est détruit de la mémoire.

II. L'identification

Le paradoxe

Hors du langage qui l'a conçu, un objet COM n'existe que par l'intermédiaire des interfaces qu'il propose. Dans le modèle, c'est la seule chose qui peut être manipulé. Lorsqu'on désire travailler avec un objet, en réalité, on désire travailler avec une interface.

Par conséquent, l'objet COM n'existe pas en tant qu'objet, seuls ses services existent.

C'est pour cette raison que les objets COM ne sont pas identifiés, ce sont les interfaces qui le sont. Et c'est là qu'il y a un paradoxe, puisqu'on identifie une interface, alors qu'elles n'existent pas vraiment.

Étant donné que les objets COM peuvent être répartis sur plusieurs ordinateurs au travers d'un réseau local, départemental ou mondial, il ne doit pas y avoir d'ambiguïté lorsqu'on demande un service sur le réseau. Un objet ne doit pas pouvoir se substituer à un autre par erreur. L'identification d'une interface doit être unique.

Par conséquent, le modèle COM prévoit deux types d'identification, l'une sans ambiguïté : le CLSID, et l'autre plus pratique mais uniquement réservée à un usage local : le ProgID.

Le ClassID

Le ClassID ou CLSID est un identifiant codé sur 128 bits. En fait, le CLSID est un GUID, c'est-à-dire un Global Universal Identifiant défini par l'OSF . On parle de GUID pour définir le type d'identifiant, et de CLSID lorsque l'identifiant est associé à une classe.

Ce nombre est si grand que pour sa représentation, Microsoft adopte le format suivant :

 
Sélectionnez
{81449977-3730-11D2-8923-00600844A34A}

Le découpage des chiffres avec la séparation '-' permet juste de « mieux lire » ce nombre où chaque chiffre est un nombre hexadécimal.

L'algorithme de génération de ce nombre est complexe et assure qu'un même identifiant ne peut être généré plus d'une fois quelque soit l'ordinateur utilisé.

Comme ce nombre est gigantesque, sa manipulation s'effectue généralement sous forme de chaîne de caractères. Mais Delphi dispose de facilités pour permettre la conversion GUID en chaîne de caractères et inversement.

Delphi défini le type TGUID pour manipuler ces nombres. Par défaut Delphi encadre la chaîne du GUID par des crochets. Il s'agit de la syntaxe permettant d'associer un GUID à une classe.

Pour définir une variable de type GUID et l'initialiser avec une valeur, il convient d'utiliser la syntaxe suivante :

 
Sélectionnez
const CLASS_TestAuto: TGUID = '{8144997C-3730-11D2-8923-00600844A34A}';

Pour associer un GUID à une interface, on utilise la syntaxe suivante :

 
Sélectionnez
ICompress = interface
  ['{F273778C-5902-11D2-8980-00600844A34A}']
  procedure Process;
end;

L'éditeur de code de Delphi permet de générer facilement un GUID dans le code en appuyant simultanément sur les touches Ctrl Shift G.

Note
Delphi propose aussi deux routines pratiques dans l'unité ComObj : GUIDToString et StringToGUID. Elles permettent respectivement de convertir un GUID en chaîne de caractères et inversement.

Le ProgID

Le ProgID est une chaîne de caractères associée à un CLSID qui permet en local de construire un objet COM par son nom en clair. Généralement, le ProgID se compose du nom du programme séparé par un point du nom du service. Par exemple : la chaîne 'word.basic' est associée au CLSID de l'objet d'automation du traitement de texte WinWord.

Dans Delphi, le progID d'un objet se compose du nom du module dans lequel se trouve l'objet COM, suivi d'un point et du nom de l'interface sans le préfixe 'I'.

Note :
Delphi propose dans l'unité ComObj deux routines pratiques ; ClassIDToProgID et ProgIDToClassID. Elles permettent respectivement de convertir un GUID de classe en ProgID et inversement.

III. L'utilisation avancée des interfaces

L'héritage d'interfaces

De même qu'une classe, une interface peut dériver d'une autre dans le but de récupérer le contrat défini dans l'interface de base. En aucun cas, on ne récupère du code…

Une interface qui dérive d'une autre interface prend toutes ses définitions et ne garde aucune référence sur celle-ci. Par conséquent, un objet ne connait pas les différentes interfaces dérivées d'une interface qu'il implémente. En clair, on ne peut parcourir les différentes interfaces dérivées. L'interface résultante forme un bloc monolithique.

En revanche, si une classe Delphi, qui implémente une interface, dérive d'une autre classe qui implémente une interface différente de la première, l'objet COM résultant conserve les différentes interfaces implémentées par chacune des classes.Il n'y a pas d'héritage d'interfaces dans ce cas.

L'héritage de coClasse

III-A. Les méthodes statiques

Du fait qu'une classe Delphi peut dériver d'une autre classe, des masquages d'interfaces peuvent se produire. En effet, supposons qu'une classe TA implémente une interface nommée IA disposant d'une entrée nommée ProcStatic. Si une classe TB dérive de TA, sans implémenter l'interface IA, elle le supporte par héritage. Par conséquent, le code suivant fonctionne :

 
Sélectionnez
type
  IA = interface
    procedure Proc; safecall;
  end;

  TA = class(TInterfacedObject, IA)
    procedure Proc; safecall;
  end;

  TB = class(TA)
  end;

...

procedure TForm1.Button1Click(Sender : TObject);
var  IntfA : IA;
begin
  IntfA:=TB.Create;
  IntfA.Proc;
end;

Si la classe TB dispose d'une méthode de même signature que celle de l'entrée de l'interface IA sans l'implémenter, cette méthode ne sera pas liée à l'interface IA, et donc ne sera pas accessible via cette interface. La sollicitation de l'entrée appellera le code implémenté dans la classe TA.

Dans le cas où la classe TB implémente aussi l'interface IA, les méthodes de signature identique à celle de l'interface seront alors liées à l'interface. Par conséquent, l'appel d'une entrée de l'interface IA, appellera le code défini dans la classe d'où vous avez obtenu une instance.

III-B. Les méthodes virtuelles

Ce cas est obtenu lorsque la classe de base TA définie une méthode virtuelle pour l'entrée de l'interface IA, comme ceci :

 
Sélectionnez
type
 
 IA = interface
   procedure ProcVirtual; safecall;
 end;

 TA = class(TInterfacedObject, IA)
   procedure ProcVirtual; virtual; safecall;
 end;

Dans ce cas, que la classe TB surcharge ou non la méthode ProcVirtual , en implémentant ou non l'interface IA, l'appel de cette entrée d'un pointeur sur l'interface d'une instance de TB, déclenche le code implémenté dans TB. Bien entendu, une interface obtenue de l'instance TB appellera la méthode définie dans TB.

La redirection

Il se peut qu'une classe implémente deux interfaces qui disposent chacune d'une entrée de signature identique. La coClasse a deux possibilités, soit elle définit une méthode unique pour ces deux entrées, soit elle définit une méthode distincte à chacune.

Pour que l'explication soit plus explicite, nous nous appuierons sur un exemple utilisant les deux interfaces suivantes :

 
Sélectionnez
type
  IZipHuffman = interface
    procedure ZipFile; safecall;
  end;

  IZipLempel = interface
    procedure ZipFile; safecall;
  end;

On suppose que l'utilisation de l'interface IZipHuffman permet une compression avec la méthode Huffman, tandis que l'interface IZipLempel propose une compression LZW. Supposons maintenant une classe désirant implémenter ces deux interfaces. Un conflit se pose pour différencier l'implémentation de ces deux entrées.

Puisque les deux entrées sont identiques, la coClasse ne peut définir qu'une méthode disposant de cette signature. Le code suivant est accepté par le compilateur Pascal :

 
Sélectionnez
TZipper = class(TInterfacedObject, IZipHuffman, IZipLempel)

procedure ZipFile; safecall;

end;

...

procedure TForm1.Button1Click(Sender: TObject);
var	zHuffman : IZipHuffman;
	zLempel : IZipLempel;

begin
 zHuffman:=TZipper.Create;
 zHuffman.ZipFile; // appel de ZipFile de TZipper

 zLempel:=TZipper.Create;
 zLempel.ZipFile; // appel de ZipFile de TZipper
end;

Dans cet exemple, une instance de la classe TZipper manipulée avec l'interface IZipHuffman ou IZipLempel produira le même appel si l'entrée ZipFile est sollicitée.

Si au contraire, on désire différencier l'entrée ZipFile pour chacune des interfaces, on se doit de donner une implémentation différente par interface. Ceci est réalisable en redirigeant les implémentations des entrées sur des méthodes nommées différemment pour éviter les conflits de nom.
La redirection s'effectue en affectant à l'entrée spécifique d'une interface une méthode de la classe de nom quelconque, mais disposant d'une signature identique.

Voici comment utiliser cette technique pour définir un code spécifique à l'entrée ZipFile des interfaces IZipHuffman et IZipLempel :

 
Sélectionnez
TZipper = class(TInterfacedObject, IZipHuffman, IZipLempel)
  procedure IZipHuffman.ZipFile = ZipFileHuffman;
  procedure IZipLempel.ZipFile = ZipFileLempel;
  procedure ZipFileHuffman; safecall;
  procedure ZipFileLempel; safecall;
end;

Par conséquent, le comportement de l'objet Zipper diffère selon l'interface avec laquelle on le manipule.

La délégation

Avec le produit Delphi, Inprise a introduit la notion de RAD avec un modèle de délégation de code dans la fiche. Cela signifie qu'un gestionnaire d'événement est implémenté dans le code de la fiche et non dans l'objet sur lequel porte l'action. Si ce choix ne respecte pas la philosophie de la programmation objet, il simplifie énormément l'écriture de code. Tout le code des objets se trouve dans la fiche.

Cette même technique est applicable à l'implémentation des interfaces. Elle a aussi pour but de simplifier l'implémentation des interfaces en n'implémentant pas l'interface là où elle est utilisée. En réalité cette technique fourni deux avantages :

  • Une classe peut utiliser l'interface implémentée par une autre classe sans faire de l'héritage.
  • Une classe peut utiliser l'interface implémentée par une autre classe sans faire d'agrégation « manuellement » par l'écriture de code.

Supposons qu'une classe existante implémente déjà une interface à laquelle vous aimeriez bien doter un nouvel objet. Par exemple, la classe TZipper implémente l'interface IZipper permettant la compression et le décompression de fichiers. Vous désirez définir un composant TZipperFileListbox dérivé de la classe TFileListbox, qui bien sûr devra être muni de la compression et de la décompression. Si vous déclarez la classe TZipperFileListbox de la manière qui suit, vous devez implémenter l'interface IZipper dans cette nouvelle classe :

 
Sélectionnez
type
  TZipperFileListbox = class(TFileListbox, IZipper)

...
end;

Et par conséquent, vous devez implémenter l'interface IUnknown et l'interface IZipper, car la classe TFileListbox n'implémente aucune de ces deux interfaces. Cela est très frustrant, puisque la classe TZipper implémente déjà IZipper et IUnknown. Avec l'héritage multiple le problème serait résolu en faisant dériver la clase TZipperFileListbox des deux classes :

 
Sélectionnez
type
  TZipperFileListbox = class(TFileListbox, TZipper)
  end;

La classe TZipperFileListbox par ce bias implémenterait les deux interfaces. Or en Pascal, comme on le sait, l'héritage multiple de classes n'existe pas !

Dans de telle situation, la seule solution envisageable est l'agrégation. La classe TZipper devient une donnée membre de la classe TZipperFileListbox et les méthodes associées aux entrées de l'interface IZipper ne font qu'appeler les méthodes correspondantes de cette donnée membre. Cette technique fonctionne bien, mais comme il convient aussi d'implémenter l'interface IUnknown, elle s'avère pénible à l'écriture.

La délégation d'interface résous élégament ce problème. Elle permet à une classe d'implémenter une interface à partir du code contenu dans une autre. Cette classe hôte n'implémente pas obligatoirement l'interface, elle doit uniquement disposer de méthodes dont les signatures correspondent à celles des entrées de l'interface à implémenter.

La syntaxe choisie pour cela s'apparente à celle de la définition d'une propriété. Il faut définir une propriété en lecture seule du type de l'interface à implémenter ou d'une classe supportant cette interface, et utiliser le mot clé implements pour préciser l'interface implémentée.

Voici la syntaxe

 
Sélectionnez
property PropName : [Interface|Class] read FData implements Interface;

Il en résulte alors deux possibilités, soit la propriété est de type interface, soit elle est de type objet. Le choix doit être pris suivant que l'on désire on non utiliser la donnée membre soit comme objet, soit comme interface.

Et voici un exemple :

 
Sélectionnez
property MonIntf : IMonIntf read FMonIntf implements IMonIntf;

Voici un exemple d'implémentation de la classe TZipperFileListbox utilisant la délégation d'interface avec une classe qui implémente l'interface voulue

 
Sélectionnez
type
  IZipper = interface
    procedure ZipFile; safecall;
    procedure UnZipFile; safecall;
  end;

  TZipper = class(TAutoInterfaced, IZipper)
    procedure ZipFile;
    procedure UnZipFile;
  end;

  TZipperFileListbox = class(TFileListbox, IZipper)
  private
    FZipper : IZipper;
  public
    constructor Create(AOwner : TComponent); override;
    property Zipper : IZipper read FZipper implements IZipper;
  end;

constructor TZipperFileListbox.Create(AOwner : TComponent);
begin
  inherited Create(AOwner);
  FZipper := TZipper.Create;
end;

Notez l'absence de destructeur pour libérer la mémoire de l'objet.

IV. Les interfaces et la POO

Le polymorphisme

Les interfaces apportent la notion de polymorphisme dans le sens où un objet quelconque peut être affecté à une variable de type interface si celui-ci supporte cette interface. Le comportement de l'objet est connu, mais son implémentation peut complètement différer entre deux objets COM.

En affectant dynamiquement un objet COM à un programme basé sur des interfaces, on obtient un comportement polymorphe du programme.

Le polymorphisme est aussi obtenu par un objet COM en supportant plusieurs interfaces. Ainsi, un composant peut être utilisé de manière différente suivant l'interface avec laquelle on le manipule.

Le polymorphisme est binaire et non basé sur la syntaxe du langage utilisé.

L'héritage multiple

La notion d'interface résous élégamment l'absence d'héritage multiple dans le langage. Un des reproches de cette notion est de ne pas avoir de contrôle sur les classes dérivées, et le cas familièrement montré est celui de l'héritage triangulaire. Les classes TB et TC dérivant de la classe TA, et une classe TD dérivant de TB et TC. Dans ce cas, la classe TD dérive de la classe TA à la fois de TB et de TC. Dans ce cas, comment sont gérées les données membres de la classe TA au niveau de TD ?

La notion d'interface et de délégation d'interface facilitent le regroupement de caractéristiques de plusieurs classes au sein d'une autre, sans procéder à de l'héritage multiple. Par ce biais, et surtout par la simplicité de la délégation d'interface, il est possible de simuler les possibilités de l'héritage multiple, sans en avoir les inconvénients.

Par exemple, si vous désirez qu'un composant dérivé de TEdit affiche l'heure dans sa zone d'édition en utilisant un thread, il suffirait de créer un nouveau composant qui hérite de TEdit et de TThread. Or, ne disposant pas d'héritage multiple, votre composant doit utiliser l'agrégation pour exécuter son code dans un thread. Une autre solution consiste à utiliser une interface et à définir un objet thread, dont le constructeur prend en paramètre un objet supportant cette interface. Si celle-ci se nomme IThread, elle ne propose qu'une entrée nommée Run. La méthode Execute de l'objet thread se contente d'appeler la méthode Run via l'interface IThread. L'objet qui désire se doter de multitraitement doit implémenter cette interface et coder son traitement dans la méthode Run . L'objet devra être construit puis fourni au constructeur de l'objet Thread. Voici un exemple complet :

 
Sélectionnez
type

 IThread = interface
  procedure Run; safecall;
 end;

 TThreadIntf = class(TThread)
  ThreadIntf : IThread;
  constructor Create(it : IThread);
  procedure Execute; override;
 end;

 TEditTime = class(TEdit, IThread)
   procedure run; safecall;
 end;


{ TEditTime }
procedure TEditTime.Run;
begin
  while true do
    text:=TimeToStr(time);
end;


{ TEditThread }
constructor TThreadIntf.Create(it: IThread);
begin
 inherited Create(true);
 ThreadIntf:=it;
 Resume;
end;

procedure TEditThread.Execute;
begin
  ThreadIntf.Run;
end;

procedure TForm1.Button1Click(Sender: TObject);
var et : TEditTime;

begin
 et:=TEditTime.create(self);
 et.Parent:=self;
 et.run;

 TThreadIntf.Create(et);
end;

Une implémentation plus concise consiste à surcharger le constructeur de base de notre composant EditTime comme ceci :

 
Sélectionnez
constructor TEditTime.Create(AOwner : TObject);
begin
  inherited Create(AOwner);
  TThreadIntf.Create(self);
end;

V. Le stockage

Les objets COM sont stockés soit dans un exécutable, soit dans une DLL ou dans un fichier d'extension .ocx suivant leur nature. Généralement, un exécutable refermera un objet d'automation, tandis qu'une DLL contiendra aussi bien un objet d'automation qu'un simple objet COM. En revanche, les ActiveX sont généralement stockés dans une DLL dont l'extension est .ocx .

Mais quelque soit le lieu de stockage, l'objet COM doit s'enregistrer dans la base des registres pour être accessible par d'autre objets.

L'enregistrement d'un objet

Un exécutable d'automation s'enregistre de manière temporaire lors de sa première exécution pour toute la durée de la session courante, et de manière définitive en lui fournissant comme paramètre de la ligne de commande /regserver . Pour supprimer son enregistrement, il suffit de l'exécuter à nouveau avec le paramètre /unregserver .

Dans les autres cas, qu'il s'agisse d'un exécutable ou d'une DLL contenant des objets COM, Windows s'attend à trouver les routines suivantes pour l'enregistrement des objets :

 
Sélectionnez
function DllRegisterServer: HResult; stdcall;
function DllUnregisterServer: HResult; stdcall;

Pour utiliser ces routines, vous pouvez soit développer un petit programme qui charge la DLL et qui appelle ses routines, soit utiliser un utilitaire tel que tregsrv.exe fourni avec Delphi dans le répertoire \bin .

Les informations de la base des registres

Pour être accessible par d'autre, un objet COM doit enregistrer son GUID en tant que sous clé de la clé HKey_Classe_Root/CLSID dans la base des registres. Cette nouvelle clé devra au minimum avoir les sous-clés suivantes si l'objet est stocké dans une DLL : InprocServer32 et ProgID.

  • La sous-clé InprocServer32 contient comme valeur par défaut, le chemin complet de la DLL qui contient l'objet COM.
  • La sous-clé ProgID contient comme valeur par défaut le ProgID de l'objet.

Mais rassurez-vous, ces informations sont automatiquement insérer dans la base des registres par le fabriquant de classe associé à l'objet COM.

La bibliothèque ActiveX

Delphi propose dans le référentiel d'objet, la possibilité de générer une bibliothèque ActiveX. Bien que le terme d'ActiveX soit utilisé, il s'agit en fait d'une DLL disposant des quatre routines nécessaires à la création d'un conteneur d'objets COM. Par conséquent, la bibliothèque ActiveX concerne aussi bien le stockage d'objets COM, d'objets d'automation que les contrôles ActiveX.

Le code de cette DLL se contente d'exporter quatre routines définies prédéfinies dans l'unité ComServ . Ces routines utilisent la même instance de l'objet ComServer que les fabriquants de classe, car cette instance est définie dans l'unité ConServ, utilisée dans la DLL et le source de la routine.

Voici la structure type du source d'une bibliothèque ActiveX :

 
Sélectionnez
library Project1;
uses ComServ;
 
 exports DllGetClassObject,
	 DllCanUnloadNow,
 	 DllRegisterServer,
	 DllUnregisterServer;

{$R *.RES}

begin

end.

Les deux dernières méthodes permettent la gestion de l'enregistrement de la DLL dans la base des registres. Ces deux routines ont déjà été explicitées au dessus.

La routine DllGetClassObjet est celle que le système appel pour obtenir une instance d'un objet. Voici son prototype :

 
Sélectionnez
function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult; stdcall;

La routine DllCanUnloadNow est aussi appelée par le système d'exploitation pour rapidement savoir si la DLL peut être déchargée de la mémoire. Cette routine accède à un compteur global d'utilisation des différents objets disponibles dans la DLL.

VI. Premier exemple : Un objet sans frontière

La création

Un objet COM peut être codé manuellement de A à Z dans un source en définissant ses interfaces, la coClasse, puis le fabriquant de classe. Seulement Delphi propose une autre solution afin de faciliter son développement et sa maintenance grâce à un expert, nommé Objet COM, disponible dans la page ActiveX du référentiel d'objets.

L'appel de cet expert vous présente une boîte de dialogue afin de préciser les caractéristiques de l'objet COM voulu :

  • Indiquez le nom de la classe sans son préfixe 'T' qui sera automatiquement ajouté.
  • Spécifiez le type d'instance voulu pour cet objet.
  • Spécifiez le modèle de thread que vous désirez pour cet objet.
  • S'il y en a, indiquez les noms des interfaces qui seront supportées par cet objet afin que Delphi les rajoute dans le partie déclarative de l'objet.
  • Associez un descriptif à cet objet.
  • Eventuellement, vous pouvez inclure une bibliothèque de type à votre objet.

L'utilisation

Pour utiliser un objet COM, il convient de connaître son interface et son GUID. Dans le cas d'un objet local, celui-ci peut aussi être accédé par son ProgID.

VI-A. Création à partir d'un GUID

La création d'un objet COM disponible sur la machine locale est réalisée par la routine CreateCOMObject définie dans l'unité ComObj . Cette routine simplifie la création d'objet COM, car elle appelle la routine de l'API Windows CoCreateInstance qui nécessite plus de paramètres.

 
Sélectionnez
const CLASS_TestZipper: TGUID = '{F2737798-5902-11D2-8980-00600844A34A}';

type

 ITestZipper = interface(IUnknown)
  ['{F2737796-5902-11D2-8980-00600844A34A}']
  procedure Process(const Filename: WideString); stdcall;
 end;

procedure TForm1.Button1Click(Sender: TObject);
var itz : ITestZipper;

begin
 itz:=CreateCOMObject(CLASS_TestZipper) as ITestZipper;
 itz.Process('Fichier.bat');
end;

Si vous disposez de l'unité associée à la bibliothèque de type de l'objet COM, vous pouvez encore simplifier la construction de l'objet en vous aidant d'une méthode statique Create définie dans une classe qui porte le nom de l'interface, mais en remplaçant le préfixe 'I' par 'co'. Par conséquent, l'exemple au dessus peut aussi s'écrire :

 
Sélectionnez
procedure TForm1.Button3Click(Sender: TObject);
var its :ITestZipper;

begin
 its:=coTestZipper.Create;
 its.Process('DDL.txt');
end;

VI-B. Création à partir d'un ProgID

La création d'un objet COM à l'aide de son ProgID n'est pas directe. En effet, il convient de rechercher dans la base des registres, un objet COM référencé dont la sous clé ProgID contenant une valeur avec la chaîne du ProgID. Si cette valeur est trouvée, alors on récupère la clé parente qui contient le GUID de l'objet COM, afin d'appeler la routine CreateCOMObject. Afin de ne pas avoir à faire cette recherche dans la base des registres, Delphi propose la routine ProgIDToClassID pour obtenir le GUID d'un objet à partir de son ProgID. Voici un exemple d'utilisation :

 
Sélectionnez
procedure TForm1.Button2Click(Sender: TObject);
var itz : ITestZipper;

begin
 itz:=CreateCOMObject(ProgIDToClassID('Zipper.TestZipper')) as ITestZipper;
 itz.Process('Fichier.bat');
end;

VII. Deuxième exemple : Les liens de fichier

Un fichier de lien porte l'extension .lnk et contient les informations permettant de retrouver le fichier pointé, même si celui-ci est physiquement déplacé ou renommé par la suite. Le système traque les opérations portant sur les ressources fichiers et garantit l'intégrité des informations contenues dans un lien de fichier.

Malheureusement, un lien de fichier ne peut être créé par programmation comme un vulgaire fichier. Cette opération nécessite l'utilisation de techniques plus avancées car elle s'appuie sur le concept COM…

La création d'un lien de fichier

Pour créer un lien sur un fichier, il faut disposer d'un objet supportant l'interface IShellLink définie dans l'unité shlobj . Cette interface est définie comme suit :

 
Sélectionnez
IShellLinkA = interface(IUnknown) { sl }
  [SID_IShellLinkA]
   function GetPath(pszFile: PAnsiChar; cchMaxPath: Integer;var pfd: TWin32FindData; fFlags: DWORD): HResult; stdcall;

   function GetIDList(var ppidl: PItemIDList): HResult; stdcall;

   function SetIDList(pidl: PItemIDList): HResult; stdcall;

   function GetDescription(pszName: PAnsiChar; cchMaxName: Integer): HResult; stdcall;

   function SetDescription(pszName: PAnsiChar): HResult; stdcall;

   function GetWorkingDirectory(pszDir: PAnsiChar; cchMaxPath:Integer): HResult; stdcall;

   function SetWorkingDirectory(pszDir: PAnsiChar): HResult; stdcall;

   function GetArguments(pszArgs: PAnsiChar; cchMaxPath: Integer):HResult; stdcall;

   function SetArguments(pszArgs: PAnsiChar): HResult; stdcall;

   function GetHotkey(var pwHotkey: Word): HResult; stdcall;

   function SetHotkey(wHotkey: Word): HResult; stdcall;

   function GetShowCmd(out piShowCmd: Integer): HResult; stdcall;

   function SetShowCmd(iShowCmd: Integer): HResult; stdcall;

   function GetIconLocation(pszIconPath: PAnsiChar;cchIconPath: Integer; out piIcon: Integer): HResult; stdcall;

   function SetIconLocation(pszIconPath: PAnsiChar;iIcon: Integer): HResult; stdcall;

   function SetRelativePath(pszPathRel: PAnsiChar; dwReserved: DWORD):HResult; stdcall;

   function Resolve(Wnd: HWND; fFlags: DWORD): HResult; stdcall;

   function SetPath(pszFile: PAnsiChar): HResult; stdcall;
end;

IShellLink = IShellLinkA;

En incluant dans votre source l'unité ShlObj , voici comment vous obtiendrez un objet supportant cette interface :

 
Sélectionnez
var isl : IShellLink;

...
  isl:=CreateComObject(CLSID_ShellLink) as IShellLink;
...

L'interface IShellLink propose les méthodes SetPath, SetDescription et SetShowCmd qui permettent de spécifier le chemin du fichier dont on désire obtenir un lien, une description à lui associer et sa visibilité. Bien que les deux derniers paramètres soient optionnels, il convient de les renseigner, d'où les appels suivants :

 
Sélectionnez
isl.SetPath(PChar(SrcFile));
 isl.SetDescription(PChar(Description));
 isl.SetShowCmd(1);  // SW_SHOW

Les variables SrcFile et Description sont des variables de type string .

Maintenant que l'on dispose de toutes les informations sur le fichier source, il convient de sauver le lien. Cette opération nécessite deux étapes, car le nom du fichier de lien doit être fourni en format WideChar. Il faut dans un premier temps créer une chaîne de caractère au format WideChar contenant le nom et l'emplacement du fichier de lien. Pour cela, vous disposez de la routine MultiByteToWideChar qui converti une chaîne de type string en WideChar. Puis, dans un deuxième temps, il convient de sauver le fichier de lien. Cette opération se réalise en obtenant l'interface IPersistFile de l'objet supportant l'interface IShellLink. Cette interface dispose d'une méthode Save autorisant la sauvegarde du fichier de lien. L'obtention de l'interface IPersistFile s'obtient en utilisant la méthode QueryInterface comme suit :

 
Sélectionnez
var
...
  ipf : IPersistFile;

...
  isl.QueryInterface(IPersistFile, ipf);
  ipf.Save(aws, true);
...

La variable aws représente le nom du fichier de lien en format WideChar. La création d'un fichier de lien peut alors être codée dans une procédure afin que le code de cette opération soit réutilisé plus facilement. En voici un exemple :

 
Sélectionnez
procedure CreateFileLink(SrcFile, DstFileLink, Description: string);
var	ipf : IPersistFile;
	isl : IShellLink;
	aws : array[0..MAX_PATH] of WideChar;

begin
  isl:=CreateComObject(CLSID_ShellLink) as IShellLink;
  isl.QueryInterface(IPersistFile, ipf);
  isl.SetPath(PChar(SrcFile));
  isl.SetDescription(PChar(Description));
  isl.SetShowCmd(SW_SHOW);
  MultiByteToWideChar(CP_ACP, 0, PChar(DstFileLink), -1, @aws, MAX_PATH);
  ipf.Save(aws, true);
end;

Par conséquent, pour créer un fichier de lien, il suffit de faire appel à cette fonction avec en paramètres le chemin du fichier d'origine, le chemin du fichier de lien et une description. Voici un exemple qui crée un lien sur le fichier calc.exe du répertoire \Windows sur la racine avec le nom calc.lnk :

 
Sélectionnez
CreateFileLink('c:\windows\calc.exe',
               'c:\calc.lnk',
               'Ceci est un test');

VIII. Troisième exemple : Se substituer au système

De nombreux objets Windows disposent d'un menu contextuel. C'est le cas d'un fichier lorsqu'on le consulte de l'explorateur ou du poste de travail Windows. Suivant le type de fichier, le menu contextuel peut présenter des options personnalisées. Ainsi, les fichiers de type batch qui portent l'extension .bat disposent de l'option Edition dans leur menu contextuel. Cette option lance Winpad pour éditer le contenu du fichier. Il s'agit d'un ajout par rapport aux options de base.

Ce côté dynamique de l'interface Windows est obtenu grâce à la base des registres. Chaque type de fichier est vu comme un objet COM s'il est répertoirié dans la clé : HKey_Classes_Root .

Deux solutions s'offrent alors à vous, soit l'option de menu spécifique à un fichier est inscrite de manière statique, soit l'option prend un caractère dynamique en lui associant un objet COM spécifique.

Les bases

Pour personnaliser un type de fichier, il convient d'enregistrer l'extension de ce fichier dans la base des registres dans la clé HKey_Classes_Root .
L'enregistrement consiste à créer une sous-clé avec le nom de l'extension, le point compris. Du fait de l'historique de Windows, les extensions sont peu lisibles car elles tiennent généralement sur trois caractères. Pour pallier à ce problème, la sous-clé au nom de l'extension doit contenir une valeur par défaut avec une chaîne de caractères identifiant de manière lisible l'extension du fichier. Ce même identifiant sera le nom d'une autre sous-clé de la clé HKey_Classes_Root dans lequel seront spécifiées toutes les personnalisations.

Par exemple, les fichiers batch ont un nœud nommé .bat qui contient la valeur par défaut batfile. Ce nom correspond à une autre clé de HKey_Classes_Root qui dispose des sous-clés suivantes : DefaultIcon , Shell et ShellEx .

  • La valeur par défaut de la sous-clé DefaultIcon contient le nom du module contenant la ressource icône à associer au fichier, suivi de l'indice de l'icône dans le module.
  • La sous-clé Shell contient les options de menu associées au fichier.
  • La sous-clé ShellEx contient des options de personnalisation avancées, telle que l'association d'une page de propriété, etc.

Chaque sous-clé de la clé Shell apparaîtra comme une option du menu contextuel associée à l'extension.

L'ajout statique

VIII-A. La clé Shell

Ainsi, si vous créez une sous-clé nommé « Coucou » dans la clé Shell associée à une extension de fichier, une option de même nom sera visible dans son menu contextuel. Si le titre de l'option est long, il est préférable de ne pas alourdir le nom de clé, et de créer une valeur par défaut disposant de la chaîne de caractères. Windows vérifie si une valeur par défaut existe et choisi la chaîne pour titre.

Pour chaque option de menu personnalisée, l'action à entreprendre si l'option est sélectionnée doit être précisée dans une sous-clé de la clé de l'option. Elle devra obligatoirement être nommée « Command » et avoir pour valeur par défaut, la séquence à exécuter. Exemple : menu contextuel statique :

Généralement, il s'agit d'un exécutable auquel on passe en paramètre le nom du fichier grâce à "%1 ». Par exemple, pour l'extension de fichier batch, voici la valeur de la clé Command :

 
Sélectionnez
%SystemRoot%\System32\NOTEPAD.EXE %1

VIII-B. Les particularités

Dans le cas où la sous-clé de Shell se nomme Open ou Print , et si ces clés ne disposent pas de chaînes de caractères pour leur valeur par défaut, alors Windows affichera « Ouvrir » et « Impression » et non « Open » et « Print ». Il s'agit là d'une exception, Windows les comprend comme des mots réservés et traduit le mot dans la langue courante du système d'exploitation.

Cette personnalisation est très simple, mais elle ne permet pas de changer de manière dynamique le nom de l'option ou le comportement de celui-ci. Ceci n'est possible que si un objet COM est associé à l'option.

L'ajout dynamique

VIII-C. La clé ShellEx

L'ajout d'une option de menu de manière dynamique s'effectue en associant à l'option de menu un objet COM. Dans ce cas, on défini ce qu'on nomme une extension du shell. Il existe sept types d'extensions possibles qui doivent être définis dans une clé nommée ShellExt du fichier associé.

A chaque type d'extension du shell correspond une sous-clé spécifique. Ainsi, tous les contextes de menu associés à une extension de fichier doivent être définis dans la sous-clé ContextMenuHandlers .

Pour chacune des options que vous désirez rajouter, il faudra définir une sous-clé de ContextMenuHandlers de nom quelconque et dont la valeur par défaut est le CLSID de l'objet COM.

VIII-D. L'objet COM

L'objet COM doit implémenter deux interfaces : IShellExtInit et IContextMenu. La première interface permet de récupérer le nom du fichier qui a sollicité le menu, tandis que la deuxième contient le code qui renvoie la chaîne de caractères de l'option de menu, une chaîne de commande, et le code qui doit être exécuté si l'option est sélectionnée.

L'interface IShellExtInit

L'interface IShellExtInit ne défini qu'une seule méthode Initialize qui vous permet de coder des instructions lorsque l'extension du Shell est déclenchée. En ce qui concerne le menu contextuel, l'implémentation de cette entrée n'est pas obligatoire, mais il s'agit du meilleur endroit pour récupérer le nom du fichier qui est sollicité.

Voici la définition de cette interface :

 
Sélectionnez
IShellExtInit = interface(IUnknown)
  [SID_IShellExtInit]
   function Initialize(pidlFolder: PItemIDList; lpdobj: IDataObject; hKeyProgID: HKEY): HResult; stdcall;
end;

Le paramètre lpdobj de l'entrée Initialize est un objet supportant l'interface IDataObject. Cette interface décrite pour le Glisser/déplacer donne des indications sur la nature des objets manipulés. Dans notre cas, IDataObject est capable de nous fournir le nom des fichiers sur lequel porte la demande du menu contextuel. Bien que les opérations à effectuer pour récupérer les différentes informations de l'interface IDataObject ont été vues dans le précédent paragraphe, nous pouvons aussi utiliser la routine QueryDragFiles pour récupérer plus simplement le nom du fichier manipulé.

L'interface IContextMenu

Lorsque le menu contextuel de l'objet est sollicité, l'entrée QueryContextMenu est appelée pour que l'objet COM puisse ajouter ses options.

Plusieurs options de menu peuvent être rajoutées, il suffit pour cela d'incrémenter de un le paramètre IndexMenu pour chacune des options et de spécifier comme valeur de retour, le nombre d'options rajoutées.

Pour identifier chaque option de menu, vous devez utiliser la valeur idCmdFirst sans dépasser idCmdLast . Lorsqu'une option est sélectionnée, la méthode InvokeCommand est appelée, et son paramètre contient l'indice associé à l'option de menu sollicitée.

Voici la définition de cette interface :

 
Sélectionnez
IContextMenu = interface(IUnknown)
 [SID_IContextMenu]
  function QueryContextMenu(Menu: HMENU;indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
  function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
  function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;

Le fabriquant de classe

Au lieu d'utiliser le fabriquant de classe d'un objet COM de base, il est préférable de définir une classe dérivée et de personnaliser l'enregistrement.

En ce qui concerne un contexte de menu, l'enregistrement consiste à :

  • créer la clé ShellEx pour l'extension de fichier si elle n'existe pas déjà.
  • créer la sous-clé ContextMenuHandlers si elle n'existe pas déjà.
  • ajouter une sous-clé et une valeur par défaut avec le GUID de l'objet COM dans la clé ContextMenuHandlers.

Une dernière opération est nécessaire pour Windows NT, il faut en effet « approuver » l'objet COM en définissant une sous-clé dans la clé SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved . Cette sous clé devra porter le GUID de l'objet COM comme nom et avoir un descriptif de l'objet comme valeur par défaut de la clé.

Le code complet

Voici le code complet d'un exemple qui permet de mettre ou de supprimer les informations d'une DLL COM dans la base des registres de notre menu contextuel associé aux fichiers de type DLL :

IX. Quatrième exemple : Les contrôles ActiveX

Pour créer un contrôle ActiveX, choisissez l'objet Contrôle ActiveX de la feuille ActiveX du référentiel d'objets. La boîte de dialogue de l'expert ActiveX apparaîtra.

Dans la première partie de cette boîte, vous devez spécifier le nom de la classe du VCL pour lequel vous désirez obtenir un ActiveX. Ce VCL doit obligatoirement être installé dans un paquet actif pour être accessible. Automatiquement, une fois que vous avez fourni le nom de la classe, Delphi compose un nom pour le contrôle ActiveX, son unité, ainsi que le source du projet à partir du nom de la classe. Vous pouvez changer le contenu de ces informations et par conséquent spécifier un nom de source de projet existant. Plusieurs contrôles ActiveX peuvent cohabiter dans le même projet.

Dans la deuxième partie, l'expert vous demande de renseigner les informations dont vous aimerez voir doté votre contrôle ActiveX. Par défaut, aucune information n'est stockée dans le contrôle. Mais vous pouvez demander que les informations suivantes soient ajoutées :

  • la licence de conception
  • les informations de version
  • la boîte à propos.

La licence de conception permet de générer un fichier d'extension .lic associé au contrôle. Ce fichier contient une clé qui est nécessaire pour utiliser le contrôle en environnement de conception. Un développeur qui n'aurait pas ce fichier ne pourrait pas se servir du contrôle pour développer une fiche, quel que soit le langage utilisé. Cette possibilité vous permet à la fois de ne pas autoriser d'autres développeurs à réutiliser votre contrôle dans leur application, et de vendre un droit d'utilisation lorsque vous désirez diffuser un contrôle. Dans un projet de type ActiveX, il peut y avoir plusieurs contrôles ; aussi, si cette option est cochée, il y aura dans le fichier .lic autant de clés qu'il y a de contrôles. Le source du projet d'un contrôle ActiveX est dit bibliothèque ActiveX ou serveur ActiveX.

Les informations de version ajoutent aux ressources du contrôle ActiveX des informations sur le module, telles que le copyright, le numéro de version, une description, etc. Pour renseigner ces informations, il faut compléter la feuille Version des options du projet. Ces informations sont obligatoires pour recenser un contrôle dans Visual Basic 4.0.

Les informations concernant la boîte A propos permettent d'inclure dans le projet une fiche modèle visible lorsque le contrôle est utilisé en mode conception dans un environnement de développement. Puisque la fiche incluse est une fiche Delphi, vous pouvez la personnaliser comme bon vous semble…

Après avoir renseigné les deux parties de la boîte de l'expert, Delphi génère un source de projet nommé bibliothèque ActiveX, une unité encapsulant le VCL afin de le transformer en ActiveX et une bibliothèque de types afin de décrire les interfaces de ce contrôle.

Comme une bibliothèque de types est associée au contrôle ActiveX, vous pourrez éditer ses informations par l'intermédiaire de l'éditeur de bibliothèque de types et, ainsi, modifier, ajouter et supprimer des informations de type. Ceci est quelquefois utile, car l'expert de Delphi n'exporte pas toutes les propriétés d'un VCL.

Le déploiement du contrôle ActiveX

Une fois le contrôle défini, il faut générer le fichier .ocx . Vous pouvez utiliser l'option Tout construire du menu Projet , mais vous pouvez aussi déployer immédiatement votre contrôle sur un serveur Web. L'avantage pratique de cette dernière possibilité est que Delphi génère une page HTML permettant de tester immédiatement le contrôle en se connectant au serveur Web. Mais l'option de déploiement d'un contrôle ActiveX permet de définir bon nombre de paramètres nécessaires au déploiement d'un objet sur le Net…

Pour déployer un contrôle ActiveX, il faut compléter les options de déploiement accessibles par l'option Options de déploiement Web… du menu Projet .

La première page vous permet de spécifier les emplacements où seront générés le contrôle et sa page de test HTML. Elle vous permet aussi de formuler une URL pour générer directement le contrôle à une adresse Web.

La deuxième page permet, si l'option Construire avec les paquets d'exécution est cochée, de spécifier les paquets qui devront être déployés avec le contrôle.

La troisième page permet de déployer le contrôle avec des fichiers complémentaires, tandis que la dernière page permet de spécifier les choix d'encodage des fichiers déployés.

Les pages de propriétés

Les pages de propriétés sont les informations disponibles pour un contrôle ActiveX par l'option Propriété de leur menu contextuel. Habituellement, cette option fait apparaître une boîte de dialogue, muni d'un composant PageControl, qui permet la personnalisation du contrôle. Exemple la boîte des propriétés du composant TeeChart.

Vous pouvez définir autant de pages de propriétés que vous le désirez. Chacune des pages se traduira par une page du composant PageControl de la boîte de dialogue des propriétés.

IX-A. La création d'une page de propriétés

Pour créer une page de propriétés pour un contrôle ActiveX, ajoutez au projet de ce contrôle un objet « Page propriétés » accessible dans le référentiel d'objet de la page ActiveX. Une nouvelle fiche sera ajoutée au projet, qui servira de support au dessin de la page de propriétés du contrôle.

Dessinez alors la page avec les informations que vous désirez proposer.

Le source de la page de propriétés dispose de deux méthodes vides. Il s'agit des méthodes UpdatePropertyPage et UpdateObject . La méthode UpdatePropertyPage est systématiquement appelée lorsque la page de propriétés s'affiche la première fois. Le code de cet événement permet donc de renseigner les différents champs de page de propriétés à partir du contrôle ActiveX. La méthode UpdateObject est quant à elle appelée lorsque la page de propriétés est validée. Cela permet de mettre à jour les propriétés du contrôle.

Une fois la page liée au contrôle, tous ses objets peuvent accéder au contrôle ActiveX par la donnée membre OleObject. Par conséquent, on peut affecter ou lire une information du contrôle par le biais de cette donnée membre. Ainsi, si votre contrôle ActiveX dispose d'une zone d'édition nommée ValeurMonetaire , voici comment vous lui affecterez une nouvelle valeur via une zone d'édition de le page de propriétés portant le nom Edit1 :

 
Sélectionnez
procedure TPropertyPage1.UpdateObject;
begin
    // Mets à jour OleObject depuis vos contrôles
  OleObject.ValeurMonetaire.Text:=Edit1.Text;
end;

Pour pouvoir synchroniser la valeur présente dans la page de propriétés avec celle du contrôle ActiveX, il convient de faire l'affectation suivante :

 
Sélectionnez
procedure TPropertyPage1.UpdatePropertyPage;
begin
    // Mets à jour vos contrôles depuis OleObject
  Edit1.Text:=OleObject.ValeurMonetaire.Text;
end;

IX-B. L'enregistrement de la page de propriétés

L'enregistrement de la page de propriétés et du contrôle est l'étape ultime pour voir apparaître la page de propriétés lorsqu'on utilise le composant en phase de conception. Cette opération s'effectue en surchargeant la méthode DefinePropertyPages de la classe TPropertyPage représentant la page de propriétés. Normalement, l'expert Delphi vous génère le corps de cette méthode qui prend en paramètre une fonction nommée DefinePropertyPage. Le corps de cette méthode se résume alors à appeler cette fonction en lui fournissant en paramètre le CLSID de la page de propriétés qui est défini dans l'unité implémentant la page de propriétés. Par défaut, il est représenté par une constante dont le préfixe est class_ suivi du nom de la classe définissant la page de propriétés sans le T.

 
Sélectionnez
type
  TActiveFormX = class(TActiveForm, IActiveFormX)
   ...
   procedure DefinePropertyPages(DefinePropertyPage : TDefinePropertyPage); override;
   ...
  end;

implementation

uses ComServ, ActiveFormProp;

{$R *.DFM}

{ TActiveFormX }

...

procedure TActiveFormX.DefinePropertyPages(DefinePropertyPage : TDefinePropertyPage);
begin
 DefinePropertyPage(Class_PropertyPage1);
end;

...

begin

end.

Après installation de l'ActiveX, la page de propriétés est accessible par le menu contextuel du contrôle :

X. Mise en œuvre du modèle COM

Le modèle COM permet la communication entre des objets de nature et de processus différents s'exécutant sur la même machine. L'extension DCOM apporte la couche réseau qui permet à des objets COM de processus situés sur des machines distantes, et sur des plates-formes différentes, de communiquer entre eux.

Initialement développé pour la plate-forme Windows, DCOM est maintenant disponible sur des plates-formes UNIX, dont Solaris, Linux et HP/UX. Pour plus de détails et pour télécharger les différentes version de DCOM en version test, connectez-vous sur le site www.sagus.com

Cet architecture, développée pour les plates-formes Windows 32 bits, repose entièrement sur le protocole réseau TCP/IP. Par conséquent, la mise en uvre d'applications distribuées nécessite dans un premier temps une bonne configuration des paramètres réseau et du protocole TCP/IP. Dans un second temps, lorsque cette couche logiciel est configurée, des outils annexes permettent la configuration des droits d'accès sur les objets COM d'un serveur. Selon que le poste utilisé fonctionne sur Windows 95 ou sur Windows NT, la configuration des accès peut différer.

Ce chapitre se décompose en quatre parties :

  • La première porte sur l'installation et la configuration de DCOM.
  • La deuxième porte sur la configuration réseau des postes sous Windows 95.
  • La troisième porte sur l'exportation des objets distribués.
  • La dernière porte sur la conception d'une application de test.

X-A. L'installation de DCOM

L'installation de DCOM est sommaire pour les postes fonctionnant sous Windows NT, puisqu'il est intégré dans ce système. En revanche, pour un poste fonctionnant sous Windows 95, vous devrez vous procurer l'exécutable DCOM95.exe et procéder à son installation.

Les outils nécessaires

Le modèle DCOM est implanté de manière native dans l'environnement Windows NT et doit être configuré par l'utilitaire DCOMCnfg . Par conséquent, aucun fichier ou programme supplémentaire ne sont nécessaires : vous pouvez directement appeler l'utilitaire DCOMCnfg à partir de la ligne de commande ou de l'option Exécuter… du menu Démarrer de la barre des tâches.

En revanche, le système Windows 95 ne dispose pas de DCOM de manière native. Il faudra vous munir des programmes DCOM95.exe et DCOM95Cfg.exe disponibles sur le serveur Web de Microsoft.

Le programme DCOM95.exe installe la couche logicielle qui permet aux applications d'utiliser le modèle COM avec d'autres machines. Quant au fichier DCm95Cfg.exe , il installe l'utilitaire DCOMCnfg qui configure DCOM, c'est-à-dire la gestion de l'exportation et des droits d'accès sur les objets COM disponibles.

Une fois configuré, un poste sous Windows 95 peut aussi bien être un client qu'un serveur d'objets DCOM.

Note :
Selon les recommandations de Microsoft, pour certaines machines, il est conseillé de disposer de la version OSR2 de Windows 95 afin d'éviter d'éventuelles erreurs de connexion entre une machine sous Windows 95 et une autre sous Windows NT.

La configuration de la base des registres

Pour utiliser DCOM, quelque soit le poste à configurer, il convient de définir la valeur nommée LegacyAuthenticationLevel à 1 de la clé /HKey_Local_Machine/Software/Microsoft/OLE/ .

En ce qui concerne le poste serveur DCOM, il faut, en plus, définir une valeur nommée EnableRemoteConnect positionnée à `Y' pour `Yes' pour la même clé. Ceci permet aux objets autorisés d'être exécuté par d'autres machines.
Voici récapitulées les deux valeurs qui doivent être configurées :

 
Sélectionnez
HKey_Local_Machine
  Software
    Microsoft
      OLE
      EnableDCom = Y
      EnableRemoteConnect = Y (pour le poste serveur uniquement)
      LegacyAuthenticationLevel = 1

Le partage des répertoires du poste serveur

Que votre serveur soit sous Window NT ou sous Windows 95, il doit obligatoirement partager les répertoires qui contiennent des objets DCOM. Les clients ne doivent pas obligatoirement avoir des droits sur le répertoire partagé, une autre configuration permettra d'attribuer des droits de la machine serveur à un client distant.

La gestion des droits d'accès s'effectue sur le poste serveur pour toutes les connections ou individuellement pour chaque objets COM partagés.

Le serveur d'appel RPC

Pour que DCOM puisse fonctionner, outre la configuration Réseau du poste, il convient pour le serveur DCOM que le service RPC soit chargé.

Le service RPC, pour Remote Procedure Call, permet l'exécution d'une procédure à distance et par conséquent, l'appel de routines liées à une interface pour un programme hôte.

Par défaut, ce service est automatiquement lancé sous Windows NT. Pour le vérifier, consultez la liste des services à partir du panneau de configuration de Windows NT, vous constaterez que le service « serveur d'appel RPC » est bien démarré.

Sous Windows 95, il convient de lancer manuellement l'exécutable rpcss à partir de la ligne de commande. Sans ce service, aucun objet ne peut être appelé à partir d'un autre poste client. Si l'exécutable rpcss.exe ne se lance pas, c'est que…

Si le poste Windows 95 est utilisé comme serveur DCOM, il est recommandé de mettre l'exécutable rpcss.exe dans le menu Démarrage.

X-B. La configuration des postes Windows 95

L'installation de DCOM sous Windows 95

DCOM n'étant pas installé de manière native avec Windows 95, il convient de se prémunir et d'exécuter l'exécutable DCOM95.exe . Cette exécutable installe des DLLs supplémentaires et l'utilitaire DCOMCfng.exe dans le répertoire de Windows.

La configuration réseau des postes Windows 95

La configuration des postes fonctionnant sous Windows 95 est la phase la plus délicate car la configuration dépend à la fois de la nature et des caractéristiques voulues pour le poste. Le poste est-il client ou serveur ? Sous quelle version de Windows 95 fonctionne-t-il ? Quel qu'en soit la réponse, le poste doit être correctement configuré en réseau.

X-B-1. Le protocole TCP/IP

Le poste doit être convenablement configuré avec le protocole TCP/IP. Pour plus de renseignements à ce sujet, reportez-vous à une documentation appropriée.

X-B-2. La page « Configuration »

Voici les paramètres de base de la page Configuration communs à la configuration d'un poste en tant que serveur ou client DCOM :

  • Le poste doit être « Client pour les réseaux Microsoft ». Si ce n'est pas le cas, ajouter cette option dans la configuration du réseau à l'aide du bouton Ajouter.
  • Dans la liste des composants réseau installés, ouvrez ensuite les propriétés du composant « Client pour les réseaux Microsoft ». Dans l'onglet « Général », décochez l'option « Ouvrir la session sur un domaine Windows NT » et sélectionnez « Se connecter et rétablir les connexions réseau ».

En ce qui concerne la configuration d'un poste en serveur d'objet COM sous Windows 95, voici le paramétrage supplémentaire à effectuer pour la même page de configuration du réseau :

  • Sélectionnez « Client pour les réseaux Microsoft » pour la boîte combo « Ouverture de session réseau principal ».
  • Cliquez sur le bouton « Partage de fichiers et d'imprimantes » et indiquez « Permettre à d'autres utilisateurs d'accéder à mes fichiers » dans le but d'ajouter le service « Fichier et imprimante partagés pour les réseaux Microsoft ».
  • Éditez ensuite les propriétés de ce service et positionnez la valeur « Browse Master » à automatique.

X-B-3. La page « Identification »

Que ce soit pour le poste serveur ou le poste client DCOM, cette page doit être convenablement renseignée afin d'identifier le poste de manière unique sur le réseau et son appartenance à un groupe commun. Cette page est accessible depuis la configuration du réseau de Windows 95.

X-B-4. La page « Contrôle d'accès »

Cette page ne doit être configurée que pour le poste serveur DCOM, il convient d'y sélectionner le contrôle d'accès au niveau utilisateur.

X-C. La configuration de DCOM

Une fois le réseau configuré, il vous appartient de rendre un objet disponible ou non à d'autres machines. Sur le poste serveur, chaque objet doit se trouver dans un répertoire partagé accessible par le poste client et DCOM doit être configuré sur le poste serveur via l'utilitaire DCOMCnfg . Si vous ne disposez pas de serveurs COM, consultez le paragraphe « Les tests par l'exemple » de ce chapitre.

L'utilitaire DCOMCnfg

Cet utilitaire permet de gérer l'accès, et donc la sécurité des objets COM disponibles sur une machine. Il existe quelques différences entre la version fournie avec Windows NT et celle de Windows 95. Ces différences sont minimes et essentiellement axées sur la sécurité. Comme pour la configuration réseau, les points communs seront les premiers abordés, puis les différences de la version sous NT seront expliquées.

X-C-1. La page Applications

Page Applications sous Windows 95

  • La première page liste tous les serveurs COM enregistrés. Si votre serveur n'est pas listé, il conviendra de le lancer au moins une fois sur le poste serveur avec le paramètre /regserver sur la ligne de commande.
  • La deuxième page défini les paramètres généraux de DCOM. Il autorise ou non DCOM sur cette machine, etc.
  • La dernière page concerne la gestion de la sécurité.

La page Propriétés par défaut

Propriétés par défaut sous Windows 95

Dans cette page, la coche « Activer le modèle COM distribué sur cet ordinateur » est l'information la plus importante. Sans elle, DCOM n'est pas disponible sur le poste.

La page Sécurité par défaut

Sécurité par défaut sous Windows 95

Cette page permet de configurer les droits d'accès aux objets COM du poste serveur par des clients n'ayant pas les droits nécessaires sur l'objet.

La sécurité sous Windows 95 est discrète par rapport à Windows NT.

Sécurité par défaut sous Windows NT

L'exportation d'un serveur COM

Pour définir les paramètres d'exportation d'un serveur COM, vous devez double-cliquer sur le nom du serveur depuis la première page. Trois nouvelles pages seront alors disponibles, nommées respectivement Général, Emplacement et Sécurité pour Windows 95. Une quatrième page nommée Identité est uniquement disponible sous Windows NT.

X-C-2. La page Général

La page Général contient des informations en lecture seule. Elle montre le nom de l'application, son type (serveur local) et l'emplacement physique de l'exécutable. Si les informations de cette page doivent être modifiées, il convient de les supprimer de la base des registres en exécutant le programme serveur avec comme paramètre de la ligne de commande /unregserver .
Modifiez ensuite les caractéristiques de votre serveur, et enregistrez le à nouveau en l'exécutant avec comme paramètre en ligne /regserver .

X-C-3. La page Emplacement

Cette page détermine la machine sur laquelle doit s'exécuter le serveur COM. Elle contient trois cases à cocher, ce qui autorise un choix multiple.

Les trois emplacements d'exécution possibles sont :

  • Exécuter l'application sur l'ordinateur qui contient les données,
  • Exécuter l'application sur cet ordinateur,
  • Exécuter l'application sur l'ordinateur suivant.

Evidemment, si les trois coches sont positionnées, Windows effectue un choix par rapport à ce qui lui semble être le moins coûteux, c'est-à-dire « Exécuter l'application sur cet ordinateur » en premier, suivi de « Exécuter l'application sur l'ordinateur qui contient des données », puis le choix « Exécuter l'application sur l'ordinateur suivant ».

Ces options positionnent des valeurs dans la clé HKey_Classes_Root\AppID\{Guid de l'objet} qui sont :

  • Active et Storage vaut Y si la coche « Exécuter l'application sur l'ordinateur qui contient les données » est positionnée.
  • Remote Server Name vaut le nom de la machine si « Exécuter l'application sur l'ordinateur suivant » est cochée.

X-C-4. La page Sécurité

Cette page permet la gestion de la sécurité pour votre serveur COM. Elle détermine si les options de sécurité par défaut doivent être utilisées ou si elles doivent être personnalisées.

X-C-5. La page Identité sous Windows NT

Du fait que l'accès aux informations et aux ressources d'un poste est contrôlé sous Windows NT, cette page permet de spécifier un profil utilisateur sous lequel devra s'exécuter le programme serveur. Ceci est important, puisque cela autorise à un programme serveur de fonctionner avec des droits différents à ceux de l'utilisateur connecté sur le poste.

La création d'un objet COM distant

L'utilisation d'un objet COM situé sur une machine distante nécessite l'appel de la routine Delphi CreateRemoteCOMObject de l'unité ComObj . Son utilisation est similaire aux routines CreateOleObject et CreateCOMObject, sauf qu'elle prend un paramètre supplémentaire : le nom de la machine serveur.

Si l'appel de la routine réussi, vous obtenez l'interface IUnknown de l'objet COM demandé. Dans le cas où une interface précise de l'objet COM vous intéresse, vous pouvez utiliser l'opérateur as pour effectuer un transtypage sûr. L'exemple qui suit montre l'utilisation de l'interface IDispatch.

 
Sélectionnez
...
var id : IDispatch;

begin
  id:=CreateRemoteCOMObject(`Nom_Machine', Class_ID) as IDispatch;
  ...
end;

XI. Les messages d'erreur

Malgré toutes les précautions que vous pourrez prendre, il se peut que la connexion ne puisse s'établir et dans ce cas, un message d'erreur apparaîtra. Sans être très exhaustive, voici une liste d'erreurs fréquentes avec la parade de correction éventuelle.

Message d'erreur Description et solution
Accès refusé Ce message intervient lorsque des problèmes de droits subsistent. Il convient de vérifier que l'objet désiré est bien accessible et qu'il dispose de droits suffisants par rapport à l'utilisateur connecté si l'objet accède à des données sur le poste serveur.
Classe non enregistréeClass not registered Cette erreur se produit lorsque vous tentez d'utiliser un serveur qui n'est pas référencé sur le poste client. Pour résoudre ce problème, exécutez au moins une fois le serveur sur le poste client.
Le serveur RPC n'est pas disponibleThe RPC server is unavailable Cette erreur survient lorsque le serveur RPC n'est pas chargé sur la machine serveur, ou lorsque la configuration réseau n'est pas correcte. N'oubliez pas que DCOM fonctionne avec le protocole TCP/IP.
Echec de l'exécution du serveurServer execution failed Ce message apparaît lorsque le serveur RPC n'est pas chargé. N'oubliez pas que le serveur RPC ne se charge pas automatiquement sous Windows 95.
Interface non supportée Interface not supported Cela arrive lorsque le niveau d'identification n'est pas correctement configuré sur le poste client et le serveur. Vérifiez la configuration de DCOM avec DCOMCnfg et vérifiez que la valeur de LegacyAuthenticationLevel vaut bien 1.
Erreur OLE 80070776OLE error 80070776 Les valeurs EnableDCOM et EnableRemoteConnect ne doivent pas être correctement configurées dans la base des registres.
Illegal function Il se peut que vous ayez mal orthographié la case du nom de votre serveur distant. Il faut respecter la case.
Violation d'accès à l'adresse XXXX du module `ModuleServeur' lecture de l'adresse FFFFFFFF Le serveur a été déchargé alors qu'il y avait encore un client connecté.
Run-time error `429' : ActiveX component can't create object La valeur EnableRemoteConnect n'est pas bien configurée.
Run-time error `70' : Permission denied Cette erreur apparaît lorsque les permissions du serveur DCOM sont mal paramètrées.

Note :

N'hésitez pas à relancer vos machines de tests si une erreur persiste alors que vous pensez avoir apporté la correction. Certains paramètres nécessitent le relancer la machine sans que Windows ne vous le demande. Par exemple, l'arrêt du partage d'un répertoire contenant des objets COM nécessite de relancer la machine sans quoi les postes clients pourront toujours accéder aux objets…

XII. Les tests par l'exemple

La création d'un serveur DCOM simple

Le serveur de test se contentera de proposer une interface ne disposant que d'une entrée nommée GetName permettant de récupérer le nom de l'utilisateur qui exécute l'objet COM.

Voici les étapes de la construction de ce programme :

  • Créez un nouveau projet.
  • Ajoutez au projet un objet d'automatisation OLE via le référentiel d'objet.
  • Nommez la classe d'automation TestDCOM.
  • Ajoutez la méthode GetName à l'interface ITestDCOM dans l'éditeur de bibliothèque de type. Changez son prototype en function GetName : WideString;
  • Cliquez sur le bouton Refresh de l'éditeur de bibliothèque de type.
  • Dans l'unité de la CoClasse TTestDCOM, programmez la méthode GetName générée par l'éditeur de bibliothèque de type comme ceci :

     
    Sélectionnez
    function TTestDCOM.GetName : WideString;
    begin
      result:='Coucou';
    end;
    
  • Compilez le projet et enregistrez le serveur COM en exécutant le programme avec en paramètre en ligne /regserver . Pour cela, vous devez avoir les droits nécessaires pour modifier la base des registres.

Si vous n'avez pas rencontré de problème à ce niveau, votre serveur DCOM est prêt à être utilisé par un client.

La création d'un client DCOM simple

Le programme test client DCOM doit créé un objet COM distant avec la routine CreateRemoteCOMObject, puis appeler l'entrée GetName. Si la connexion ne peut se faire, une exception sera soulevée dès la tentative de création de l'objet COM. L'affichage du nom de la machine est superflu pour un test de connexion ! Connaissant le GUID de l'objet, ainsi que la machine sur laquelle il se trouve, voici le code associé à un bouton permettant d'effectuer le test de connexion :

 
Sélectionnez
procedure TForm1.Button1Click(Sender: TObject);

const  Class_TestDCOM: TGUID = '{03B614E2-C339-11D1-A55E-00600879F449}';

var  v : variant;

begin
  v:=CreateRemoteCOMObject(edComputerName.Text, Class_TestDCOM) as IDispatch;
  ShowMessage(v.GetName);
end;

N'oubliez pas d'inclure l'unité ComObj dans une clause uses de votre unité.

XIII. Les services de l'automatisation OLE

L'automatisation OLE permet à un processus de piloter un objet COM contenu dans un autre processus ou une DLL. Cette possibilité est à la fois disponible en local ou entre deux machines distantes.

Le pilotage induit le fait que l'objet piloté s'exécute sur la machine où il est stocké. Bien qu'un objet COM défini sur une plate-forme Windows puisse communiquer avec un objet défini sur la plate-forme Machintosh, en aucun cas l'objet défini pour Machintosh peut s'exécuter sur la plate-forme Windows et inversement.

Techniquement, un objet d'automatisation est un objet COM qui supporte l'interface IDispatch. Cette interface autorise l'invocation de méthode par leur nom, plutôt que par leur position dans la table des méthodes virtuelle de l'objet.

Le processus qui pilote un objet par automation se nomme contrôleur d'automatisation, tandis que l'objet piloté sera dit serveur d'automatisation. Si un processus intègre à la fois le serveur et le contrôleur d'automatisation OLE, on qualifie ce processus de serveur d'automatisation intégré au processus (InProcess).

XIV. Le contrôleur d'automatisation

Un contrôleur, comme son nom l'indique, contrôle un serveur d'automatisation OLE. Pour ce faire, il doit avoir connaissance des services du serveur, c'est-à-dire les méthodes d'automatisation. Vous devez donc disposer sous une forme quelconque de la documentation sur ces services. S'il est disponible, vous pouvez utiliser les informations de la bibliothèque de types associé à l'objet pour obtenir ces informations.

Word liste toutes ses méthodes d'automatisation en choisissant les « Commandes de Word » dans la boîte de macro word.

Vous pouvez aussi utiliser l'interface associée. Il faut pour cela importer la bibliothèque de type qui se nomme word8.olb et se trouve dans le répertoire d'Office97.

L'invocation par nom

Normalement pour manipuler un objet COM, vous devez disposer d'une interface supportée par l'objet. L'automatisation OLE est définie par l'interface IDispatch. Elle apporte les entrées nécessaires à l'automatisation. En aucun cas, cette interface définie les entrées liées aux services proposés par l'objet. Pourtant, l'interface IDispatch permet l'utilisation des services de l'objet par invocation de méthode par leur nom.

Cette interface est définie de façon :

 
Sélectionnez
IDispatch = interface(IUnknown)
    ['{00020400-0000-0000-C000-000000000046}']

     function GetTypeInfoCount(out Count: Integer): HResult; stdcall;

     function GetTypeInfo(Index, LocaleID: Integer; out TypeInfo): HResult; stdcall;

     function GetIDsOfNames(const IID: TGUID; Names: Pointer;
			    NameCount, LocaleID: Integer; DispIDs: Pointer): HResult; stdcall;

     function Invoke(DispID: Integer; const IID: TGUID; LocaleID: Integer;
     		     Flags: Word; var Params; VarResult, ExcepInfo, ArgErr: Pointer): HResult; stdcall;
end;

L'entrée GetIDsOfNames permet de trouver le dispId d'une méthode par son nom. Une fois le dispId trouvé, il suffit d'appeler l'entrée Invoke avec cette valeur pour déclencher l'appel de la méthode voulue. Bien évidemment, un travail supplémentaire est nécessaire pour fournir les paramètres éventuels de la méthode. Cette technique se nomme appel par invocation.

Si vous connaissez le dispId d'une routine et ses paramètres, vous pouvez court-circuiter l'appel de l'entrée GetIDsOfNames afin de rendre l'appel plus rapide. Mais ceci est pénible à mettre en œuvre.

Inprise a intégré de manière native dans son langage les appels par invocation à l'aide du type variant . Une variable de ce type pointant sur un objet d'automatisation OLE permet d'invoquer les entrées de celui-ci avec la syntaxe par point du Pascal objet. L'implémentation d'un contrôleur d'automatisation OLE est alors une tâche très simple, puisque l'objet d'automatisation apparaît comme un objet Delphi.

Pour obtenir l'interface IDsipatch d'un objet d'automatisation OLE à partir de son ProgID, il convient d'utiliser la routine CreateOLEObject pour une connexion locale, CreateRemoteOLEObject pour une connexion distante.

Voici un exemple permettant d'imprimer un document Word par automation :

 
Sélectionnez
implementation

var WordApp : Variant ;

procedure TForm1.CreateForm(Sender : TObject) ;
begin
  WordApp :=CreateOLEObject(`word.basic') ;
  WordApp.FileOpen(`MonFichier');
  WordApp.FilePrint;
  WordApp.FileQuit;
end ;

En réalité, il faut interpréter l'instruction WordApp.FileQuit; comme :

 
Sélectionnez
WordApp.GetIDsOfNames();
 WordApp.Invoke(); 
  • L'inconvénient de cette méthode est la surcharge de code induite pour l'appel d'une entrée.
  • L'avantage est que l'invocation se faisant par une chaîne de caractères, il n'y a aucun risque de blocage de l'application du fait de l'invocation d'une méthode inconnue ou d'une différence de paramètres.

L'invocation par vtable ou dualinterface

Une autre solution d'appeler les méthodes d'un objet d'automatisation est d'utiliser l'interface de celui-ci si elle est disponible. Dans ce cas, les appels sont plus rapides, car il n'y a pas de recherche du DispId de la méthode par son nom avant l'invocation. Avec l'interface, vous disposez d'un pointeur sur la vTable de l'objet et vous appelez directement ses méthodes.

Une connexion de ce type se fait en utilisant la routine CreateComObject ou CreateRemoteComObject.

  • L'inconvénient de cette méthode est que le contrôleur d'automatisation doit être recompilé s'il y a des changements dans l'interface de l'objet d'automatisation et l'utilisation d'une mauvaise version de l'interface risque de provoquer des blocages de l'application.
  • L'avantage de cette méthode repose principalement sur la vitesse d'exécution du code.

Les avantages de l'invocation

Comme nous l'avons vu au paragraphe « Connexion avec invocation par nom », il suffit, pour appeler une méthode d'un serveur d'automatisation OLE, d'utiliser la notation par point, comme pour une méthode d'une classe du langage Pascal objet :

 
Sélectionnez
WordApp.FileOpen(`Lisezmoi.doc') ;

Si la commande du serveur nécessite plusieurs paramètres, il suffit de séparer chaque paramètre par une virgule dans l'appel de la méthode. Par exemple, pour ouvrir un fichier en lecture seule, voici ce qu'il convient de faire :

 
Sélectionnez
procedure TForm1.Button1Click(Sender : TObject);
begin
 WordApp.FileOpen('Lisezmoi.doc',1);
end;

Si le paramètre voulu se trouve à une position supérieure, il suffit d'insérer des virgules jusqu'à la position voulue :

 
Sélectionnez
procedure TForm1.Button1Click(Sender : TObject);
begin
 WordApp.FileOpen('Lisezmoi.doc',,,,1);
end;

On peut aussi nommer les paramètres fournis, ce qui est très pratique s'ils se trouvent à des positions éloignées. Ajoutons qu'avec cette possibilité l'ordre des paramètres n'a aucune importance.

 
Sélectionnez
procedure TForm1.Button1Click(Sender : TObject);
begin
 WordApp.FileOpen('Lisezmoi.doc', LectureSeule :=1);
end;

XV. Le serveur d'automatisation

La création d'un serveur d'automatisation

Pour créer un serveur d'automatisation, ajoutez dans votre projet un composant de type automation disponible dans la feuille ActiveX du référentiel d'objets. Indiquez pour le nom de classe de votre objet, le nom de l'objet.

Delphi génère alors une unité pour la coClasse et une bibliothèque de type pour définir l'interface de l'objet. Le nom de la coClasse est le nom de l'objet préfixé par `I', tandis que le nom de l'interface est le nom de l'objet préfixé par `I'.

XV-A. Expert objet automation

Après avoir validé la boîte de dialogue, l'éditeur de bibliothèque de type s'affiche et montre les nœuds.

XV-B. La librairie de type du serveur d'automatisation

L'application Project1 contient une interface nommée ITestComObj et une coClasse nommée TestComObj.

Les services du serveur

L'ajout de service à un objet d'automatisation COM passe par l'ajout d'entrées dans une interface via l'éditeur de bibliothèque de type. Par conséquent, reportez-vous à son explication dans le chapitre portant sur COM.

La directive safecall

Les méthodes des objets COM sont souvent qualifiées par le mot clé safecall . En précisant une méthode de cette nature, le compilateur Pascal protège automatiquement le corps de la méthode des exceptions non trappées. En clair, une méthode définie comme suit :

 
Sélectionnez
type

  IUneInterface = interface
    procedure FaitQQ; safecall;
  end;

  TMonObject = class(TUneClasse, IUneInterface)
    ...
    procedure FaitQQ; safecall;
  end;

procedure TMonObjet.FaitQQ;
begin
  Montraitement;
end;
doit en réalité être lu de cette manière :

procedure TMonObjet.FaitQQ; safecall;
begin
  try
    Montraitement;
  except

  end;
end;

Le but de ce remplacement est d'empêcher qu'une exception ne se propage hors de la méthode en l'étouffant. C'est à la fois une question de sécurité et une question pratique pour les développements distribués, et donc pour l'automatisation.

En effet, si l'appel d'une méthode produit le soulèvement d'une exception dans celle-ci, une boîte de dialogue sera affichée. Lorsque le client et l'objet se trouvent sur la même machine, il suffit de fermer cette boîte de dialogue pour débloquer le serveur.

En revanche, si l'objet COM se trouve sur une machine distante, non seulement vous serez obligé de vous déplacer pour fermer la boîte de dialogue, mais vous ne serez pas averti que l'objet a déclenché une exception. L'objet ou/et votre application sembleront bloqués, d'où l'avantage d'empêcher qu'une exception se soulève et donc de préfixer les méthodes par la directive safecall .

L'enregistrement d'un serveur

Pour enregistrer temporairement le serveur d'automatisation défini dans un exécutable, il suffit de lancer une première fois l'exécution du programme. En revanche, pour enregistrer le serveur de manière durable, il convient de fournir /regserver en paramètre de la ligne de commande de l'exécutable. Et, pour ne plus enregistrer un serveur, il convient de fournir /unregserver .

Pour enregistrer un serveur d'objets d'automatisation de type DLL, il convient d'utiliser l'utilitaire tregsvr.exe fourni avec Delphi.

XVI. Introduction à MTS

Microsoft Transaction Service est un environnement logiciel basée sur le modèle COM dont le but est de simplifier et gérer l'utilisation d'objets COM en milieu distribué.

Son installation seule autorise l'utilisation d'objets en réseau qui n'étaient pas initialement conçus pour être utilisés de manière partagée. Cet environnement isole complètement un composant COM pour chaque processus client et gère les sessions de réseau, les données partagées, les connexions aux bases de données, le muti-traitement, la sécurité des applications et les transactions atomiques. Dans une transaction atomique, toutes les opérations entreprissent durant la transaction par n'importe quel objet sollicité sur le réseau peuvent être validées ou annulées simultanément.

La plupart des apports de cet environnement ne nécessitent pas de modification du code des objets existant. Seules les fonctions les plus avancées nécessitent une programmation spécifique, notamment en ce qui concerne la sécurité.

Cet environnement n'est pas nouveau, il existe depuis plus de 3 ans sous différentes formes. Désormais, avec la version 4.0 de NT, il apparaît comme un composant de MMC (Microsoft Management Console), l'interface de gestion distribuée avec Windows NT. Avec la version 5.0 de NT, MTS bénéficiera d'un référentiel d'objets.

Puisque un objet COM de base profite directement de la plupart des mécanismes de MTS, ce chapitre montre les avantages de cet environnement, l'installation et la gestion d'un objet COM, ainsi que la gestion des transactions au sein de MTS.

XVI-A. Introduction à MTS

L'environnement MTS simplifie l'administration des serveurs car un de ses gros avantage est qu'il n'est pas nécessaire de relancer le serveur lorsqu'un objet publié est modifié. En remplaçant un objet par une nouvelle version, tous les nouveaux clients bénéficient des nouvelles possibilités.

MTS gère l'activation et la désactivation des objets COM, il est donc capable de montrer à tout instant l'état de chaque composant installé. De la même manière, il est capable de lister tous les objets en cours d'utilisation qu'ils soient utilisés sans une transaction ou non. Il se comporte alors comme un outil de monitoring d'objets COM.

Les composants MTS sont installés dans des Lots (ou paquets suivant les traductions) et un composant peut appartenir à plusieurs Lots différent.

Un Lot donne un certain nombre d'avantages aux composants qui y sont installé. En fait un Lot isole un groupe de composants, permettant ainsi à ces composants de partager des données globales.

MTS apporte essentiellement les sept caractéristiques qui suivent :

  • Connexion aux bases de données.
  • Applications MTS sécurisées.
  • Gestion des composants.
  • Monitoring des transactions.
  • Activation JIT (spécifique aux objets MTS).
  • Propriétés partagées (spécifiques aux objets MTS).
  • Sécurité améliorée (spécifique aux objets MTS).

Les apports de MTS

XVI-B. La connexion aux bases de données

Les connexions bases de données compatibles avec MTS, telles que ODBC et BDE utilisent une gestion basée sur le recyclage de connexion afin de fournir plus de clients. Le principe est simple, dés qu'un client n'utilise plus l'objet MTS connecté à une base de données, sa connexion est récupérée si un autre client en fait la demande.

Non seulement cette technique permet de fournir plus de clients, mais elle permet aussi de consommer moins de ressources, et par conséquent de gagner en vitesse d'exécution, puisqu'il y a moins d'ouvertures et de fermetures de bases de données.

XVI-C. Les applications MTS sécurisées

MTS utilise les droits de Windows NT pour accéder aux paquets et aux objets référencés. Par conséquent, l'administration et la gestion de la sécurité des objets restent homogènes et donc plus sûres.

XVI-D. La gestion des composants

Un serveur MTS peut solliciter un objet sur une autre machine serveur, bien qu'il dispose lui-même de ce composant dans le but de mieux répartir la charge de travail entre tous les serveurs du réseau.

Dans le cas où le client n'est pas configuré pour accéder à ce serveur, le serveur courant peut configurer la machine du client s'il dispose de DCOM afin de lui donner les moyens d'y accéder.

XVI-E. Le monitoring des transactions

Sans que l'objet MTS soit prévu pour supporter les transactions, un serveur MTS peut faire participer cet objet dans une transaction et relancer la transaction en cas d'échec d'une transaction.

XVI-F. L'activation JIT (spécifique aux objets MTS)

Un serveur MTS active les composants que lorsque cela est nécessaire. Cela signifie que si le code d'un objet est très peu sollicité, alors l'objet peut être déchargé de la mémoire, puis rechargé lorsqu'il est à nouveau sollicité. Du fait que durant sa vie, un objet peut être chargé et déchargé plusieurs fois, il perd à chaque fois son contexte. On dit que l'objet est « Standless ».

Mais cette même gestion est aussi applicable pour une autre demande d'un autre client sur un même objet. Le serveur désactive l'objet pour le premier client afin de servir le deuxième. L'avantage est que le serveur n'a pas besoin de créer plusieurs instances de l'objet, accélérant ainsi l'utilisation d'un objet.

En revanche, pour les objets conçus pour MTS, la désactivation n'est possible que si la transaction en cours est terminée. Ceci intervient lorsque la méthode SetAbort ou SetComplete est appelée. Dans ce cas, la désactivation de l'objet est immédiate.

XVI-G. Les propriétés partagées (spécifiques aux objets MTS)

Les composants peuvent partager des données entre composants du même paquet. Cette option permet de synchroniser des composants entre eux ou de conserver l'état des objets. Bien que devant être gérée par l'objet, cette possibilité offre une solution complète pour le partage des données.

XVI-H. La sécurité améliorée (spécifique aux objets MTS)

La sécurité se décompose en deux types : programmatique et déclarative.

La sécurité programmatique s'appuie sur la sécurité NT existant. Elle est disponible pour tous les composants MTS. Elle permet d'affecter des droits aux paquets, aux composants et aux interfaces. Cette sécurité est disponible pour tous les systèmes qui disposent de DCOM.

En revanche, la sécurité déclarative n'est disponible que pour les paquets sous Windows NT. Elle est basée sur des rôles. Un rôle étant un groupe d'utilisateurs pouvant appeler les interfaces d'un composant.

Exemple de développement MTS

Dans les développements multiniveaux, les différentes couches peuvent mettre en jeu un ou plusieurs ordinateurs. Sur chacun des ordinateurs, un ou plusieurs objets peuvent être sollicités pour réaliser une tâche unique. Par exemple, une chaîne de production simplifiée de bilboquet disposant de trois machines créant chacune une pièce nécessaire à l'élaboration du jouet, peut être modélisée par le schéma suivant :

Exemple, le poste client envoi une requête à chacune des machines serveur dans le but que celles-ci réalisent la pièce nécessaire. Lorsque les trois demandes sont réalisées, le poste client peut ordonner à une machine tiers, l'assemblage des trois pièces. En revanche, si une des trois machines tombe en panne ou est incapable de réaliser la tâche demandée, le poste client doit en être averti.

Le rôle de MTS est de permettre au poste client de valider (commit) ou d'annuler (rollback) toutes les demandes d'un client.

Le service MTS gère aussi la gestion des ressources du système en chargeant et en déchargeant les DLL de serveur d'objet MTS.

De plus, si le service estime que le système nécessite des ressources, il est capable de décharger de la mémoire certains objets MTS en cours d'utilisation.

XVI-I. Le gestion des transactions

Lorsqu'un objet est utilisé dans une transaction, il doit signaler l'état de son traitement à l'aide des méthodes SetComplete ou SetAbort . Ceci peut être réalisé soit par l'objet lui-même, soit par le programme client. Dans ce dernier cas, l'objet MTS doit disposer d'entrée permettant l'appel de ces méthodes.

Ces méthodes positionnent un flag pour la transaction. Si à la sortie d'une méthode de l'objet MTS le flag reste positionné à SetAbort, une exception est soulevée dans l'application ayant appelée l'entrée. Dans ce cas, l'exception soulevée doit être gérée afin d'annuler la transaction avec l'appel de l'entrée Abort de l'objet transaction. Si aucune exception n'est soulevée, alors la transaction peut être validée avec l'appel de la méthode Commit . Voici un exemple d'une transaction faisant intervenir deux objets MTS :

 
Sélectionnez
procedure TForm1.Button15Click(Sender: TObject);
 var Transaction : ITransactionContextEx;

begin
  Transaction:=CreateTransactionContextEx;
  OleCheck(Transaction.CreateInstance(CLASS_ObjectFirst,IObjectFirst, ObjectFirst));
  OleCheck(Transaction.CreateInstance(CLASS_ObjectSecond,IObjectSecond, ObjectSecond));

 try
  ObjectFirst.DoWork;
  ObjectSecond.DoWork;
  Transaction.Commit;

 except

  Transaction.Abort;
    Raise;
  end;

end;

Dans un premier temps, on créé l'objet transaction, puis les deux objets MTS en protégeant leur appel par la routine OleCheck.

Dans un bloc protégé de type try..except , les traitements de chaque objet sont appelé. En cas de réussite des traitements, la transaction est validée par un Commit . Si un des traitements déclenche l'appel de SetAbort , alors une exception est soulevée provoquant l'annulation de la transaction.

XVI-J. Le rollback des opérations

Lorsqu'une transaction est annulée, il est nécessaire de défaire toutes les opérations ayant abouties avant l'échec.

XVI-K. Les traitements multientrées

Le terme de traitement multientrées désigne un traitement nécessitant l'appel de plus d'une entrée du même objet MTS pour pouvoir être complète. Dans ce cas, il est possible de positionner un flag avec l'appel de DisableCommit pour empêcher toute réussite de l'appel de la méthode SetComplete .

Pour autoriser la complétude du traitement de l'objet, la méthode EnableCommit doit être appelée.

XVI-L. La méthode IsInTransaction

La méthode IsInTransaction permet à un objet MTS de tester s'il est utilisé dans une transaction. Ceci est pratique pour par exemple n'autoriser l'utilisation d'un objet que si celui-ci participe à une transaction.