Friday, February 01, 2008

TWinControl.Controls Enumerator

I'm still having fun with enumerators. The latest I wrote is a filtering enumerator for TWinControl.Controls[] that allows me to write code like this:

var
control: TControl;

for control in EnumControls(pnlAttributes, TSpeedButton) do
TSpeedButton(control).Enabled := false;

I found it interesting that Borland built a Components[] enumerator in the VCL, but not a Controls[] enumerator.


The EnumControls interface is simple. It takes a starting point for enumeration and an optional class filter. By specifying the latter, you tell the enumerator that you're only interested in child controls of a specified class.

function EnumControls(parent: TWinControl; 
matchClass: TClass = nil): IControlEnumeratorFactory;

As it is used in a for..in loop, EnumControl must return a class or interface that implements GetEnumerator function and that is exactly what it does. GetEnumerator, on the other hand, creates an instance of the TControlEnumerator class, which implements the actual enumeration.

type
TControlEnumerator = class
strict private
ceClass : TClass;
ceIndex : integer;
ceParent: TWinControl;
public
constructor Create(parent: TWinControl; matchClass: TClass);
function GetCurrent: TControl;
function MoveNext: boolean;
property Current: TControl read GetCurrent;
end; { TControlEnumerator }

IControlEnumeratorFactory = interface
function GetEnumerator: TControlEnumerator;
end; { IControlEnumeratorFactory }

On the implementation side, EnumControls creates an instance of the TControlEnumeratorFactory class, which implements the IControlEnumeratorFactory interface.

function EnumControls(parent: TWinControl; matchClass: TClass = nil): IControlEnumeratorFactory;
begin
Result := TControlEnumeratorFactory.Create(parent, matchClass);
end; { EnumControls }

type
TControlEnumeratorFactory = class(TInterfacedObject, IControlEnumeratorFactory)
strict private
cefClass: TClass;
cefParent: TWinControl;
public
constructor Create(parent: TWinControl; matchClass: TClass);
function GetEnumerator: TControlEnumerator;
end; { GetEnumerator }

TControlEnumeratorFactory just stores parent and matchClass parameters for later use in the GetEnumerator function.

constructor TControlEnumeratorFactory.Create(parent: TWinControl; matchClass: TClass);
begin
cefParent := parent;
cefClass := matchClass;
end; { TControlEnumeratorFactory.Create }

function TControlEnumeratorFactory.GetEnumerator: TControlEnumerator;
begin
Result := TControlEnumerator.Create(cefParent, cefClass);
end; { TControlEnumeratorFactory.GetEnumerator }

GetEnumerator creates an instance of the TControlEnumerator class, which implements the actual enumeration in a pretty standard manner. Only the MoveNext method is slightly more complicated than usual because it must optionally check the matchClass parameter.

constructor TControlEnumerator.Create(parent: TWinControl; matchClass: TClass);
begin
inherited Create;
ceParent := parent;
ceClass := matchClass;
ceIndex := -1;
end; { TControlEnumerator.Create }

function TControlEnumerator.GetCurrent: TControl;
begin
Result := ceParent.Controls[ceIndex];
end; { TControlEnumerator.GetCurrent }

function TControlEnumerator.MoveNext: boolean;
begin
Result := false;
while ceIndex < (ceParent.ControlCount - 1) do begin
Inc(ceIndex);
if (ceClass = nil) or (ceParent.Controls[ceIndex].InheritsFrom(ceClass)) then begin
Result := true;
break; //while
end;
end; //while
end; { TControlEnumerator.MoveNext }

It's instructive to see how all those parts play together when the code fragment from the beginning of this blog entry is executed.

for control in EnumControls(pnlAttributes, TSpeedButton) do
TSpeedButton(control).Enabled := false;


  • EnumControls is called. It creates an instance of the IControlEnumeratorFactory. TControlEnumeratorFactory constructor stores parent and matchClass parameters into internal fields.

    • TControlEnumeratorFactory.GetEnumerator is called. It creates an instance of the TControlEnumerator and passes it internal copies of parent and matchClass parameters.

      • TControlEnumerator's MoveNext and Current are used to enumerate over the parent's child controls.

    • For..in loop terminates and compiler automatically destroys the object, created by the GetEnumerator method.

  • Sometime later, the method containing this for..in loop terminates and compiler automatically frees the IControlEnumeratorFactory instance created in first step.

The TWinControl.Controls enumerator plus some other stuff is available at http://gp.17slon.com/gp/files/gpvcl.zip.

4 comments:

  1. Anonymous16:51

    The enumerator CLASS could be changed in an enumerator RECORD and hence the enumerators data is created on the stack instead of on the heap and hence will perform just a tiny bit better. (one getmem and freemem less).

    ReplyDelete
  2. I know, I've read Hallvard's article :)

    But still I don't care. One getmem and one freemem don't matter. Even if you call this for..in 1000 times a second, those getmems and freemems won't add up to a measurable difference. Delphi's memory manager is so blindingly fast. [Great thanks to Pierre le Riche. He did incredible job.]

    ReplyDelete
  3. Anonymous10:24

    Did you consider using a record for the factory(not only the enumerator)? I have tried it and it works. That way you dont have to use interface/refcounted object to make sure the factory is destroyed.

    ReplyDelete
  4. No, I didn't - but it is a really interesting idea. I'll check it out.

    ReplyDelete