Le modèle COM1998
Par
Dick LANTIM
Le modèle COM sert de base à tous les nouveaux développements chez Microsoft.Ses spécifications sont régies et définies par l'Active Group, un consortium de constructeurs.
Cette présentation a pour but d'expliquer les différents concepts de ce modèle.
Cette connaissance est importante pour la création d'objets COM et leur utilisation, mais également pour comprendre le code généré par les experts Delphi, ainsi que les outils disponibles dans ce produit.
Ces concepts de base sont nécessaires pour la compréhension des deux autres présentations portant sur le même thème.
1. Les bases du modèle
1.1. Le concept d'interface
1.2. La déclaration en langage Pascal
1.3. Le mot clé interface
1.4. Les propriétés
1.5. La portabilité d'une interface
1.6. La CoClasse
1.7. L'interface au niveau binaire
1.8. La recherche d'interface
1.9. Le compteur de référence
2. L'identification
3. L'utilisation avancée des interfaces
3.1. Les méthodes statiques
3.2. Les méthodes virtuelle
4. Les interfaces et la POO
5. Le stockage
6. Premier exemple : Un objet sans frontière
6.1. Création à partir d'un GUID
6.2. Création à partir d'un ProgID
7. Deuxième exemple : Les liens de fichier
8. Troisième exemple : Se substituer au système
8.1. La clé Shell
8.2. Les particularités
8.3. La clé ShellEx
8.4. L'objet COM
9. Quatrième exemple : Les contrôles ActiveX
9.1. La création d'une page de propriétés
9.2. L'enregistrement de la page de propriétés
10. Mise en oeuvre du modèle COM
10.1. L'installation de DCOM
10.2. La configuration des postes Windows 95
10.2.1. Le protocole TCP/IP
10.2.2. La page "Configuration"
10.2.3. La page "Identification"
10.2.4. La page "Contrôle d'accès"
10.3. La configuration de DCOM
10.3.1. La page Applications
10.3.2. La page Général
10.3.3. La page Emplacement
10.3.4. La page Sécurité
10.3.5. La page Identité sous Windows NT
11. Les messages d'erreur
12. Les tests par l'exemple
13. Les services de l'automatisation OLE
14. Le contrôleur d'automatisation
15. Le serveur d'automatisation
15.1. Expert objet automation
15.2. La librairie de type du serveur d'automatisation
16. Introduction à MTS
16.1. Introduction à MTS
16.2. La connexion aux bases de données
16.3. Les applications MTS sécurisées
16.4. La gestion des composants
16.5. Le monitoring des transactions
16.6. L'activation JIT (spécifique aux objets MTS)
16.7. Les propriétés partagées (spécifiques aux objets MTS)
16.8. La sécurité améliorée (spécifique aux objets MTS)
16.9. Le gestion des transactions
16.10. Le rollback des opérations
16.11. Les traitements multi-entrées
16.12. La méthode IsInTransaction
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.
Etant 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 inter-opé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.
1.1. 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.
1.2. 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 :
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.
1.3. 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.
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.
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.
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.
1.5. 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 :
IZipper = interface(IUnknown)
['{F2737788-5902-11D2-8980-00600844A34A}']
function Get_Filename: Integer; stdcall;
procedure Set_Filename(Value: Integer); stdcall;
end;
[
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'un 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.
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 :
type
IZipHuffman = interface
procedure ZipFile; safecall;
end;
TZipper = class(TInterfacedObject, 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 :
type
IZipHuffman = interface
procedure ZipFile; safecall;
end;
IFile = interface
function GetFilename : string; safecall;
procedure SetFilename(f : string); safecall;
end;
TZipper = class(TInterfacedObject, IZipHuffman, IFIle)
procedure ZipFile; safecall;
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 ?
A 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 le définition de l'interface IUnknown :
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.
1.7. 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.
1.8. 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.
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);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
TestZip(TVideo.Create);
end;
procedure TMp3.ZipName;
begin
ShowMessage('ZipperName');
end;
1.9. 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.
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 :
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.
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.
Etant donnée 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 :
{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 :
const CLASS_TestAuto: TGUID = '{8144997C-3730-11D2-8923-00600844A34A}';
Pour associer un GUID à une interface, on utilise la syntaxe suivante :
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.
3. 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
3.1. 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 :
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.
3.2. Les méthodes virtuelle
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 :
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 :
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 :
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;
zLempel:=TZipper.Create;
zLempel.ZipFile;
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 :
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 :
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 :
type
TZipperFileListbox = class(TFileListbox, TZipper)
end;
La classe TZipperFileListbox par ce bias implémenterais 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
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ées membre soit comme objet, soit comme interface.
Et voici un exemple :
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
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.
4. 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 multi-traitement 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 :
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;
procedure TEditTime.Run;
begin
while true do
text:=TimeToStr(time);
end;
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 :
constructor TEditTime.Create(AOwner : TObject);
begin
inherited Create(AOwner);
TThreadIntf.Create(self);
end;
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 :
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 :
library Project1;
uses ComServ;
exports DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;
begin
end.
Les deux dernière méthodes permettent la gestion de l'enregistrement de la DLL dans la base des registres. Ces deux routines ont déjà été explicitée au dessus.
La routine DllGetClassObjet est celle que le système appel pour obtenir une instance d'un objet. Voici son prototype :
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.
6. 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.
6.1. 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.
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 :
procedure TForm1.Button3Click(Sender: TObject);
var its :ITestZipper;
begin
its:=coTestZipper.Create;
its.Process('DDL.txt');
end;
6.2. 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 :
procedure TForm1.Button2Click(Sender: TObject);
var itz : ITestZipper;
begin
itz:=CreateCOMObject(ProgIDToClassID('Zipper.TestZipper')) as ITestZipper;
itz.Process('Fichier.bat');
end;
7. 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 :
IShellLinkA = interface(IUnknown)
[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 :
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 :
isl.SetPath(PChar(SrcFile));
isl.SetDescription(PChar(Description));
isl.SetShowCmd(1);
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 :
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 :
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 :
CreateFileLink('c:\windows\calc.exe',
'c:\calc.lnk',
'Ceci est un test');
8. 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 noeud 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
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 :
%SystemRoot%\System32\NOTEPAD.EXE %1
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
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.
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 :
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 :
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 :
9. 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.
9.1. 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 :
procedure TPropertyPage1.UpdateObject;
begin
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 :
procedure TPropertyPage1.UpdatePropertyPage;
begin
Edit1.Text:=OleObject.ValeurMonetaire.Text;
end;
9.2. 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ée 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.
type
TActiveFormX = class(TActiveForm, IActiveFormX)
...
procedure DefinePropertyPages(DefinePropertyPage : TDefinePropertyPage); override;
...
end;
implementation
uses ComServ, ActiveFormProp;
...
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 :
10. Mise en oeuvre 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.
10.1. 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 logiciel 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 :
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.
10.2. 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.
10.2.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 document appropriée.
10.2.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".
- Editez ensuite les propriétés de ce service et positionnez la valeur "Browse Master" à automatique.
10.2.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.
10.2.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.
10.3. 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.
10.3.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.
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 .
10.3.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.
|