Gestion d'événements Automation sous Delphi

Laurent Dardenne

Mise en œuvre et utilisation dans une application Delphi de la gestion d'événements d'un serveur COM.
La réalisation progressive d'une application simple tout au long de l'article permettra d'appréhender cette technique et d'utiliser l'Expert Objet Automation pour créer le serveur COM.

Traduction de l'article Writing COM Automation Events disponible sur le site Borland.com ♪

- Titre : Gestion d'événements Automation sous Delphi

- Auteur : Laurent Dardenne

- Parution : 16 juin 2004

- Mise à jour : 28 mai 2007

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2004 Dardenne Laurent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Public concerné

- II. Qu'est-ce qu'un événement Automation ?

- II-A. Rappel

- III. Comment est-ce que cela fonctionne

- IV. Écrivons le serveur

- V. Le client

- VI. Annexe

- VI-A. Passage de paramètres

- VI-B. Voir Aussi

I - Public concerné

Débutant

Avancé

Expert

[ ]___[X]

[X]___[ ]

[ ]___[ ]

Prérequis :
Connaissances de base sur les objets COM, cf. Le modèle COM/DCOM et MTS par Dick Lantim.

Testé avec Delphi 5 sous XP pro.
Version 1.1

II - Qu'est-ce qu'un événement Automation ?

Le modèle client/serveur de COM avec lequel vous avez probablement déjà travaillé permet au client d'appeler le serveur par une interface qu'il supporte. C'est très bien quand un client appelle le serveur pour effectuer une action ou pour rechercher des données, mais que se passe-t-il quand le serveur veut demander quelque chose au client ? Les interfaces entrantes sont unidirectionnelles c'est-à-dire qu'il n'y a aucune manière pour que le serveur échange des informations avec ses clients.
C'est ici que le modèle d'événements de serveur entre en jeu.

Un serveur qui supporte des événements répond non seulement aux appels du client, mais peut également signaler son statut et faire ses propres demandes auprès du client.
Par exemple : Un client fait une demande au serveur pour télécharger un fichier. Au lieu d'attendre que le serveur ait fini le téléchargement avant de continuer (comme avec le modèle précédent), le client peut se charger d'une autre tâche. Lorsque le serveur a terminé, il peut déclencher un événement qui indique au client la fin du traitement demandé, à la réception de cet événement le client y répond en conséquence.

II-A - Rappel

Interface COM
C'est le moyen par lequel un objet expose ses services aux clients. Un objet COM fournit une interface pour chaque ensemble de méthodes (fonctions membres) et de propriétés connexes (membres de données et/ou contenu).

Serveur COM
C'est un module .EXE, .DLL ou .OCX contenant le code d'un objet COM. Les implémentations d'objets résident sur les serveurs. Un objet COM implémente une ou plusieurs interfaces.
Chaque objet COM doit être implémenté dans un serveur qui peut être de type serveur en processus, serveur hors processus ou serveur distant
Pour plus de détails voir l'index de l'aide en ligne de Delphi, recherchez : Serveurs en processus.

Client COM
Il s'agit du code appelant les interfaces afin d'obtenir du serveur les services demandés. Les clients savent ce qu'ils veulent obtenir du serveur (via l'interface); les clients ne savent pas comment en interne le serveur fournit les services.
Le client COM le plus courant à implémenter est un contrôleur Automation. Delphi facilite le processus de création de clients en vous permettant d'installer des serveurs COM, tels qu'un document Word ou une diapositive PowerPoint, en tant que composants sur la palette de composants.

III - Comment est-ce que cela fonctionne

La manière dont un client est appelé par le serveur n'est pas trop différente dans le concept, de l'appel inverse c'est-à-dire lorsque le client appel le serveur. Le serveur définit et déclenche des événements tandis que le client est responsable de relier et de traiter ces événements. Ceci est accompli par des événements d'interface ou d'interface sortante définie dans la bibliothèque de type de l'objet serveur, le plus souvent un fichier .TLB.

Une interface sortante signifie que l'objet serveur COM sait comment se comporter en client pour cette interface, c'est-à-dire s'adresser à son ou ses clients.

Mais avant de commencer, il est temps de définir une certaine terminologie.

Point de connexion (Connection Point) : une entité décrivant l'accès à des événements d'interface.

Source d'événement (Event Source) : un objet définissant une interface sortante et déclenchant des événements, typiquement le serveur Automation.

Collecteur d'événements (Event Sink) : un objet implémentant une interface d'événement et répondant aux événements, typiquement le client.

Récepteur ( Advise ) : en reliant un collecteur d'événements à un point de connexion, vous faites en sorte que les méthodes du collecteur d'événements puissent être consultées par la source.

Il s'agit des principales pièces du modèle d'événements. Si je devais résumer cet article, je dirais simplement ceci : un collecteur d'événements est relié à une source par l'intermédiaire d'un point de connexion, permettant ainsi à la source de déclencher des événements implémentés par le collecteur d'événements.

L'objectif est maintenant de réaliser ce modèle d'événements au travers d'un serveur et d'un client simple. Le client aura un bouton qui appellera une méthode de serveur. Cette méthode se déclenchera, le client gérera l'événement et affichera une simple information par l'intermédiaire d'un control TMemo.

IV - Écrivons le serveur

N'importe quel serveur d'automation qui veut communiquer en utilisant des événements à besoin de définir une interface sortante, et doit implémenter une interface entrante pour trouver et se rattacher à ces interfaces. Cette interface entrante est IConnectionPointContainer.
Sous Delphi l'objet TConnectionPoints implémente cette interface.

Le client emploiera cette interface pour trouver ou énumérer les points de connexion supportés par le collecteur d'événements avec les méthodes FindConnectionPoint et EnumConnectionPoints. Un objet point de connexion IConnectionPoint sera retourné, le client peut alors appeler la méthode Advise pour relier son collecteur d'événements au point de connexion.

Ces deux interfaces sont définies pour Delphi 5 dans l'unité ACTIVEX.PAS. Vous n'avez pas à écrire ce code manuellement, l'expert Objet Automation le fera pour vous.

Ouvrez une nouvelle application ( notre serveur ) et ajoutez sur la forme un objet TMemo. Maintenant, démarrez l'expert Automation, menu Fichier|Nouveau|Onglet ActiveX|Objet d'automation. Vous devez renseigner un nom de coClass dans le champ ‘Nom de CoClass :', nous emploierons le nom SimpleEventServer.

Avant de cliquer sur 'OK', validez l'option ‘Générer le code de support d'événement'. Il s'agit de l'implémentation pour vos traitements qui seront reliés par l'objet IConnectionPointContainer comme décrit dans le paragraphe ci-dessus.

Le résultat est une unité contenant votre objet TSimpleServer et ces définitions de coClass. Delphi générera également une bibliothèque de type qui inclue les interfaces duales entrantes typiques : ISimpleEventServer et ISimpleEventServerDisp, plus une que vous n'avez pas vue auparavant, votre interface d'événements sortante ISimpleEventServerEvents.
Note : les interfaces duales offrent les avantages combinés des interfaces VTable et des dispinterfaces.
Cliquez sur ‘OK'.

L'interface ISimpleEventServer doit maintenant exposer une méthode pour appeler notre client. Ouvrez l'éditeur de bibliothèque de type (s'il n'est pas déjà ouvert, sinon menu Voir| Bibliothèque de type), positionnez-vous sur l'interface ISimpleEventServer et ajoutez une méthode CallServer (clic droit, menu Nouveau|Méthode). Maintenant pour le serveur nous devons définir une méthode qui déclenchera l'événement. Positionnez-vous sur l'interface l'IsimpleEventServerEvents et créez la méthode EventFired. Ceci étant fait, cliquez sur l'icône de la barre d'outils 'rafraîchir l'implémentation'.

Vous noterez qu'une méthode a été ajoutée dans le code source. Lorsque le client appellera le serveur, le serveur informera l'utilisateur par l'intermédiaire de l'objet TMemo et déclenchera son événement EventFired.

Implémentez, dans l'unité créée par l'expert Automation, la procédure CallServer :

procedure TSimpleEventServer.CallServer;
begin
Form1.Memo1.Lines.Add('2 - J''ai été appelé par un client.');
if FEvents different nil then
begin
FEvents.EventFired;
Form1.Memo1.Lines.Add('3 - Je déclenche un événement.');
end;
end;

Ajouter dans la clause uses de la partie interface, le nom d'unité du serveur qui correspond à l'unité de la fiche principale qui contient le TMemo.

Note : FEvents est notre interface sortante. Nous la vérifions avant de déclencher l'événement. Ceci s'assure qu'un client écoute réellement. Si on ne lui a pas relié un collecteur d'événements client, elle renverra NIL.

Ceci étant dit le serveur est terminé ! Compilez-le, puis exécutez-le une fois pour l'enregistrer, bien qu'il n'y ait aucun appel implicite à une quelconque procédure ou fonction d'enregistrement dans la base de registre.
Cet autoenregistrement se faisant lors de l'appel de CoRegisterClassObject imbriquée dans la méthode RegisterClassObject d'un descendant de TComObjectFactory.
On peut aussi utiliser en ligne de commande les paramètres /RegServer ou /UnRegServer.

Les objets COM d'une DLL devant quant à eux être enregistrés explicitement par un appel à l'utilitaire en ligne de commande REGSVR32. Cette DLL devant implémenter et exporter les quatre méthodes suivantes :
DllRegisterServer, DLLUnegisterServer, DllGetClassObject et DllCanUnloadNow.

V - Le client

Ouvrez une nouvelle application ( notre client ) et ajoutez sur la forme un objet TMemo et un objet TButton. Maintenant ajoutez dans la clause uses de la partie interface, le nom d'unité de votre serveur (xxx_TLB) ainsi nous accèderons aux types et méthodes de ce serveur. Ajoutez dans la clause uses de la partie implémentation, l'unité ComObj et ActiveX ou Variant pour D6 et D7. Votre forme principale aura besoin de propriétés pour contenir les interfaces de l'objet serveur et de l'objet collecteur d'événements.

Déclarez-les dans des propriétés privées comme suit :

…
private
{ Déclarations privées }
FServer: ISimpleEventServer;
FEventSink: IUnknown;
FConnectionToken: integer;
…

Une fois les fondations réalisées, nous pouvons commencer à nous occuper de la seule tâche difficile dans la gestion des événements COM : l'exécution du collecteur d'événements. On commence par définir l'objet collecteur d'événements, qui est un objet Automation, puis on implémente la méthode IDispatch. Le travail du collecteur d'événements est de déléguer les appels au serveur par le biais de sa méthode Invoke. Il appelle alors l'implémentation locale pour l'événement.

Afin de les cloisonner, nous laisserons ces réalisations dans l'unité principale et utiliserons une référence à la forme principale dans l'objet collecteur pour les appeler plus tard.
Voici la définition du collecteur d'événements :

TEventSink = class(TInterfacedObject, IUnknown,IDispatch)
private
FController: TForm1;
{ Méthodes de l'interface IUknown }
function QueryInterface(const IID: TGUID; out      Obj):HResult;stdcall;
{ Méthodes de l'interface IDispatch}
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;
public
constructor Create(Controller: TForm1);
end;

La plupart de ces méthodes n'ont pas besoin d'être implémentées. Le simple fait qu'elle retourne S_OK est suffisant, excepté pour les méthodes QueryInterface et Invoke. Ce code de retour indique que l'appel a réussi. Ces méthodes sont employées par le serveur pour obtenir les interfaces et appeler vos gestionnaires d'événement.

function TEventSink.QueryInterface(const IID: TGUID; out Obj):HResult;stdcall;
begin
if GetInterFace(IID,Obj) then
Result := S_OK
else if IsEqualIID(IID,ISimpleEventServerEvents) then
Result := QueryInterface(IDispatch,Obj)
else
Result := E_NOINTERFACE;
end;

Cette méthode QueryInterface s'occupe d'abord de récupérer les pointeurs d'interface de ses propres méthodes IDispatch et Iunknown par l'appel de la méthode GetInterface.
Si l'appel de GetInterface échoue et que QueryInterface est questionnée au sujet de l'interface ISimpleEventServerEvents, elle s'appellera récursivement pour obtenir le pointeur de l'interface sortante.

function TEventSink.Invoke(DispID: integer; const IID: TGUID; LocaleID: integer;
Flags: Word; var Params; VarResult,ExcepInfo,ArgErr:Pointer): HResult;
begin
Result := S_OK;
case DispID of
2 : FController.OnEventFired;
end;
end;

Ici le nombre 2 indique l'identificateur de répartition automation d'une méthode, qui est déclaré par la directive dispid en la faisant suivre d'une constante entière :

ISimpleEventServerEvents = dispinterface
['{F8A71D6A-B9FD-4CEE-AC3F-3E3DC9CBA16B}']
procedure EventFired; dispid 2;
end;

Naturellement l'instruction case ci-dessus, aurait plus de cas si nous avions plus d'événements, mais cette interface et ce collecteur en supportent seulement un.
Notez que la méthode Invoke appelle notre gestionnaire par une référence locale à l'objet Form principale où notre gestionnaire d'événement est déclaré. Le constructeur du collecteur d'événements devrait renseigner les propriétés comme ceci :

constructor TEventSink.Create(Controller: TForm1);
begin
inherited Create;
FController := Controller;
end;

Avec ces méthodes et objets en place, tout ce qui reste à faire est de relier le collecteur à la source.
Nous ferons ceci dans le gestionnaire d'événement OnCreate du client :

procedure TForm1.FormCreate(Sender: TObject);
begin
FServer := CoSimpleEventServer.Create;
FEventSink := TEventSink.Create(Form1);
InterfaceConnect(FServer, ISimpleEventServerEvents,FEventSink,FconnectionToken);
end;

Note : rappelez-vous que la variable FEventSink est une interface IUnknown que nous recevons et interfaçons par le constructeur de TEventSink, et pas par une référence standard. Ceci nous permettra de lui relier un point de connexion.

D'abord, nous créons notre serveur en utilisant ses coClass. Puis nous créons une instance de notre collecteur d'événements. Comme je l'ai dit plutôt, la méthode InterfaceConnect est plus facile à employer, mais vous perdez en fonctionnalités et en compréhension.

Vous pouvez l'employer ici en passant à l'interface de votre serveur :

- l'IID de l'interface d'événements que vous questionnez ;

- l'interface IUnknown de votre collecteur d'événements nouvellement créé ;

- un nombre entier représentant le numéro de la connexion.

Attention vous devez mémoriser ce nombre entier, représentant la connexion, si vous souhaitez correctement la libérer. Vous pouvez terminer la connexion simplement en appelant la méthode InterfaceDisconnect avec le même nombre entier que vous avez reçu lors de l'appel à la méthode InterfaceConnect.

L'IID est l'identificateur de l'interface, voici un exemple  :

IID_ISimpleEventServer: TGUID = '{D80B53C6-19C2-47CA-A8AA-7329ED34A0F3}';

Dans la méthode Destroy on s'assure de libérer les ressources et de déconnecter l'interface IConnectionPoint précédemment connectée :

procedure TForm1.FormDestroy(Sender: TObject);
begin
InterfaceDisconnect(FServer,ISimpleEventServer,FConnectionToken);
FServer := nil;
FEventSink := nil;
end;

Ajoutons l'appel de la méthode du serveur COM :

procedure TForm1.Button1Click(Sender: TObject);
begin
Memo1.Lines.Add('1 - J''appelle le serveur.');
FServer.CallServer;
Memo1.Lines.Add('Fin.');
end;

Il nous reste à ajouter la méthode qui nous intéresse le plus, celle qui gère la réception des événements :

procedure TForm1.OnEventFired;
begin
Memo1.Lines.Add('4 - J''ai reçu un événement.');
end;

Maintenant, compilez et exécutez le client. Le serveur devrait démarrer ! Vous êtes maintenant relié pour écouter des événements.
Cliquez sur le bouton du client pour tester votre événement.

Code source de l'exemple.
Un exemple supplémentaire se trouve dans le répertoire d'installation de Delphi…\Delphi\Demos\Activex\Oleauto\Autoserv.

VI - Annexe

L'article précédent ne donne pas de détail sur la méthode IDispatch::Invoke je vous propose donc quelques éclaircissements provenant du SDK.

Les libraires de type d'objet COM importées sous Delphi contiennent des composants dérivés de la classe TOleServer, exemple :

TWordApplication = class(TOleServer);

Si vous utilisez ce type de composant la méthode TOleServer.Connect crée automatiquement la connexion au collecteur d'événements, vous n'aurez donc pas besoin de prendre en charge la gestion des événements du serveur COM.
Par contre si vous utilisez des serveurs COM via des variants, par exemple :

MsWord := CreateOleObject('Word.Basic');

Alors dans ce cas vous devez prendre en charge la gestion des événements du serveur COM.

VI-A - Passage de paramètres

Les arguments d'une méthode ou d'une propriété appelée sont passés dans un record de type tagDISPPARAMS. Cette structure se compose d'un pointeur sur un tableau d'arguments représentant des variants, d'un pointeur sur un tableau de DISPID pour les arguments nommés, et du nombre d'arguments dans chaque tableau.

PDispParams = ^TDispParams;
{$EXTERNALSYM tagDISPPARAMS}
tagDISPPARAMS = record
rgvarg: PVariantArgList; // tableau d'arguments.
rgdispidNamedArgs: PDispIDList; // Dispatch IDs des arguments nommés
cArgs: Longint; // Nombre d'arguments.
cNamedArgs: Longint; // Nombre d'arguments nommés
end;
TDispParams = tagDISPPARAMS;
{$EXTERNALSYM DISPPARAMS}
DISPPARAMS = TDispParams;

Les arguments sont passés dans le tableau rgvarg[ ], avec le nombre d'arguments passés dans cArgs.
Les arguments dans le tableau sont insérés du dernier au premier, ainsi le tableau rgvarg[0] contient le dernier argument et rgvarg[ cArgs -1 ] contient le premier argument. La méthode ou la propriété peut changer les valeurs des éléments du tableau rgvarg[], mais seulement si elle positionne la valeur VT_BYREF dans la variable flag . Sinon, considérez les éléments du tableau en Read-Only.

L'appel de la méthode Invoke peut 'contenir' des arguments nommés aussi bien que des arguments de position.
Si cNamedArgs vaut 0, tous les éléments du tableau rgvarg[] représentent des arguments de position.
Si cNamedArgs est différent de 0, chaque élément du tableau rgdispidNamedArgs[] contient le DISPID d'un argument nommé, et la valeur de l'argument est dans l'élément correspondant du tableau rgvarg[ ].

Les DISPID des arguments nommés sont toujours contigus dans le tableau rgdispidNamedArgs, et leurs valeurs se trouvent dans les premiers cNamedArgs éléments du tableau rgvarg. Des arguments nommés ne peuvent pas être consultés par position, et des arguments de position ne peuvent pas être nommés.
Le DISPID d'un argument est sa position zéro-basée dans la liste d'argument. Par exemple, la méthode suivante prend trois arguments :

Ccredit.CheckCredit(BstrCustomerID : String,   // DISPID = 0.
BstrLenderID: String,      // DISPID = 1.
cLoanAmt : Currency)       // DISPID = 2.
Begin
{code omis}
End ;

Si vous incluez le DISPID avec chaque argument nommé, vous pouvez passer les arguments nommés à la méthode Invoke dans n'importe quel ordre. Par exemple, si une méthode doit être appelée avec deux arguments de position, suivis de trois nommés ici les arguments (A, B, et C), employer la syntaxe suivante :
cArgs vaudrait 5, et cNamedArgs vaudrait 3.

object.method("arg1", "arg2", A : = "argA", B : = "argB", C : = "argC");

Le premier argument de position serait dans rgvarg[4].
Le deuxième argument de position serait dans le rgvarg[3]. L'ordre des arguments nommés n'est pas important pour l'exécution de l'implémentation de IDispatch, mais ces arguments sont généralement passés dans l'ordre inverse.
L'argument A serait dans rgvarg[2], avec le DISPID de A dans rgdispidNamedArgs[2].
L'argument B serait dans rgvarg[1], avec le DISPID de B dans rgdispidNamedArgs[1].
L'argument C serait dans rgvarg[0], avec le DISPID de C dans rgdispidNamedArgs[0].

Le diagramme suivant illustre les tableaux et leur contenu.

Vous pouvez également employer la méthode Invoke sur des membres avec des arguments optionnels, mais tous les arguments optionnels doivent être de type Variant. Comme avec des arguments requis, le contenu du vecteur d'argument dépend si les arguments sont de type position ou nommés. Le membre invoqué doit s'assurer que les arguments sont valides. Invoke passe simplement le record tagDISPPARAMS qu'elle reçoit.
Omettre des arguments nommés est simple. Vous passez les arguments dans le tableau rgvarg et leur DISPIDs dans le tableau rgdispidNamedArgs.

Pour omettre l'argument nommé B de l'exemple précédent :
vous placez la valeur de C dans rgvarg[0] et son DISPID dans rgdispidNamedArgs[0], la valeur de A dans rgvarg[1] et son DISPID dans rgdispidNamedArgs[1].
Les arguments de position suivants occuperaient les éléments 2 et 3 des tableaux. Dans ce cas, cArgs vaut 4 et cNamedArgs vaut 2.
Si les arguments sont de type position (non nommés), vous placez le nombre d'arguments possibles dans cArgs, 0 dans cNamedArgs, et passerez VT_ERROR comme type des arguments omis, avec le code de statut DISP_E_PARAMNOTFOUND comme valeur.

Par exemple, le code suivant prépare le contexte permettant l'appel de la méthode Quit de MS Word.

procedure TForm1.Button1Click(Sender: TObject);
var vMSWord,
vWDocument, vWDocuments,
MonParagraph : Variant;

begin
// Ouvre Word
vMSWord:= CreateOleObject('Word.Application');
vMSWord.Visible:= true;

// Ajoute un document
vWDocuments:= vMSWord.Documents;
vWDocument:= vWDocuments.Add;

// Ajoute un paragraphe
MonParagraph:=vWDocument.Paragraphs.Add;

// Insére du texte, Word demandera confirmation de l'enregistrement
MonParagraph.Range.insertBefore('Demande de confirmation de l''enregistrement.');

// À titre indicatif, si on souhaite tester que l'objet COM possède bien
// une interface IDispatch on peut effectuer le test suivant
if VarType(vMSWord) = varDispatch then
begin
CallMember(IUnknown(vMSWord) As IDispatch);
end;
vMSWord:=Unassigned;

end;

Le code suivant appelle la méthode Quit de MS Word via la méthode Invoke de l'interface _Application.

procedure CallMember(Dispatch : IDispatch);

Var VarResult: OleVariant ; // Paramètre de la Méthode Invoke
ArgErr : DWord ;
Params: TDispParams;
lDispID: Integer;
ExcepInfo: TExcepInfo;

Resultat: HResult;
Member: Widestring;
LocalVariantArg : array[0..3] of TVariantArg;

begin
ldispid:=0;
// Méthode Nommée Quit de l'interface _Application ( MS Word )
Member:='Quit';

FillChar(ExcepInfo, SizeOf(ExcepInfo),0);
FillChar(Params, SizeOf(DispParams),0);
FillChar(LocalVariantArg,SizeOf(LocalVariantArg),0);
// Tableau recevant les 3 paramètres de la méthode
Params.rgvarg:=@LocalVariantArg;

// Recherche le numéro d'identificateurs de dispatch (dispID)
// De la méthode contenue dans Member
OLECheck(Dispatch.GetIDsOfNames(GUID_NULL, @Member, 1,
LOCALE_USER_DEFAULT, @ldispid)) ;

// Les derniers paramètres ne sont pas renseignés,
// mais initialisés en conséquence
Params.rgvarg[0].vt:= varError; // vt_error
Params.rgvarg[0].scode:= DISP_E_PARAMNOTFOUND;
Params.rgvarg[1].vt:= varError; // vt_error
Params.rgvarg[1].scode:= DISP_E_PARAMNOTFOUND;

// Le premier paramètre est un integer
Params.rgvarg[2].vt:= varInteger; // vt_i4
// Sa valeur, pour Word, correspond à une demande d'enregistrement
Params.rgvarg[2].ival:= wdPromptToSaveChanges;

// La méthode attend 3 paramètres
Params.cArgs:= 3;
// Aucun argument nommé
Params.cNamedArgs:= 0;

// Appel de la méthode de l'interface
Resultat:=Dispatch.Invoke(ldispid,GUID_NULL,0,DISPATCH_METHOD,Params,
@VarResult, @ExcepInfo,@ArgErr);

// En cas d'erreur on doit lever une exception
// Par exemple si Params.cArgs:= 1;
if not(Resultat=0)
then DispatchInvokeError(Resultat, ExcepInfo);
end;

En cas d'appel retournant une erreur, vous pouvez utiliser la méthode DispInvokeError qui déclenche une exception en utilisant les informations contenues dans le record TExcepInfo.

Le code appelant est responsable de la libération de toutes les chaînes de caractères et des objets référencés par rgvarg[ ] ou placés dans VarResult.
Comme avec d'autres paramètres qui sont passés par valeur, si le membre appelé doit maintenir l'accès à une chaîne de caractères après le retour, vous devrez copier la chaîne de caractères. De même, si le membre a besoin d'accéder à un pointeur d'objet après le retour, elle doit appeler la fonction AddRef sur l'objet. Un exemple commun se produit quand une propriété d'objet est changée pour se rapporter à un nouvel objet, en utilisant le drapeau de DISPATCH_PROPERTYPUTREF.
Pour ceux qui implémentent IDispatch.Invoke, l'automation fournit la fonction DispGetParam pour rechercher des paramètres du vecteur d'argument et les contraindre au type approprié.

Le record tagVARIANT est employé pour décrire les variants utilisés dans le record tagDISPPARAMS.
La méthode Invoke renvoie une des valeurs suivantes (valeurs déclarées dans l'unité Windows) :
S_OK
L'appel a réussi.

DISP_E_BADPARAMCOUNT
Le nombre d'éléments fournis dans le paramètre DISPPARAMS est différent du nombre d'arguments accepté par la méthode ou la propriété.

DISP_E_BADVARTYPE
Un des arguments dans le tableau rgvarg n'est pas un type variant valide.

DISP_E_EXCEPTION
L'application doit lever une exception. Dans ce cas-ci, la structure passée dans ExcepInfo devrait être complétée.

DISP_E_MEMBERNOTFOUND
Le membre demandé n'existe pas, ou l'appel à Invoke a essayé de placer une valeur dans une propriété en Read-Only.

DISP_E_NONAMEDARGS
Cette implémentation de IDispatch ne supporte pas des arguments nommés.

DISP_E_OVERFLOW
Un des arguments dans le tableau rgvarg n'a pas pu être contraint dans le type indiqué.

DISP_E_PARAMNOTFOUND
Un des paramètres de DISPIDs ne correspond pas à un paramètre de la méthode. Dans ce cas-ci, ArgErr devrait être positionné sur le premier argument qui contient l'erreur.

DISP_E_TYPEMISMATCH
Un ou plusieurs des arguments n'a pas pu être contraint. L'index du premier paramètre, du tableau rgvarg, avec le type incorrect est retourné dans le paramètre ArgErr.

DISP_E_UNKNOWNINTERFACE
L'identifiant d'interface passé dans le paramètre IID n'est pas la constante IID_NULL.

DISP_E_UNKNOWNLCID
Le membre invoqué interprète les arguments de chaîne selon le LCID, mais le LCID n'est pas identifié. Si le LCID n'est pas nécessaire pour interpréter des arguments, cette erreur ne devrait pas être retournée.

DISP_E_PARAMNOTOPTIONAL
Un paramètre requis a été omis.

Cas particulier
Lorsqu'on souhaite passer en paramètre une chaîne de caractères (VT_BSTR), le code suivant ne fonctionne pas dans l'environnement COM :

Params.rgvarg[x].vt := VT_BSTR;
Params.rgvarg[x].bstrVal := 'Ceci est un test.';

Le type attendu par la propriété bstrval est un PWideChar, il est préférable d'utiliser la fonction StringToOLEStr qui attend un paramètre de type String en entrée et renvoie une valeur de type PWideChar. Elle est définie dans l'unité System.

Exemple :

Params.rgvarg[0].vt := VT_BSTR;
Params.rgvarg[0].bstrVal := StringToOLEStr('Ceci est un test.');

À l'inverse si on doit manipuler des variables de type PWideChar représentant des chaînes COM, la fonction inverse OleStrToString renvoie une « vraie » string.

Si vous possédez une version de Delphi livrée avec les sources et que souhaitez approfondir l'implémentation de la méthode Invoke, vous pouvez consultez la méthode DispatchInvoke de l'unité ComObj.

VI-B - Voir Aussi

Le design pattern Observer par Pierre Castelain.

Introduction aux objets Automations (COM) avec Delphi par Michael Moreno.

Outil d'import d'interfaces d'événement COM.