Implementing Professional Drag & Drop In VCL/CLX Applications
Implementing Professional Drag & Drop In VCL/CLX Applications
- Introduction
- Component Drag & Drop or "Here Thar Be Drag (ons)"
- Testing The Theory
- Customising Drag Operations
- Drag Control Objects
- Custom Components And Drag Image Lists
- VCL Inter-Module Dragging
- CLX Inter-application Dragging
- Summary
- About Brian Long
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.
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 byclicking 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 thesourceof the drag, ordrag source, and the one under the mouse when the button is released is called thetarget,drag targetordrop 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 (thecm_Dragmessage, 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 (theTCursortype value ofcrDragby default, which looks like). In the VCL, this default cursor is obtained from the dragged control’sDragCursorproperty. 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.
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’sDragModeproperty todmAutomatic(it defaults todmManual). This property is defined inTControl, so all visual controls will have it, be they componentised versions of real Windows/Qt controls or not. The effect of settingDragModetodmAutomaticis that clicking the left mouse button down on a control will automatically start a drag operation without any extra code.
Without using thisDragModesetting, you can start a drag operation manually with a call to the control’sBeginDragmethod.BeginDraghas one mandatory parameter, which is aBooleanthat dictates whether the drag operation begins immediately. One reason for callingBeginDragis 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 theControlkey is held down, for example. Listing 1 shows how an edit control’sOnMouseDownevent 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 ofTruepassed toBeginDragimmediately starts a drag operation. On the other hand a value ofFalsewill 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 anOnMouseDownevent 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;
WhenFalseis passed toBeginDrag, 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 toMouse.DragThreshold(Mouseis a global object instance, created in the Controls/QControls unit). Alternatively, you can pass an optional second parameter toBeginDrag. This parameter defaults to –1, meaning that theMouse.DragThresholdvalue will be used.
A value ofdmAutomaticassigned to theDragModeproperty of a Delphi 1 to 3 control causes the control to callBeginDrag(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 polymorphicBeginAutoDragmethod (declared asdynamic), which calls:
BeginDrag(Mouse.DragImmediate, Mouse.DragThreshold)
You can change the values ofMouse.DragImmediateandMouse.DragThresholdto affect global drag operations (despite the online help in Delphi 5 suggesting these properties are read-only). Custom components can also override theBeginAutoDragmethod to change what happens if the user drags them whenDragModeis set todmAutomatic. In factTCustomFormoverrides it and does nothing in the re-implementation, to ensure that the user cannot drag a form if itsDragModeproperty is set todmAutomatic.
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 anOnDragOverevent 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 isAccept, which is avarparameter. You set this toFalseif you do not accept a drag from the suggested source. It defaults toTrue, which means that by default, anOnDragOverevent handler will accept any source. However, if you do not make anOnDragOverevent handler, the control will not accept any drag operations.
You can see a simpleOnDragOverevent handler in Listing 5. This is anOnDragOverevent 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 theAcceptparameter being set toTrueis 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’sDragCursorproperty, which defaults tocrDrag(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.
Senderrepresents the control whose event is firing, and this is happening because the control indicated by theSourceparameter is currently being dragged over it at the position indicated by theXandYparameters, relative toSender.
TheStateparameter tells how the mouse is moving relative to the control under the mouse. As a dragging operation proceeds, when the mouse enters a control, itsOnDragOverevent is triggered withStateset todsDragEnter. It is also repeatedly triggered as the mouse moves over the control (StateisdsDragMove) 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 (StateisdsDragLeave).
You can use theStateparameter 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;
OnDragOveris called, if present, from theDragOverdynamic protected method. Custom component classes can override this method to provide additional functionality when a drag operation moves over them, if necessary.
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’sOnDragDropevent handler is invoked. Listing 7 shows an emptyOnDragDropevent handler.
Listing 7: An OnDragDrop event handler
procedure TForm1.ImageDragDrop(Sender, Source: TObject; X, Y: Integer);
begin
end;
The parameters are a subset of theOnDragOverevent handler’s parameters. The user dragged theSourcecontrol and dropped it onSender.XandYare the co-ordinates relative to the control that was dropped on (Sender).
The code in anOnDragDropevent 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 theSourceparameter will be required.
TheOnDragDropevent handler is called from the publicDragDropdynamic 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’sDragDropmethod, passing the source object andXandYco-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);
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. ATEdit(edtPath), aTImage(imgLoadedImg), aTButton(btnGetPath) and aTListBox(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 anOnClickevent handler for the button, anOnChangehandler for the edit control and an OnDblClickevent 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 usesSelectDirectoryto allow a directory to be chosen.SelectDirectoryis in the FileCtrl VCL unit, and the QDialogs CLX unit. As the listing shows, there are two forms ofSelectDirectory. 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, theIncludeTrailingBackslashroutine does this job, whereas Delphi 6 prefers you to useIncludeTrailingPathDelimiter.
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 theDragModeproperty todmAutomatic, which means any left-click anywhere on the listbox will start a drag operation. Alternatively, you can make anOnMouseDownevent handler with code like that shown in Listing 3. Since aTFileListBoxis indirectly inherited fromTCustomListBox, 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 anOnDragOverevent 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 anOnDragDropevent handler for the image component. The statement in the file listbox’sOnDblClickevent 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’sOnDblClickevent handler from within the image’sOnDragDropevent 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 theSenderparameter to the event handler, just in case the event handler makes use of that parameter. In an event handler,Sendershould 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
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’sDragCursorproperty. This property defaults tocrDrag, 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 namedPacMan. The code in the program required to load this custom cursor into a control’sDragCursorproperty 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 publicDraggingmethod. This parameterless function returnsTrueif 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 inOnDragOverandOnDragDropevent handlers) to verify whether a certain control is currently in the process of being dragged.
To complement theBeginDragmethod, controls also have anEndDragmethod that allows you to programmatically terminate a drag operation.EndDragtakes aBooleanparameter calledDrop. IfDropisTrueand 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 aCancelDragprocedure 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 theEscapekey. The programmer can terminate the drag positively or negatively using the dragged control’sEndDragmethod, or cancel it withCancelDrag.
If a custom component needs to do anything particularly special when a drag operation is cancelled, it can override the protected dynamic methodDragCanceled. By default, this does nothing. However the VCL version of theTCustomListBoxclass overrides it to fix certain mouse usability issues that arise when a drag operation is cancelled.
The Controls/QControls unit offers another global routine calledFindDragTarget. This takes aTPointrecord 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 suitableOnDragOverandOnDragDropevent handlers. The only extra checking performed by this routine is dictated by the additionalBooleanparameter (AllowDisabled) that controls whether disabled controls will be considered for returning. If no control can be found at the specified position,FindDragTargetreturnsnil.
Whilst theOnDragOverevent handler’sStateparameter 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’sOnStartDragevent handler (see Listing 12) will be triggered as soon as a drag operation on it starts, either through a call to itsBeginDragmethod, or by being clicked on whenDragModeis set todmAutomatic. We will look more closely at what we can do with this event, which was introduced in Delphi 2,later in the paper.
A correspondingOnEndDragevent 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-presentSenderparameter is the control that is no longer being dragged.Targetrepresents the control thatSenderwas dropped on, but which can benilin the case of a terminated drag operation. TheXandYco-ordinates, relative toTargetare also passed as parameters, though ifTargetisnilthese 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;
TheOnEndDragevent is also triggered after execution of theDragCanceledmethod. If the drag is terminated successfully with a drop, the source’sOnEndDragevent occurs after the target’sOnDragDropevent.
OnStartDragandOnEndDragare called from the protected dynamic methodsDoStartDrag&DoEndDragrespectively, which again can be overridden by new component classes to perform additional tasks specific to the component being written.
When a drag and drop application has a drop target control that can accept many different dragged source controls, the implementation of theOnDragOverandOnDragDropevent 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. TheOnStartDragevent handler of all draggable controls can each create an instance of a class inherited fromTDragObject. 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
WhilstTDragObjectis the key base class, you will typically be interested in inheriting from the more ableTDragControlObjectclass. 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 theDragObjectvarparameter of the source control’sOnStartDragevent handler, which defaults tonil. Having done this, when the source control is dragged over and dropped on a target control, one of the parameters of the target control’sOnDragOverandOnDragDropevent handlers changes.
Specifically, theSourceparameter 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, theSourceparameter would only represent your drag object if you did not inherit fromTDragControlObject. Instead you must inherit directly fromTDragObject. Delphi 4 (and later) remedies this problem. You can inherit from any point in the hierarchy andSourcewill 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 fromTDragObjectin order to be passed as theSourceparameter. 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 anOnStartDragevent handler (Delphi 2 neglected to describe it in the help). However, this information is completelyincorrect. 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 inFigure 5there is a new drag control class calledTDragControlObjectEx. If you create an instance of this class, it will indeed be freed automatically. However instances of any classes thatTDragControlObjectExinherits from must be explicitly freed in code.
When using custom drag objects, you should verify in theOnDragOverevent handler (and maybe also in theOnDragDropevent handler) that theSourceparameter 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 clearera little later, you should use this expression instead:
IsDragObject(Source)
In a normal application, the effect of these two expressions will be identical, howeverIsDragObjectcaters for other scenarios that the functionality of theisoperator 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’sOnStartDragcreates an instance of a drag object.
Since CLX has a problem with certain drag object classes, the CLX version usesTDragObjectas 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 fromTDragObject. The main VCL project uses a drag object class inherited fromTDragControlObject, which is the most useful base class (and which takes the dragged control as its constructor parameter).
The new class in DragObjects.Dpr is calledTTextDragObjectand has just one extra string data field calledDatawhich is given the piece of text as appropriate from each control.
The target panel’sOnDragOverandOnDragDropevent handlers are therefore considerably simpler in implementation, as they just treat theSourceparameter as aTTextDragObjectand read theDatafield. All controls that have anOnStartDragevent handler share anOnEndDragevent 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 theDragObjectparameter in theOnStartDragevent handler with its defaultnilvalue, or have noOnStartDragevent handler at all, the VCL/CLX creates aTDragControlObjectinstance to represent the drag operation (Delphi 6 changes this to be aTDragControlObjectExinstance). This behind-the-scenes drag objectwillbe 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,TDragObjecthas a protected virtual methodGetDragCursorthat takes aBooleanvarparameter calledAccept, along with the mouse co-ordinates. It is supposed to return aTCursorvalue depending on whether the target control accepts the dragged control or not. It is hard-coded to return eithercrNoDroporcrDrag, 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 adrag 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 virtualGetDragImagesmethod which is supposed to return an image list containing the custom drag image. The CLX uses a single, global image list as returned by theDragImageListfunction in the QControls unit. The drag object uses the virtualGetDragImageIndexmethod to decide which image from the list should be used.
TDragControlObjectis the useful class that inherits fromTDragObject. It keeps a reference to the control being dragged in the publicControlproperty (although the original Delphi 2 implementation had the protected and public sections of the class the wrong way round, and so theControlproperty was actually protected).
In the VCL,TDragControlObjectoverridesGetDragCursorand uses the stored reference to the control to return eithercrNoDropor the control’sDragCursorproperty (as opposed to being fixed tocrDrag). Again, CLX offers no opportunity to customise the drag cursor itself.
The VCLTDragControlObjectclass also overrides theGetDragImagesmethod and calls the control’sGetDragImagesmethod, rather than returningnil. Admittedly, the only controls that do anything in theirGetDragImagesmethods areTCustomTreeViewandTCustomListView(and their descendants), but the scope is there for controls to supply their own drag image list to enhance the drag cursor (seelater). 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 CLXTDragControlObjectclass overridesGetDragImageIndexto 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).
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 bothGetDragImagesandGetDragCursor.GetDragCursoris 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’sDragCursorproperty.
GetDragImagesis overridden to create an instance of aTDragImageList. This class, which was introduced in Delphi 3, inherits fromTCustomImageListand is an ancestor of theTImageListcomponent 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 theSetDragImagemethod, which takes three parameters. The first is the index of the drag image. When adding the drag image into the image list, you typically callAddorAddMasked, 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 ifSetDragImageis 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 issuelater).
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 seeGetDragCursorreturning thePacMancursor 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;
TheGetDragImagesmethod 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. TheAddMaskedimage 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. TheControlStyleproperty help is very misleading with respect to thecsDisplayDragImagesetting. It suggests that including this flag in a control’sControlStyleproperty 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 thecsDisplayDragImagevalue in itsControlStyleset property. Only tree views and list views include this member in theirControlStyleproperty 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’sOnCreateevent handler iterates through all its controls, addingcsDisplayDragImageinto theControlStyleproperty. 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 theControlStyleproperty, as is done by theFixControlStylesprocedure 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;
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 fromTDragControlObjectin 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 fromTDragControlObject, but it won’t be passed as theSourceparameter to anyOnDragOverorOnDragDropevent 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 handlersSourceparameter.
In the CLX code, the custom drag object overrides itsGetDragImageIndexmethod 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 callingTextHeightorTextWidth, 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'sAddMaskedmethod 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 thecsDisplayDragImagesymbol, but does not refer to it anywhere.
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 theAltkey is held down (anOnMouseDownevent handler does this). The button’sOnStartDragevent handler creates a custom drag object of typeTControlDragObjectwhich is inherited fromTDragControlObjectand 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’sOnEndDragevent 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
The project described above is difficult to translate directly into CLX since CLX offers noPaintTomethod 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 overriddenGetDragImageHotSpotmethod 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 inFigure 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 aGetDragImagesvirtual method that is automatically called by the drag objects that inherit fromTDragControlObject. 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 includecsDisplayDragImagein theirControlStyleproperty.
Similarly, all CLX controls haveGetDragImageIndexandGetDragImageHotSpotvirtual methods that are automatically called by drag objects that inherit fromTDragControlObject. 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.TDragButtoninherits fromTButton, and can be found in the DragButton.pas unit.TDragEditis inherited fromTEdit, 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 userCtrl+clicks on them. This is done in an overridden version of theMouseDownmethod, which normally is only responsible for triggering theOnMouseDownevent. They also both define a private data field calledFDragImages, which is aTDragImageList. They add thecsDisplayDragImagesetting into theirControlStyleproperty inside the constructor. The other thing they do is to override theGetDragImagesmethod 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’sPaintTomethod, telling it to paint a copy of itself onto the bitmap’s canvas.AddMaskedis used to add the bitmap to the image list, specifying that all pixels that matchclBtnFace(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’sLockandUnlockmethods in Delphi 2, thePaintTomethod 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 theGetDragImageIndexandGetDragImageHotSpotmethods and manipulate the global CLX drag image list in a similar way to has been done in the sample projects so far.
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’sApplicationobject needs to have itsHandleproperty assigned the value of the EXE’sApplication.Handle. The exported DLL routine takes a window handle (assumed to be theApplicationobject handle) and assigns it to its ownApplication.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 theOnCloseevent handler assigning a value ofcaFreeto itsActionparameter (avarparameter), 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 withTDragObject(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 aTCustomEdit(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;
ATMemois a descendant ofTCustomEditand 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’sisexpression will be asking the DLL’s memo object whether its VMT (virtual method table) matches that ofTCustomEdit(or some descendant), but will be referring to the implementation ofTCustomEditin the code compiled into the EXE. Since the memo inherits from the version ofTCustomEditcompiled into the DLL, the VMTs of the two versions ofTCustomEditwill be at different addresses and soiswill returnFalse.
It is probably a good idea that it fails, asTCustomEditis 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 oneTCustomEditobject (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 aforementionedIsDragObjectfunction. Drag objects are instances of quite shallow classes, not far fromTObjectin 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.
IsDragObjectdoes not useisto find out if the object in question (passed as theSourceparameter toOnDragOverandOnDragDrop) inherits fromTDragObject. Instead, it compares the class name of the given object against the class name ofTDragObject. 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 returnTrueorFalse.
Clearly you could write a similar routine that would do the same job for edit controls or memos, but the fact thatIsDragObjectexists already suggests that it is easiest to use custom drag objects when dragging between forms from different binary modules.
AssumingIsDragObjectreturnsTrue, you can apply a static typecast toSourceto turn it into a reference to yourTDragObjectdescendant. In this case, the custom drag object inherits fromTDragObjectdirectly (notTDragControlObject). This means that the code will work in all Delphi versions from 2 onwards, but it does mean that theDragCursorproperty 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 theOnStartDragandOnEndDragevent handlers for the memo from the DLL along with theOnDragOverandOnDragDropevent 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 anInstancemethod 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,TDragObjectdefines a virtual methodGetNamethat 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 calledIsInterAppDrag. This returnsTrueif 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 theSourceparameter if no known MIME type is found (Sourcewill 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 thetext/plainMIME type usingAddDragFormat. After emptying the stream it then stores the bitmap image in it and again usesAddDragFormatto set it up as theimage/bmpMIME 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 anOnDragOverevent 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, usingSupportedDragFormats. Then if plain text is available (checked withSaveDragDataToStream), 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
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.
Brian Longused to work atBorlandUK, 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, pleaseget in touchor visitBrian's Web site.
Brian authored aBorland Pascal problem-solving bookin 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor forMastering Delphi 7andMastering Delphi 2005and also contributed a chapter toDelphi for .NET Developer Guide. Brian is a regular columnist inThe Delphi Magazineand 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.