Encapsulation d'une interface IEnumVariant dans un itérateur
Date de publication : 12/11/2005 , Date de mise à jour : 12/11/2005
Par
Laurent Dardenne (Contributions)
Pour énumérer des collections d'objets COM on peut utiliser l'interface IEnumVaraint dédiée à ce traitement.
A partir de Delphi 2005 il est possible d'utiliser un itérateur pour parcourir une collection d'objets.
Afin de simplifier l'écriture du code d'applications utilisant intensivement les collections d'objets COM, j'ai encapsulé l'usage de l'interface IEnumVariant au sein d'un itérateur.
Vous trouverez dans ce tutoriel l'implémentation de cette solution.
1. Public concerné
1-1. Les sources
2. L'interface IEnumVariant
3. L'interface IDispatch
4. Un cas d'école
4-1. Exemple d'énumération avec IEnumVariant
5. Les itérateurs Delphi
5-1. Implémentation de la classe à énumérer
5-1-1. Le champ FObjetCollection
5-1-2. Les propriétés Item et NextItem
5-1-3. La méthode GetEnumerator
5-2. Implémentation de la classe énumérateur
6. Le résultat
7. IEnumVariant sous .NET
1. Public concerné
Testé avec Delphi 2005 sous XP Pro SP1.
Version 1.0
1-1. Les sources
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 :
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 :
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 :
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 :
procedure WordlLangage;
var Word : Variant;
Element: OleVariant;
IEnum : IEnumVariant;
Langue : Variant;
Nombre : LongWord;
begin
Word := CreateOleObject('Word.Application');
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 :
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 :
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.
While IEnum.Next(1,Element,Nombre)=S_OK Do
Begin
Langue := IUnknown(Element) As Language;
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 :
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 :
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.
type
TCustomEnumerateurEnumVariant = class
private
FObjetCollection: IEnumVariant;
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;
TEnumVariant = class(TCustomEnumerateurEnumVariant)
Function GetEnumerator: TEnumerateur;
end;
TEnumerateur = class
strict private
FListe : TEnumVariant;
public
constructor Create (AList : TEnumVariant);
function GetCurrent: IUnknown;
function MoveNext :Boolean;
property Current : IUnknown read GetCurrent;
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 :
FObjetCollection:=(Enum._NewEnum) as IEnumVariant |
car ce code provoque à la compilation l'erreur suivante :
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 :
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 :
Function TCustomEnumerateurEnumVariant.GetCollection(ACollection : IDispatch):IEnumVariant;
Var VarResult: OleVariant ;
Params: TDispParams;
lProperty: Widestring;
lDispID: Integer;
Resultat: HResult;
ExcepInfo: TExcepInfo;
begin
Result:=Nil;
lProperty:='_NewEnum';
FillChar(Params, SizeOf(DispParams),0);
OLECheck(ACollection.GetIDsOfNames(GUID_NULL, @lProperty, 1,LOCALE_USER_DEFAULT, @ldispid)) ;
Resultat:=ACollection.Invoke(ldispid,GUID_NULL,0,DISPATCH_PROPERTYGET,Params,@VarResult, @ExcepInfo,Nil);
if Resultat<> 0
then DispatchInvokeError(Resultat, ExcepInfo);
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 :
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.
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.
constructor TEnumerateur.Create(AList: TEnumVariant);
begin
inherited;
FListe := AList;
end; |
La méthode GetCurrent renvoi la dernière valeur lue dans la collection.
function TEnumerateur.GetCurrent: IUnknown;
begin
Result := FListe.Item;
end; |
La méthode MoveNext se positionne sur la prochaine valeur si elle existe.
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é :
procedure Erreur;
var Word : Variant;
Element: IUnKnown;
IEnum : TEnumVariant;
begin
try
Word := CreateOleObject('Word.Application');
IEnum:=TEnumVariant.Create(Word.Languages);
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 :
...
ObjectEnumerator:=TEnumVariant.Create(wmiService.InstancesOf('systemrestore',
wbemFlagReturnWhenComplete+wbemQueryFlagShallow,
nil));
For MonObjetWMI in ObjectEnumerator do
begin
PropertyEnumerator:= TEnumVariant.Create((MonObjetWMI as SWBemObject).Properties_);
For MaProprieteWMI in PropertyEnumerator do
begin
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 :
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 :
procedure Erreur;
var msWord : TWordApplication;
Element: Language;
Empty : TObject;
begin
try
msWord := TWordApplication.Create;
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 :
Procedure Enumere;
var
WMILocator : ManagementScope;
WmiService : ConnectionOptions
Path : ManagementPath;
Options : ObjectGetOptions;
EnumOptions: EnumerationOptions;
SRClass : ManagementClass;
SRList: ManagementObjectCollection;
MonObjetWMI: ManagementObject;
begin
try
WmiService:=ConnectionOptions.Create;
With WmiService do
begin
EnablePrivileges:=False;
Timeout:=InfiniteTimeout;
end;
Path:=ManagementPath.Create('\\.\root\Default:SystemRestore');
WMILocator:= ManagementScope.Create(Path,WmiService);
WMILocator.Connect;
Options:=ObjectGetOptions.Create;
SRClass:=ManagementClass.Create(Path.Path, 'SystemRestore', Options);
EnumOptions:=EnumerationOptions.Create;
With EnumOptions do
Begin
ReturnImmediately:=True;
DirectRead:=True;
end;
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))
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.
 
|