One frequently asked question that still gets asked today goes like this: “How do I create an array of [component] and fill it with existing [component] instances from the form?” Where [component] usually is TLabel, TCheckbox or TEdit.
I’m going to outline some solutions here.
Let’s start by defining some parameters:
- We have got a (VCL) form
- On that form there are several controls of the same type. Let’s make them CheckBoxes.
- We want to do something with all these controls
- In order to make this easier, we want to create an array that contains all these controls
So, this is the form:
The form declaratin looks like this:
type TForm1 = class(TForm) grp_1: TGroupBox; chk_1: TCheckBox; chk_2: TCheckBox; chk_3: TCheckBox; grp_2: TGroupBox; chk_4: TCheckBox; chk_5: TCheckBox; chk_6: TCheckBox; b_OK: TButton; b_Cancel: TButton; private public end;
As you can see, it contains six CheckBoxes grouped in two GroupBoxes.
Now, for some reason, we want to disable all those CheckBoxes.
Of course we could do it like this:
chk_1.Enabled := False; chk_2.Enabled := False; chk_3.Enabled := False; chk_4.Enabled := False; chk_5.Enabled := False; chk_6.Enabled := False;
And for just 6 CheckBoxes we probably would. But lets assume there are 30 and we don’t only want to disable them all but also sometimes enable or check or uncheckm them all. That starts to get a bit tedious, so wouldn’t it be great if we could just create an array of TCheckbox and fill it with those pesky things? We could then just write:
for chk in CheckBoxArr do begin chk.Enabled := false; end;
But how do we get this array? If you ever programmed in Visual Basic, you know the concept of control arrays, which you get by “simply” setting an Index property of the controls. They would then all have the same name and you could access them individually by TheCheckBox[i]. While that looks nice at first glance, just consider setting these indexes correctly for 30 CheckBoxes using the (crappy) property editor that VB had. I hated it. But I deviate …
We want a function which we can pass a TForm parameter that returns a TCheckBoxArray:
type TCheckBoxArray = array of TCheckBox; function AllCheckboxes(_frm: TForm): TCheckBoxArray;
If you look closely at the form declaration above you will notice that all CheckBoxes follow a common naming pattern: They all start with ‘chk_’ followed by a number. So we could use FindComponent to get them:
function GetAllCheckboxes(_frm: TForm): TCheckBoxArray; var i: Integer; cmp: TComponent; begin SetLength(Result, _frm.ComponentCount); i := 1; repeat cmp := _frm.FindComponent('chk_' + IntToStr(i)); if cmp <> nil then begin Result[i - 1] := cmp as TCheckBox; Inc(i); end; until cmp = nil; SetLength(Result, i - 1); end;
But if you think about it this is quite a bit of work, because you must make sure that your CheckBoxes all conform to that name pattern and for the above code to work, there must not be any gaps in the numbering. We usually want those CheckBoxes to have meaningful names rather than numbers, so it’s chk_Green, chk_Blue, chk_LightOn rather than chk_1, chk_2 etc. Also, what if you accidentally add another control that conforms to the name pattern but is not a CheckBox?
So FindComponent is out. What alternatives are there?
There is the Component array of the form, that contains every component on the form whose Owner is the form (this is usually the case for all components on a form when the form has been created using the form designer). So we enumerate all components and check whether they are CheckBoxes:
function GetAllCheckboxes(_frm: TForm): TCheckBoxArray; var i: Integer; cmp: TComponent; cnt: Integer; begin SetLength(Result, _frm.ComponentCount); cnt := 0; for i := 0 to _frm.ComponentCount - 1 do begin cmp := _frm.Components[i]; if cmp is TCheckBox then begin Result[cnt] := TCheckBox(cmp); Inc(cnt); end; end; SetLength(Result, cnt); end;
Much better. We don’t need any naming pattern and we only get CheckBoxes.
I mentioned above, that the form’s Components property contains all components placed on the form using the form designer. But components can also be created in code and there you can pass any other control as the Owner:
chk := TCheckbox.Create(grp_1); chk.Name := ''; chk.Parent := grp_1; chk.Top := 8; chk.Left := 8; chk.Caption := 'CheckBox created in code.';
The function will not find such a CheckBox because by passing grp_1 to its constructor we add it the Components property of the GroupBox rather than the form. That’s perfectly OK. We also clear the name, which would prevent it from being found by FindComponent too. But if you write code like this, you probably know what you are doing and won’t have read this article up to here.
I could stop now, but there is another use case: What if we want to get an array of only those CheckBoxes in one of the GroupBoxes? We could pass that GroupBox as a second parameter to the function and check that the Parent property of the CheckBoxes matches the GroupBox. But there is a simpler way: Each WinControl (that is: Control that itself can contain other controls, which includes GroupBoxes, Panels etc. and of course Forms or Frames) has got a Controls property that contains those controls. So we make a simple change to the function:
function GetAllCheckboxes(_Parent: TWinControl): TCheckBoxArray; var i: Integer; ctrl: TControl; cnt: Integer; begin SetLength(Result, _Parent.ControlCount); cnt := 0; for i := 0 to _Parent.ControlCount - 1 do begin ctrl := _Parent.Controls[i]; if ctrl is TCheckBox then begin Result[cnt] := TCheckBox(ctrl); Inc(cnt); end; end; SetLength(Result, cnt); end; // [...] begin CheckBoxes := GetAllCheckboxes(grp_1);
This will return all CheckBoxes placed on the GroupBox grp_1.
But note that …
CheckBoxes := GetAllCheckboxes(Form1);
… will now return an empty array! None of the CheckBoxes have been placed on the form itself, so they will not be in the form’s Controls property.