I. Public concerné▲
Testé avec Delphi 2005 sous XP Pro SP1.
Version 1.0
I-A. Les sources▲
II. 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éfinit les fonctions membres suivantes :
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'unité ActiveX.
III. L'interface IDispatch▲
À 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'exécution, mais inconnues lors de la compilation.
Vous trouverez d'autres informations à son sujet dans ce tutoriel ou encore dans cette FAQ.
IV. 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 contextes, cette règle de nommage peut ne pas exister.
Dans ces cas-là, nous utiliserons le fichier de librairie (xxx_TLB.PAS) fourni ou extrait à 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
;
À 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.
IV-A. 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
//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 :
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.
// 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 :
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.
V. 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 deux classes :
- une classe sur la laquelle on souhaite itérer, ici il s'agit de la classeTEnumVariant ;
- 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
// 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
;
// Énumérateur pour la classe conteneur.
// Il doit implémenter une suite de méthodes comme indiqué 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 classe 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.
V-A. 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'élément courant, implémenté par la propriété Item ;
- l'élément suivant, implémenté par la propriété NextItem.
V-A-1. Le champ FObjetCollection▲
Afin de pouvoir manipuler différents types d'interfaces 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 appelle dynamiquement la propriété _NewEnum et renvoie la collection IEnumVariant attendue :
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.
V-A-2. Les propriétés Item et NextItem▲
Les deux 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.
V-A-3. La méthode GetEnumerator▲
Il ne nous reste plus qu'à 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.
V-B. 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 renvoie 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 appelle le destructeur de la classe TEnumerateur.Destroy, ici TObject.
Sous .NET l'itérateur, ici TEnumerateur, doit implémenter l'interface IDisposable.
VI. Le résultat▲
Voici le code d'origine modifié :
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 évidemment 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 deux ou trois niveaux comme c'est le cas sous WMI, vous n'avez plus qu'à appeler le constructeur puis à itérer sur l'instance renvoyée.
Par exemple :
...
// Affecte un énumérateur pour la collection d'objets 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ée 'Name'.
With
(MaProprieteWMI as
SWBemProperty) do
begin
If
Name='CreationTime'
...
VII. 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 renvoie 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;
// 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 :
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'instances
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ée directement dans l'itérateur For..in..do.
En conclusion la classe d'itérateur présenté précédemment n'est valable que pour Delphi Win32.