From: Delphi 3 - User Interface Design. By Warren Kovach, Copyright © 1997 Prentice Hall

Chapter 4

Status Bars and Toolbars

Most modern programs will have a status bar at the bottom of the window, as well as toolbars at the top or side. Delphi makes adding these easy, as the MDI and SDI application templates automatically implement simple versions of these for you. This chapter will show you how to work with these and extend their usefulness.

Status bars

The purpose of a status bar is to provide feedback to the user regarding the state of the program. It can contain markers that indicate whether particular program modes are on or off (i.e. recording a script or text editor insert/overwrite mode) and information about the open document, such as current page and line number. It can also display hints that more fully describe the currently selected toolbar button, menu item or other user interface object (see Chapter 3, section on `Hints'). Finally, it can contain a progress bar that displays how much of a lengthy process has occurred. Here is an example of a typical status bar:

Figure 4-1 _ A typical status bar

There are two ways to create status bars. In all versions of Delphi, 16-bit and 32-bit, status bars can be created with one or more panels. One that represents the whole status bar can be placed on the form with the Align property set to alBottom. Onto this can be placed one or more others, aligned to alLeft or alRight, with the last one on the right or left aligned to alClient. The bevel style of these can be set to achieve the desired 3-d effect, whether it be raised or lowered. If needed, controls such as the program bar are then inserted into the required panel. Text panels are written to using the Caption property.

With Delphi 2 and 3 the Windows status bar common control can be used. This is encapsulated in the object TStatusBar. It provides a status bar with any number of panels in a single component. Text on each panel can be set separately, along with font and alignment. TStatusBar also has a SizeGrip property that lets you specify whether to display a distinctive corner in the lower right. This acts as a clue to the user that the window can be resized by dragging that corner. It also provides a larger area to grab on to for those users who have problems positioning the mouse exactly on the window border. It looks like this:

Figure 4-2 _ A Windows 95 status bar

When placed on a form the TStatusBar automatically aligns itself to the bottom. Initially it has just one panel. The text for this single panel can be accessed through the property SimpleText. If more then one panel is desired then the SimplePanel property must be set to false. New panels are then added through the property editor of the Panels property. This property is a zero-based list of the individual panels (which are TStatusPanel objects), including their size, alignment and text. The caption for each panel is set through its Text property. For example, to set the text of the second panel we need the following statement:

StatusBar.Panels[1].Text := 'Modified';

If you wish to place something else on a panel besides text you must set the TStatusPanel's Style property to psOwnerDraw in the property editor. Then you must create a handler for the OnDrawPanel event where you do the actual drawing of the contents.

Let's say we want to add a progress bar to one of the panels. In the Delphi 1 way of creating status bars (i.e. using several TPanel components) we could simply drop a gauge component on the appropriate panel and set its alignment to alClient. This won't work with TStatusBar; any attempt to drop something onto it just pushes the status bar further down. Instead we must create the component at run time and set its position on top of the panel in the OnDrawPanel event handler. The code looks like this:

The source code for this example is in project \CHAP4\STATPROG\STATPROG.DPR. This project will not work under Delphi 1, as it requries the TStatusBar VCL component.

Listing 4-1 _ Adding a progress bar to a TStatusPanel, from STATMAIN.PAS

type
  TMainForm = class(TForm)
    { ... }
    StatusBar: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure StatusBarDrawPanel(StatusBar: TStatusBar;
      Panel: TStatusPanel; const Rect: TRect);
  private
    { Private declarations }
    ProgressBar1: TProgressBar;
    { ... }
  public
    { Public declarations }
    { ... }
  end;

procedure TMainForm.FormCreate(Sender: TObject);
begin
  { ... }

  ProgressBar1 := TProgressBar.Create(Self);
  ProgressBar1.Parent := StatusBar;
end;

procedure TMainForm.StatusBarDrawPanel
                     (StatusBar: TStatusBar;
                      Panel: TStatusPanel; 
                      const Rect: TRect);
begin
  if Panel = StatusBar.Panels[1] then
    With ProgressBar1 do begin
      Top := Rect.Top;
      Left := Rect.Left;
      Width := Rect.Right - Rect.Left;
      Height := Rect.Bottom - Rect.Top;
    end;
end;

First, we declare a TProgressBar field in the main form, then create the component dynamically in the FormCreate method. It is important to set the Parent of the progress bar to the status bar; otherwise the control will not be drawn. The Owner is set to the form (Self) by passing it to the constructor.

We then create a handler for the TStatusBar.OnDrawPanel event. This passes three parameters to the method: the status bar and panel objects and a TRect giving the current size of the area to be drawn. This event is mostly called when the status bar is resized; the Rect parameter gives the new size.

In the event handler we first check to see if the panel being drawn is the one with our progress bar. If so then we simply set the position of the progress bar to match the rectangle. This will fill the whole panel with the bar.

Toolbars

Toolbars are another feature that no self-respecting modern Windows program can do without. These give you a series of buttons on a panel, usually at the top of the screen. These then give the user direct access to menu items, document styles or even more complex operations such as running macros.

Programs with complex user interfaces and a myriad of commands often provide a facility for the user to customize the toolbars. This usually consists of a list of available commands, primarily the menu items, along with bitmaps for those commands The user can then select which will be shown. The program might even provide several toolbars that can be displayed or hidden at various times, depending on what the user is doing. For example, a word processor might allow graphic elements such lines and boxes to be drawn on the text. The user will not always need these, so the toolbar with these commands can be turned on or off at will.

Another feature of customization is to have both smaller and larger bitmaps for each item (perhaps 16x16 and 24x24 pixels). Users with poorer eyesight will appreciate the more visible buttons.

The style of buttons can vary depending on your program. You could stick to the standard types of small buttons commonly used by Microsoft and Borland, such as these from the MDI application template:

Figure 4-3 _ Toolbar from the Borland MDI application template

If your program is fairly simple with a limited number of major commands you might go for having somewhat larger buttons with a different design style, such as these:

Figure 4-4 _ Toolbar from the statistical program Oriana

Figure 4-5 _ Toolbar from WinZip

Using the standard buttons has its advantages, in that the user is familiar with the buttons from other programs. However, tastefully designed custom buttons can give your program a distinctive edge.

The main advantage behind toolbars is that a large number of user selectable controls can be packed into a small area. However, the graphics for the buttons must be designed carefully so that they are easily understood by the user. You could add text labels to the buttons to make them clearer. However, this makes the buttons much bigger. This is fine if there are just a few buttons. If you need to have a large number of buttons on the toolbar then these will be too big.

An alternative approach is to add tooltips to the buttons. These are small text boxes containing a brief description of what the button does. When the mouse cursor hovers over the button for a certain length of time (while the user thinks about what the button is) this is displayed next to the button. It is an unobtrusive way to provide text captions for the buttons. The implementation of tooltips is described in more detail in Chapter 6 (section `Tooltips or help hints'). You can also have longer descriptions of the buttons as hints on the status bar; see Chapter 4, section `Hints' and Chapter 6, section `Status bar messages' for more on this.

Finally, a very useful feature for toolbars is to allow the user to decide where they are placed. This can be done by choosing an option from a dialog box, but a neater solution is to make them dockable toolbars. This means that the user can drag the toolbar to any portion of the screen. If it is dragged to one edge the toolbar will be redrawn there, either horizontally or vertically depending on the edge. If it is dragged to the middle of the screen then it is turned into a free-floating window; effectively a toolbox rather than a toolbar.

Toolbars in Delphi 1 and 2

The simplest way to create toolbars in Delphi 1 and 2 is to place a TPanel component on the form, with its Align property set to alTop. Onto this are placed a series of buttons, usually TSpeedButtons. These have a number of properties that make them useful for toolbars, particularly those that let a group of buttons act as a set, like radio buttons. Also, you can specify that a button remains pressed in when clicked. The alternative bitmapped button, TBitBtn, lacks these features and is better suited for use in dialog boxes. Other controls can also be added. Drop-down combo boxes are particularly useful for easily selecting from a number of options, such as fonts or text styles.

The key to having buttons act as a group is their GroupIndex property. If you wish to have three buttons in a set, perhaps to allow the user to choose one of three possible drawing tools, simple set the GroupIndex of all three to the same non-zero number. You can specify which button is pressed initially by setting its Down property to true. All others will be set to false, since only one button within a set can be down at any one time. Here is an example of a grouped set of buttons:

Figure 4-6 _ Toolbar with grouped buttons

Note that some of the buttons in the above toolbar have a different appearance than in Figure 4-3. This is because they are disabled (that is, their Enabled property is set to false). Normally a disabled button will have the same image (or glyph) as the enabled one, but it will be in one colour rather than multicoloured. There also might be shading effects to make it look like it is embossed.

Delphi will attempt to alter the image on disabled buttons to make them look different. However, the results are not always very good. Instead, you can design a separate bitmap to represent the button when it is disabled. These alternative bitmaps are actually drawn beside the regular one in the same .BMP file. So, if you are designing 16x16 pixel bitmaps you would actually create one 32 pixels wide and 16 pixels high, then draw the regular and disabled images side-by-side.

This double bitmap is then assigned to the Glyph property of the speed button as normal and the NumGlyphs property is set to 2. Now when the button is disabled the second bitmap will be displayed. Third and fourth bitmaps can also be designed; these will be used for when the button is the middle of being clicked and for when the button will remain pressed down. These are optional, though.

The actual glyph can be designed in Delphi's Image Editor or any other bitmap editing program. Delphi comes with a large number of bitmaps ready to use for buttons. These are in the IMAGES\BUTTONS subdirectory of the Delphi directory. They will all have a similar style so you can populate most or all of your toolbar with these and it will look consistent. If you are designing your own buttons to go along with these you should strive to give them a similar style.

New Win95 common controls

One problem with using TPanels and TSpeedButtons for the toolbar is that the placement of the buttons can be a bit fiddly. You must make sure they are spaced evenly and that they are all the same size and aligned properly. Each bitmap must also be loaded separately. The grid and alignment features of the form editor help but it can still be difficult.

Delphi 3 adds a series of new types of toolbar in the form of a TToolBar class. This encapsulates the Windows 95/NT 4 toolbar common control. It gives you much better control over the alignment and spacing of the buttons as well as the organisation of the glyphs.

The first step to using the TToolBar component is to drop one on your form (it lives on the Win32 component palette). It will automatically align itself to the top of the form. Next you need to load the images. This is done through the encapsulation of another Windows 95/NT 4 common control, the image list. This is also on the Win32 palette in the form of the TImageList component. This can hold a series of bitmaps and allows various properties of each to be specified.

Once a TImageList component has been dropped on the form double clicking it will bring up the following dialog box:

Figure 4-7 _ Delphi image list editor

This allows you to add new images from the list by clicking the Add button and selecting a file. The loaded images are shown at the bottom and the currently selected one is in the larger window. The attributes of this, such as the colour that will become transparent at run time, can then be set. The images for the buttons are best loaded in the same order that they will be on the toolbar.

Next you must associate the list with the toolbar. There are three properties of TToolBar that let you do this. Images points to a TImageList that holds the regular bitmaps for the buttons. DisabledImages points to a list of images for the disabled state. Finally HotImages references a list of images that will be used when the mouse cursor is hovering over the button. Images is the only required one; if the others are not assigned then the same image will be used for all states of the button.

The final step is to add buttons to the toolbar. This is done by right-clicking on the TToolBar component and choosing Add Button from the context menu. This must be done for each button. You can add gaps between the buttons by choosing Add Separator instead. If you have set the Images property then those images will automatically appear on the buttons. Even better, if you set the Images property to another image list those new images are automatically shown. This can be very useful for changing the appearance of the buttons at run time.

The buttons are automatically aligned and spaced, so there is no need to do this yourself. You can adjust the size of all buttons at one time with the ButtonWidth and ButtonHeight properties. The toolbar can be made to automatically adjust the placement of its buttons with the Wrapable property. If the toolbar is resized so that the buttons do not all fit in one row it will wrap the remaining ones around to a second row.

You can also choose to have captions shown below the images with the ShowCaptions boolean property. This can easily be changed at run time, allowing the user to decide whether to have the text shown.

Each button and separator is an instance of TToolButton. A list of these is maintained in TToolBar.Buttons. The properties of each button can be modified individually. This will especially need to be done for disabling buttons or to created grouped sets of buttons, as discussed in the last section.

The mechanism for grouping buttons is different; you must set the Grouped property of all buttons in the set (which must be adjacent) to true. The Style property of these must also be set to tbsCheck, rather than the default tbsButton. Only one grouped set of buttons can occur in a toolbar. If you need another set you will have to create them as a separate toolbar.

Of course other controls, such as combo boxes, can be added to the toolbar. These will automatically be aligned with the buttons.

Drop-down menu buttons

Toolbar buttons don't necessarily need to perform an action immediately. They can be a link to another user interface element. For example, as mentioned in the chapter on menus, toolbar buttons can be associated with a pop-up menu. When the button is pressed a menu is displayed immediately below the button.

This is easy to do. You simply need to add a TPopupMenu component to the form and set its AutoPopup property to false. Then, in response to the OnClick event of the button call the pop-up menu's Popup method:

procedure TMainForm.FavouritesBtnClick(Sender: TObject);
begin
  PopupMenu1.Popup(ClientOrigin.x+FavouritesBtn.Left,
                   ClientOrigin.y+(FavouritesBtn.Top+
                                   FavouritesBtn.Height));
end;

The Popup method takes two parameters, which are the x/y coordinates of the position of menu. This is in relation to the entire screen, so we must take the ClientOrigin into account. We find the coordinates of the bottom left of the button and place the menu there.

The button must have some sort of visual clue that it will produce a menu. This is best done with a downward pointing arrow. This example takes a similar approach to that used in Internet Explorer and adds a small black triangle to the right side of the button bitmap:

Figure 4-8 _ A drop-down menu button in action

The above buttons are of the flat type, described in the next section. An alternative approach that can be used with regular 3-D buttons is to add a separate button with the down-arrow at the side. You could do this as a pair of buttons, one with the regular bitmap and one with the black downward pointing triangle. You can then trap the mouse down and up events of each to force the other button to the same state, so that both are pressed at once. You can also have the OnClick handler of each calling the other so they work in sync. Each button must first check the Sender to make sure it doesn't go into an endless loop. If the OnClick event was triggered by the other button don't call its OnClick handler again. These methods will tie two buttons called ToolButton4 and ToolButton5 together:

procedure TMainForm.ToolButton4MouseDown
                      (Sender: TObject;
                       Button: TMouseButton; 
                       Shift: TShiftState; X, Y: Integer);
begin
  ToolButton5.Down := true;
end;

procedure TMainForm.ToolButton5MouseDown
                      (Sender: TObject;
                       Button: TMouseButton; 
                       Shift: TShiftState; X, Y: Integer);
begin
  ToolButton4.Down := true;
end;

procedure TMainForm.ToolButton4MouseUp
                      (Sender: TObject;
                       Button: TMouseButton; 
                       Shift: TShiftState; X, Y: Integer);
begin
  ToolButton5.Down := false;
end;

procedure TMainForm.ToolButton5MouseUp
                      (Sender: TObject;
                       Button: TMouseButton; 
                       Shift: TShiftState; X, Y: Integer);
begin
  ToolButton4.Down := false;
end;

procedure TMainForm.ToolButton4Click(Sender: TObject);
begin
  { ... }
  if Sender <> ToolButton5 then ToolButton5Click(Sender);
end;

procedure TMainForm.ToolButton5Click(Sender: TObject);
begin
  { ... }
  if Sender <> ToolButton4 then ToolButton4Click(Sender);
end;

Under Delphi 3, though, you can do this automatically by setting the TToolButton.Style property to tbsDropDown. When this is done a half-width button with an arrow will be placed to the right, as in this example:

Figure 4-9 _ A tbsDropDown style button

The OnClick event handler then pops up the menu just as with the flat buttons.

CoolBars and flat buttons

Microsoft has introduced a dramatically different style of buttons and toolbars in version 3 of its Internet Explorer. These are what have been called CoolBars and their associated flat buttons. A CoolBar is a container control that can hold other controls, such as toolbars. It can have several bands that the user can move and resize at run time. Each band can have a different toolbar, image or other control, singly or in sets.

For the CoolBar and flat buttons to work you must have a recent version of the system file COMCTL32.DLL. A version of this ships with Windows 95, but it doesn't have these new controls. You must have at least version 4.70 (dated 9 August 1996 or later).

Unfortunately this file cannot be freely distributed. However, if you install Internet Explorer version 3, which can be freely downloaded from Microsoft's web site, then the latest version of this DLL will be installed. It is also installed with some other Microsoft software that uses these new controls, most notably Office 97. Presumably this will eventually be incorporated into the shipping version of Windows.

Another distinctive feature is that you can specify a bitmap that is used as a backdrop behind the controls, such as in the marbled effect in the figure below. This comes from Borland's demonstration program for CoolBars/Toolbars and the web browser Internet components (this can be found in the subdirectory DEMOS\COOLSTUF in your Delphi 3 directory):

Figure 4-10 _ CoolBar example

The resizable nature of the CoolBar is very useful if you need to have a lot of controls and buttons available to the user at one time. If there are too many to fit on one row they can be placed on two panels (or bands) of a CoolBar. Then the user can slide the divider back and forth to expose the ones they need at that time. In the above example, the user has resized the rightmost band so that only the label Address is visible. This contains a combo box with the web address, but it is now hidden. When the user needs to use the combo box the panel divider can simply be moved to the left to expose it, as shown in Figure 4-11.

Figure 4-11 _ The CoolBar with the address exposed

The user might decide that the address should be on the left rather than the right. All he or she needs to do is point to a space on the band that has no controls (the mouse cursor will turn into a hand with a single finger extended) and drag it to the left side. For this to be possible the TCoolBar.FixedOrder property must be false (which is the default).

Creating a CoolBar

To create a CoolBar simply select the TCoolBar component on the Win32 component palette and drop it on your form. As you add each new component to the CoolBar a new band is created for each. You can also add new sections through the property editor for the Bands property. If you need to add a group of controls onto a single band they will have to be contained within a TPanel, TToolBar or other suitable container component.

Each band is a TCoolBand object and the properties for these can be set. The most important ones are:

· Break _ indicates whether the band should start a new row

· FixedSize _ determines if the band can be resized

· Control _ the component that is contained on that band

· ParentBitmap and Bitmap _ specifies whether the bitmap of the whole CoolBar should be used for the background or a different one

Cool buttons

If you wish to have bitmaps in the background of your CoolBar then you must use the TToolBar and its associated TToolButtons, rather than a TPanel and speed buttons. These are the only ones that provide the transparency that allows the bitmaps to show through. To specify that the bar and buttons should be transparent you must set the TToolBar.Flat property to true.

You may think that this is a radically new definition of the word `flat'. In fact, the transparency is a deliberate side effect of the new flat style of buttons defined by Microsoft. These are buttons that, rather than having a raised 3-D look, normally appear flush with the background. When the mouse moves over the button it is raised up This provides visual feedback to the user indicating that it can be pressed. (Microsoft have added a similar effect to top-level menus in the new Office 97 program suite, but as of this writing the programming interface hasn't been documented.) Figure 4-10 shows one of the buttons raised; the others are flat. The same figure also shows another new style of button, the list button. These have the glyph to the left side, rather than centred above the caption. To get buttons like this simply set the TToolBar.List property to true.

For the transparency to work properly the images on the buttons must have transparency information. The easiest way to do this is to specify the transparent colour in the image list editor (see Figure 4-7). If you create a bitmap with a white background and you wish to have all areas of that colour transparent then simply set the transparent colour to clWhite.

Dockable toolbars

As mentioned earlier, allowing the user to customize the toolbars is a nice addition to a professional looking program. One possible feature is the ability to change the placement of the toolbar. This can be done with an option in a Preferences dialog box (perhaps a radio button group with four options, top, left, bottom and right), but a neater method is to allow the user to simply drag the toolbar to its new location.

Delphi visual components support drag and drop operations, providing a number of properties and events that let each component trap dragging and dropping operations and to decide what to do with them. You could implement dockable toolbars by setting the DragMode property of the toolbar base (the TPanel or TToolBar that forms the container) to dmAutomatic, which lets the user start dragging it. You then need to create OnDragOver and OnDragDrop event handlers for each component that you may drag the toolbar over, including other child forms.

This can get a bit complex, however. You can have all components share the same event handlers (checking the Sender parameter if necessary if different types of components must react differently) but you must still remember to link those handlers to each new component you add.

A simpler method is to create a new component, descended from TPanel or TToolBar, that traps mouse clicks and movements to support dragging on its own, with no reference to other components (other than the parent form).

Our design criteria for this draggable toolbar is that it should give the user visual feedback as to what is happening when they drag the toolbar. We can do this with a rectangular outline of the same size as the toolbar. This is moved around the screen as the users drags to indicate the current position. Moving an outline like this is faster than trying to redraw the whole toolbar at each mouse move.

During dragging it should indicated in some way where the toolbar will end up when it is dropped. We can do this by changing the orientation of the dragging outline to indicate if it will be at the sides or the top or bottom. Finally, when it is dropped the toolbar should align itself to the appropriate edge of the form and rearrange the buttons to fit the new orientation.

The source code for this example is in project \CHAP4\DRAGBAR\TESTDRAG.DPR. The component in DRAGPANL.PAS will need to be installed on your palette before opening this project.

The following unit implements a toolbar component meeting these criteria:

Listing 4-2 _ A draggable toolbar, from DRAGPANL.PAS

unit Dragpanl;

interface

uses
  SysUtils, WinTypes, WinProcs, Messages, Classes, 
  Graphics, Controls, Forms, Dialogs, ExtCtrls;

type
  TDragPanel = class(TPanel)
  private
    { Private declarations }
    Dragging : boolean;
    HotZone,
    DragRect : TRect;
    OldX,
    OldY,
    XOffset,
    YOffset    : integer;
    ParentForm : TForm;
    CurHotZone : TAlign;
    procedure DrawDragRect;
    procedure Loaded; override;
    function NewHotZone(x,y:integer):TAlign;
    procedure InvertDragRect;
    function OrientationChanged
               (CurAlign, NewAlign : TAlign) : boolean;
    procedure RearrangeButtons;
  protected
    { Protected declarations }
    procedure MouseDown(Button: TMouseButton; 
                        Shift: TShiftState;
                        X, Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; 
                        X, Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; 
                      Shift: TShiftState;
                      X, Y: Integer); override;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
  published
    { Published declarations }
  end;

procedure Register;

implementation

constructor TDragPanel.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Dragging := false;
end;

procedure TDragPanel.Loaded;
begin
  inherited Loaded;
  ParentForm := GetParentForm(self);
end;

procedure TDragPanel.InvertDragRect;
var
  DragHeight,
  DragWidth,
  temp : integer;
  Rect : TRect;
begin
  DragHeight := DragRect.Bottom - DragRect.Top;
  DragWidth  := DragRect.Right - DragRect.Left;
  with Rect do begin
    Left   := DragRect.Left + (XOffset - YOffset);
    Right  := Left + DragHeight;
    Top    := DragRect.Top + (YOffset - XOffset);
    Bottom := Top + DragWidth;
  end;
  DragRect := Rect;

  { swap offsets }
  temp := XOffset;
  XOffset := YOffset;
  YOffset := temp;
end;

procedure TDragPanel.DrawDragRect;
var
  DragDC : hDC;
  Rect   : TRect;
begin
  DragDC := GetWindowDC(ParentForm.Handle);
  try
    Rect := DragRect;
    OffsetRect(Rect,0,(ParentForm.ClientOrigin.Y 
                     - ParentForm.Top));
    DrawFocusRect(DragDC,Rect);
  finally
    ReleaseDC(ParentForm.Handle,DragDC);
  end;
end;

function TDragPanel.NewHotZone(x,y:integer):TAlign;
begin
  if (x + Left) > HotZone.Right then
    Result := alRight
  else if (x + Left) < HotZone.Left then
    Result := alLeft
  else if (y + Top) > HotZone.Bottom then
    Result := alBottom
  else
    Result := alTop;
end;

function TDragPanel.OrientationChanged(CurAlign, 
                             NewAlign : TAlign) : boolean;
begin
  Result := ((CurAlign in [alTop, alBottom]) and 
             (NewAlign in [alLeft,alRight])) or
            ((CurAlign in [alLeft,alRight]) and 
             (NewAlign in [alTop, alBottom]));
end;

procedure TDragPanel.RearrangeButtons;
var
  RowTop,
  RowLeft,
  temp,
  i : integer;
begin
  RowTop := MaxInt;
  RowLeft := MaxInt;
  for i := 0 to ControlCount -1 do
    with Controls[i] do begin
      if Top < RowTop then RowTop := Top;
      if Left < RowLeft then RowLeft := Left;
    end;
  if Align in [alLeft,alRight] then begin
    for i := 0 to ControlCount -1 do
      with Controls[i] do begin
        Top := Left;
        Left := RowLeft;
      end;
  end
  else begin
    for i := 0 to ControlCount -1 do
      with Controls[i] do begin
        Left := Top;
        Top := RowTop;
      end;
  end;
end;

procedure TDragPanel.MouseDown(Button: TMouseButton;
                               Shift: TShiftState; 
                               X, Y: Integer);
begin
  DragRect := BoundsRect;
  XOffset := X;
  YOffset := Y;
  OldX := X;
  OldY := Y;
  DrawDragRect;
  with HotZone do begin
    Left   := ParentForm.Width div 6;
    Right  := (ParentForm.Width div 6) * 5;
    Top    := ParentForm.Height div 2;
    Bottom := ParentForm.Height div 2;
  end;
  Dragging := true;
  Screen.Cursor := crDrag;
  CurHotZone := Align;
  inherited MouseDown(Button,Shift, X, Y);
end;

procedure TDragPanel.MouseMove(Shift: TShiftState; 
                               X, Y: Integer);
var
  HotZone : TAlign;
begin
  if Dragging then begin
    { Erase old rectangle }
    DrawDragRect;
    { update coordinates and draw new rectangle }
    OffsetRect(DragRect,X - OldX, Y - OldY);
    OldX := X;
    OldY := Y;
    HotZone := NewHotZone(x,y);
    if HotZone <> CurHotZone then begin
      if OrientationChanged(HotZone,CurHotZone) then
         InvertDragRect;
      CurHotZone := HotZone;
    end;
    DrawDragRect;
  end;
  inherited MouseMove(Shift, X, Y);
end;

procedure TDragPanel.MouseUp(Button: TMouseButton;
                             Shift: TShiftState; 
                             X, Y: Integer);
var
  CurAlign : TAlign;
begin
  if Dragging then begin
    DrawDragRect;
    CurAlign := Align;
    Align := CurHotZone;
    if OrientationChanged(CurAlign,CurHotZone) then
      RearrangeButtons;
  end;
  Dragging := false;
  Screen.Cursor := crDefault;
  Inherited MouseUp(Button, Shift,X, Y);
end;

procedure Register;
begin
  RegisterComponents('D3UID', [TDragPanel]);
end;
end.

The new component contains several fields for keeping track of the dragging operation. The boolean variable Dragging indicates that a drag operation is underway. DragRect keeps the coordinates of the rectangular outline that is dragged around the screen. HotZone stores the limits of the areas where the toolbar snaps to an edge. For example, if the toolbar is dragged closer than 1/6 of the screen width to the left edge, the toolbar will be aligned to that edge when it is released. The Left, Right, Top and Bottom fields of this TRect record store these hot zone limits.

We need to keep track of where the toolbar has been dragged from so that we can erase the old outline and draw a new one. This is done with OldX, OldY, XOffest and YOffset. CurHotZone keeps track of which hot zone we've just been in, so we know if we've strayed into a new one. This is stored as a TAlign enumerated type so that we can simply use its alLeft, alRight, etc. settings. When we need to move the toolbar to a new position we can then simply set its Align property to this variable. Finally ParentForm stores a pointer to the form on which the toolbar is placed. We need this for drawing the outline.

The component is initialized by setting Dragging to false in the Create constructor. We also retrieve a pointer to the parent form by calling the Delphi function GetParentForm in the Loaded method. We must do it here rather than in Create because the components have not yet been loaded and initialized in the constructor.

The dragging operation begins when the mouse button is pressed. This is trapped by overriding the MouseDown method. This initializes the DragRect to be the same size and position as the toolbar; the BoundsRect method returns a rectangle with the coordinates for the component. We then save the X/Y coordinates of the mouse as the button was pressed. These coordinates are pixels from the upper left of the component itself so they serve as the offset from that corner to the place we are `holding' the toolbar while we drag it; therefore we save them in XOffset and YOffset. We also save them in OldX and OldY so that we keep track of the old position.

After this we can draw the dragging outline (described later), then calculate the coordinates of the hot zones. The left and right hot zones are 1/6 of the form width at either side. If the toolbar is dropped in either it will be aligned to that side. Otherwise we split the screen in half horizontally. If the toolbar is dropped in the top half it goes to the top, otherwise it goes to the bottom. You could make the top and bottom hot zones 1/6 the form height as well, so that nothing happens if it is dropped in the middle. An alternative (not implemented here) is to turn the toolbar into a floating toolbox that can be placed anywhere on the screen.

Once all this is done we set Dragging to true, change the screen cursor into the standard dragging form (an arrow with a page of paper underneath it) and save the current alignment of the toolbar. Finally we call the inherited MouseDown so any other processing can take place.

The dragging outline

Your first attempt to draw an outline of the dragged toolbar might be to draw it on the canvas of the form. This would be obscured by any other components and child windows. What we need to do is draw it over the top of everything. We also need to draw it in such a way that it can be removed when the mouse moves to a new location. We cannot simply draw a white rectangle because it would not be visible against a white background; it must be drawn by reversing whatever colour is beneath each part of the rectangle.

This can all be done by using an XOR drawing operation. This performs a boolean exclusive-or operation on each pixel, changing a colour into its complement. When the same graphic object is drawn a second time at the same place it will reverse the drawing and restore the colours to their original state, thus erasing the object.

To do this over the entire form we need to drop down to the Windows API and call the function DrawFocusRect, which performs an XOR drawing of a rectangle on a window. Note that this is different from the TCanvas method of the same name. That method also does an XOR drawing, but only on the specified canvas. It is used mainly for making buttons look focused.

To use the Windows graphics operations like DrawFocusRect we must first obtain a device context for the desired drawing surface. This is a Windows resource that provides a device-independent layer between your program and the actual video screen. Rather than drawing directly to the screen you draw to the device context. Windows then takes care of adjusting for the varying resolution and colour depth.

We can ask Windows to provide a device context for our use in a number of ways. In our DrawDragRect method we will use the GetWindowDC function, which allows us to draw on the whole window. Since device contexts are a limited resource, particularly in Windows 3.1, we make sure we free it after use with a try..finally block and ReleaseDC.

To draw on the device context we first save a temporary copy of the DragRect, then use OffsetRect to adjust the drawing coordinates. We use the difference between the form's ClientOrigin and its Top to adjust for the height of the title bar and menu. Then we simply pass the rectangle to DrawFocusRect, along with our device context.

Dragging

The actual dragging operations are done by trapping the mouse movements; we do this by overriding the MouseMove method. Within this we first check to see if we are actually dragging the component (that is, the mouse button is being held down). If not we simply call the ancestral method.

If we are dragging our first job is to erase the old dragging outline. We simply call DrawDragRect again while DragRect still has the same coordinates as the last time it was called. We then use OffsetRect again to adjust DragRect. This is done by taking the difference between the old mouse X/Y coordinates and the new ones passed to the method in the parameters X and Y. We then save the new coordinates in OldX and OldY for use next time around.

Next we check if we have moved into a different hot zone by calling the NewHotZone method. This takes our new X/Y coordinates and compares those to the regions defined in HotZone. It returns the alignment type appropriate for the current hot zone. Since the X/Y coordinates passed to the MouseMove method are in relation to the upper left of the TDragPanel component we must convert those to form-based coordinates by adding Left or Top as appropriate.

If we are in a new hot zone we next call the function Orientation-Changed. This simply compares two alignments to see if the orientation of the toolbar will change (e.g. it will go from a horizontal alTop alignment to a vertical alLeft one). If it has changed we call InvertDragRect to adjust the DragRect coordinates to draw the outline in the new orientation. This is done with reference to the offset coordinates of the place where we are `holding' the outline, that is the XOffset and YOffset variables.

Lets say our rectangle is vertical, with an XOffset of 30 and a YOffset of 80. The rectangle on the left shows this:

Figure 4-12 _ Inverting a rectangle

To find the new Left coordinate of the inverted rectangle (the one on the right in Figure 4-12) we simply add the difference between XOffset and YOffset (-50) to the current Left coordinate. This moves the left side of the rectangle 50 pixels further to the left. Likewise we add the inverse difference (+50) to the Top coordinate; this moves it 50 pixels down. Previously we calculated the width and height of the rectangle, so we add these to the new Left and Top to get the position of the lower right of the rectangle. Finally we swap the values for XOffset and YOffset to match the new orientation.

After this we update the saved value for the current hot zone, then draw a new dragging outline with the new position and orientation. We keep performing the above actions each time the mouse moves while the button is still held down.

When the mouse button is released we erase the outline with a final call to DrawDragRect. We then save the current toolbar alignment and set the Align property to the new value, returned by CutHotZone. This will move the toolbar to which ever edge the mouse was closest to when the button was released.

Rearranging buttons

We also check to see if we've just changed the orientation of the toolbar. If so, we will need to rearrange the buttons to fit. To do this we will need to find out where to place the buttons (i.e. the left or top coordinate that they will all be aligned to) then swap the left and top coordinates of each button.

This is done by walking through the list of controls on the toolbar. Note that we do this using the Controls list for our TDragPanel. This is the list of all controls contained within the TDragPanel; these will all have the panel as their Parent. We must use this list rather than Components, which represents all components that are owned by the current one (i.e. the ones that have the current control as their Owner). All buttons, panels, etc. are owned by the form, not their containing control, so the TDragPanel.Components list will be empty.

First we find the leftmost and topmost button by walking through the Controls list looking for the smallest values of Top and Left. We can't assume that the first button will be first in the list; we must check them all.

After this we adjust the buttons' coordinates for the new orientation. If we are going from horizontal to vertical (that is the new alignment is alLeft or alRight) we place the Left coordinate in the Top property, then set the Left value to the minimal value found for all buttons. This now places the button lower down and aligned to the left. We do the opposite if we are going to horizontal.

This method will preserve the spacing between groups of buttons. It does assume, however, that the buttons are square and of the same size, and also that there is just one row or column of them. If you have rectangular buttons, or other controls such as combo boxes that are wider than they are high, then you will need to adjust the width/height of the panel to accommodate the full size of all controls.

Note that the step of rearranging the buttons is only needed if the TDragPanel is based on a TPanel. You could easily derive this dockable toolbar from Delphi 3's TToolBar instead. The toolbar common control automatically rearranges buttons when its shape changes (if the Wrapable property is set to true), so you can skip the RearrangeButtons method.