Enumerating a collection of Variants
Many times when you are working with COM objects you need iterate over a collection, and usually the way to do this is using the _NewEnum function (which return a IUnknown interface) implemented by the COM class and then assign that value to a IEnumVariant variable. something like this
var Enum : IEnumVariant; iValue : LongWord; begin oEnum := IUnknown(ACollection._NewEnum) as IEnumVariant; while oEnum.Next(1, AItem, iValue) = S_OK do begin //do something end;
This not look very complicated, but when do you have to work with many nested COM collections, the code turns more difficult to follow. Check this sample :
begin; FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator'); FWMIService := FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', ''); wmiDiskDrives := FWMIService.ExecQuery('SELECT Caption, DeviceID FROM Win32_DiskDrive','WQL',wbemFlagForwardOnly); oEnum := IUnknown(wmiDiskDrives._NewEnum) as IEnumVariant;//first enumerator while oEnum.Next(1, wmiDiskDrive, iValue) = 0 do begin DeviceID:=StringReplace(String(wmiDiskDrive.DeviceID),'\','\\',[rfReplaceAll]); wmiDiskPartitions := FWMIService.ExecQuery(Format('ASSOCIATORS OF {Win32_DiskDrive.DeviceID="%s"} WHERE AssocClass = Win32_DiskDriveToDiskPartition',[String(DeviceID)]),'WQL',wbemFlagForwardOnly); oEnum2 := IUnknown(wmiDiskPartitions._NewEnum) as IEnumVariant;//second enumerator while oEnum2.Next(1, wmiDiskPartition, iValue) = 0 do begin wmiLogicalDisks := FWMIService.ExecQuery(Format('ASSOCIATORS OF {Win32_DiskPartition.DeviceID="%s"} WHERE AssocClass = Win32_LogicalDiskToPartition',[String(wmiDiskPartition.DeviceID)]),'WQL',wbemFlagForwardOnly); oEnum3 := IUnknown(wmiLogicalDisks._NewEnum) as IEnumVariant;//third enumerator while oEnum3.Next(1, wmiLogicalDisk, iValue) = 0 do begin Writeln(Format('Drive letter associated with disk drive %s %s Partition %s is %s',[String(wmiDiskDrive.Caption),String(wmiDiskDrive.DeviceID),String(wmiDiskPartition.DeviceID),String(wmiLogicalDisk.DeviceID)])); wmiLogicalDisk:=Unassigned; end; wmiDiskPartition:=Unassigned; end; wmiDiskDrive:=Unassigned; Writeln; end; end;
Now look the same code using a for in loop in vbscript, which looks much more easy to follow and understeand
ComputerName = "." Set wmiServices = GetObject ("winmgmts:{impersonationLevel=Impersonate}!//" & ComputerName) Set wmiDiskDrives = wmiServices.ExecQuery("SELECT Caption, DeviceID FROM Win32_DiskDrive") For Each wmiDiskDrive In wmiDiskDrives WScript.Echo "Disk drive Caption: " & wmiDiskDrive.Caption & VbNewLine & "DeviceID: " & " (" & wmiDiskDrive.DeviceID & ")" query = "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='" & wmiDiskDrive.DeviceID & "'} WHERE AssocClass = Win32_DiskDriveToDiskPartition" Set wmiDiskPartitions = wmiServices.ExecQuery(query) For Each wmiDiskPartition In wmiDiskPartition Set wmiLogicalDisks = wmiServices.ExecQuery ("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" & wmiDiskPartition.DeviceID & "'} WHERE AssocClass = Win32_LogicalDiskToPartition") For Each wmiLogicalDisk In wmiLogicalDisks WScript.Echo "Drive letter associated with disk drive = " & wmiDiskDrive.Caption & wmiDiskDrive.DeviceID & VbNewLine & " Partition = " & wmiDiskPartition.DeviceID & VbNewLine & " is " & wmiLogicalDisk.DeviceID Next Next Next
Now look back again the delphi code using for..in loop for iterate over the COM collection
begin; FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator'); FWMIService := FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', ''); wmiDiskDrives := FWMIService.ExecQuery('SELECT Caption, DeviceID FROM Win32_DiskDrive','WQL',wbemFlagForwardOnly); for wmiDiskDrive in GetOleVariantEnum(wmiDiskDrives) do begin DeviceID:=StringReplace(String(wmiDiskDrive.DeviceID),'\','\\',[rfReplaceAll]); wmiDiskPartitions := FWMIService.ExecQuery(Format('ASSOCIATORS OF {Win32_DiskDrive.DeviceID="%s"} WHERE AssocClass = Win32_DiskDriveToDiskPartition',[String(DeviceID)]),'WQL',wbemFlagForwardOnly); for wmiDiskPartition in GetOleVariantEnum(wmiDiskPartitions) do begin wmiLogicalDisks := FWMIService.ExecQuery(Format('ASSOCIATORS OF {Win32_DiskPartition.DeviceID="%s"} WHERE AssocClass = Win32_LogicalDiskToPartition',[String(wmiDiskPartition.DeviceID)]),'WQL',wbemFlagForwardOnly); for wmiLogicalDisk in GetOleVariantEnum(wmiLogicalDisks) do Writeln(Format('Drive letter associated with disk drive %s %s Partition %s is %s',[String(wmiDiskDrive.Caption),String(wmiDiskDrive.DeviceID),String(wmiDiskPartition.DeviceID),String(wmiLogicalDisk.DeviceID)])); end; Writeln; end; end;
The delphi code now looks more cleaner and easy to follow. Now you are wondering how this was made? the answer is writting a class with an enumerator and a function which returns an interface with the implemented enumerator.
check how the class looks
type IOleVariantEnum = interface function GetCurrent: OLEVariant; function MoveNext: Boolean; property Current: OLEVariant read GetCurrent; end; IGetOleVariantEnum = interface function GetEnumerator: IOleVariantEnum; end; TOleVariantEnum = class(TInterfacedObject, IOleVariantEnum, IGetOleVariantEnum) private FCurrent : OLEVariant; FEnum : IEnumVARIANT; public function GetEnumerator: IOleVariantEnum; constructor Create(Collection: OLEVariant); function GetCurrent: OLEVariant; function MoveNext: Boolean; property Current: OLEVariant read GetCurrent; end;
As you see we have a base interface (IOleVariantEnum) with the methods to implement a enumerator, another interface (IGetOleVariantEnum) which return a enumerator and finally a class which descend from both interfaces and implements the logic of the enumerator.
The implementation of the last class is very simple and uses the IEnumVARIANT interface to iterate over the collection and return the current item.
constructor TOleVariantEnum.Create(Collection: OLEVariant); begin inherited Create; FEnum := IUnknown(Collection._NewEnum) As IEnumVARIANT; //Set the COM enumerator for the variants collection end; function TOleVariantEnum.MoveNext: Boolean; var iValue : LongWord; begin FCurrent:=Unassigned;//clear the previous value stored in FCurrent avoiding memory leaks Result:= FEnum.Next(1, FCurrent, iValue) = S_OK; //Get the next item in the collection into FCurrent end; function TOleVariantEnum.GetCurrent: OLEVariant; begin Result:=FCurrent; end; function TOleVariantEnum.GetEnumerator: IOleVariantEnum; begin Result:=Self; end;
And now finally the function which makes the magic
function GetOleVariantEnum(Collection:OleVariant):IGetOleVariantEnum; begin Result := TOleVariantEnum.Create(Collection); end;
Now the good part of this is which you don’t need to worry of release the value returned by the GetOleVariantEnum function.
Finally you can rewrite your code using the TOleVariantEnum class
const wbemFlagForwardOnly = $00000020; var FSWbemLocator : OLEVariant; FWMIService : OLEVariant; FWbemObjectSet: TOleVariantEnum; FWbemObject : OLEVariant; begin; FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator'); FWMIService := FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', ''); FWbemObjectSet:= TOleVariantEnum.Create(FWMIService.ExecQuery('SELECT ProcessId FROM Win32_Process','WQL',wbemFlagForwardOnly)); try for FWbemObject in FWbemObjectSet do Writeln(Format('Pid %d',[Integer(FWbemObject.ProcessId)])); finally FWbemObjectSet:=nil; end; end;
or simply using the GetOleVariantEnum function.
const wbemFlagForwardOnly = $00000020; var FSWbemLocator : OLEVariant; FWMIService : OLEVariant; FWbemObjectSet: OLEVariant; FWbemObject : OLEVariant; begin; FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator'); FWMIService := FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', ''); FWbemObjectSet:= FWMIService.ExecQuery('SELECT ProcessId FROM Win32_Process','WQL',wbemFlagForwardOnly); for FWbemObject in GetOleVariantEnum(FWbemObjectSet) do Writeln(Format('Pid %d',[Integer(FWbemObject.ProcessId)])); end;
Enumerating a Variant Array
Tipically when you need iterate over a Variant Array you must get the bounds of the array using the VarArrayLowBound and VarArrayHighBound functions and from there you can access every item in the array only using the [] notation.
for i := VarArrayLowBound(AvarArray, 1) to VarArrayHighBound(AvarArray, 1) do Item:=AvarArray[i];
Now using the same base interfaces you can implement a class to get a enumerator for this kind of arrays.
TOleVariantArrayEnum = class(TInterfacedObject, IOleVariantEnum, IGetOleVariantEnum) private FCollection : OLEVariant; FIndex : Integer; FLowBound : Integer; FHighBound : Integer; public function GetEnumerator: IOleVariantEnum; constructor Create(Collection: OLEVariant); function GetCurrent: OLEVariant; function MoveNext: Boolean; property Current: OLEVariant read GetCurrent; end;
and the implementation
constructor TOleVariantArrayEnum.Create(Collection: OLEVariant); begin inherited Create; FCollection:=Collection; FLowBound :=VarArrayLowBound(FCollection, 1); FHighBound:=VarArrayHighBound(FCollection, 1); FIndex:=FLowBound-1; end; function TOleVariantArrayEnum.GetCurrent: OLEVariant; begin Result:=FCollection[FIndex]; end; function TOleVariantArrayEnum.GetEnumerator: IOleVariantEnum; begin Result:=Self; end; function TOleVariantArrayEnum.MoveNext: Boolean; begin Result := FIndex < FHighBound; if Result then Inc(FIndex); end;
and the helper function
function GetOleVariantArrEnum(Collection:OleVariant):IGetOleVariantEnum; begin Result := TOleVariantArrayEnum.Create(Collection); end;
And now you can iterate over the array using a for..in loop
for Item in GetOleVariantArrEnum(AvarArray) do
you can get the full source code of these classes and interfaces here.
November 4, 2011 at 6:41 am
Do you plan to use “for FWbemObject in FWbemObjectSet do” style in WMI Code Creator?
November 4, 2011 at 8:59 am
Alex, No for the moment, Actually this kind of code is used in the internal source code of the application, but not in the generated code because I want to keep the generated code compatible with old versions of delphi and free external of dependencies.
November 4, 2011 at 7:54 pm
Hi Rodrigo, I was wondering if you can help me.
I can’t listen to any WMI event.
Every time I call ExecNotificationQueryAsync I get the exception “Not Supported”, witch is
WBEM_E_NOT_SUPPORTED
2147749900 (0x8004100C)
I tried with .net libraries (C#) and I get the same error.
Simple queries are working although.
Any ideas?
Thanks in advance!
November 4, 2011 at 8:39 pm
I need which you show me the code that you are using to execute the event. specially the WQL sentence..
November 4, 2011 at 8:44 pm
It’s the code generated by your generator, http://paste2.org/p/1758999
I tried lots of events, but they all throw the same exception.
November 4, 2011 at 8:57 pm
Which version of delphi and windows are you using?
November 4, 2011 at 8:59 pm
Win7 Home Premium, 32bits. Same error in both D7 and D2010.
November 4, 2011 at 9:11 pm
Just to be sure I tested the code in several machines and is working fine, the issue it seems related to your WMI Service. you can diagnose the problem with the WBEMtest.exe tool and the “Notfication query option” and with the The WMI Diagnosis Utility http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=7684
November 4, 2011 at 8:17 pm
Hi,
I have to be honest with you…
I’ve been following your blog for the past year, and man I’m impressed… stunned in fact.
Congrats for your magnificent work, not just this one of course.
After more than 15y I’m still in love with Delphi because of this pearls…
Please, do continue… :-)
November 4, 2011 at 8:40 pm
Thanks for your words and for take the time to leave a comment.
November 4, 2011 at 9:24 pm
The diagnosis utility says:
..119 23:19:05 (0) ** Unsupported Windows version – 32-bit (7600) – User ‘ARTHUR-NOTE\ARTHUR’ on computer ‘ARTHUR-NOTE’.
For some reason my windows install sucks!
May 9, 2014 at 10:35 am
Hi everyone!
Thanks Rodrigo for the code!! ;)
** My english it’s not very good.. sorry :(
I’m user of Delphi 7 and can’t use “for … in ….” syntax
So I make another way to doit easily in Delphi 7.
This method use the TOleVariantEnum class in this article.
First put some example implementation
/// example1 (loop for entire collection)
fs := CreateOleObject(‘Scripting.FileSystemObject’);
folder := fs.GetFolder(‘C:\folder’);
while EnumNext(folder.SubFolders) do
ShowMessage(EnumCurrent(folder.SubFolders).Path);
// example2 (with break in loop, ej: searching etc)
fs := CreateOleObject(‘Scripting.FileSystemObject’);
folder := fs.GetFolder(‘C:\swsetup’);
while EnumNext(folder.SubFolders) do
begin
ShowMessage(EnumCurrent(folder.SubFolders).Path);
break;
end;
EnumFree(folder.SubFolders); // this function free memory of the object list (use only if brek the loop because when EnumNext rich the end free the object)
as seen we have only 3 functions:
EnumNext(Collection); EnumCurrent(Collection); EnumFree(Collection);
// IMPLEMENTATION
// object to put in a list identifing the collection
type
TOLO=class
public
Collection: OleVariant;
Obj: TOleVariantEnum;
constructor Create(Col: OleVariant);
destructor Destroy; Override;
end;
var
EOOL: TList; // global list
// TOLO implementation
constructor TOLO.Create(Col: OleVariant);
Begin
Collection := Col;
Obj := TOleVariantEnum.Create(Collection);
end;
destructor TOLO.Destroy;
Begin
if Obj nil then
Obj.Free;
inherited;
end;
// internal functions
function LocateOOL(Collection: OleVariant): TOleVariantEnum;
var
i: integer;
Begin
if EOOL.Count = 0 then
result := TOLO(EOOL.Items[EOOL.Add(TOLO.Create(Collection))]).Obj
else
for i := 0 to EOOL.Count -1 do
if TVarData(TOLO(EOOL.Items[i]).Collection).VPointer = TVarData(Collection).VPointer then
Begin
Result := TOLO(EOOL.Items[i]).Obj;
break;
end;
end;
function DeleteOOL(Collection: OleVariant): TOleVariantEnum;
var
i: integer;
Begin
if EOOL.Count = 0 then exit;
for i := 0 to EOOL.Count -1 do
if TVarData(TOLO(EOOL.Items[i]).Collection).VPointer = TVarData(Collection).VPointer then
Begin
TOLO(EOOL.Items[i]).Obj.Free;
EOOL.Delete(i);
break;
end;
end;
function EnumNext(Collection: OleVariant): Boolean;
begin
result := LocateOOL(Collection).MoveNext;
if Not Result then
DeleteOOL(Collection);
end;
function EnumCurrent(Collection: OleVariant): OleVariant;
begin
result := LocateOOL(Collection).Current;
end;
procedure EnumFree(Collection: OleVariant);
begin
DeleteOOL(Collection);
end;
// and don’t forget create the list
initialization
EOOL:= TList.Create;
finalization
EOOL.Free;
// Cheers!’