Home DevTools Library Delphi by Design Articles  
Site Map
Search
About Us
 
 
 

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. Here’s some help while you help yourself.

If you’re a regular reader of this column, you were probably expecting to see an article on docking and mouse wheel support in Delphi 4. Well, I’ll 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 collections—an 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 Borland’s 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 bar’s display, clear the menu bar’s Menu property and reassign it to the menu component. Figure 1 shows a sample form, which is using the TMenuBar component.

DbD55_Figure1.gif (5519 bytes)

Figure 1: TMenuBar's docking menus.

Mouse Wheel Support

I’m a big fan of the mouse wheel, although I didn’t 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 bar’s 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 system’s mouse. Next, the list box is scrolled to the new position by sending a wm_VScroll message to the control. After that, the list box’s 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, I’ve 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.

DbD55_Figure2.gif (36086 bytes)

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. Let’s 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 bar’s 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, you’ll 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 item’s 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 collection’s 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 collection’s 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 parameter—not copy it.

The heart of the TRkBarChart component resides in its Paint method. I won’t 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 item’s 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, don’t hesitate to send your suggestions to me at rkonopka@raize.com. v

Copyright © 1999 The Coriolis Group, Inc. All rights reserved.


Listing 1 - RkWhlLst.pas

unit RkWhlLst;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  StdCtrls;

type
  TRkWheelListBox = class( TListBox )
  protected
    function DoMouseWheelUp( Shift: TShiftState;
                           MousePos: TPoint ): Boolean; override;
    function DoMouseWheelDown( Shift: TShiftState;
                           MousePos: TPoint ): Boolean; override;
  published
    property OnMouseWheelUp;
    property OnMouseWheelDown;
  end;

implementation

{=============================}
{== TRkWheelListBox Methods ==}
{=============================}

function TRkWheelListBox.DoMouseWheelUp( Shift: TShiftState;
                                     MousePos: TPoint ): Boolean;
var
  Info: TScrollInfo;
begin
  Info.cbSize := SizeOf( Info );
  Info.fMask := sif_Pos;
  GetScrollInfo( Handle, sb_Vert, Info );
  Info.nPos := Info.nPos - Mouse.WheelScrollLines;
  if Info.nPos >= 0 then
  begin
    SendMessage( Handle, wm_VScroll, MakeLong( sb_ThumbPosition,
                                               Info.nPos ), 0 );
    SetScrollInfo( Handle, sb_Vert, Info, True );
  end;
  Result := True;
end;

function TRkWheelListBox.DoMouseWheelDown( Shift: TShiftState;
                                     MousePos: TPoint ): Boolean;
var
  Info: TScrollInfo;
begin
  Info.cbSize := SizeOf( Info );
  Info.fMask := sif_Pos;
  GetScrollInfo( Handle, sb_Vert, Info );
  Info.nPos := Info.nPos + Mouse.WheelScrollLines;
  SendMessage( Handle, wm_VScroll, MakeLong( sb_ThumbPosition,
                                             Info.nPos ), 0 );
  SetScrollInfo( Handle, sb_Vert, Info, True );
  Result := True;
end;

end.
                  

Listing 2 - RkBarCht.pas

unit RkBarCht;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs;

type
  TRkBar = class( TCollectionItem )
  private
    FCaption: string;                 // String to display on bar
    FColor: TColor;                               // Color of bar
    FValue: Integer;      // Value which determines height of bar
  protected
    function GetDisplayName: string; override;
    procedure SetCaption( const Value: string ); virtual;
    procedure SetColor( Value: TColor ); virtual;
    procedure SetValue( Value: Integer ); virtual;
  public
    constructor Create( Collection: TCollection ); override;

    procedure Assign( Source: TPersistent ); override;
  published
    // Published properties of collection items are
    // automatically streamed
    property Caption: string
      read FCaption
      write SetCaption;

    property Color: TColor
      read FColor
      write SetColor;

    property Value: Integer
      read FValue
      write SetValue;
  end;


  TRkBarChart = class;               // Forward Class Declaration

  TRkBars = class( TCollection )
  private
    FChart: TRkBarChart;
    function GetItem( Index: Integer ): TRkBar;
    procedure SetItem( Index: Integer; Value: TRkBar );
  protected
    function GetOwner: TPersistent; override;
    procedure Update( Item: TCollectionItem ); override;
  public
    // Note: No override on constructor
    constructor Create( Chart: TRkBarChart );

    function Add: TRkBar;
    function AddBar( AValue: Integer; AColor: TColor;
                     const ACaption: string ): TRkBar;

    // Array property provides access to collection items
    property Items[ Index: Integer ]: TRkBar
      read GetItem
      write SetItem; default;
  end;


  TRkBarChart = class( TGraphicControl )
  private
    FBars: TRkBars;
    FShowCaptions: Boolean;
  protected
    procedure SetBars( Value: TRkBars ); virtual;
    procedure SetShowCaptions( Value: Boolean ); virtual;
  public
    constructor Create( AOwner: TComponent ); override;
    destructor Destroy; override;

    procedure Paint; override;

    // AddBar uses a default parameter
    procedure AddBar( AValue: Integer; AColor: TColor;
                      const ACaption: string = '' );
  published
    property Bars: TRkBars
      read FBars
      write SetBars;

    property ShowCaptions: Boolean
      read FShowCaptions
      write SetShowCaptions
      default False;
  end;


implementation

{====================}
{== TRkBar Methods ==}
{====================}

constructor TRkBar.Create( Collection: TCollection );
begin
  inherited Create( Collection );
  FCaption := '';
  FColor := clHighlight;
  FValue := 0;
end;

procedure TRkBar.Assign( Source: TPersistent );
begin
  if Source is TRkBar then
  begin
    Caption := TRkBar( Source ).Caption;
    Color := TRkBar( Source ).Color;
    Value := TRkBar( Source ).Value;
  end
  else
    inherited Assign( Source );
end;

function TRkBar.GetDisplayName: string;
begin
  Result := FCaption;
  if Result = '' then
    Result := inherited GetDisplayName;
end;

procedure TRkBar.SetCaption( const Value: string );
begin
  if FCaption <> Value then
  begin
    FCaption := Value;
    Changed( False );       // Causes TRkBars.Update to be called
  end;
end;

procedure TRkBar.SetColor( Value: TColor );
begin
  if FColor <> Value then
  begin
    FColor := Value;
    Changed( False );       // Causes TRkBars.Update to be called
  end;
end;

procedure TRkBar.SetValue( Value: Integer );
begin
  if FValue <> Value then
  begin
    FValue := Value;
    Changed( False );       // Causes TRkBars.Update to be called
  end;
end;


{=====================}
{== TRkBars Methods ==}
{=====================}

constructor TRkBars.Create( Chart: TRkBarChart );
begin
  // Inherited constructor is passed the "type" of the collection
  // item that the collection will manage.
  inherited Create( TRkBar );
  FChart := Chart;
end;

function TRkBars.Add: TRkBar;
begin
  Result := TRkBar( inherited Add );
end;

function TRkBars.AddBar( AValue: Integer; AColor: TColor;
                         const ACaption: string ): TRkBar;
begin
  Result := Add;
  Result.Value := AValue;
  Result.Color := AColor;
  Result.Caption := ACaption;
end;


function TRkBars.GetItem( Index: Integer ): TRkBar;
begin
  Result := TRkBar( inherited GetItem( Index ) );
end;

procedure TRkBars.SetItem( Index: Integer; Value: TRkBar );
begin
  inherited SetItem( Index, Value );
end;

// Note: You must override GetOwner in Delphi 3 and higher to get
//       the correct streaming behavior

function TRkBars.GetOwner: TPersistent;
begin
  Result := FChart;
end;

procedure TRkBars.Update( Item: TCollectionItem );
begin
  // If Item is nil, assume all items have changed
  // Otherwise, Item represents the item that has changed
  FChart.Refresh;
end;


{=========================}
{== TRkBarChart Methods ==}
{=========================}

constructor TRkBarChart.Create( AOwner: TComponent );
begin
  inherited Create( AOwner );

  // Create instance of TRkBars collection
  FBars := TRkBars.Create( Self );
  FShowCaptions := False;
  Width := 200;
  Height := 100;
end;

destructor TRkBarChart.Destroy;
begin
  FBars.Free;
  inherited Destroy;
end;


procedure TRkBarChart.AddBar( AValue: Integer; AColor: TColor;
                              const ACaption: string = '' );
begin
  FBars.AddBar( AValue, AColor, ACaption );
end;

procedure TRkBarChart.SetBars( Value: TRkBars );
begin
  FBars.Assign( Value );
end;

procedure TRkBarChart.SetShowCaptions( Value: Boolean );
begin
  if FShowCaptions <> Value then
  begin
    FShowCaptions := Value;
    Refresh;
  end;
end;

procedure TRkBarChart.Paint;
var
  I, Gap, BarWidth, MaxBarHeight: Integer;
  BarHeight, BarLeft, YOffset: Integer;
  R: TRect;
begin
  with Canvas do
  begin
    Pen.Color := clBlack;
    MoveTo( 0, 0 );
    LineTo( 0, Height - 1 );
    LineTo( Width - 1, Height - 1 );

    if FBars.Count > 0 then
    begin
      Gap := 5;                       // 5 pixel gap between bars
      BarWidth := ( Width - FBars.Count * Gap ) div FBars.Count;
      MaxBarHeight := 0;
      for I := 0 to FBars.Count - 1 do
      begin                           // Calculate Max Bar Height
        if FBars[ I ].Value > MaxBarHeight then
          MaxBarHeight := FBars[ I ].Value;
      end;

      if MaxBarHeight > 0 then
      begin
        for I := 0 to FBars.Count - 1 do
        begin
          Brush.Color := FBars[ I ].Color;
          BarLeft := ( I + 1 ) * ( BarWidth + Gap ) - BarWidth;
          BarHeight := Round( FBars[ I ].Value /
                              MaxBarHeight * Height );
          SetTextAlign( Handle, ta_Center );
          R := Rect( BarLeft, Height - BarHeight,
                     BarLeft + BarWidth, Height );
          InflateRect( R, -1, -1 );
          if FShowCaptions then
          begin
            YOffset := ( BarHeight div 2 ) +
                       ( TextHeight( FBars[I].Caption ) div 2 );
            if FBars[ I ].Color = clWhite then
              Font.Color := clBlack
            else
              Font.Color := clWhite;
            TextRect( R, R.Left + BarWidth div 2,
                      Height - YOffset, FBars[ I ].Caption );
          end
          else
            TextRect( R, 0, 0, '' );
        end; { for I }
      end;
    end;
  end; { with Canvas }
end; {= TRkBarChart.Paint =}

end.