Collect More Than Beanie Babies with Delphi Collections
by Ray Konopka
May/June 1999, Vol. 10, No. 1 --
Download Source Code
Delphi collections can manage lists of arbitrary objects, but they are not drop-in
solutions. As with many things, collections help those who help themselves. Heres
some help while you help yourself.
If youre a regular reader of this column, you were probably expecting to see an
article on docking and mouse wheel support in Delphi 4. Well, Ill be covering the
mouse wheel, but decided not to cover docking in this issue. Why? To be honest, docking
has gone through some changes with each update to Delphi 4. At this writing, a third
update for Delphi 4 is about to be released. I felt it better to hold off on covering
docking until I had a chance to see if any changes were made in the new update. In place
of docking, I will cover collectionsan important topic that unfortunately
has little useful documentation.
Dockable Menus
Although I have postponed full coverage of docking, I will give you a little tip
regarding creating dockable menus like those used in the Delphi 4 IDE. The menu in the
Delphi 4 main window is just a TToolbar component that is configured in a
particular way. Although it is possible to manually configure the toolbar so that it
mimics a menu, the easy way to accomplish this is to download the TMenuBar
component from Borlands Web site, at www.borland.com/devsupport/delphi/downloads/index.aspl.
It is listed toward the end of the Delphi 4 list of downloads.
After downloading and installing this component into Delphi 4, it is very easy to
create docking menus. Unfortunately, the component does not come with any documentation
and although easy to use, there are a couple of issues that you need to be aware of.
The first step is to drop a TControlBar on your form and align it to
the top of the form. This component is located on the Additional page and will serve as
the docking site for the menu bar. Next, drop a TMenuBar component onto
the TControlBar. At this point, you may want to customize the appearance
of the control bar by changing its border and setting its AutoSize
property to True.
Next, drop a TMainMenu component onto the form and populate the menu
items. When you finish, you will notice that the form displays the menu as a normal menu.
To eliminate this, simply clear the Menu property of the form. Next, set
the Menu property of the menu bar component to reference the MainMenu1.
Now the menu bar component displays each submenu as a button.
When using the TMenuBar component, you will not be able to select any
of the menu items at design-time from the menu bar component. To specify event handlers
for the menu items, you need to use the menu editor. If you use the menu editor to make
changes to the menu, such as changing a menu name or adding a new menu, the menu bar will
not be updated when you close the menu editor. To update the menu bars display,
clear the menu bars Menu property and reassign it to the menu
component. Figure 1 shows a sample form, which is using the TMenuBar
component.
Figure 1: TMenuBar's docking menus.
Mouse Wheel Support
Im a big fan of the mouse wheel, although I didnt start out that way. When
the mouse wheel first came out, I used it for about a week. During that time the wheel
kept getting in the way. To make matters worse, at the time only a few applications even
supported the mouse wheel. I would start to use the wheel in one application, but then
switch to another application that did not support it, and I had to go back to the old way
of scrolling.
As a result, I went back to my old mouse without the wheel for a while. As time passed,
more and more applications began supporting the mouse wheel. I then bought a new computer
that came with a Microsoft IntelliMouse. Taking a break from the mouse wheel made all the
difference in the world. Now almost all of the applications that I use supports the mouse
wheel. Even Delphi 4 added support for mouse wheel scrolling within the IDE.
Unfortunately, Delphi 4 only exposes mouse wheel support at the form level for
developers, even though the necessary mouse wheel events are defined in TControl. As a
result, it is possible to respond to the mouse wheel events at the component level, but we
need to create a new descendant and override a couple of methods.
Listing 1 shows the source code for the RkWhlLst
unit, which implements the TRkWheelListBox component. This custom list
box allows a user to scroll through the contents of the list box by rotating the mouse
wheel instead of using the scroll bar.
As you can see from the class declaration, only two methods need to be overridden in
order to support mouse wheel interactions. Specifically, the TRkWheelListBox
overrides the DoMouseWheelUp and DoMouseWheelDown event
dispatch methods. For completeness, the component also exposes the OnMouseWheelUp
and OnMouseWheelDown events, which allow the end user to create custom
event handlers for these two actions.
The implementation of the event dispatch methods will depend on the component. In the
case of our list box, the GetScrollInfo and SetScrollInfo
API functions are used. Specifically, a TScrollInfo variable is
initialized and then passed to the GetScrollInfo function, which records
the current scroll bar position in the Info variable. Next, the scroll
bars position is adjusted. The value is either incremented or decremented by the
number of lines specified in the Mouse.WheelScrollLines property. The
global Mouse object provides access to the attributes of the
systems mouse. Next, the list box is scrolled to the new position by sending a wm_VScroll
message to the control. After that, the list boxs scrolling information is updated
with a call to SetScrollInfo.
The final step is to specify the return value for the event dispatch function. You
should return False, if you want the parent window to handle the event.
In our case, we are handling the event ourselves, and so we set Result
equal to True.
By the way, I should also point out that you can alter the way the control responds to
mouse wheel scrolling by testing the Shift parameter for other keys
pressed at the same time. For example, in the Delphi code editor, when you rotate the
mouse wheel, the editor scrolls either up or down by the number of lines specified in the WheelScrollLines
property. However, if you hold down the Ctrl key and rotate the mouse wheel, the editor
scrolls by pages.
Unfortunately, the initial release of Delphi 4 did not correctly set the Shift
parameter. Fortunately, the problem was corrected in the second update patch for Delphi 4,
which is available at www.borland.com/devsupport/delphi/downloads/index.aspl.
I cannot emphasize enough that Delphi developer should stay up-to-date with patches for
Delphi. For correctly handling mouse wheel interactions, you will want to download and
install at least Update #2 for Delphi 4. At the time of this writing, a third update to
Delphi 4 is just about to be released. Be sure to periodically check out the Borland web
site.
Collections
For the remainder of this article I will cover collections, a little-known but
very powerful feature of Delphi. Collections are used for managing lists of items.
However, unlike the TList or the TStringList classes,
collections manage items that are of the same type. Two common examples of collections are
the Panels property of TStatusBar and the Columns
property of TDBGrid.
There are several benefits to using collections. The most important is that collections
provide automatic streaming support for its items. That is, when the collection is sent to
a stream such as a form file or the clipboard, the published properties of each item are
automatically saved to that stream. In addition, collection-based properties have built-in
design-time support via a common property editor that is used by all collection-based
properties. Therefore, the same editor that is used to define the separate panels of a
status bar is also used to define the custom columns of a data-aware grid.
These benefits do come at a price, though. In particular, you do not use an instance of
a generic class like you do when using a TList. Instead you need to
create custom descendants of the TCollection and TCollectionItem
classes. Fortunately, the process is simple and straightforward. As a demonstration,
Ive created a custom bar chart component that utilizes a collection-based property
to manage the list of bars.
The TRkBarChart component is shown in Figure 2, along with the
standard collection editor. The bars displayed by the chart can be customized at
design-time by using the collection editor, which is invoked by clicking on the ellipsis
button next to the Bars property in the Object Inspector. The editor
allows a developer to add, delete, and move items. When an item is selected in the editor,
the published properties for that item appear in the Object Inspector.
Figure 2: Customizing the attributes of an individual bar.
As mentioned above, in order to utilize a collection, you must create custom
descendants of TCollectionItem and TCollection. This is
necessary because these base classes only provide collection management. That is,
the TCollection class knows how to manage instances of TCollectionItem;
for example, adding new items, removing items, and reordering items. However, to be truly
useful, the collection items must have application-specific data associated with them.
Since TCollectionItem does not provide any mechanism for doing this, we
must create a descendant class of TCollectionItem that defines one or
more custom properties.
In addition, because TCollection only knows how to handle TCollectionItem
instances, we need to create a descendant of TCollection that is able to
handle our custom collection items. Listing 2 shows the source
code for the RkBarCht unit, which implements the TRkBar
and TRkBars classes and the TRkBarChart component.
Lets take a look at each of these classes in detail.
The TCollectionItem Descendant
The main purpose of the TCollectionItem descendant is to define the
published properties that describe an item. The TRkBar class defines
three published properties: Value, Color, and Caption.
The Value property determines the height of the bar. The actual height of
the bar is affected by the bars Value, the height of the chart, and
any other bars in the collection. Each bar is filled with the color specified in the Color
property while the text specified in the Caption property is displayed in
the center of the bar.
The properties are implemented using the standard property syntax. Notice that each
property has an associated write access method (for example, SetCaption).
The write access methods are responsible for storing the new property value and, more
importantly, for instructing the collection object that the item has changed. This second
task is accomplished by calling the Changed method, which takes a single Boolean
parameter. Pass True in Changed if all the items in the
collection have been changed, otherwise pass False to indicate that only
one item has changed. In most examples, youll pass False to Changed.
The Changed method in turn calls the Update method of
the collection object, but more on that later.
In addition to defining the properties and access methods, you also need to override
the Assign method, which is used when making copies of an item. For
example, suppose you need to copy the Bars collection of one chart to
another. To do so, you would use a statement such as:
Chart2.Bars.Assign( Chart1.Bars );
The TCollection.Assign method iterates through each item in the source
collection (Chart1) and adds a new item in the destination collection (Chart2).
It then calls the items Assign method to copy the contents from the
source item. Therefore, we must override the TRkBar.Assign method and
copy the property values that we just defined.
Another method that you may want to consider overriding is GetDisplayName.
The standard collection editor uses this method to determine how an item appears in the
editor list. By default, GetDisplayName simply returns the class name of
the item. However, TRkBar overrides this method to return the Caption
if it is not empty; otherwise, the inherited GetDisplayName is called.
This is why the fifth item in Figure 2 appears as 4 - TRkBar in the editor.
The TCollection Descendant
Recall that TCollection only knows how to handle TCollectionItem
objects. Therefore, we create the TRkBars collection to handle TRkBar
items. As you can see from the class declaration for TRkBars, our
descendant needs to keep track of the bar chart that owns the collection. In addition, TRkBars
defines an Add method function for adding new TRkBar
items to the collection. It also defines an array property to provide access to the
individual TRkBar items and overrides the GetOwner
method to ensure proper streaming. Finally, TRkBars overrides the Update
method so that the bar chart can respond to item changes.
The FChart private field handles keeping track of the bar chart that
owns the collection. However, in order to declare the FChart field, a
forward class declaration for TRkBarChart must be added before the TRkBars
class declaration. The FChart field is initialized in the
collections constructor after the inherited constructor is called.
Take a closer look at the call to the inherited constructor in TRkBars.Create.
It is important to notice that the type TRkBar is passed to the
inherited constructor. This is necessary because the inherited TCollection
class needs to know what type of items it will be managing. For example, when the TCollection.Add
method is called to create a new item, it will use the class passed to its constructor to
create the actual item.
However, the type of item returned by TCollection.Add is TCollectionItem,
which is of course accurate because TRkBar descends from TCollectionItem.
However, it would be more useful to receive an instance of TRkBar,
instead. Therefore, the TRkBars class defines a new Add
method. Notice that the override keyword is not used in the declaration
because the new version returns a different type, namely TRkBar.
The implementation of TRkBars.Add is actually quite simple. We simply
call the inherited Add method and typecast the return value as a TRkBar.
The typecast is safe because we are guaranteed that the inherited Add
method will create a TRkBar instance since this is the type that was
passed to the inherited constructor.
The TRkBars class redefines the Items array property
(and the associated access methods) for the same reasons it redefines the Add
method. In particular, the inherited Items property uses the TCollectionItem
type where the TRkBar type would be more appropriate.
One of the most important methods that must be overridden is GetOwner,
which ensures that the collection and all of its items are correctly saved to a stream.
The method simply returns the reference to the chart component that is passed to the
constructor.
The final task is to override the Update method. Although overriding
this method is optional, for collections that will be used in a visual component, you will
definitely want to provide a custom Update method. Recall that when a
property of TRkBar is modified, the corresponding write access method
calls the Changed method, which in turn calls the collections Update
method.
If the Changed method is passed a True value, the Item
parameter of the Update method will be nil. In this
case, you can assume that every item in the collection has changed. If the Changed
method is passed a False value, the Item parameter of
the Update method will reference the actual item that has changed. By now
you should realize that although the Item parameter is of type TCollectionItem,
we can safely typecast it to TRkBar if necessary. In our example, TRkBars.Update
simply instructs the bar chart to refresh its display.
The Bar Property
At this point, we have a custom collection class that can manage a list of bars for a
bar chart. The final step is to create a charting component that will actually use our new
collection classes.
The TRkBarChart component is a simple graphic control that defines two
properties: Bars and ShowCaptions. ShowCaptions
simply allows the user to control whether the bars are displayed with their captions. The Bars
property is much more interesting. First, because TRkBars is a class, we
must construct an instance of the collection in the TRkBarChart
constructor. Of course, this means that we must also free the instance in the destructor.
The SetBars write access method also looks a little different. Instead
of using the assignment operator (:=) to copy the Value parameter to the FBars
field, the Assign method is used. This ensures that the collection and
its items are actually copied. If the assignment operator were used, the FBars
field would reference the same collection instance referenced by the Value
parameternot copy it.
The heart of the TRkBarChart component resides in its Paint
method. I wont describe all that is going on, but I do want to emphasize how the FBars
collection is used. After drawing the axes, the Paint method loops
through the items in FBars to determine the maximum value. Each
items value is accessed by FBars[I].Value. Note that because the
Items property of TRkBars is declared as the default property, the above
statement is equivalent to FBars.Items[I].Value. Once the maximum height
is obtained, each bar item is displayed using the color value and caption string stored in
the FBars[I].Color and FBars[I].Caption properties,
respectively.
On the Drawing Board
Next time, we will finally take a closer look at docking menu support in Delphi 4. By
the way, if there is a particular topic that you would like to see covered, dont
hesitate to send your suggestions to me at rkonopka@raize.com.
v
Copyright © 1999 The Coriolis Group, Inc. All rights reserved.
|