5

I was trying to figure out how to search for a Label by its Caption:

for I := ComponentCount - 1 downto 0 do
begin
  if Components[i] is TLabel then
    if Components[i].Caption = mnNumber then
    begin
      Components[i].Left := Left;
      Components[i].Top := Top + 8;
    end;
end;

I get an error: Undeclared identifier: 'Caption'.
How can I resolve this issue?

kobik
  • 21,001
  • 4
  • 61
  • 121
Glen Morse
  • 2,437
  • 8
  • 51
  • 102

5 Answers5

13

Iterating over Components[] is the wrong approach. That just yields the components that are owned by the form. You will miss any components that are added dynamically, and not owned by the form, or components that are owned by frames.

Instead you should use Controls[]. However, that only yields first generation children. If there is deeper parent/child nesting then you need to recurse. That's more work. I use some helpers to make it easy. I've wrapped them up in this unit:

unit ControlEnumerator;

interface

uses
  System.SysUtils, System.Generics.Collections, Vcl.Controls;

type
  TControls = class
  private
    type
      TEnumerator<T: TControl> = record
        FControls: TArray<T>;
        FIndex: Integer;
        procedure Initialise(WinControl: TWinControl; Predicate: TFunc<T, Boolean>);
        class function Count(WinControl: TWinControl; Predicate: TFunc<T, Boolean>): Integer; static;
        function GetCurrent: T;
        function MoveNext: Boolean;
        property Current: T read GetCurrent;
      end;
      TEnumeratorFactory<T: TControl> = record
        FWinControl: TWinControl;
        FPredicate: TFunc<T, Boolean>;
        function Count: Integer;
        function Controls: TArray<T>;
        function GetEnumerator: TEnumerator<T>;
      end;
  public
    class procedure WalkControls<T: TControl>(WinControl: TWinControl; Predicate: TFunc<T, Boolean>; Method: TProc<T>); static;
    class function Enumerator<T: TControl>(WinControl: TWinControl; Predicate: TFunc<T, Boolean>=nil): TEnumeratorFactory<T>; static;
    class function ChildCount<T: TControl>(WinControl: TWinControl; Predicate: TFunc<T, Boolean>=nil): Integer; static;
  end;

implementation

{ TControls.TEnumerator<T> }

procedure TControls.TEnumerator<T>.Initialise(WinControl: TWinControl; Predicate: TFunc<T, Boolean>);
var
  List: TList<T>;
  Method: TProc<T>;
begin
  List := TObjectList<T>.Create(False);
  Try
    Method :=
      procedure(Control: T)
      begin
        List.Add(Control);
      end;
    WalkControls<T>(WinControl, Predicate, Method);
    FControls := List.ToArray;
  Finally
    List.Free;
  End;
  FIndex := -1;
end;

class function TControls.TEnumerator<T>.Count(WinControl: TWinControl; Predicate: TFunc<T, Boolean>): Integer;
var
  Count: Integer;
  Method: TProc<T>;
begin
  Method :=
    procedure(Control: T)
    begin
      inc(Count);
    end;
  Count := 0;
  WalkControls<T>(WinControl, Predicate, Method);
  Result := Count;
end;

function TControls.TEnumerator<T>.GetCurrent: T;
begin
  Result := FControls[FIndex];
end;

function TControls.TEnumerator<T>.MoveNext: Boolean;
begin
  inc(FIndex);
  Result := FIndex<Length(FControls);
end;

{ TControls.TEnumeratorFactory<T> }

function TControls.TEnumeratorFactory<T>.Count: Integer;
begin
  Result := TEnumerator<T>.Count(FWinControl, FPredicate);
end;

function TControls.TEnumeratorFactory<T>.Controls: TArray<T>;
var
  Enumerator: TEnumerator<T>;
begin
  Enumerator.Initialise(FWinControl, FPredicate);
  Result := Enumerator.FControls;
end;

function TControls.TEnumeratorFactory<T>.GetEnumerator: TEnumerator<T>;
begin
  Result.Initialise(FWinControl, FPredicate);
end;

class procedure TControls.WalkControls<T>(WinControl: TWinControl; Predicate: TFunc<T, Boolean>; Method: TProc<T>);
var
  i: Integer;
  Control: TControl;
  Include: Boolean;
begin
  if not Assigned(WinControl) then begin
    exit;
  end;
  for i := 0 to WinControl.ControlCount-1 do begin
    Control := WinControl.Controls[i];
    if not (Control is T) then begin
      Include := False;
    end else if Assigned(Predicate) and not Predicate(Control) then begin
      Include := False;
    end else begin
      Include := True;
    end;
    if Include then begin
      Method(Control);
    end;
    if Control is TWinControl then begin
      WalkControls(TWinControl(Control), Predicate, Method);
    end;
  end;
end;

class function TControls.Enumerator<T>(WinControl: TWinControl; Predicate: TFunc<T, Boolean>): TEnumeratorFactory<T>;
begin
  Result.FWinControl := WinControl;
  Result.FPredicate := Predicate;
end;

class function TControls.ChildCount<T>(WinControl: TWinControl; Predicate: TFunc<T, Boolean>): Integer;
begin
  Result := Enumerator<T>(WinControl, Predicate).Count;
end;

end.

Now you can solve your problem like this:

var
  lbl: TLabel;
....
for lbl in TControls.Enumerator<TLabel>(Form) do
  if lbl.caption=mnNumber then
  begin
    lbl.Left := Left;
    lbl.Top := Top + 8;
  end;

Or you could make use of a predicate to put the caption test inside the iterator:

var
  Predicate: TControlPredicate;
  lbl: TLabel;
....
Predicate := function(lbl: TLabel): Boolean
  begin
    Result := lbl.Caption='hello';
  end;
for lbl in TControls.Enumerator<TLabel>(Form, Predicate) do
begin
  lbl.Left := Left;
  lbl.Top := Top + 8;
end;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • will give this a try later... about time for me to close up for the day :D – Glen Morse Feb 15 '13 at 12:12
  • 2
    +1 I like how you're using the same method to count controls and extract controls, I like your use of `TControlPredicate` for filtration. – Cosmin Prund Feb 15 '13 at 12:27
  • @CosminPrund Thank you. Right now, I'm converting it all to typesafe generics. Much cleaner. – David Heffernan Feb 15 '13 at 12:36
  • 1
    I like the concept and the demonstration of generics. But although, in a way, this solution is more complete than mine or Cosmin Prund's, it is an awful lot of code for a simple, and usually very specific problem. I doubt if you should want such a generic solution for this problem at all. If it is important to find those labels, maybe they should be referred in a specific list or array. Traversing through components feels to me as a quick&dirty solution, that you shouldn't want in solid production code anyway. What's next? A FilterByTag method? – GolezTrol Feb 15 '13 at 12:55
  • 1
    @GolezTrol Yes, this is over-engineered for this specific problem. It can be re-used again and again though. I have about 30 uses in my codebase. The problem with traversing through the components, is that it doesn't find all the controls. Not all controls are in `Components[]`. – David Heffernan Feb 15 '13 at 13:06
  • I know. Usually they are, but in specific cases they may be owned by a panel or something else (or not at all). But in these cases, that usually has a specific reason, and they will probably be owned by specific other controls, like a panel. I can't imagine a situation where you would have absolutely no idea of which control is the owner and yet you need to find all those labels. I've never needed more than a simple loop or sometimes a recursive loop like the one demonstrated by Cosmin, although the use of generic (or a simple callback) might turn that one in a version that is reusable. – GolezTrol Feb 15 '13 at 13:43
10

The final piece of information fell into place in your comment to Golez's answer: your Labels are created at run-time, so there's a chance they don't have the Form as an owner. You'll need to use the Controls[] array to look at all the controls that are parented by the form, and look recursively into all TWinControl descendants because they might also contain TLabel's.

If you're going to do this allot and for different types of controls, you'll probably want to implement some sort of helper so you don't repeat yourself too often. Look at David's answer for a ready-made solution that manages to include some "bells and whistles", beyond solving the problem at hand; Like the ability to use anonymous functions to manipulate the found controls, and it's ability use an anonymous function to filter controls based on any criteria.

Before you start using such a complicated solution, you should probably understand the simplest one. A very simple recursive function that simply looks at all TControls on all containers starting from the form. Something like this:

procedure TForm1.Button1Click(Sender: TObject);

  procedure RecursiveSearchForLabels(const P: TWinControl);
  var i:Integer;
  begin
    for i:=0 to P.ControlCount-1 do
      if P.Controls[i] is TWinControl then
        RecursiveSearchForLabels(TWinControl(P.Controls[i]))
      else if P.Controls[i] is TLabel then
        TLabel(P.Controls[i]).Caption := 'Test';
  end;

begin
  RecursiveSearchForLables(Self);
end;

Using David's generic code, the above could be re-written as:

procedure TForm1.Button1Click(Sender: TObject);
begin
  TControls.WalkControls<TLabel>(Self, nil,
    procedure(lbl: TLabel)
    begin
      lbl.Caption := 'Test';
    end
  );
end;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Cosmin Prund
  • 25,498
  • 2
  • 60
  • 104
  • 4
    +1 You are quite right, one needs to understand this approach before contemplating a more generic approach – David Heffernan Feb 15 '13 at 13:41
  • I like this approach, Due to not wanting to add all other answers code. Mostly due to this is a one time search that will hardly ever be used. Thanks – Glen Morse Feb 18 '13 at 04:51
2

ComponentCount is only for the count. Use the Components array to find the actual components. For easy, you can put the label in a TLabel variable, which will also allow you to use label-specific properties that are not visible in TComponent. You could use with for this as well, but I think it degrades readability.

var
  l: TLabel;


for I := ComponentCount -1 downto 0 do
begin
  if Components[i] is TLabel then // Check if it is.
  begin
    l := TLabel(Components[i]); // Typecast, to reach it's properties.
    if l.Caption = mnNumber then
    begin
      l.Left := Left;
      l.Top := Top +8;
    end;
  end;
end;
GolezTrol
  • 114,394
  • 18
  • 182
  • 210
  • 3
    `Components[]` won't always find all the labels on the form. It will miss the ones that aren't owned by the form. You need to use `Controls[]`. – David Heffernan Feb 15 '13 at 11:09
  • 1
    For simple code like this, the safest bet is `Components[]`, the `Controls[]` will miss all the Labels that were dropped on Panels! If one is only using the designer and is not using Frames, there's *no way* to get a Label on a form without it being found using the `Components[]` array. The 100% solution would be a recursive procedure using `Controls[]` that calls itself for all `TWinControl` controls. That'll find Labels on panels, on frames, even Labels created at runtime with no parent. I don't think the `Controls[]` solution would be a good answer for this question. – Cosmin Prund Feb 15 '13 at 11:34
  • @CosminPrund Naturally you need to recurse. I have helpers to do just that. – David Heffernan Feb 15 '13 at 11:46
  • 1
    @David, you have a helper for everything it seems. I stand by my opinion, a solution involving a recursive helper method and the use of `Controls[]` is not a good solution for this question. Let's give that answer when the user notices that it's labels from frames are not found (ie: slightly more advanced topic). – Cosmin Prund Feb 15 '13 at 11:50
  • I added this, its not working. it maybe due to the creation of the label was at run time? still debuggin it to make sure this did not solve the issue – Glen Morse Feb 15 '13 at 12:07
1

The compiler doesn't know your Components[i] is a TLabel.

You need to cast your component to Tlabel like this:

for I := ComponentCount - 1 downto 0 do
begin
  if Components[i] is TLabel then //here you check if it is a tlabel
    if TLabel(Components[i]).Caption = mnNumber then //and here you explicitly tell the
    begin                                            //compiler to treat components[i]
      TLabel(Components[i]).Left := Left;            //as a tlabel.
      TLabel(Components[i]).Top := Top + 8;         
    end;
end;

This is needed because components[i] doesn't know the caption.

Pieter B
  • 1,874
  • 10
  • 22
0

Try this:

  for I := ControlCount-1 downto 0 do
  begin
    if Controls[i] is TLabel then // Check if it is.
    begin
      if (Controls[i] as TLabel).Caption = mnNumber then
      begin
        (Controls[i] as TLabel).Left := Left;
        (Controls[i] as TLabel).Top := Top +8;
      end;
    end;
  end;
Zeina
  • 1,573
  • 2
  • 24
  • 34
  • 1
    **This will miss all of the Labels that were dropped on Containers like TPanel**! The `Controls[]` array only lists the controls that are parented by the given `TWinControl`. You need to provide a recursive function that analyses all the `TWinControl` children for this to be a solution. – Cosmin Prund Feb 15 '13 at 11:36
  • 2
    not nice. AS is an expensive operator. If you use it (redundant after IS operator) then you'd only use it once, not three times in a row. – Arioch 'The Feb 15 '13 at 11:46
  • 2
    Better to simply typecast `TLabel(Controls[i]).Caption` or use a local variable as implemented by [@GolezTrol](http://stackoverflow.com/a/14892866/937125). – kobik Feb 15 '13 at 13:32