1. Public concerné

Image non disponible

Testé avec Delphi 2005 sous XP Pro SP1.
Version 1.0

1-1. Les sources

Les fichiers sources des différents exemples :
FTP.
HTTP.

L'article au format PDF.

2. L'interface IEnumVariant

Extrait du SDK

L'automation définit l'interface IEnumVariant afin de fournir une manière standard pour des clients ActiveX d'itérer les objets d'une collection. Chaque objet collection doit exposer une propriété en lecture seule appelée _NewEnum afin d'informer les clients de l'ActiveX que l'objet supporte l'itération. La propriété _NewEnum renvoie un objet énumérateur qui supporte IEnumVariant.

L'interface IEnumVariant fournit une manière d'itérer les éléments contenus dans un objet collection. Cette interface est supportée par un objet énumérateur qui est retourné par la propriété _NewEnum de l'objet collection, comme dans la figure suivante :

Image non disponible

L'interface IEnumVariant définie les fonctions membre suivante :

Nom de méthode Description
Clone Crée une copie de l'état courant de l'énumération
Next Renvoie l'élément suivant dans l'ordre d'énumération
Resets Réinitialise l'ordre d'énumération au premier élément
Skip Modifie l'ordre d'énumération, saute les n prochains éléments

Pour le détail des membres voir le SDK.

L'interface IEnumVariant est déclarée dans l'unite ActiveX.

3. L'interface IDispatch

A travers la méthode IDispatch.Invoke un client peut invoquer un nombre quelconque de méthodes.
Cette interface autorise l'invocation de méthode par leur nom, plutôt que par leur position dans la table des méthodes virtuelle de l'objet. Elle permet donc le "late binding" c'est à dire l'appel de méthodes connues lors de l'éxécution mais inconnues lors de la compilation.
Vous trouverez d'autres informations à son sujet dans ce tutoriel ou encore dans cette FAQ.

4. Un cas d'école

Aprés ces quelques présentations des interfaces que nous allons utiliser tout au long de ce tutoriel, je vous propose un exemple permettant de récupérer les langages supportés par l'application Microsoft Word.

Mais avant d'aller plus loin, la première chose à savoir ici est 'est-ce que l'objet COM que je souhaite manipuler propose une collection ?'. Par convention Microsoft nomme au pluriel les objets collection mais dans d'autres contexte cette régle de nommage peut ne pas exister.
Dans ces cas là nous utiliserons le fichier de librairie (xxx_TLB.PAS) fournie ou extraite à l'aide de Delphi.

Recherchons dans le fichier Word2000.pas l'interface Languages :

 
Sélectionnez

 Languages = interface(IDispatch)
    ['{0002096E-0000-0000-C000-000000000046}']
    function Get__NewEnum: IUnknown; safecall;
     ... 
    property _NewEnum: IUnknown read Get__NewEnum;
    ...
  end;

A partir du moment où l'interface hérite de IDispatch on retrouve la dispinterface associée :

 
Sélectionnez

LanguagesDisp = dispinterface
    ['{0002096E-0000-0000-C000-000000000046}']
    property _NewEnum: IUnknown readonly dispid -4;
    ...
  end;

Dans la déclaration de l'interface Languages on retrouve bien la propriété _NewEnum qui nous indique la présence d'une collection IEnumVariant.
On peut donc continuer.

4-1. Exemple d'énumération avec IEnumVariant

Voici l'exemple récupérant la collection des langages supportés par MS Word :

 
Sélectionnez

procedure WordlLangage; 
var Word  : Variant; 
    Element: OleVariant; 

    IEnum : IEnumVariant; 
    Langue : Variant; 
    Nombre : LongWord; 
begin 
   //Crée l'objet Automation Word
  Word := CreateOleObject('Word.Application'); 

   // Obtention de la liste des langues 
  IEnum:=IUnKnown(Word.Languages._NewEnum) as IEnumVariant; 

Ici la variable Word est déclarée en tant que Variant mais la propriété _NewEnum renvoie une interface IUnknown qui est l'ancêtre des interfaces (Elle est en quelque sorte le TObject pour les interfaces).
On doit donc dans un premier temps transtyper le variant en IUnknown pour éviter l'erreur suivante :

 
Sélectionnez

E2015 : Opérateur non applicable à ce type d'opérande.

Puis dans un second temps transtyper l'interface IUnknown en IEnumVariant qui correspond à l'interface que l'on souhaite manipuler.


Voici un extrait de la documentation de Delphi 2005 concernant cette opération :

 
Sélectionnez

Interrogation d'interface
 
Vous pouvez utiliser l'opérateur as afin d'effectuer des transtypages d'interface avec vérification. Ce mécanisme
s'appelle l'interrogation d'interface ; il produit une expression de type interface depuis une référence d'objet 
ou une autre référence d'interface à partir du type réel (à l'exécution) de l'objet. Une interrogation 
d'interface a la forme suivante :
 objet as interface
où object est une expression de type interface, de type variant ou désignant une instance d'une classe qui 
implémente une interface, et où interface est une interface déclarée avec un GUID.
 
L'interrogation d'interface renvoie nil si object vaut nil. Sinon, elle transmet le GUID de interface à la méthode 
QueryInterface de object et déclenche une exception sauf si QueryInterface renvoie zéro. Si QueryInterface renvoie 
zéro (ce qui indique que la classe de object implémente interface), l'interrogation d'interface renvoie une 
référence sur object.

Note : Cette opération as sur une interface incrémente le compteur de référence.

L'appel de la méthode Next permet d'itérer sur la collection des langages, ici avec un pas de 1.

 
Sélectionnez

   // Puis utilisation de l'énumérateur de collection fourni 
  While IEnum.Next(1,Element,Nombre)=S_OK Do 
  Begin 
     // Il est indispensable de transtyper la variable Element 
     // dans le type de la collection manipulée
    Langue := IUnknown(Element) As Language; 
     // Accés aux informations
    Writeln(IntToStr(Langue.ID),' ',Langue.Name,' ',Langue.NameLocal); 
  End; 
  
  Word.Quit; 
  Word := unassigned; 
end;

Cette méthode renvoie en revanche une variable de type OleVariant. On doit donc ici aussi transtyper la variable avec le type d'interface que la collection héberge. On retrouve ce type dans le prototype de la méthode Languages.Item :

 
Sélectionnez

 Languages = interface(IDispatch)
    ['{0002096E-0000-0000-C000-000000000046}']
    ...
	function Item(var Index: OleVariant): Language; safecall;
	...
 end;	

Voici un extrait de la documentation de Delphi 2005 concernant la différence entre le type Variant et OleVariant :

 
Sélectionnez

Le type OleVariant existe sur les deux plates-formes Windows et Linux. La différence principale entre Variant et
OleVariant est que Variant peut contenir des types de données que seule l'application en cours sait traiter. 

OleVariant contient uniquement des types de données compatibles avec OLE Automation, ce qui signifie que ces 
types de données peuvent être transférés entre programmes ou sur le réseau sans qu'il soit nécessaire de savoir si 
l'autre extrémité saura manipuler les données.

5. Les itérateurs Delphi

Maintenant que nous avons abordé les problématiques de base liées à l'interface IEnumVariant, passons à l'implémentation de l'itérateur.

Vous trouverez ici un tutoriel sur les bases des itérateurs sous Delphi 2005 et supérieure.
Comme indiqué dans le chapitre 3.3 du précédent tutoriel cité, pour créer un itérateur nous devons implémenter 2 classes :

  • une classe sur la laquelle on souhaite itérer, ici il s'agit de la classeTEnumVariant
  • et une classe "itérateur" qui prend en charge les opérations d'itération sur la collection, ici il s'agit de la classe TEnumerateur.

Note : dans l'exemple la classe TCustomEnumerateurEnumVariant permet de spécialiser les classes dérivées.

 
Sélectionnez

type
    // Conteneur générique pour une collection IEnumVariant
  TCustomEnumerateurEnumVariant = class
  private
     // Contient la collection
    FObjetCollection: IEnumVariant;
     // Accés remote possible
    FItem:  OleVariant;
  public
    Constructor Create(Enum : IDispatch);
    Function GetNextItem:Boolean;
    Function GetCollection(ACollection:IDispatch) :IEnumVariant;

    Property Item:OleVariant read FItem;
    Property NextItem:Boolean read GetNextItem;
  end;

  TEnumerateur = class;

   // Conteneur spécialisé pour une interface implémentant IEnumVariant
  TEnumVariant = class(TCustomEnumerateurEnumVariant)
          //Méthode nécessaire pour la prise en charge de l'itération
    Function GetEnumerator: TEnumerateur;
  end;

   // Enumérateur pour la classe conteneur.
   // Il doit implémenter une suite de méthodes comme indiquée ci-dessous.
  TEnumerateur = class
  strict private
    FListe : TEnumVariant; //Référence la collection concernée par l'itération
  public
    constructor Create (AList : TEnumVariant);
     //Membre de classse nécessaire pour la prise en charge de l'itération
    function GetCurrent: IUnknown;
    function MoveNext :Boolean;    // Méthode publique
    property Current : IUnknown read GetCurrent; // Propriété en Read Only
  end;

Voyons maintenant le détail de l'implémentation.

5-1. Implémentation de la classe à énumérer

La classe TCustomEnumerateurEnumVariant permet d'encapsuler l'interface IEnumVariant.
Nous avons besoin de connaître les informations suivantes :

  • la collection IEnumVariant, il s'agit du champ FObjetCollection
  • l'élement courant, implémenté par la propriété Item
  • l'élément suivant, implémenté par la propriété NextItem

5-1-1. Le champ FObjetCollection

Afin de pouvoir manipuler différent type d'interface implémentant IEnumVariant, il nous faut manipuler une interface IDispatch.
Mais du coup ce choix ne permet plus l'accés à la propriété _NewEnum de la manière suivante :

 
Sélectionnez

  FObjetCollection:=(Enum._NewEnum) as IEnumVariant

car ce code provoque à la compilation l'erreur suivante :

 
Sélectionnez

  E2003 : Identificateur non déclaré : '_NewEnum'

Comme on ne connaît plus le type de l'interface, via la librairie de type, on appellera dynamiquement la propriété _NewEnum commune aux interfaces implémentant IEnumVariant.

Voici donc le constructeur qui renseignera la collection :

 
Sélectionnez

Constructor TCustomEnumerateurEnumVariant.Create(Enum : IDispatch);
begin
 Inherited;
 FObjetCollection:=GetCollection(Enum);
end;

La méthode GetCollection appel dynamiquement la propriété _NewEnum et renvoie la collection IEnumVariant attendue :

 
Sélectionnez

Function TCustomEnumerateurEnumVariant.GetCollection(ACollection : IDispatch):IEnumVariant;
Var VarResult: OleVariant ; // Résultat de la méthode Invoke
    Params: TDispParams;    // Tableau de paramètre, ici 0
    lProperty: Widestring;  // Contient le nom de la propriété à appeler    
    lDispID: Integer;       // Numéro de DispId de la propriété
    Resultat: HResult;      // Résultat de l'appel COM
    ExcepInfo: TExcepInfo;  // En cas d'erreur d'appel

begin
  Result:=Nil;
  lProperty:='_NewEnum';
  FillChar(Params, SizeOf(DispParams),0);

   // Recherche le numéro d'identificateurs de dispatch (dispID) de la propriété contenue dans lProperty
  OLECheck(ACollection.GetIDsOfNames(GUID_NULL, @lProperty, 1,LOCALE_USER_DEFAULT, @ldispid)) ;

   // Appel de la propriété de l'interface
  Resultat:=ACollection.Invoke(ldispid,GUID_NULL,0,DISPATCH_PROPERTYGET,Params,@VarResult, @ExcepInfo,Nil);

   // En cas d'erreur on doit lever une exception
  if Resultat<> 0
   then DispatchInvokeError(Resultat, ExcepInfo);

   //Transtype le résultat obtenu en Collection de variant
  Result := IUnKnown(VarResult) as IEnumVariant;
end;

Note : Si l'interface ACollection n'implémente pas la propriété _NewEnum, l'appel de Invoke déclenchera l'exception EOleSysError : Nom inconnu.

Vous trouverez dans ce tutoriel le détail de l'appel de la méthode IDispatch.Invoke.

5-1-2. Les propriétés Item et NextItem

Les 2 autres informations nécessaires sont gérées dans la méthode GetNextItem :

 
Sélectionnez

Function TCustomEnumerateurEnumVariant.GetNextItem:Boolean;
var NombreElement : LongWord;
begin
 Result:=(FObjetCollection.Next(1, FItem, NombreElement) = S_OK);
end;

On retrouve donc ici la méthode Next de l'interface IEnumVaraint. Elle récupére, dans la propriété FItem, la prochaine valeur de la collection FObjetCollection.
Cette fonction renvoie False si la fin de la collection est atteinte.

5-1-3. La méthode GetEnumerator

Il ne nous reste plus qu'a implémenter la méthode GetEnumerator nécessaire pour la prise en charge de l'itération de notre nouvelle classe.

 
Sélectionnez

function TEnumVariant.GetEnumerator: TEnumerateur;
begin
  Result := TEnumerateur.Create(Self);
end;

Cette méthode est appelée automatiquement par le compilateur et renvoie une instance de la classe itérateur TEnumerateur que nous allons aborder maintenant.

5-2. Implémentation de la classe énumérateur

La classe TEnumerateur à proprement dit ne fait que référencer la collection IEnumVariant et permet de la parcourir.
Le constructeur, appelé par la méthode GetEnumerator de la classe à itérer, renseigne la référence à la collection de la classe à énumérer.

 
Sélectionnez

constructor TEnumerateur.Create(AList: TEnumVariant);
begin
  inherited;
  FListe := AList;
end;

La méthode GetCurrent renvoi la dernière valeur lue dans la collection.

 
Sélectionnez

function TEnumerateur.GetCurrent: IUnknown;
begin
  Result := FListe.Item;
end;

La méthode MoveNext se positionne sur la prochaine valeur si elle existe.

 
Sélectionnez

function TEnumerateur.MoveNext: Boolean;
begin
 Result :=FListe.GetNextItem;
end;

Cette classe ne fait tout simplement qu'implémenter le pattern de collection imposé (prescribed collection pattern) en encapsulant la gestion de la collection hébergée dans notre classe à itérer.

On peut dorénavant énumérer cette collection dans une instruction For...in...do begin...end;, où :

  • la partie in est liée à l'appel de la méthode TEnumVariant.GetEnumerator,
  • la partie For est liée à l'appel de la méthode TEnumerateur.GetCurrent,
  • la partie do begin est liée à l'appel de la méthode TEnumerateur.MoveNext,
  • la partie end appel le destructeur de la classe TEnumerateur.Destroy, ici TObject.

Sous .NET l'itérateur, ici TEnumerateur, doit implémenter l'interface IDisposable.

6. Le résultat

Voici le code d'origine modifié :

 
Sélectionnez

procedure Erreur;
var Word  : Variant;
    Element: IUnKnown;

    IEnum :  TEnumVariant;
begin
try
  Word := CreateOleObject('Word.Application');

  // Obtention de la liste des langues
  IEnum:=TEnumVariant.Create(Word.Languages);

  // Puis utilisation de l'énumérateur de collection fourni
  For Element in IEnum Do
  Begin
    With (Element As Language) do
     Writeln(IntToStr(ID),' ',Name,' ',NameLocal);
  End;
 Finally
  IEnum.Free;
  Word.Quit;
  Word := unassigned;
 end;  
end;

Bien évidement comme dans cet exemple on utilise qu'une seule collection, l'apport de l'itérateur ne semble pas important.
En revanche dans le cas d'itération sur 2 ou 3 niveaux comme c'est le cas sous WMI, vous n'avez plus qu'a appeler le constructeur puis à itérer sur l'instance renvoyée.

Par exemple :

 
Sélectionnez

...
   // Affecte un énumérateur pour la collection d'objet SWbemObjectSet
  ObjectEnumerator:=TEnumVariant.Create(wmiService.InstancesOf('systemrestore',
                                             wbemFlagReturnWhenComplete+wbemQueryFlagShallow,
                                             nil));

  For MonObjetWMI in ObjectEnumerator do
  begin
      // Affecte un énumérateur pour la collection d'objet SWbemPropertySet
   PropertyEnumerator:= TEnumVariant.Create((MonObjetWMI as SWBemObject).Properties_);

   For MaProprieteWMI in PropertyEnumerator do
   begin
       // Retrouve le nom du service via la propriété de classe nommé 'Name'.
    With (MaProprieteWMI as SWBemProperty)  do
     begin
      If Name='CreationTime'   
	  ...   

7. IEnumVariant sous .NET

Le chargement du PIA Microsoft.Office.Interop.Word.dll dans l'utilitaire ILDasm nous permet de visualiser le Wrapper COM/Interop du serveur MS Word 2003.

Pour reprendre notre exemple contrôlons l'interface Languages :

Image non disponible

Ici l'interface Languages implémente la méthode GetEnumerator qui renvoi une interface IEnumerator.
IEnumerator est l'interface de base pour tous les énumérateurs et prend en charge une itération simple sur une collection. Elle contient les membres MoveNext et Current.

Sous DotNet pour qu'une classe ou une interface puisse itérer sur une collection, elle doit implémenter l'interface IEnumerable qui expose la méthode GetEnumerator.

Sous dotNet l'interface COM IEnumVariant est mappée sur l'interface System.Collection.IEnumerator.

Ce qui implique que notre gestion de l'itérateur n'est plus nécessaire car déjà prise en charge par .NET :

 
Sélectionnez

procedure Erreur;
// Exemple précédent modifié 
var msWord : TWordApplication; // L'application Word
    Element: Language;
    Empty  : TObject; // Pour les paramètres optionnels

begin
 try
  msWord := TWordApplication.Create;

  // Puis utilisation de la collection native via System.Collection.IEnumerator
  For Element in msWord.Languages Do
   With Element do
    Console.WriteLine('{0} {1} {2}',ID,Name,NameLocal);

 Finally
  Empty := System.Reflection.Missing.Value;
  msWord.Application.Quit(Empty,Empty,Empty);
 end;
end;

Pour l'exemple d'itération de classe WMI voici l'adaptation :

 
Sélectionnez

Procedure Enumere;
// Enumére les points de restauration système Via WMI
var
  WMILocator : ManagementScope;  //TSWbemLocator
  WmiService : ConnectionOptions;//SWbemServices;
  Path : ManagementPath;
  Options : ObjectGetOptions;
  EnumOptions: EnumerationOptions;
  SRClass : ManagementClass;

  SRList: ManagementObjectCollection;
  MonObjetWMI: ManagementObject;

begin
 try
    //préparation des options de connections
   WmiService:=ConnectionOptions.Create;
   With WmiService do
   begin
    EnablePrivileges:=False; // valeur par défaut
    Timeout:=InfiniteTimeout; // InfiniteTimeout valeur par défaut
   end;

   // Création du chemin d'accés à l'espace de nom
  Path:=ManagementPath.Create('\\.\root\Default:SystemRestore');

   // Création d'une connexion avec le chemin préparé
  WMILocator:= ManagementScope.Create(Path,WmiService);
  WMILocator.Connect;

    // Prépare les options
  Options:=ObjectGetOptions.Create; // InfiniteTimeout par défaut

  SRClass:=ManagementClass.Create(Path.Path, 'SystemRestore', Options);

  EnumOptions:=EnumerationOptions.Create;
  With EnumOptions do
  Begin
   ReturnImmediately:=True;
   DirectRead:=True;
  end;

   //Récupére la collection d'instance
  SRList:= SRClass.GetInstances(EnumOptions);

  For MonObjetWMI in SRList do
   Writeln(AdjustLineBreaks(MonObjetWMI.GetText(TextFormat.Mof)));

 Except
  on E:ManagementException do
   begin
    Console.WriteLine('ErrorCode {0}',E.ErrorCode);
    Writeln('Message : ' + E.Message);
    Writeln('Source : ' + E.Source);
    if Assigned(ManagementBaseObject(E.ErrorInformation)) //extended error object
     then Console.WriteLine('Extended Description : {0}',E.ErrorInformation['Description']);
  end;
 end;
end;

La classe ManagementObjectCollection implémentant l'interface IEnumerable, sous Delphi la variable SRList peut être utilisé directement dans l'itérateur For..in..do.

En conclusion la classe d'itérateur présenté précédement n'est valable que pour Delphi Win32.