The Road to Delphi

Delphi – Free Pascal – Oxygene

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

12 Comments

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.

Author: Rodrigo

Just another Delphi guy.

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

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

    • 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. 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!

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

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

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s