Control Arrays in Delphi

Michael Chapin


Visual Basic programmers have access to a built-in capability known as a control array. A control array is a group of controls that you can access through elements in an array, instead of by a unique name. This makes it easy to run through the array in a loop to disable all the controls or check their properties for a particular value. Although Delphi doesn't have a built-in control array, it's easy to implement.

CONTROL arrays are great for many applications. Not only do they make it easy to access related controls, but they can also help access controls created on the fly. In this article, I'll show you how to implement control arrays in Delphi and introduce you to the versatile TList object. The application we'll write to showcase this functionality is a small light organ with five panels that change color randomly on a timer tick.

Strategy for implementation

A control array, as the name implies, is nothing more than an array of controls. You can implement this type of structure in Delphi through a couple of different methods. The first solution you might consider is to simply make a dynamic array of type TObject. While this works, it isn't as flexible as using a TList object. A TList is an object that can be used as container for anything. Each entry is a generic pointer to something; so anything you can point to can be placed in a TList. These lists are easy to use but have a couple of gotchas that aren't well documented.

Creating the TList

Of course, before you can work with an object, you have to create it. I've seen a lot of questions on newsgroups like this: "Why do I get a GPF when I add anything to a TList object?" The problem is that the developer asking the question probably forgot to actually create the TList object. The compiler knew about the list (because it had been declared) and happily tried to stuff something into it. But since the Create method had never been executed, the program accessed a Nil pointer and hiccuped. First declare it, then be sure and create it somewhere in the code:


Var

  MyList  : TList;

...

{ Somewhere in the code }

  MyList := TList.Create;

  

That's all there is to it. I generally put the creation code for global lists in the OnCreate event of an application and destroy them in the OnClose event.

Creating the example application

Now that list is available, I'll show you how to create an application to put it to use. To test this technique, perform the following steps:

  1. Create a new project and name it LIGHTORG.
  2. Set the name property on the form to MainForm.
  3. Drop a MainMenu component on the window, and you have an instant testing platform.

The application you're going to build is a simple Light Organ. A Light Organ displays colors instead of playing sounds. This application will have five panels that randomly change color when a timer event fires.

  1. In the MainMenu add an item &Test.
  2. Underneath this menu item add '&Start Organ', 'S&top Organ and 'E&xit' menu items. You'll attach event handlers to these items to start and stop the panels from changing colors.
  3. Drop a Timer object on form, set the interval property to 100, and Enabled property to False.
  4. Add five TButton objects near the bottom of the form. Align their tops and space them equally using the alignment palette.

Creating the example application lists

Next are some variables, constants, and one message handler (see Listing 1). You need to add two variables, BtnList and PanelList, to define your control arrays. I've made them global by defining them in a unit that holds all global variables for the application (then including that unit in the Uses clause for all the other application units). The MyColors array is just an array of colors that will show in the panels. The values were pulled out of \DELPHI\DOC\GRAPHICS.INT.


Listing 1. Variables and constants for the Light Organ.

Const

  NumColors = 15;

  MyColors : Array[0..NumColors - 1] Of 

    LongInt = (clBlack, clMaroon, clGreen, 

    clOlive, clNavy, clPurple, clTeal,

    clGray, clSilver, clRed, clLime, clBlue, 

    clFuchsia, clAqua, clWhite

  );



var

  MainForm  : TMainForm;

  BtnList   : TList;

  PanelList : TList;

  

BtnList will be your list of the buttons that were placed on the form. PanelList will hold the panels that will be created dynamically.

The first order of business is to create the lists. You'll probably want to do this in the OnCreate event. I usually do all initialization in the OnCreate so the controls don't have to be created beforehand. In this event you should also call the standard library function Randomize to set up the random number generator. Listing 2 shows the code for the OnCreate event.


Listing 2. Main form creation.

procedure TMainForm.FormCreate(Sender: TObject);

begin

  BtnList := TList.Create;

  PanelList := TList.Create;

  Randomize;

end;



Filling in the lists

The lists have now been created but are empty. In the OnCreate event, the form and its components haven't been created yet. And, of course, you can't fill the list with controls until the controls are created. The best time, then, is in the OnShow event. OnShow is called after all components and pre-created child forms have been created. Listing 3 shows the code for the OnShow event.


Listing 3. OnShow event code.

procedure TMainForm.FormShow(Sender: TObject);

Var

  i, y  : Integer;

  btn   : TButton;

  panel : TPanel;

begin

  BtnList.Add(Button1);

  BtnList.Add(Button2);

  BtnList.Add(Button3);

  BtnList.Add(Button4);

  BtnList.Add(Button5);



  y := Button1.Top - 50;

  For i := 0 To 4 Do

  Begin

    panel := TPanel.Create(self);

    panel.parent := Self;

    btn := TButton(BtnList.items[i]);

    panel.Left := btn.Left;

    panel.top := y;

    panel.width := 40;

    panel.height := 40;

    PanelList.Add(panel);

  End;

end;



The first five lines simply add the five pre-created buttons to BtnList. That's all you need to do for controls that have been placed on the form. It's a different story for the five panels. You're responsible for doing everything for these controls because they're created dynamically at runtime. The program has to create the control, place it on the form, and show it.

Creating the control isn't difficult, just call the Create method for the control as you would for anything else. The gotcha here is that you have to add the following line:


panel.parent := Self;



The control must be assigned a parent. If the parent isn't assigned, the control won't show itself. This is necessary even though the parent form is supplied in the Create method. This bit of information is very poorly documented.

The rest of the loop sets up the panel and positions it on the screen. For positioning I access the button list to get the left coordinate for the panel. This shows how easy it is to access a control within a list. Finally the panel is added to PanelList.

Cleanup

Most programs allocate chunks of memory and generally need memory cleanup when the program terminates. This program is no exception. At first glance you might ask "What are you talking about? You just made some panels and attached them to the form. Won't Delphi clean them up with the rest when the form is destroyed?" The simple answer is no. You've bypassed Delphi's form and component creation and did it yourself. So if you create it, you have to destroy it. I generally use the OnClose event to do the cleanup. Listing 4 shows the code for the OnClose event.


Listing 4. OnClose event.

procedure TMainForm.FormClose(Sender: TObject;

                       var Action: TCloseAction);

Var

  i : Integer;

begin

  Timer1.Enabled := False;

  BtnList.Free;

  For i := 0 To 4 Do

  Begin

    TPanel(PanelList.items[i]).Free;

  End;

  PanelList.Free;

end;



A couple of points about the behavior of Tlist and the listing. Notice that I free BtnList with no ceremony. I can do this since Delphi has already destroyed those components when the form was destroyed. Indeed, if I were to try to destroy these components, Delphi would raise an exception. I was onlyusing the list as a convenience. For PanelList it's a different story.

Keep two things in mind here. First, TList.Free or TList.Clear don'tdestroy anything in the list. TList.Clear just resets the count to zero. TList.Free just destroys the list itself. It does nothing with the objects pointed to in the list. If any of the pointers in a list point to areas of allocated memoryÊincluding components and those objects allocated with GetMem or NewÊthey must be freed before the list can be destroyed. Otherwise access to those objects is lost forever, and you've created a memory leak.

The other thing to keep in mind is that TList onlystores generic pointers. Retrieving an object from a TList always requires that you typecast to the target type (in this case, TPanel):


TPanel(PanelList.items[i]).Free;



Miscellaneous event handlers

You now have all the parts to a working program except for one set of items, the event handlers for the various program components. The menu items aren't especially exciting. Start Organ and Stop Organ simply enable and disable the timer, respectively. Exit simply calls close for the application. All I did for the buttons was to make a common procedure that puts the button caption into a MessageDlg. The major program functionality is in the Timer message handler. This handler is called every time the timer fires, in this case every 100 milliseconds. Code for the handler is in Listing 5.


Listing 5. Timer event handler.

procedure TMainForm.Timer1Timer(Sender: TObject);

Var

  panel : TPanel;

  i, c  : Integer;

begin

  i := Random(5);

  c := Random(NumColors);

  panel := PanelList.items[i];

  panel.color := MyColors[c];

end;



There isn't much in the timer event handler. It calls randomly with an argument of 5 to get a panel index, and it again calls randomly with a NumColors argument to get a color. The PanelList is accessed to get the panel at the index. The panel's color is changed to random color selected in Listing 1.

Conclusion

I've presented a method for implementing control arrays in Delphi with TLists and have shown you how to create and destroy components at runtime with them. BtnList simply held a collection of components already on the form. It's main purpose was to simplify access to all properties of buttons in the list. Using it simplified the code for creating the panels considerably. PanelList was used in the timer event handler to simplify the code for changing the color of the panels.

Michael Chapin is a freelance developer working primarily in Windows games and graphics applications. E-mail mchapin@vcn.com.

Delphi Developer Table of Contents
Pinnacle's Home Page


Copyright 1996 Pinnacle Publishing, Inc. All rights reserved.