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
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
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 :
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
Now look the same code using a for in loop in vbscript, which looks much more easy to follow and understeand
[sourcecode language=”vb” wraplines=”false”]
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
[/sourcecode]
Now look back again the delphi code using for..in loop for iterate over the COM collection
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
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
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
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.
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
And now finally the function which makes the magic
[sourcecode language=”delphi” wraplines=”false”]
function GetOleVariantEnum(Collection:OleVariant):IGetOleVariantEnum;
begin
Result := TOleVariantEnum.Create(Collection);
end;
[/sourcecode]
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
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
or simply using the GetOleVariantEnum function.
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
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.
[sourcecode language=”delphi” wraplines=”false”]
for i := VarArrayLowBound(AvarArray, 1) to VarArrayHighBound(AvarArray, 1) do
Item:=AvarArray[i];
[/sourcecode]
Now using the same base interfaces you can implement a class to get a enumerator for this kind of arrays.
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
and the implementation
[sourcecode language=”delphi” wraplines=”false”]
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;
[/sourcecode]
and the helper function
[sourcecode language=”delphi” wraplines=”false”]
function GetOleVariantArrEnum(Collection:OleVariant):IGetOleVariantEnum;
begin
Result := TOleVariantArrayEnum.Create(Collection);
end;
[/sourcecode]
And now you can iterate over the array using a for..in loop
[sourcecode language=”delphi” wraplines=”false”]
for Item in GetOleVariantArrEnum(AvarArray) do
[/sourcecode]
you can get the full source code of these classes and interfaces here.
