Athena

Implementing Professional Drag & Drop In VCL/CLX Applications

Brian Long (www.blong.com)

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

The Delphi VCL has supported drag and drop operations ever since version 1, way back in 1995. It was enhanced just a little in the first 32-bit version, Delphi 2 in 1996, but apart from that, the basic mechanisms have remained much the same. With the introduction of CLX in Delphi 6 and Kylix 1, the basic principles of drag and drop remain the same, although as we will see, the more advanced areas need to be treated differently.

Whilst using drag and drop in a Delphi/Kylix application is made very easy by the component library support, little seems to be written on the subject, which makes doing more interesting variations on the standard theme more complicated.

This paper will investigate the subject of drag and drop in Delphi/Kylix applications. It looks firstly at the basic component library support for dragging and dropping within a Delphi application. Then it moves on to look at how custom drag objects can be used to enhance the appearance of drag operations, and also how they can simplify more complex drag operations.

You can download the files that accompany this paper by clicking here.

Component Drag & Drop or "Here Thar Be Drag (ons)"

Both the VCL and CLX have built-in facilities for supporting dragging and dropping within a given application. A drag and drop operation relies upon the user clicking the left (or sometimes the right) mouse button down on some control, then moving their mouse (whilst keeping the button held down) over to another control, and finally releasing the mouse button.

The control that initially gets clicked on is called the source of the drag, or drag source, and the one under the mouse when the button is released is called the target, drag target or drop target. As far as the user is concerned, they are under the impression that they are physically dragging the source control (or information shown on it) onto the target control. In truth of course, the user is just moving the mouse with a button held down, however the terminology used (and maybe the cursor image displayed) upholds the user’s view.

It is down to the application to interpret this particular mouse operation and do something sensible with it. This includes changing the mouse cursor to suggest a drag operation is occurring, as well as indicating whether the control under the mouse is happy to accept the dragged source control or not. Of course it also involves doing something when the user ends a drag operation by dropping the dragged control on another control.

Fortunately the VCL and CLX libraries make short shrift of these requirements. The tricky stuff is dealt with by the code in the Controls/QControls unit sending internal component messages/events around under appropriate circumstances (the cm_Drag message, with various parameters in the case of the VCL, and a variety of Qt events in CLX). As far as the Delphi programmer is concerned, there are three simple steps to get drag and drop working.

Firstly, you must enable dragging from the source control. Then, when someone starts dragging from the source, a drag operation will be started. The VCL will set the mouse cursor appropriately as you move your mouse around the screen. By default, it will be a No Entry type cursor: .

The next step is to make the potential target controls indicate that they are happy to accept the source being dropped. At run-time, this is indicated by the cursor changing to a normal drag cursor (the TCursor type value of crDrag by default, which looks like ). In the VCL, this default cursor is obtained from the dragged control’s DragCursor property. CLX does not define this property, as Qt deals with the normal cursor management.

The final step is to implement what happens if the user drops the source control onto the target control. By default, nothing happens except the drag operation is terminated.

Let’s go through these steps one at a time, looking at what possibilities the component libraries offers.

What A Drag!

Normally, when you click and drag on an arbitrary control on a form, nothing particularly special happens. Specifically, no drag operation starts off (no cursor changes, no ability to drop, etc.).

You can cause a drag operation to start in one of two ways. It can either be done automatically or manually. To start drag operations automatically, set the source control’s DragMode property to dmAutomatic (it defaults to dmManual). This property is defined in TControl, so all visual controls will have it, be they componentised versions of real Windows/Qt controls or not. The effect of setting DragMode to dmAutomatic is that clicking the left mouse button down on a control will automatically start a drag operation without any extra code.

Without using this DragMode setting, you can start a drag operation manually with a call to the control’s BeginDrag method. BeginDrag has one mandatory parameter, which is a Boolean that dictates whether the drag operation begins immediately. One reason for calling BeginDrag is that you might want to only allow drag operations under some special circumstances.

Take an edit or memo control for example. These already use mouse dragging operations to highlight text. If you want to permit text selection as well as allow dragging from the edit/memo, you could restrict drag operations to only start when the Control key is held down, for example. Listing 1 shows how an edit control’s OnMouseDown event handler could achieve this.

Listing 1: Allowing dragging from an edit control, without affecting text selection

procedure TForm1.Edit1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  //Check this is an edit and Ctrl is pressed
  if (Sender is TCustomEdit) and (ssCtrl in Shift) then
    TCustomEdit(Sender).BeginDrag(True)
end;

Alternatively, you may want to start a drag operation with the right mouse button, as opposed to the left. Windows dragging supports the right mouse button, but normal VCL dragging is only normally invoked by the left mouse button. Listing 2 shows how an event handler might accomplish this (note that CLX seems to be currently limited to dragging with the left mouse button, although a fix is pending).

Listing 2: Starting a drag operation with the right mouse button

procedure TForm1.Edit1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbRight then
    (Sender as TControl).BeginDrag(False)
end;

A value of True passed to BeginDrag immediately starts a drag operation. On the other hand a value of False will only start the full drag operation when the mouse moves a certain number of pixels from where it was clicked. The point of this is for normal clicks to be permitted (where the mouse doesn’t move, or moves only slightly, thereby not causing the drag to start), whilst still allowing dragging operations (which only kick in when the mouse moves a certain number of pixels).

For example, a listbox control could use the code shown in Listing 3 as an OnMouseDown event handler. This would allow you to left-click on items in the list as usual, without starting drag operations, and would also allow you to start a drag operation by left-clicking on an item (not on a blank area) and dragging the mouse.

Listing 3: Manually starting a listbox drag operation

procedure TForm1.ListBox1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  { Check this is a listbox left mouse button event }
  if (Sender is TCustomListBox) and (Button = mbLeft) then
    with TCustomListBox(Sender) do
      { Verify mouse is over a listbox item }
      if ItemAtPos(Point(X, Y), True) <> -1 then
        { Start a non-immediate drag operation }
        BeginDrag(False)
end;

When False is passed to BeginDrag, the user must move the mouse 5 pixels to start a drag. In Delphi 1, 2 and 3 this is a fixed value, but Delphi 4 (and later) and Kylix allow you to specify alternative pixel distances. You can change the default non-immediate mouse drag distance threshold by assigning a new value to Mouse.DragThreshold (Mouse is a global object instance, created in the Controls/QControls unit). Alternatively, you can pass an optional second parameter to BeginDrag. This parameter defaults to –1, meaning that the Mouse.DragThreshold value will be used.

A value of dmAutomatic assigned to the DragMode property of a Delphi 1 to 3 control causes the control to call BeginDrag(True) when the left mouse button is clicked on it (an immediate drag operation starts). Delphi 4 and later changes this. The control now calls the protected polymorphic BeginAutoDrag method (declared as dynamic), which calls:

BeginDrag(Mouse.DragImmediate, Mouse.DragThreshold)

You can change the values of Mouse.DragImmediate and Mouse.DragThreshold to affect global drag operations (despite the online help in Delphi 5 suggesting these properties are read-only). Custom components can also override the BeginAutoDrag method to change what happens if the user drags them when DragMode is set to dmAutomatic. In fact TCustomForm overrides it and does nothing in the re-implementation, to ensure that the user cannot drag a form if its DragMode property is set to dmAutomatic.

Will You Accept This Object?

After allowing some drag operations to start, using one of the two ways described above, we now need to get some other target control (or controls) to indicate that they will accept something dragged from the source. This is done by writing an OnDragOver event handler for the target control(s) (an empty one is shown in Listing 4). The event handler has a number of parameters that give information on the drag operation.

Listing 4: An OnDragOver event handler

procedure TForm1.ImageDragOver(Sender, Source: TObject;
  X, Y: Integer; State: TDragState; var Accept: Boolean);
begin

end;

The most important parameter is Accept, which is a var parameter. You set this to False if you do not accept a drag from the suggested source. It defaults to True, which means that by default, an OnDragOver event handler will accept any source. However, if you do not make an OnDragOver event handler, the control will not accept any drag operations.

You can see a simple OnDragOver event handler in Listing 5. This is an OnDragOver event handler for a memo component, that will accept something dragged only from an edit control.

Listing 5: A simple OnDragOver event handler

procedure TForm1.Memo1DragOver(Sender, Source: TObject;
  X, Y: Integer; State: TDragState; var Accept: Boolean);
begin
  if Source is TEdit then
    Accept := True
  else
    Accept := False
  { The above can be written more succinctly as: }
  {  Accept := Source is TEdit }
end;

The effect of the Accept parameter being set to True is that the usual No Entry drag cursor will change to a more suitable cursor when the mouse is over a target control that accepts it. In a VCL application, this cursor is usually specified by the dragged control’s DragCursor property, which defaults to crDrag (drag cursors in CLX applications do not seem to be customisable). The user will be allowed to drop the dragged control on the target, although at this point nothing will happen when they do so.

Sender represents the control whose event is firing, and this is happening because the control indicated by the Source parameter is currently being dragged over it at the position indicated by the X and Y parameters, relative to Sender.

The State parameter tells how the mouse is moving relative to the control under the mouse. As a dragging operation proceeds, when the mouse enters a control, its OnDragOver event is triggered with State set to dsDragEnter. It is also repeatedly triggered as the mouse moves over the control (State is dsDragMove) and potentially triggered one last time when the mouse moves out of a control, or the drag operation is terminated whilst the mouse is over the control (State is dsDragLeave).

You can use the State parameter to start certain operations, allocate various resources or whatever, as the user starts dragging across a given control. You can then stop the operation, or free the resources when the user drags the mouse out of the control, or the drag operation is terminated whilst over that control.

Listing 6 shows a simple (if entirely academic) application of this. Assuming the target component (a memo) is happy to accept the source (an edit), information about the drag operation is displayed in a label whilst the mouse is moved over the target. At any point when the drag operation is not active, or when the target control is not the memo, the label is invisible.

Listing 6: An OnDragOver event handler that uses the State parameter

procedure TForm1.Memo1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  Accept := Source is TEdit;
  if Accept then
    case State of
      dsDragEnter: Label1.Show;
      dsDragMove: Label1.Caption :=
        Format('Dragging %s to %s at (%d,%d)',
          [(Source as TControl).Name,
           (Sender as TControl).Name, X, Y]);
      dsDragLeave: Label1.Hide
    end;
end;

OnDragOver is called, if present, from the DragOver dynamic protected method. Custom component classes can override this method to provide additional functionality when a drag operation moves over them, if necessary.

Dropping Off...

When the user eventually performs the "drop" part of the drag and drop operation, by releasing the mouse over a target control that claims to accept it, the target control’s OnDragDrop event handler is invoked. Listing 7 shows an empty OnDragDrop event handler.

Listing 7: An OnDragDrop event handler

procedure TForm1.ImageDragDrop(Sender, Source: TObject; X, Y: Integer);
begin

end;

The parameters are a subset of the OnDragOver event handler’s parameters. The user dragged the Source control and dropped it on Sender. X and Y are the co-ordinates relative to the control that was dropped on (Sender).

The code in an OnDragDrop event handler can do whatever is necessary to implement the drop. If the control in question can accept drops from multiple sources, acting differently for each one, then some more checking of the Source parameter will be required.

The OnDragDrop event handler is called from the public DragDrop dynamic method. Component classes can potentially perform custom drop functionality in an overridden version of this method. Being public, you could also get the same behaviour as a drag and drop operation by directly calling a target control’s DragDrop method, passing the source object and X and Y co-ordinates. For example, this statement gets the same end result as the user initiating a drag operation from an edit control and dropping it on a memo.

Memo1.DragDrop(Edit1, 0, 0);

Testing The Theory

Having got through the three basic steps for drag and drop, let’s build a simple application that employs drag ‘n’ drop. It will be built first of all with no drag and drop support, and then we will retro-fit drag and drop support into the application.

The program allows the user to choose a directory containing some bitmap files. When any directory is chosen, its bitmap files are shown in a listbox. If the use double-clicks on a bitmap file, the file will be loaded into an image component also on the form.

Add the following key components to the form of a new application, changing their names to what is enclosed in brackets. A TEdit (edtPath), a TImage (imgLoadedImg), a TButton (btnGetPath) and a TListBox (lstImages). Their properties, including size and position, are shown in Listing 8.

Listing 8: Property values for the first drag and drop application

object imgLoadedImg: TImage
  Left = 248
  Top = 8
  Width = 156
  Height = 243
end
object edtPath: TEdit
  Left = 8
  Top = 24
  Width = 201
  Height = 21
end
object btnGetPath: TButton
  Left = 216
  Top = 24
  Width = 21
  Height = 21
  Caption = '...'
end
object lstImages: TListBox
  Left = 8
  Top = 72
  Width = 225
  Height = 179
  Sorted = True
end

Now to make the program work, make an OnClick event handler for the button, an OnChange handler for the edit control and an OnDblClick event handler for the listbox, as per the code in Listing 9.

Listing 9: loading a file into an image component

function FixPath(const Path: String): String;
const
{$ifdef MSWINDOWS}
  Slash = '\';
{$endif}
{$ifdef LINUX}
  Slash = '/';
{$endif}
begin
  Result := Path;
  if Result[Length(Result)] <> Slash then
    Result := Result + Slash;
end;

procedure TForm1.btnGetPathClick(Sender: TObject);
var
  Path: String;
const
{$ifdef MSWINDOWS}
  Root = '';
{$endif}
{$ifdef LINUX}
  Root = '/';
{$endif}
begin
  //Use this call in VCL apps earlier than Delphi 3
  if SelectDirectory(Path, [], 0) then
  //Use this call in VCL apps in Delphi 3 or later, or in CLX apps
  if SelectDirectory('Locate image directory', Root, Path) then
    edtPath.Text := FixPath(Path)
 end;

procedure TForm1.edtPathChange(Sender: TObject);
var
  Path: String;
  RetVal: Integer;
  SR: TSearchRec;
begin
  Path := FixPath((Sender as TEdit).Text) + '*.bmp';
  RetVal := FindFirst(Path, faArchive, SR);
  if RetVal = 0 then
    try
      lstImages.Clear;
      while RetVal = 0 do
      begin
        lstImages.Items.Add(SR.Name);
        RetVal := FindNext(SR)
      end
    finally
      FindClose(SR)
    end;
end;

procedure TForm1.lstImagesDblClick(Sender: TObject);
begin
  imgLoadedImg.Picture.LoadFromFile(
    FixPath(edtPath.Text) + lstImages.Items[lstImages.ItemIndex])
end;

The button uses SelectDirectory to allow a directory to be chosen. SelectDirectory is in the FileCtrl VCL unit, and the QDialogs CLX unit. As the listing shows, there are two forms of SelectDirectory. The first version uses a pure VCL dialog to navigate your directories and has been present in all 32-bit versions of Delphi. The latter version was introduced in Delphi 4 and is the only available version in CLX.

Whenever the edit control is changed, it checks to see if the current text represents a directory containing bitmap files. If it is, it clears the listbox and refills it will all the bitmap file names.

The listbox simply responds to having any of its items double-clicked and loads the selected bitmap file into the image component. All these event handlers make use of a simple utility routine that ensures the directory name in the edit control definitely has a directory separator character at the end. In Delphi 5, the IncludeTrailingBackslash routine does this job, whereas Delphi 6 prefers you to use IncludeTrailingPathDelimiter.

This gives us an application that has no support for drag and drop, but which does allow bitmap files to be loaded into an image component. So now we can add the important drag and drop support with the three previously outlined steps.

The first step is to enable the drag from the source (the listbox). This can be done simply by setting the DragMode property to dmAutomatic, which means any left-click anywhere on the listbox will start a drag operation. Alternatively, you can make an OnMouseDown event handler with code like that shown in Listing 3. Since a TFileListBox is indirectly inherited from TCustomListBox, the same code will work fine.

The second step is to tell the image component to accept anything dragged from the listbox. This involves making an OnDragOver event handler for the image component with the following logical assignment within it:

Accept := Source = lstImages

Finally, when the user drops on the image component we need to load the file as selected in the file listbox into the image. This requires an OnDragDrop event handler for the image component. The statement in the file listbox’s OnDblClick event handler (Listing 9) could be duplicated in the new event handler, but code duplication is usually a bad thing, and is to be avoided. Besides, in a real application, the code that would need duplicating might be considerably larger.

Instead, we will invoke the file listbox’s OnDblClick event handler from within the image’s OnDragDrop event handler. You can do this directly, as in:

lstImagesDblClick(lstImages)

or you can do it indirectly, by referring to the event property of the component in question, as shown in Listing 10, although Delphi 1 does not support this syntax.

Listing 10: Invoking the listbox's OnDblClick event handler

procedure TForm1.imgLoadedImgDragDrop(Sender, Source: TObject; X,
  Y: Integer);
begin
  { If lstImages has an OnDblClick event handler... }
  if Assigned(lstImages.OnDblClick) then
    { ... invoke it }
    lstImages.OnDblClick(lstImages)
end;

In both cases, the code ensures that the listbox is passed as the Sender parameter to the event handler, just in case the event handler makes use of that parameter. In an event handler, Sender should always refer to the object whose event is being handled.

The application is now complete, and you can find two versions of it in the files that accompany this paper, both called ImgView.dpr. The VCL version is in the VCL subdirectory and the CLX version is in the CLX subdirectory. You should find that you can drag a bitmap file name from the listbox onto the image component, which will then load the bitmap and display it. You can see a file being dragged onto the image component in Figure 1.

Figure 1: An application that supports drag and drop

Customising Drag Operations

The VCL/CLX libraries have a number of routines up their metaphorical sleeves that can be used to analyse and customise drag and drop operations.

Custom Drag Cursors in VCL Programs

As has been mentioned, when a dragged control is over a target control that will accept it, the mouse cursor changes. In a VCL application it changes to the dragged control’s DragCursor property. This property defaults to crDrag, but you can change it to other values to modify the drag cursor appearance. You can either choose one of the pre-defined system cursors, or use a custom mouse cursor.

CLX does not support custom drag cursors, as Qt handles this side of things. The Cursor.Dpr VCL project uses a custom drag cursor, as shown in Figure 2.

Figure 2: Dragging from an edit to a memo with a custom drag cursor

To load a custom mouse cursor in a VCL application, make a Windows resource file containing the cursor (using Resource Workshop, or the Image Editor that comes with Delphi, or some other tool if you prefer). A sample cursor resource file accompanies this paper (PacCur16.res for Delphi 1 and PacCur32.res for all 32-bit Windows versions) containing a cursor named PacMan. The code in the program required to load this custom cursor into a control’s DragCursor property is shown in Listing 11.

Listing 11: Loading a custom drag cursor from a Windows resource file

const
  crPacMan = 1; { Use values bigger than 0 }
...
{$ifdef Win32}
  {$R PacCur32.res}
{$else}
  {$R PacCur16.res}
{$endif}

procedure TForm1.FormCreate(Sender: TObject);
begin
  Screen.Cursors[crPacMan] :=
    LoadCursor(HInstance, 'PacMan');
  Edit1.DragCursor := crPacMan
end;

Utility Routines

All VCL and CLX controls have a public Dragging method. This parameterless function returns True if the control is being dragged (which means that a drag operation was initiated through that control and has not yet terminated). This allows any piece of code (not just the code in OnDragOver and OnDragDrop event handlers) to verify whether a certain control is currently in the process of being dragged.

To complement the BeginDrag method, controls also have an EndDrag method that allows you to programmatically terminate a drag operation. EndDrag takes a Boolean parameter called Drop. If Drop is True and the mouse is over a control that will accept the drag, then the dragged control is dropped. Under all other circumstances, the drag operation is cancelled.

More generically, the VCL Controls unit (in Delphi 2 and later) and CLX QControls unit implement a CancelDrag procedure which cancels the current drag operation (if there is one) without dropping the dragged object.

A drag operation can therefore be terminated in a number of ways. The user can positively terminate a drag by dropping the control on a target that accepts it. They can also cancel the drag by dropping the control on something that does not accept it, or by pressing the Escape key. The programmer can terminate the drag positively or negatively using the dragged control’s EndDrag method, or cancel it with CancelDrag.

If a custom component needs to do anything particularly special when a drag operation is cancelled, it can override the protected dynamic method DragCanceled. By default, this does nothing. However the VCL version of the TCustomListBox class overrides it to fix certain mouse usability issues that arise when a drag operation is cancelled.

The Controls/QControls unit offers another global routine called FindDragTarget. This takes a TPoint record that describes a screen location and is designed to return the control that occupies that position. Whilst its name suggests it will return a target control that is ripe for accepting things, it does no such checking. It will return the control at the specified screen position, and that control may or may not have suitable OnDragOver and OnDragDrop event handlers. The only extra checking performed by this routine is dictated by the additional Boolean parameter (AllowDisabled) that controls whether disabled controls will be considered for returning. If no control can be found at the specified position, FindDragTarget returns nil.

Whilst the OnDragOver event handler’s State parameter can enable you to start operations when the user drags one control into another one, and then stop those operations when the control is dragged back out, there are two events that allow you to do more widespread operations.

A control’s OnStartDrag event handler (see Listing 12) will be triggered as soon as a drag operation on it starts, either through a call to its BeginDrag method, or by being clicked on when DragMode is set to dmAutomatic. We will look more closely at what we can do with this event, which was introduced in Delphi 2, later in the paper.

A corresponding OnEndDrag event handler is called when the drag operation stops (also shown in Listing 12. This can either be because the control was dropped, or because the operation was terminated in some way. This event (which has been around since Delphi 1) takes four parameters. The ever-present Sender parameter is the control that is no longer being dragged. Target represents the control that Sender was dropped on, but which can be nil in the case of a terminated drag operation. The X and Y co-ordinates, relative to Target are also passed as parameters, though if Target is nil these parameters will both be 0.

Listing 12: OnStartDrag and OnEndDrag event handlers

procedure TForm1.Label1StartDrag(Sender: TObject;
  var DragObject: TDragObject);
begin

end;

procedure TForm1.Label1EndDrag(Sender, Target: TObject;
  X, Y: Integer);
begin

end;

The OnEndDrag event is also triggered after execution of the DragCanceled method. If the drag is terminated successfully with a drop, the source’s OnEndDrag event occurs after the target’s OnDragDrop event.

OnStartDrag and OnEndDrag are called from the protected dynamic methods DoStartDrag & DoEndDrag respectively, which again can be overridden by new component classes to perform additional tasks specific to the component being written.

Drag Control Objects

When a drag and drop application has a drop target control that can accept many different dragged source controls, the implementation of the OnDragOver and OnDragDrop event handlers can end up getting a little complex. Often, whilst there are a variety of dragged source controls that can be accepted, they will ultimately all provide the same sort of information, for example a file name.

To simplify this sort of scenario, you can use custom drag objects (sometimes called drag control objects), which were introduced in a very understated fashion in Delphi 2. The OnStartDrag event handler of all draggable controls can each create an instance of a class inherited from TDragObject. This object is used to represent the information that is being transferred from the dragged control to the drop target.

Figure 3 showing the original drag object hierarchy from Delphi 2 and 3.

Figure 3: The drag control object hierarchy in Delphi 2 and 3

The updated hierarchy for Delphi 4, Delphi 5 and Kylix 1 is shown in Figure 4.

Figure 4: The drag control object hierarchy in Delphi 4 and 5, and Kylix 1

The most recent update for Delphi 6 can be seen in Figure 5.

Figure 5: The drag control object hierarchy in Delphi 6

Whilst TDragObject is the key base class, you will typically be interested in inheriting from the more able TDragControlObject class. We will look into some of the capabilities of these two classes throughout the rest of this article.

To set the custom drag object up, you assign the created instance to the DragObject var parameter of the source control’s OnStartDrag event handler, which defaults to nil. Having done this, when the source control is dragged over and dropped on a target control, one of the parameters of the target control’s OnDragOver and OnDragDrop event handlers changes.

Specifically, the Source parameter now refers to the custom drag object, instead of the dragged control itself. Since the custom drag object is given to the target control in these event handlers, it can be interrogated for useful information, which could be any additional data fields or properties defined in the class.

If you are writing code that might be compiled in various versions of 32-bit Delphi you must be careful about which class you inherit your custom drag object class from. In Delphi 2 and 3, the Source parameter would only represent your drag object if you did not inherit from TDragControlObject. Instead you must inherit directly from TDragObject. Delphi 4 (and later) remedies this problem. You can inherit from any point in the hierarchy and Source will correctly represent your custom drag object.

In a similar way, the initial release of the CLX libraries in Kylix 1.0 contains a similar restriction, whereby the custom drag object must inherit from TDragObject in order to be passed as the Source parameter. Hopefully this CLX issue will also be fixed soon to allow inheriting from the more useful drag object classes.

The online help in Delphi 3, 4 and 5 and also in Kylix 1 claims that you do not need to free the drag object created in an OnStartDrag event handler (Delphi 2 neglected to describe it in the help). However, this information is completely incorrect. The only drag objects that are automatically freed are those created by the program on your behalf when you do not create your own drag objects.

In Delphi 6, things are a little better. As you can see in Figure 5 there is a new drag control class called TDragControlObjectEx. If you create an instance of this class, it will indeed be freed automatically. However instances of any classes that TDragControlObjectEx inherits from must be explicitly freed in code.

When using custom drag objects, you should verify in the OnDragOver event handler (and maybe also in the OnDragDrop event handler) that the Source parameter is actually a drag object before performing any typecasts. The normal Delphi way of doing this would involve using an expression like:

Source is TDragObject

However in VCL applications, for reasons that will become clearer a little later, you should use this expression instead:

IsDragObject(Source)

In a normal application, the effect of these two expressions will be identical, however IsDragObject caters for other scenarios that the functionality of the is operator does not.

The sample project DragObjects.Dpr (available in the accompanying files in both the VCL and the CLX directories) tries to show the general idea. It has a number of controls on a form that can be dragged onto a panel, such as a listbox, a button, a combobox and a label. The plan is that the various controls all provide one piece of textual information, but the text is meant to come from different properties (the active listbox item, the button’s caption, and so on). To try and help out, each control’s OnStartDrag creates an instance of a drag object.

Since CLX has a problem with certain drag object classes, the CLX version uses TDragObject as the base class to inherit from. Since there is a similar problem in Delphi 2 and 3, a second VCL version of the project is supplied, DragObjects2.dpr, which also inherits from TDragObject. The main VCL project uses a drag object class inherited from TDragControlObject, which is the most useful base class (and which takes the dragged control as its constructor parameter).

The new class in DragObjects.Dpr is called TTextDragObject and has just one extra string data field called Data which is given the piece of text as appropriate from each control.

The target panel’s OnDragOver and OnDragDrop event handlers are therefore considerably simpler in implementation, as they just treat the Source parameter as a TTextDragObject and read the Data field. All controls that have an OnStartDrag event handler share an OnEndDrag event handler that frees the drag object. A number of these event handlers from the VCL project are shown in Listing 13.

Listing 13: Using drag control objects

type
  TTextDragObject = class(TDragControlObject)
  public
    Data: String;
  end;

  TForm1 = class(TForm)
    ...
  private
    FDragObject: TTextDragObject;
  end;
...
procedure TForm1.Label1StartDrag(Sender: TObject;
  var DragObject: TDragObject);
begin
  FDragObject := TTextDragObject.Create(Sender as TLabel);
  FDragObject.Data := TLabel(Sender).Caption;
  DragObject := FDragObject;
end;

procedure TForm1.ListBox1StartDrag(Sender: TObject;
  var DragObject: TDragObject);
begin
  FDragObject := TTextDragObject.Create(Sender as TListBox);
  with TListBox(Sender) do
    FDragObject.Data := Items[ItemIndex];
  DragObject := FDragObject;
end;

procedure TForm1.SharedEndDrag(Sender, Target: TObject; X, Y: Integer);
begin
  //All draggable controls share this event handler
  FDragObject.Free;
  FDragObject := nil
end;

procedure TForm1.Panel1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  //It is tempting to write this...
  //Accept := Source is TTextDragObject
  //...however we are advised to write this instead in VCL apps
  Accept := IsDragObject(Source)
end;

procedure TForm1.Panel1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  //The OnDragOver event handler verified we are dealing with a
  //drag object so there is no chance of getting a normal control
  (Sender as TPanel).Caption := TTextDragObject(Source).Data
end;

You should be able to see that using custom drag objects allows a drag and drop application to be readily extensible. You can add more controls to a form which can be dragged from and, so long as they all create the same custom drag object and fill in the required data fields, the drop target need not be changed at all. It will continue to work regardless of the drag source, because it gets the information from the custom drag object, not the drag source itself.

Although this business of creating drag objects is being introduced as if it is something over and above the normal VCL/CLX drag and drop support, in truth it is not. The VCL/CLX source creates a drag object for each drag operation anyway. If you leave the DragObject parameter in the OnStartDrag event handler with its default nil value, or have no OnStartDrag event handler at all, the VCL/CLX creates a TDragControlObject instance to represent the drag operation (Delphi 6 changes this to be a TDragControlObjectEx instance). This behind-the-scenes drag object will be automatically freed by the component library, unlike those you create yourself.

Customising The Drag Cursor Further

Drag objects can also be used as more flexible ways of specifying the drag cursor to be used when source controls are accepted or rejected, potentially using an image list component.

In the VCL, TDragObject has a protected virtual method GetDragCursor that takes a Boolean var parameter called Accept, along with the mouse co-ordinates. It is supposed to return a TCursor value depending on whether the target control accepts the dragged control or not. It is hard-coded to return either crNoDrop or crDrag, but can be overridden to return any cursor needed.

More interestingly, you can enhance the drag operation further by supplying an image list that will be merged with the drag cursor during the drag operation, giving a drag image. You have probably seen the sort of effect being described when dragging files in Windows Explorer. In this case, the drag cursor is enhanced by a feint representation of the item being dragged around (see Figure 6, where README.TXT is being dragged into a directory).

Figure 6: A customised drag cursor showing the file being dragged

Drag objects provide the means to accomplish this using image lists to hold these extra images, however the implementation differs greatly between the VCL and CLX. The VCL drag object offers a virtual GetDragImages method which is supposed to return an image list containing the custom drag image. The CLX uses a single, global image list as returned by the DragImageList function in the QControls unit. The drag object uses the virtual GetDragImageIndex method to decide which image from the list should be used.

TDragControlObject is the useful class that inherits from TDragObject. It keeps a reference to the control being dragged in the public Control property (although the original Delphi 2 implementation had the protected and public sections of the class the wrong way round, and so the Control property was actually protected).

In the VCL, TDragControlObject overrides GetDragCursor and uses the stored reference to the control to return either crNoDrop or the control’s DragCursor property (as opposed to being fixed to crDrag). Again, CLX offers no opportunity to customise the drag cursor itself.

The VCL TDragControlObject class also overrides the GetDragImages method and calls the control’s GetDragImages method, rather than returning nil. Admittedly, the only controls that do anything in their GetDragImages methods are TCustomTreeView and TCustomListView (and their descendants), but the scope is there for controls to supply their own drag image list to enhance the drag cursor (see later). However, right now we are not interested in how components can supply custom image lists, but instead how the drag control object can do this.

In a similar way, the CLX TDragControlObject class overrides GetDragImageIndex to call the control’s same-named method, although none of the default controls do anything other than return -1 (meaning no drag cursor enhancing image is available).

VCL Custom Drag Images

A variation on the DragObjects.Dpr project is given in DragImage.Dpr. This project has a label and a listbox that can both be dragged onto a panel. The label gives its caption to the panel and the listbox gives its active item, and so a custom drag object is used to hold this information string. However, as well as adding the textual data field, this custom drag object class is designed to enhance the drag image.

The VCL version overrides both GetDragImages and GetDragCursor. GetDragCursor is overridden to provide a custom drag cursor for all controls that make one of these drag objects. It uses the same PacMan cursor as used in the earlier project. Notice that a custom drag object can be used to allow many drag source controls to have the same drag cursor, without having to set each control’s DragCursor property.

GetDragImages is overridden to create an instance of a TDragImageList. This class, which was introduced in Delphi 3, inherits from TCustomImageList and is an ancestor of the TImageList component class. It provides enough functionality to cater for the requirements of drag cursor building. You can add any number of images to the drag image list, and then tell the image list which one it should use to enhance the drag operation.

It will default to using the first image (at position 0), but you should make the point of telling the component, to be sure. To tell the image list which image to use, it has the SetDragImage method, which takes three parameters. The first is the index of the drag image. When adding the drag image into the image list, you typically call Add or AddMasked, both of which return the index of the newly inserted image.

The other two parameters are the co-ordinates of the hot spot, relative to the top left of the drag image. These both default to 0 if SetDragImage is not called. This means that no matter where the mouse is on the control when you start dragging, the top left of the drag image will be at the mouse cursor position, and so will be drawn from the mouse position to the right.

In many cases it will be more desirable to pass in co-ordinates that indicate the relative position of the mouse pointer in relation to the item being dragged, particularly if the drag image is a representation of the item being dragged. Think about dragging a file or folder in Windows Explorer. If you start the drag operation with the mouse at the right side of the item, the drag image will still overlay the item being dragged. In other words, the drag image hot spot gets specified based upon where the mouse is relative to the dragged item. This should be taken into account at some point, but let’s try it using 0 values first (we’ll come back to this issue later).

Listing 14 shows the VCL custom drag object class with the two additional methods. The drag image list is stored in a private data field and the destructor ensures that it gets destroyed. You can see GetDragCursor returning the PacMan cursor when needed.

Listing 14: Setting up a drag image list

type
  TTextDragObject = class(TDragControlObject)
  private
    FDragImages: TDragImageList;
    FData: String;
  protected
    function GetDragCursor(Accepted: Boolean; X, Y: Integer): TCursor; override;
    function GetDragImages: TDragImageList; override;
  public
    constructor Create(Control: TControl; Data: String); reintroduce;
    destructor Destroy; override;
    property Data: String read FData;
  end;
...
constructor TTextDragObject.Create(Control: TControl; Data: String);
begin
  inherited Create(Control);
  FData := Data;
end;

destructor TTextDragObject.Destroy;
begin
  FDragImages.Free;
  inherited;
end;

function TTextDragObject.GetDragCursor(Accepted: Boolean;
  X, Y: Integer): TCursor;
begin
  if Accepted then
    Result := crPacMan
  else
    Result := inherited GetDragCursor(Accepted, X, Y)
end;

function TTextDragObject.GetDragImages: TDragImageList;
var
  Bmp: TBitmap;
  Txt: String;
  BmpIdx: Integer;
begin
  if not Assigned(FDragImages) then
    FDragImages := TDragImageList.Create(nil);
  Result := FDragImages;
  Result.Clear;
  Bmp := TBitmap.Create;
  try
    //Make up some string to write on bitmap
    Txt := Format('      The control called %s says "%s" at %s',
      [Control.Name, Data, FormatDateTime('h:nn am/pm', Time)]);
    Bmp.Canvas.Font.Name := 'Arial';
    Bmp.Canvas.Font.Style := Bmp.Canvas.Font.Style + [fsItalic];
    Bmp.Height := Bmp.Canvas.TextHeight(Txt);
    Bmp.Width := Bmp.Canvas.TextWidth(Txt);
    //Fill background with olive
    Bmp.Canvas.Brush.Color := clOlive;
    Bmp.Canvas.FloodFill(0, 0, clWhite, fsSurface);
    //Write a string on bitmap
    Bmp.Canvas.TextOut(0, 0, Txt);
    Result.Width := Bmp.Width;
    Result.Height := Bmp.Height;
    //Make olive pixels transparent, whilst adding bmp to list
    BmpIdx := Result.AddMasked(Bmp, clOlive);
    Result.SetDragImage(BmpIdx, 0, 0);
  finally
    Bmp.Free;
  end
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  Screen.Cursors[crPacMan] := LoadCursor(HInstance, 'PacMan');

  ControlStyle := ControlStyle + [csDisplayDragImage];
  for I := 0 to ControlCount - 1 do
    with Controls[I] do
      ControlStyle := ControlStyle + [csDisplayDragImage];
end;

The GetDragImages method is a little more involved. If no drag image list has been created yet, one gets created by the method. Then a bitmap is set up, large enough to hold the image that is chosen to represent the dragged item. In this case the code simply makes a string describing the drag source, the dragged information and the time that the drag started. This information is written onto the bitmap, the bitmap is added into the image list and the image list is told which bitmap to use for the enhanced drag image.

In order to get transparent areas in the image, the bitmap was initially flood-filled with olive. The AddMasked image list method was used to add the bitmap to the image list whilst specifying that all olive pixels are to become transparent.

Unfortunately, whilst on first glances (and after checking with the help) this would seem enough to do the job, it is not. The ControlStyle property help is very misleading with respect to the csDisplayDragImage setting. It suggests that including this flag in a control’s ControlStyle property will make the enhanced drag image for that control be used whenever and wherever the control is dragged. Unfortunately, the enhanced drag image will only be used when the mouse is over any control that has this setting, or when the mouse is not over any form in the project.

So the image list will only be used when the mouse is either not over a possible target (which means when the mouse is not over any form in the application) or when it is over a control that has the csDisplayDragImage value in its ControlStyle set property. Only tree views and list views include this member in their ControlStyle property so the drag image list will only be used when the mouse is over a tree view or list view, or entirely off the form. This has been logged as a bug in the VCL, as opposed to a bug in the online help.

To fix this problem in the application, the form’s OnCreate event handler iterates through all its controls, adding csDisplayDragImage into the ControlStyle property. Once this is done, we get what we were after. When a control is dragged over something that does not accept it, it looks like Figure 7 and when it is over something that does accept it, it looks like Figure 8.

Figure 7: A No Drop drag cursor enhanced by a drag image list

Figure 8: A custom drag cursor enhanced by a drag image list

Note that this application has a simple form, where each control on the form has no child controls. In a more complex form, you will need to recursively loop through each control and its children, setting the ControlStyle property, as is done by the FixControlStyles procedure in Listing 15.

Listing 15: Generic solution to fix the ControlStyle problem

procedure FixControlStyles(Parent: TControl);
var
  I: Integer;
begin
  Parent.ControlStyle := Parent.ControlStyle + [csDisplayDragImage];
  if Parent is TWinControl then
    with TWinControl(Parent) do
      for I := 0 to ControlCount - 1 do
        FixControlStyles(Controls[I]);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FixControlStyles(Self);
  ...
end;

CLX Custom Drag Images

A CLX version of DragImage.dpr is also supplied with this paper. It endeavours to do much the same as the VCL version with the same UI, but has to use different code in places.

The custom drag object inherits from TDragControlObject in order to get custom drag image display support, which poses a problem what with the aforementioned issue with the lineage of custom drag objects. We can create a drag object inherited from TDragControlObject, but it won’t be passed as the Source parameter to any OnDragOver or OnDragDrop event handlers.

As long as we are aware of this issue, we can work around it while it lasts. In all cases so far we have stored the drag object in a data field declared in the form. We will continue to do this, and when access to the drag object is required, we will talk directly to the data field, rather than to the event handlers Source parameter.

In the CLX code, the custom drag object overrides its GetDragImageIndex method to return the drag image position in the global drag image list (the VCL does not have a global image list – each drag object needs to create one of its own). The drag object constructor deals with setting up the drag image and adding it to the global drag image list. It uses much the same general code as in Listing 14, but modified as appropriate, as you can see in Listing 16.

Listing 16: Setting up a custom drag image in CLX

type
  TTextDragObject = class(TDragControlObject)
  private
    FImgIdx: Integer;
    FData: String;
  protected
    function GetDragImageIndex: Integer; override;
  public
    constructor Create(Control: TControl; const Data: String); reintroduce;
    property Data: String read FData;
  end;

constructor TTextDragObject.Create(Control: TControl; const Data: String);
var
  Bmp: TBitmap;
  Txt: String;
begin
  inherited Create(Control);
  FData := Data;
  Bmp := TBitmap.Create;
  try
    //Make up some string to write on bitmap
    Txt := Format('The control called %s says "%s" at %s',
      [Control.Name, Data, FormatDateTime('h:nn am/pm', Time)]);
    Bmp.Canvas.Font := Form1.Font;
    Bmp.Canvas.Font.Name := 'Arial';
    Bmp.Canvas.Font.Size := 10;
    Bmp.Canvas.Font.Style := Bmp.Canvas.Font.Style + [fsItalic];
    //Give bitmap a non-szero size so we can call TextHeight/TextWidth
    //Qt does not permit this on a zero-sized canvas
    Bmp.Height := 1;
    Bmp.Width := 1;
    Bmp.Height := Bmp.Canvas.TextHeight(Txt);
    Bmp.Width := Bmp.Canvas.TextWidth(Txt);
    //Fill background with white, which will be the transparent colour
    Bmp.Canvas.Brush.Color := clWhite;
    Bmp.Canvas.FillRect(Rect(0, 0, Bmp.Width, Bmp.Height));
    //Write a string on bitmap
    Bmp.Canvas.TextOut(0, 0, Txt);
    //Add bitmap to image list, making the white pixels transparent
    DragImageList.Width := Bmp.Width;
    DragImageList.Height := Bmp.Height;
    FImgIdx := DragImageList.AddMasked(Bmp, clWhite);
  finally
    Bmp.Free
  end
end;

function TTextDragObject.GetDragImageIndex: Integer;
begin
  Result := FImgIdx
end;

Before calling TextHeight or TextWidth, the bitmap must be given a physical size to avoid the underlying Qt object objecting. Also, the bitmap is added straight to the global image list, DragImageList.

Another apparent issue in the initial CLX source is that the image list's AddMasked method seems to be temperamental. The problem is that transparency appears only to be honoured when the mask colour is white. If you try it with other colours, no transparent areas are generated.

The CLX application running in Delphi 6 looks much the same as the one in Figure 8, but without the custom cursor image.

Having overcome this problem, the next issue to concern yourself with is that custom drag images do not show up on Windows 98 (and probably also Windows 95 and Windows Me). I am currently working under the assumption this is a limitation of the Qt library.

It should probably be mentioned that the initial release of the CLX library does define the csDisplayDragImage symbol, but does not refer to it anywhere.

VCL Drag Image Hot Spots

It was mentioned earlier that sometimes it is important to tell the drag image list where the drag image hot spot is. This is usually the case when the drag image is a representation of the control or item being dragged. Specifying a hot spot (using the relative position of the mouse cursor to the dragged item) allows the drag image to start being drawn overlaid on the dragged item no matter where the mouse is at the start of the drag operation.

A sample VCL project called DragHotSpot.dpr shows the idea. This project has a button on it that can be dragged around the form if the Alt key is held down (an OnMouseDown event handler does this). The button’s OnStartDrag event handler creates a custom drag object of type TControlDragObject which is inherited from TDragControlObject and has a custom constructor, CreateWithHotSpot. The class contains code to take a copy of the button’s image and draw it onto a bitmap. The button’s OnEndDrag event handler frees the drag object it. Listing 17 shows the code described so far.

Listing 17: Making a VCL custom drag image with a hot spot

type
  TControlDragObject = class(TDragControlObject)
  private
    FDragImages: TDragImageList;
    FX, FY: Integer;
  protected
    function GetDragImages: TDragImageList; override;
  public
    constructor CreateWithHotSpot(Control: TWinControl; X, Y: Integer);
    destructor Destroy; override;
  end;
...
constructor TControlDragObject.CreateWithHotSpot(Control: TWinControl; X, Y: Integer);
begin
  inherited Create(Control);
  FX := X;
  FY := Y;
end;

destructor TControlDragObject.Destroy;
begin
  FDragImages.Free;
  inherited;
end;

function TControlDragObject.GetDragImages: TDragImageList;
var
  Bmp: TBitmap;
  Idx: Integer;
begin
  if not Assigned(FDragImages) then
    FDragImages := TDragImageList.Create(nil);
  Result := FDragImages;
  Result.Clear;
  //Make bitmap that is same size as control
  Bmp := TBitmap.Create;
  try
    Bmp.Width := Control.Width;
    Bmp.Height := Control.Height;
    Bmp.Canvas.Lock;
    try
      //Draw control in bitmap
      (Control as TWinControl).PaintTo(Bmp.Canvas.Handle, 0, 0);
    finally
      Bmp.Canvas.UnLock
    end;
    FDragImages.Width := Control.Width;
    FDragImages.Height := Control.Height;
    //Add bitmap to image list, making the grey pixels transparent
    Idx := FDragImages.AddMasked(Bmp, clBtnFace);
    //Set the drag image and hot spot
    FDragImages.SetDragImage(Idx, FX, FY);
  finally
    Bmp.Free
  end
end;

procedure TForm1.Button1StartDrag(Sender: TObject;
  var DragObject: TDragObject);
var
  Pt: TPoint;
begin
  //Get cursor pos
  GetCursorPos(Pt);
  //Make cursor pos relative to button
  Pt := Button1.ScreenToClient(Pt);
  //Pass info to drag object
  FDragObject := TControlDragObject.CreateWithHotSpot(Button1, Pt.X, Pt.Y);
  //Modify the var parameter
  DragObject := FDragObject
end;

procedure TForm1.Button1EndDrag(Sender, Target: TObject; X, Y: Integer);
begin
  FDragObject.Free;
  FDragObject := nil;
end;

The result of all this is that no matter where you mouse is relative to the draggable button, the drag image always starts in exactly the same screen location as the button, which is what was required. Figure 9 shows the program when the button has been dragged by a click near its bottom right hand corner. Notice the mouse is pointing at the bottom right hand corner of the drag image.

Figure 9: A VCL drag image with a hot spot

CLX Drag Image Hot Spots

The project described above is difficult to translate directly into CLX since CLX offers no PaintTo method in its controls. However, a modified version of this particular project can be manufactured which manually draws an image of a button on a bitmap canvas in order to get the hotspot code running. The custom drag object code is shown in Listing 18 where you can see an overridden GetDragImageHotSpot method as well as a custom constructor that takes the hot spot co-ordinates.

Listing 18: Making a CLX custom drag image with a hot spot

type
  TControlDragObject = class(TDragControlObject)
  private
    FX, FY: Integer;
    FImgIdx: Integer;
  protected
    function GetDragImageHotSpot: TPoint; override;
    function GetDragImageIndex: Integer; override;
  public
    constructor CreateWithHotSpot(Control: TWinControl; X, Y: Integer);
  end;
...
constructor TControlDragObject.CreateWithHotSpot(Control: TWinControl; X,
  Y: Integer);
var
  Bmp: TBitmap;
  TextSize: TSize;
  Text: String;
begin
  inherited Create(Control);
  FX := X;
  FY := Y;
  //Make image and add it to drag image list
  //Make bitmap that is same size as control
  Bmp := TBitmap.Create;
  try
    Bmp.Canvas.Font := TControlAccess(Control).Font;
    //Qt Canvas must not have non-zero size for TextHeight/TextWidth to work
    Bmp.Height := Control.Height;
    Bmp.Width := Control.Width;
    //Draw button face with white background for transparency
    DrawButtonFace(Bmp.Canvas, Rect(0, 0, Bmp.Width, Bmp.Height), 2, False, True, False, clWhite);
    //Write a string on bitmap
    Text := (Control as TButton).Caption;
    TextSize := Bmp.Canvas.TextExtent(Text);
    Bmp.Canvas.TextOut(
      (Bmp.Width - TextSize.cx) div 2,
      (Bmp.Height - TextSize.cy) div 2, Text);
    //Add bitmap to image list, making the grey pixels transparent
    DragImageList.Width := Bmp.Width;
    DragImageList.Height := Bmp.Height;
    FImgIdx := DragImageList.AddMasked(Bmp, clWhite);
  finally
    Bmp.Free
  end
end;

function TControlDragObject.GetDragImageHotSpot: TPoint;
begin
  Result := Point(FX, FY)
end;

function TControlDragObject.GetDragImageIndex: Integer;
begin
  Result := FImgIdx
end;

procedure TForm1.Button1StartDrag(Sender: TObject;
  var DragObject: TDragObject);
var
  Pt: TPoint;
begin
  //Get cursor pos
  GetCursorPos(Pt);
  //Make cursor pos relative to button
  Pt := Button1.ScreenToClient(Pt);
  //Pass info to drag object
  FDragObject := TControlDragObject.CreateWithHotSpot(Button1, Pt.X, Pt.Y);
  DragObject := FDragObject;
end;

procedure TForm1.Button1EndDrag(Sender, Target: TObject; X, Y: Integer);
begin
  FreeAndNil(FDragObject)
end;

The program can be seen running in Figure 10. Again, to get transparency in the drag image (due to the CLX image list limitation), the button image was drawn with a white background, and white was specified as the transparent pixel colour. Also, due to the Qt limitation, the drag image will not show on Windows 98 (or 95).

Figure 10: A CLX drag image with a hot spot

Custom Components And Drag Image Lists

The point was made earlier that all VCL controls have a GetDragImages virtual method that is automatically called by the drag objects that inherit from TDragControlObject. Only list views and tree views override this method (the Win32 Common Controls API supports setting up drag image lists automatically for these controls). These are also the only components to include csDisplayDragImage in their ControlStyle property.

Similarly, all CLX controls have GetDragImageIndex and GetDragImageHotSpot virtual methods that are automatically called by drag objects that inherit from TDragControlObject. None of the CLX components override these methods in Kylix 1.

You can write your own custom component classes that manage their own drag image by overriding whichever of these methods are appropriate, and writing custom code. Two sample VCL components accompany this paper to show the general idea. TDragButton inherits from TButton, and can be found in the DragButton.pas unit. TDragEdit is inherited from TEdit, and can be found in the DragEdit.pas unit.

Both these control classes do a number of similar things. They both automatically start a drag operation if the user Ctrl+clicks on them. This is done in an overridden version of the MouseDown method, which normally is only responsible for triggering the OnMouseDown event. They also both define a private data field called FDragImages, which is a TDragImageList. They add the csDisplayDragImage setting into their ControlStyle property inside the constructor. The other thing they do is to override the GetDragImages method to add some image into their drag image list, specifying a hotspot based upon where the mouse is relative to themselves.

In the case of the button component, it adds a bitmap that is a transparent representation of itself to the image list (see Figure 11). This is achieved by calling the button’s PaintTo method, telling it to paint a copy of itself onto the bitmap’s canvas. AddMasked is used to add the bitmap to the image list, specifying that all pixels that match clBtnFace (the button’s main colour) should be made transparent. Listing 19 shows how this is all achieved. Unfortunately, due to the lack of the canvas’s Lock and Unlock methods in Delphi 2, the PaintTo method is ineffective in that version. Delphi 3 (and later) supports it fine, however.

Listing 19: A button component supplying a custom drag image list

type
  TDragButton = class(TButton)
  private
    FDragImages: TDragImageList;
  protected
    function GetDragImages: TDragImageList; override;
    procedure MouseDown(Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;
...
constructor TDragButton.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ControlStyle := ControlStyle + [csDisplayDragImage]
end;

destructor TDragButton.Destroy;
begin
  FDragImages.Free;
  inherited;
end;

function TDragButton.GetDragImages: TDragImageList;
var
  Bmp: TBitmap;
  BmpIdx: Integer;
  Pt: TPoint;
begin
  if not Assigned(FDragImages) then
    FDragImages := TDragImageList.Create(Self);
  Bmp := TBitmap.Create;
  try
    Bmp.Width := Width;
    Bmp.Height := Height;
    Bmp.Canvas.Lock;
    try
      PaintTo(Bmp.Canvas.Handle, 0, 0);
    finally
      Bmp.Canvas.Unlock
    end;
    FDragImages.Width := Width;
    FDragImages.Height := Height;
    BmpIdx := FDragImages.AddMasked(Bmp, clBtnFace);
    //Where is mouse relative to control?
    GetCursorPos(Pt);
    Pt := ScreenToClient(Pt);
    //Specify drag image and hot spot
    FDragImages.SetDragImage(BmpIdx, Pt.X, Pt.Y);
    Result := FDragImages;
  finally
    Bmp.Free
  end
end;

procedure TDragButton.MouseDown(Button: TMouseButton; Shift: TShiftState;
  X, Y: Integer);
begin
  inherited;
  //Automatically start dragging on a Ctrl-click
  if ssCtrl in Shift then
    BeginDrag(True)
end;

Figure 11: Another enhanced drag cursor, this time managed by the component itself

The edit component tries something different. It loads a bitmap (of Athena) that is compiled in as a Windows resource (see Listing 20). Again, when adding the image to the image list, the common background colour (clSilver) is specified as the transparent colour. Clearly, having a copy of the large Athena bitmap hanging off the drag cursor is not very practical, but it does emphasise the scope of what you can achieve with custom drag images.

Listing 20: An edit component supplying a custom image list

function TDragEdit.GetDragImages: TDragImageList;
var
  Bmp: TBitmap;
  BmpIdx: Integer;
  Pt: TPoint;
begin
  if not Assigned(FDragImages) then
    FDragImages := TDragImageList.Create(Self);
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromResourceName(HInstance, 'Athena');
    FDragImages.Width := Bmp.Width;
    FDragImages.Height := Bmp.Height;
    BmpIdx := FDragImages.AddMasked(Bmp, clSilver);
    //Where is mouse relative to control?
    GetCursorPos(Pt);
    Pt := ScreenToClient(Pt);
    //Specify drag image and hot spot
    FDragImages.SetDragImage(BmpIdx, Pt.X, Pt.Y);
    Result := FDragImages;
  finally
    Bmp.Free
  end
end;

Clearly, similar CLX components can also be written which override the GetDragImageIndex and GetDragImageHotSpot methods and manipulate the global CLX drag image list in a similar way to has been done in the sample projects so far.

VCL Inter-Module Dragging

There is one more benefit of using custom drag objects that we should look into before leaving the subject, although it only applies to VCL applications. It involves dragging between a form created in a DLL and a form created in either a different DLL, or the main EXE. Incidentally, if you are using Delphi packages (special types of DLLs specific to Delphi and C++Builder) this issue does not arise, and so is irrelevant. This only applies to normal DLLs.

A pair of projects are in the files that accompany this paper which represent an executable and a DLL (ExeDrag.Dpr and DllDrag.Dpr respectively). These can be compiled and executed from any 32-bit version of Delphi. There is also a project group (ExeAndDllDragging.Bpg) containing these two projects that can be used in Delphi 4 or later.

The DLL contains a form class and exports a routine that displays it. In order for forms created in DLLs to display correctly, the DLL’s Application object needs to have its Handle property assigned the value of the EXE’s Application.Handle. The exported DLL routine takes a window handle (assumed to be the Application object handle) and assigns it to its own Application.Handle. Without this, each form from the DLL would have an extra icon on the task bar.

The form is destroyed when closed, thanks to the OnClose event handler assigning a value of caFree to its Action parameter (a var parameter), as shown in Listing 21.

Listing 21: A routine exported from a DLL that creates and shows a form

procedure ShowForm(ApplicationHandle: HWnd); stdcall;
begin
  //Set Application object window handle to match that in the EXE,
  //meaning we do not get another task bar button for the form
  Application.Handle := ApplicationHandle;
  TDLLForm.Create(Application).Show
end;

procedure TDLLForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  //The form frees itself when closed
  Action := caFree
end;

The form in the DLL has a memo which can have selected text dragged from it (by dragging with the right mouse button).

Note that there is a problem with TDragObject (which captures the mouse during a drag operation, and handles the resulting mouse messages) in that it does not react to the right mouse button being released in Delphi 2 or 3. So dragging with the right mouse button only works well in Delphi 4 or later (as do a number of other things relating to drag and drop, as we have seen). When you release the right mouse button, you must follow this with a click of the left mouse button, if running with the earlier versions of Delphi.

The form in the EXE has an edit control which is coded to accept anything dragged from a TCustomEdit (or any descendant of that class). Listing 22 shows both of these sections of code.

Listing 22: Drag and drop code from both the DLL and the EXE

//This code is from the DLL form
procedure TDLLForm.Memo1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  //Check for right mouse button, and no other buttons/keys
  if Shift = [ssRight] then
    (Sender as TCustomEdit).BeginDrag(True)
end;

//This code is from the EXE form
procedure TExeForm.Edit1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  Accept := Source is TCustomEdit
end;

procedure TExeForm.Edit1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  (Sender as TCustomEdit).Text := (Source as TCustomEdit).SelText
end;

A TMemo is a descendant of TCustomEdit and so the code might be expected to work. However, because the memo lives in the DLL and the edit control’s event handlers are in the EXE, things don’t go according to plan. The edit appears not to want to accept anything from the memo.

The EXE’s is expression will be asking the DLL’s memo object whether its VMT (virtual method table) matches that of TCustomEdit (or some descendant), but will be referring to the implementation of TCustomEdit in the code compiled into the EXE. Since the memo inherits from the version of TCustomEdit compiled into the DLL, the VMTs of the two versions of TCustomEdit will be at different addresses and so is will return False.

It is probably a good idea that it fails, as TCustomEdit is a class that is quite far down the VCL hierarchy, and there is always the possibility that the DLL and EXE are compiled with different versions of Delphi. Each version of Delphi makes various changes around the VCL. Consequently, the internal layout of data fields and the content of the VMT could be rather different. Treating one TCustomEdit object (compiled with one version of Delphi) as if it were the other (compiled with a different version) could cause havoc.

So the way around this problem is to use custom drag objects to represent the information being dragged across, in conjunction with the aforementioned IsDragObject function. Drag objects are instances of quite shallow classes, not far from TObject in the VCL hierarchy. Things are less likely to change in these classes from one version to the next as they are with component classes, although they still do. Consequently, it is still important to ensure that the DLL and EXE are compiled with the same version of Delphi.

IsDragObject does not use is to find out if the object in question (passed as the Source parameter to OnDragOver and OnDragDrop) inherits from TDragObject. Instead, it compares the class name of the given object against the class name of TDragObject. If there is no match, it goes back to the ancestor of the supplied object and tries again. Eventually, it will either find a match or it won’t, so the function will return True or False.

Clearly you could write a similar routine that would do the same job for edit controls or memos, but the fact that IsDragObject exists already suggests that it is easiest to use custom drag objects when dragging between forms from different binary modules.

Assuming IsDragObject returns True, you can apply a static typecast to Source to turn it into a reference to your TDragObject descendant. In this case, the custom drag object inherits from TDragObject directly (not TDragControlObject). This means that the code will work in all Delphi versions from 2 onwards, but it does mean that the DragCursor property values will be ignored if you set them. It also means that trying to use the application in Delphi 2 or 3 will show up the problem of dragging with the right mouse button.

Listing 23 shows the OnStartDrag and OnEndDrag event handlers for the memo from the DLL along with the OnDragOver and OnDragDrop event handlers of the edit control in the EXE, now that they have been fixed to work as required.

Listing 23: Drag and drop code that works between a DLL and an EXE

//This code is from the DLL form
procedure TDLLForm.Memo1StartDrag(Sender: TObject;
  var DragObject: TDragObject);
begin
  DragObject := TTextDragObject.Create;
  TTextDragObject(DragObject).Data := (Sender as TMemo).SelText;
  FDragObject := DragObject
end;

procedure TDLLForm.Memo1EndDrag(Sender, Target: TObject; X, Y: Integer);
begin
  FDragObject.Free;
  FDragObject := nil
end;

//This code is from the EXE form
procedure TExeForm.Edit1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  Accept := IsDragObject(Source)
end;

procedure TExeForm.Edit1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  (Sender as TCustomEdit).Text := TTextDragObject(Source).Data
end;

Since we are on the subject of DLLs at the moment it might be useful to mention that a drag object has an Instance method that returns the instance handle for the module that created it. An instance handle is the address at which that module was loaded, so for EXEs, the instance handle will always be $400,000, but will be different for all the DLLs in a given application’s address space.

Additionally, TDragObject defines a virtual method GetName that returns (by default) the object’s class name as a string. This can be overridden in descendant classes.

CLX Inter-Application Dragging

Whilst the VCL drag and drop support does not extend to inter-application dragging (you must use Windows COM programming to achieve this), the CLX support does. However, with the first CLX code library, this only works on Linux in Kylix applications. Windows applications steadfastly refuse to oblige to follow their Kylix counterparts' lead.

Firstly, you should know that the base custom drag object has a read-only property called IsInterAppDrag. This returns True if the drag operation comes from another application.

It turns out to be very straightforward to deal with inter-application drag and drop. The basic idea is that when you start the drag operation, you set up a stream full of data that is to be made available to the drop target. You specify the type of data being dragged using a MIME data type string so the drop target can interrogate what has been made available.

When the drop is made, the drop target can check whether the data is in a known format, and if so can read it from the stream and do what it likes with it.

This approach can also be used inside a single application so you can drag from a control and drop either in the same application or in a different application. Any CLX drop target can firstly check if a known MIME data type has been dragged, and then go back to checking the Source parameter if no known MIME type is found (Source will represent a control in the same application, or a custom drag object).

MIMEDrag.dpr is the first project that explores this MIME-based dragging. It is a single project which allows controls on one form to have their content dragged to controls on another form. On each form is a memo and an image component. When either control from the first form is dragged onto either of the controls on the second form, the drag operation uses MIME-encoded data.

Listing 24 shows the code shared by both controls on the first form. It first fills a stream with the memo’s content and sets that up as the text/plain MIME type using AddDragFormat. After emptying the stream it then stores the bitmap image in it and again uses AddDragFormat to set it up as the image/bmp MIME type.

Listing 24: Starting a MIME-based drag operation

procedure TForm1.SharedStartDrag(Sender: TObject;
  var DragObject: TDragObject);
var
  MS: TMemoryStream;
begin
  MS := TMemoryStream.Create;
  try
    Memo1.Lines.SaveToStream(MS);
    MS.Position := 0;
    if not AddDragFormat('text/plain', MS) then
      raise Exception.Create('Failed to add text drag format');
    //Reset memory stream so it can be re-used
    MS.SetSize(0);
    Image1.Picture.Bitmap.SaveToStream(MS);
    MS.Position := 0;
    if not AddDragFormat('image/bmp', MS) then
      raise Exception.Create('Failed to add bitmap drag format');
  finally
    MS.Free;
  end;
end;

At the other side, the controls in the second form share an OnDragOver event handler which accepts anything that looks like plain text or a Delphi bitmap. Assuming something is dropped, the code fills a listbox with all the supported formats, using SupportedDragFormats. Then if plain text is available (checked with SaveDragDataToStream), it is written in the memo, and if a Delphi bitmap is available, it is given to the image component. Listing 25 shows how this is done.

Listing 25: Receiving MIME-based data

procedure TForm2.SharedDragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
var
  Formats: TStrings;
begin
  Formats := TStringList.Create;
  SupportedDragFormats(Formats);
  try
    Accept :=
      (Formats.IndexOf('text/plain') <> -1) or
      (Formats.IndexOf('image/bmp') <> -1)
  finally
    Formats.Free
  end;
end;

procedure TForm2.SharedDragDrop(Sender, Source: TObject; X, Y: Integer);
var
  MS: TMemoryStream;
begin
  MS := TMemoryStream.Create;
  try
    //Display supported MIME formats in listbox
    ListBox1.Clear;
    SupportedDragFormats(ListBox1.Items);
    if SaveDragDataToStream(MS, 'text/plain') then
    begin
      MS.Position := 0;
      Memo1.Lines.LoadFromStream(MS);
    end;
    //Reset memory stream
    MS.SetSize(0);
    if SaveDragDataToStream(MS, 'image/bmp') then
    begin
      MS.Position := 0;
      Image1.Picture.Bitmap.LoadFromStream(MS);
    end;
  finally
    MS.Free;
  end;
end;

Note that because the application is using standard MIME types, it will accept text or bitmaps dragged from anywhere. For example, you can successfully drag a selection of text or an image from a Microsoft Word document onto the application, when running in Windows.

MIMEDrag.dpr is a single project, but exactly the same code can be split across two executables with the same degree of success. This has been done with the projects MIMEApp1.dpr and MIMEApp2.dpr. The resultant projects look just like the original two-form project and the drag and drop works just as well as it did before. Figure 12 shows the two applications running after one of the controls on the left form was dragged to one of the controls on the right form.

Figure 12: Inter-application drag and drop using MIME types

Summary

This paper has endeavoured to describe the rich VCL and CLX support for easy drag and drop in your applications. Whilst it is very flexible and customisable and can yield impressive visual results, all you need to start with is three simple steps to add drag & drop support into your application. The rest can be added as needed, piece by piece.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.