![]() |
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:
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.
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 Copyright 1996 Pinnacle Publishing, Inc. All rights reserved. |