Skip to content
The Road to Delphi — Delphi, VCL, VCL Styles, UxTheme and WinAPI

Implementing a Delphi for..in loop on COM collections and Variant Arrays

,

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.

12 responses

  1. Do you plan to use “for FWbemObject in FWbemObjectSet do” style in WMI Code Creator?

    1. 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.

  2. arthurprs Avatar
    arthurprs

    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!

    1. I need which you show me the code that you are using to execute the event. specially the WQL sentence..

      1. arthurprs Avatar
        arthurprs

        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.

        1. Which version of delphi and windows are you using?

          1. arthurprs Avatar
            arthurprs

            Win7 Home Premium, 32bits. Same error in both D7 and D2010.

          2. 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

  3. AntunesN Avatar
    AntunesN

    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… :-)

    1. Thanks for your words and for take the time to leave a comment.

  4. arthurprs Avatar
    arthurprs

    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!

  5. Diego Avatar
    Diego

    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!’

Leave a Reply

Discover more from

Subscribe now to keep reading and get access to the full archive.

Continue reading