Chapter 6
Working with controls

Controls are visual components that the user can interact with at runtime. This chapter describes a variety of features common to many controls.

Implementing drag-and-drop in controls

Drag-and-drop is often a convenient way for users to manipulate objects. You can let users drag an entire control, or let them drag items from one control--such as a list box or tree view--into another.

Starting a drag operation

Every control has a property called DragMode that determines how drag operations are initiated. If DragMode is dmAutomatic, dragging begins automatically when the user presses a mouse button with the cursor on the control. Because dmAutomatic can interfere with normal mouse activity, you may want to set DragMode to dmManual (the default) and start the dragging by handling mouse-down events.

To start dragging a control manually, call the control's BeginDrag method. BeginDrag takes a Boolean parameter called Immediate. If you pass True, dragging begins immediately. If you pass False, dragging does not begin until the user moves the mouse a short distance. Calling BeginDrag(False) allows the control to accept mouse clicks without beginning a drag operation.

You can place other conditions on whether to begin dragging, such as checking which mouse button the user pressed, by testing the parameters of the mouse-down event before calling BeginDrag. The following code, for example, handles a mouse-down event in a file list box by initiating a drag operation only if the left mouse button was pressed.

procedure TFMForm.FileListBox1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbLeft then  { drag only if left button pressed }
    with Sender as TFileListBox do  { treat Sender as TFileListBox }
      begin
        if ItemAtPos(Point(X, Y), True) >= 0 then  { is there an item here? }
          BeginDrag(False);  { if so, drag it }
    end;
end;

Accepting dragged items

When the user drags something over a control, that control receives an OnDragOver event, at which time it must indicate whether it can accept the item if the user drops it there. The drag cursor changes to indicate whether the control can accept the dragged item. To accept items dragged over a control, attach an event handler to the control's OnDragOver event.

The drag-over event has a parameter called Accept that the event handler can set to True if it will accept the item. If Accept is True, the application sends a drag-drop event to the control.

The drag-over event has other parameters, including the source of the dragging and the current location of the mouse cursor, that the event handler can use to determine whether to accept the drop. In the following example, a directory tree view accepts dragged items only if they come from a file list box.

procedure TFMForm.DirectoryOutline1DragOver(Sender, Source: TObject; X,
  Y: Integer; State: TDragState; var Accept: Boolean);
begin
  if Source is TFileListBox then
    Accept := True;
  else
    Accept := False;
end;

Dropping items

If a control indicates that it can accept a dragged item, it needs to handle the item should it be dropped. To handle dropped items, attach an event handler to the OnDragDrop event of the control accepting the drop. Like the drag-over event, the drag-drop event indicates the source of the dragged item and the coordinates of the mouse cursor over the accepting control. The latter parameter allows you to monitor the path an item takes while being dragged; you might, for example, want to use this information to change the color of components as they are passed over.

In the following example, a directory tree view, accepting items dragged from a file list box, responds by moving files to the directory on which they are dropped.

procedure TFMForm.DirectoryOutline1DragDrop(Sender, Source: TObject; X,
  Y: Integer);
begin
  if Source is TFileListBox then
    with DirectoryOutline1 do
      ConfirmChange('Move', FileList.FileName, Items[GetItem(X, Y)].FullPath);
end;

Ending a drag operation

A drag operation ends when the item is either successfully dropped or released over a control that cannot accept it. At this point an end-drag event is sent to the control from which the item was dragged. To enable a control to respond when items have been dragged from it, attach an event handler to the control's OnEndDrag event.

The most important parameter in an OnEndDrag event is called Target, which indicates which control, if any, accepts the drop. If Target is nil, it means no control accepts the dragged item. The OnEndDrag event also includes the coordinates on the receiving control.

In this example, a file list box handles an end-drag event by refreshing its file list.

procedure TFMForm.FileList1EndDrag(Sender, Target: TObject; X, Y: Integer);
begin
  if Target <> nil then FileList1.Update;
end;

Customizing drag and drop with a drag object

You can use a TDragObject descendant to customize an object's drag-and-drop behavior. The standard drag-over and drag-drop events indicate the source of the dragged item and the coordinates of the mouse cursor over the accepting control. To get additional state information, derive a custom drag object from TDragObject and override its virtual methods. Create the custom drag object in the OnStartDrag event.

Normally, the source parameter of the drag-over and drag-drop events is the control that starts the drag operation. If different kinds of control can start an operation involving the same kind of data, the source needs to support each kind of control. When you use a descendant of TDragObject, however, the source is the drag object itself; if each control creates the same kind of drag object in its OnStartDrag event, the target needs to handle only one kind of object. The drag-over and drag-drop events can tell if the source is a drag object, as opposed to the control, by calling the IsDragObject function.

Drag objects let you drag items between a form implemented in the application's main EXE file and a form implemented in a DLL, or between forms that are implemented in different DLLs.

Changing the drag mouse pointer

You can customize the appearance of the mouse pointer during drag operations by setting the source component's DragCursor property.

Implementing drag-and-dock in controls

Descendants of TWinControl can act as docking sites and descendants of TControl can act as child windows that are docked into docking sites. For example, to provide a docking site at the left edge of a form window, align a panel to the left edge of the form and make the panel a docking site. When dockable controls are dragged to the panel and released, they become child controls of the panel.

Making a windowed control a docking site

To make a windowed control a docking site,

  1. Set the DockSite property to True.
  2. If the dock site object should not appear except when it contains a docked client, set its AutoSize property to True. When AutoSize is True, the dock site is sized to 0 until it accepts a child control for docking. Then it resizes to fit around the child control.

Making a control a dockable child

To make a control a dockable child,

  1. Set its DragKind property to dkDock. When DragKind is dkDock, dragging the control moves the control to a new docking site or undocks the control so that it becomes a floating window. When DragKind is dkDrag (the default), dragging the control starts a drag-and-drop operation which must be implemented using the OnDragOver, OnEndDrag, and OnDragDrop events.
  2. Set its DragMode to dmAutomatic. When DragMode is dmAutomatic, dragging (for drag-and-drop or docking, depending on DragKind) is initiated automatically when the user starts dragging the control with the mouse. When DragMode is dmManual, you can still begin a drag-and-dock (or drag-and-drop) operation by calling the BeginDrag method.
  3. Set its FloatingDockSiteClass property to indicate the TWinControl descendant that should host the control when it is undocked and left as a floating window. When the control is released and not over a docking site, a windowed control of this class is created dynamically, and becomes the parent of the dockable child. If the dockable child control is a descendant of TWinControl, it is not necessary to create a separate floating dock site to host the control, although you may want to specify a form in order to get a border and title bar. To omit a dynamic container window, set FloatingDockSiteClass to the same class as the control, and it will become a floating window with no parent.

Controlling how child controls are docked

A docking site automatically accepts child controls when they are released over the docking site. For most controls, the first child is docked to fill the client area, the second splits that into separate regions, and so on. Page controls dock children into new tab sheets (or merge in the tab sheets if the child is another page control).

Three events allow docking sites to further constrain how child controls are docked:

property OnGetSiteInfo: TGetSiteInfoEvent;
TGetSiteInfoEvent = procedure(Sender: TObject; DockClient: TControl; var   InfluenceRect: 
TRect; var CanDock: Boolean) of object;

OnGetSiteInfo occurs on the docking site when the user drags a dockable child over the control. It allows the site to indicate whether it will accept the control specified by the DockClient parameter as a child, and if so, where the child must be to be considered for docking. When OnGetSiteInfo occurs, InfluenceRect is initialized to the screen coordinates of the docking site, and CanDock is intialized to True. A more limited docking region can be created by changing InfluenceRect and the child can be rejected by setting CanDock to False.

property OnDockOver: TDockOverEvent;
TDockOverEvent = procedure(Sender: TObject; Source: TDragDockObject; X, Y:  Integer; State: 
TDragState; var Accept: Boolean) of object;

OnDockOver occurs on the docking site when the user drags a dockable child over the control. It is analogous to the OnDragOver event in a drag-and-drop operation. Use it to signal that the child can be released for docking, by setting the Accept parameter. If the dockable control is rejected by the OnGetSiteInfo event handler (perhaps because it is the wrong type of control), OnDockOver does not occur.

property OnDockDrop: TDockDropEvent;
TDockDropEvent = procedure(Sender: TObject; Source: TDragDockObject; X, Y:  Integer) of 
object;

OnDockDrop occurs on the docking site when the user releases the dockable child over the control. It is analogous to the OnDragDrop event in a normal drag-and-drop operation. Use this event to perform any necessary accommodations to accepting the control as a child control. Access to the child control can be obtained using the Control property of the TDockObject specified by the Source parameter.

Controlling how child controls are undocked

A docking site automatically allows child controls to be undocked when they are dragged and have a DragMode property of dmAutomatic. Docking sites can respond when child controls are dragged off, and even prevent the undocking, in an OnUnDock event handler:

property OnUnDock: TUnDockEvent;
TUnDockEvent = procedure(Sender: TObject; Client: TControl; var Allow: Boolean)   of object;

The Client parameter indicates the child control that is trying to undock, and the Allow parameter lets the docking site (Sender) reject the undocking. When implementing an OnUnDock event handler, it can be useful to know what other children (if any) are currently docked. This information is available in the read-only DockClients property, which is an indexed array of TControl. The number of dock clients is given by the read-only DockClientCount property.

Controlling how child controls respond to drag-and-dock operations

Dockable child controls have two events that occur during drag-and-dock operations: OnStartDock, analogous to the OnStartDrag event of a drag-and-drop operation, allows the dockable child control to create a custom drag object. OnEndDock, like OnEndDrag, occurs when the dragging terminates.

Working with text in controls

The following sections explain how to use various features of rich edit and memo controls. Some of these features work with edit controls as well.

Setting text alignment

In a rich edit or memo component, text can be left- or right-aligned or centered. To change text alignment, set the edit component's Alignment property. Alignment takes effect only if the WordWrap property is True; if word wrapping is turned off, there is no margin to align to.

For example, the following code attaches an OnClick event handler to the Character|Left menu item, then attaches the same event handler to both the Right and Center menu items on the Character menu.

procedure TEditForm.AlignClick(Sender: TObject);
begin
  Left1.Checked := False;  { clear all three checks }
  Right1.Checked := False;
  Center1.Checked := False;
  with Sender as TMenuItem do Checked := True;  { check the item clicked }
  with Editor do  { then set Alignment to match }
    if Left1.Checked then
      Alignment := taLeftJustify
    else if Right1.Checked then
      Alignment := taRightJustify
    else if Center1.Checked then
      Alignment := taCenter;
end;

Adding scroll bars at runtime

Rich edit and memo components can contain horizontal or vertical scroll bars, or both, as needed. When word-wrapping is enabled, the component needs only a vertical scroll bar. If the user turns off word-wrapping, the component might also need a horizontal scroll bar, since text is not limited by the right side of the editor.

To add scroll bars at runtime,

  1. Determine whether the text might exceed the right margin. In most cases, this means checking whether word wrapping is enabled. You might also check whether any text lines actually exceed the width of the control.
  2. Set the rich edit or memo component's ScrollBars property to include or exclude scroll bars.

The following example attaches an OnClick event handler to a Character|WordWrap menu item.

procedure TEditForm.WordWrap1Click(Sender: TObject);
begin
  with Editor do
  begin
    WordWrap := not WordWrap;  { toggle word-wrapping }
    if WordWrap then
      ScrollBars := ssVertical  { wrapped requires only vertical }
    else
      ScrollBars := ssBoth;  { unwrapped might need both }
      WordWrap1.Checked := WordWrap;  { check menu item to match property }
  end;
end;

The rich edit and memo components handle their scroll bars in a slightly different way. The rich edit component can hide its scroll bars if the text fits inside the bounds of the component. The memo always shows scroll bars if they are enabled.

Adding the Clipboard object

Most text-handling applications provide users with a way to move selected text between documents, including documents in different applications. The Clipboard object in Delphi encapsulates the Windows Clipboard and includes methods for cutting, copying, and pasting text (and other formats, including graphics). The Clipboard object is declared in the Clipbrd unit.

To add the Clipboard object to an application,

  1. Select the unit that will use the Clipboard.
  2. Search for the implementation reserved word.
  3. Add Clipbrd to the uses clause below implementation.

For example, in an application with a child window, the uses clause in the unit's implementation part might look like this:

uses
  MDIFrame, Clipbrd;

Selecting text

Before you can send any text to the Clipboard, that text must be selected. Highlighting of selected text is built into the edit components. When the user selects text, it appears highlighted.

Table 6.1 lists properties commonly used to handle selected text.

Table 6.1   Properties of selected text

Property

Description

SelText

Contains a string representing the selected text in the component.

SelLength

Contains the length of a selected string.

SelStart

Contains the starting position of a string.

Selecting all text

The SelectAll method selects the entire contents of the rich edit or memo component. This is especially useful when the component's contents exceed the visible area of the component. In most other cases, users select text with either keystrokes or mouse dragging.

To select the entire contents of a rich edit or memo control, call the RichEdit1 control's SelectAll method.

For example,

procedure TMainForm.SelectAll(Sender: TObject);
begin
  RichEdit1.SelectAll;  { select all text in RichEdit }
end;

Cutting, copying, and pasting text

Applications that use the Clipbrd unit can cut, copy, and paste text, graphics, and objects through the Windows Clipboard. The edit components that encapsulate the standard Windows text-handling controls all have methods built into them for interacting with the Clipboard. (See "Using the Clipboard with graphics" for information on using the Clipboard with graphics.)

To cut, copy, or paste text with the Clipboard, call the edit component's CutToClipboard, CopyToClipboard, and PasteFromClipboard methods, respectively.

For example, the following code attaches event handlers to the OnClick events of the Edit|Cut, Edit|Copy, and Edit|Paste commands, respectively:

procedure TEditForm.CutToClipboard(Sender: TObject);
begin
  Editor.CutToClipboard;
end;
procedure TEditForm.CopyToClipboard(Sender: TObject);
begin
  Editor.CopyToClipboard;
end;
procedure TEditForm.PasteFromClipboard(Sender: TObject);
begin
  Editor.PasteFromClipboard;
end;

Deleting selected text

You can delete the selected text in an edit component without cutting it to the Clipboard. To do so, call the ClearSelection method. For example, if you have a Delete item on the Edit menu, your code could look like this:

procedure TEditForm.Delete(Sender: TObject);
begin
  RichEdit1.ClearSelection;
end;

Disabling menu items

It is often useful to disable menu commands without removing them from the menu. For example, in a text editor, if there is no text currently selected, the Cut, Copy, and Delete commands are inapplicable. An appropriate time to enable or disable menu items is when the user selects the menu. To disable a menu item, set its Enabled property to False.

In the following example, an event handler is attached to the OnClick event for the Edit item on a child form's menu bar. It sets Enabled for the Cut, Copy, and Delete menu items on the Edit menu based on whether RichEdit1 has selected text. The Paste command is enabled or disabled based on whether any text exists on the Clipboard.

procedure TEditForm.Edit1Click(Sender: TObject);
var
  HasSelection: Boolean;  { declare a temporary variable }
begin
  Paste1.Enabled := Clipboard.HasFormat(CF_TEXT);  {enable or disable the Paste 
                                               menu item}
  HasSelection := Editor.SelLength > 0;  { True if text is selected }
  Cut1.Enabled := HasSelection;  { enable menu items if HasSelection is True }
  Copy1.Enabled := HasSelection;
  Delete1.Enabled := HasSelection;
end;

The HasFormat method of the Clipboard returns a Boolean value based on whether the Clipboard contains objects, text, or images of a particular format. By calling HasFormat with the parameter CF_TEXT, you can determine whether the Clipboard contains any text, and enable or disable the Paste item as appropriate.

"Working with graphics and multimedia" provides more information about using the Clipboard with graphics.

Providing a pop-up menu

Pop-up, or local, menus are a common ease-of-use feature for any application. They enable users to minimize mouse movement by clicking the right mouse button in the application workspace to access a list of frequently used commands.

In a text editor application, for example, you can add a pop-up menu that repeats the Cut, Copy, and Paste editing commands. These pop-up menu items can use the same event handlers as the corresponding items on the Edit menu. You don't need to create accelerator or shortcut keys for pop-up menus because the corresponding regular menu items generally already have shortcuts.

A form's PopupMenu property specifies what pop-up menu to display when a user right-clicks any item on the form. Individual controls also have PopupMenu properties that can override the form's property, allowing customized menus for particular controls.

To add a pop-up menu to a form,

  1. Place a pop-up menu component on the form.
  2. Use the Menu Designer to define the items for the pop-up menu.
  3. Set the PopupMenu property of the form or control that displays the menu to the name of the pop-up menu component.
  4. Attach handlers to the OnClick events of the pop-up menu items.

Handling the OnPopup event

You may want to adjust pop-up menu items before displaying the menu, just as you may want to enable or disable items on a regular menu. With a regular menu, you can handle the OnClick event for the item at the top of the menu, as described in "Disabling menu items".

With a pop-up menu, however, there is no top-level menu bar, so to prepare the pop-up menu commands, you handle the event in the menu component itself. The pop-up menu component provides an event just for this purpose, called OnPopup.

To adjust menu items on a pop-up menu before displaying them,

  1. Select the pop-up menu component.
  2. Attach an event handler to its OnPopup event.
  3. Write code in the event handler to enable, disable, hide, or show menu items.

In the following code, the EditEditClick event handler described previously in "Disabling menu items" is attached to the pop-up menu component's OnPopup event. A line of code is added to EditEditClick for each item in the pop-up menu.

procedure TEditForm.Edit1Click(Sender: TObject);
var
  HasSelection: Boolean;
begin
  Paste1.Enabled := Clipboard.HasFormat(CF_TEXT);
  Paste2.Enabled := Paste1.Enabled;  {Add this line}
  HasSelection := Editor.SelLength <> 0;
  Cut1.Enabled := HasSelection;
  Cut2.Enabled := HasSelection;  {Add this line}
  Copy1.Enabled := HasSelection;
  Copy2.Enabled := HasSelection;  {Add this line}
  Delete1.Enabled := HasSelection;
end;

Adding graphics to controls

Windows list-box, combo-box, and menu controls have a style available called "owner draw," which means that instead of using Windows' standard method of drawing text for each item in the control, the control's owner (generally, the form) draws each item at runtime. The most common use for owner-draw controls is to provide graphics instead of, or in addition to, text for items. For information on using owner-draw to add images to menus, see "Adding images to menu items".

All owner-draw controls contain lists of items. By default, those lists are lists of strings, which Windows displays as text. You can associate an object with each item in a list to make it easy to use that object when drawing items.

In general, creating an owner-draw control in Delphi involves these steps:

  1. Setting the owner-draw style
  2. Adding graphical objects to a string list
  3. Drawing owner-drawn items

Setting the owner-draw style

Both list boxes and combo boxes have a property called Style. Style determines whether the control uses the default drawing (called the "standard" style) or owner drawing. Grids use a property called DefaultDrawing to enable or disable the default drawing.

List boxes and combo boxes have additional owner-draw styles, called fixed and variable, as Table 6.2 describes. Owner-draw grids are always fixed: although the size of each row and column might vary, the size of each cell is determined before drawing the grid

Table 6.2   Fixed vs. variable owner-draw styles 

Owner-draw style

Meaning

Examples

Fixed

Each item is the same height, with that height determined by the ItemHeight property.

lbOwnerDrawFixed, csOwnerDrawFixed

Variable

Each item might have a different height, determined by the data at runtime.

lbOwnerDrawVariable, csOwnerDrawVariable

.

Adding graphical objects to a string list

Every string list has the ability to hold a list of objects in addition to its list of strings.

For example, in a file manager application, you may want to add bitmaps indicating the type of drive along with the letter of the drive. To do that, you need to add the bitmap images to the application, then copy those images into the proper places in the string list as described in the following sections.

Adding images to an application

An image control is a nonvisual control that contains a graphical image, such as a bitmap. You use image controls to display graphical images on a form. You can also use them to hold hidden images that you'll use in your application. For example, you can store bitmaps for owner-draw controls in hidden image controls, like this:

  1. Add image controls to the main form.
  2. Set their Name properties.
  3. Set the Visible property for each image control to False.
  4. Set the Picture property of each image to the desired bitmap using the Picture editor from the Object Inspector.

The image controls are invisible when you run the application.

Adding images to a string list

Once you have graphical images in an application, you can associate them with the strings in a string list. You can either add the objects at the same time as the strings, or associate objects with existing strings. The preferred method is to add objects and strings at the same time, if all the needed data is available.

The following example shows how you might want to add images to a string list. This is part of a file manager application where, along with a letter for each valid drive, it adds a bitmap indicating each drive's type. The OnCreate event handler looks like this:

procedure TFMForm.FormCreate(Sender: TObject);
var
  Drive: Char;
  AddedIndex: Integer;
begin
  for Drive := 'A' to 'Z' do  { iterate through all possible drives }
  begin
    case GetDriveType(Drive + ':/') of  { positive calues mean valid drives }
     DRIVE_REMOVABLE:  { add a tab }
       AddedIndex := DriveTabSet.Tabs.AddObject(Drive, Floppy.Picture.Graphic);
     DRIVE_FIXED:  { add a tab }
       AddedIndex := DriveTabSet.Tabs.AddObject(Drive, Fixed.Picture.Graphic);
     DRIVE_REMOTE:  { add a tab }
       AddedIndex := DriveTabSet.Tabs.AddObject(Drive, Network.Picture.Graphic);
    end;
    if UpCase(Drive) = UpCase(DirectoryOutline.Drive) then  { current drive? }
     DriveTabSet.TabIndex := AddedIndex;  { then make that current tab }
  end;
end;

Drawing owner-drawn items

When you set a control's style to owner draw, Windows no longer draws the control on the screen. Instead, it generates events for each visible item in the control. Your application handles the events to draw the items.

To draw the items in an owner-draw control, do the following for each visible item in the control. Use a single event handler for all items.

  1. Size the item, if needed.

    Items of the same size (for example, with a list box style of lsOwnerDrawFixed), do not require sizing.

  2. Draw the item.

Sizing owner-draw items

Before giving your application the chance to draw each item in a variable owner-draw control, Windows generates a measure-item event. The measure-item event tells the application where the item appears on the control.

Windows determines the size the item (generally, it is just large enough to display the item's text in the current font). Your application can handle the event and change the rectangle Windows chose. For example, if you plan to substitute a bitmap for the item's text, change the rectangle to be the size of the bitmap. If you want a bitmap and text, adjust the rectangle to be big enough for both.

To change the size of an owner-draw item, attach an event handler to the measure-item event in the owner-draw control. Depending on the control, the name of the event can vary. List boxes and combo boxes use OnMeasureItem. Grids have no measure-item event.

The sizing event has two important parameters: the index number of the item and the size of that item. The size is variable: the application can make it either smaller or larger. The positions of subsequent items depend on the size of preceding items.

For example, in a variable owner-draw list box, if the application sets the height of the first item to five pixels, the second item starts at the sixth pixel down from the top, and so on. In list boxes and combo boxes, the only aspect of the item the application can alter is the height of the item. The width of the item is always the width of the control.

Owner-draw grids cannot change the sizes of their cells as they draw. The size of each row and column is set before drawing by the ColWidths and RowHeights properties.

The following code, attached to the OnMeasureItem event of an owner-draw list box, increases the height of each list item to accommodate its associated bitmap.

procedure TFMForm.DriveTabSetMeasureTab(Sender: TObject; Index: Integer;
  var TabWidth: Integer);  { note that TabWidth is a var parameter}
var 
  BitmapWidth: Integer;
begin
  BitmapWidth := TBitmap(DriveTabSet.Tabs.Objects[Index]).Width;
  { increase tab width by the width of the associated bitmap plus two }
  Inc(TabWidth, 2 + BitmapWidth);
end;

Note: You must typecast the items from the Objects property in the string list. Objects is a property of type TObject so that it can hold any kind of object. When you retrieve objects from the array, you need to typecast them back to the actual type of the items.

Drawing each owner-draw item

When an application needs to draw or redraw an owner-draw control, Windows generates draw-item events for each visible item in the control.

To draw each item in an owner-draw control, attach an event handler to the draw-item event for that control.

The names of events for owner drawing always start with OnDraw, such as OnDrawItem or OnDrawCell.

The draw-item event contains parameters indicating the index of the item to draw, the rectangle in which to draw, and usually some information about the state of the item (such as whether the item has focus). The application handles each event by rendering the appropriate item in the given rectangle.

For example, the following code shows how to draw items in a list box that has bitmaps associated with each string. It attaches this handler to the OnDrawItem event for the list box:

procedure TFMForm.DriveTabSetDrawTab(Sender: TObject; TabCanvas: TCanvas;
  R: TRect; Index: Integer; Selected: Boolean);
var
  Bitmap: TBitmap;
begin
  Bitmap := TBitmap(DriveTabSet.Tabs.Objects[Index]);
  with TabCanvas do
  begin
  Draw(R.Left, R.Top + 4, Bitmap);  { draw bitmap }
  TextOut(R.Left + 2 + Bitmap.Width,  { position text }
    R.Top + 2, DriveTabSet.Tabs[Index]);  { and draw it to the right of the 
                                          bitmap }
  end;
end;