How to receive data dragged from other applications
Introduction
In article #11 we looked at how to catch files dragged and dropped on our application from Explorer. That's fine as far as it goes, but what about catching objects dragged and dropped from other applications? For example, how do we catch a text selection dragged from a text editor or word processor, or what about an HTML selection dragged from your web browser?
In order to receive such objects we have to make our application into a "drop target" that is recognised by Windows. We do this using COM (or OLE) drag and drop. As is usual with COM, we have to create an object that implements an interface – IDropTarget in this case – and then tell Windows about it. Windows then calls the methods of our implementation of IDropTarget to notify us of drag drop events.
If we accept a dropped object, Windows makes the dropped object's data available via an object that supports the IDataObject interface. This object can provide the data in one or more data formats. Several different delivery mechanisms may be supported and objects can be targetted at different output devices. Source applications may also advise how the data should be rendered. Our application needs to examine the available data formats and decide if it can accept any of them. If so we must also be able to extract the data from the data object.
In this article we will:
- Show how to register an application's window as a drop target.
- Interact with Windows to give visual feedback about whether we will accept a drop.
- Show how to examine all data formats supported by an object.
- Give examples of how to extract data from certain kinds of data object.
The first thing we need to do is to get to know the IDropTarget and IDataObject interfaces. We begin this in the next section.
About IDropTarget
IDropTarget descends directly from IUnknown and implements four additional methods. The definition of the interface is found in Delphi's ActiveX unit and is reproduced in Listing 1.
1type
2 IDropTarget = interface(IUnknown)
3 ['{00000122-0000-0000-C000-000000000046}']
4 function DragEnter(const dataObj: IDataObject; grfKeyState: Longint;
5 pt: TPoint; var dwEffect: Longint): HResult;
6 stdcall;
7 function DragOver(grfKeyState: Longint; pt: TPoint;
8 var dwEffect: Longint): HResult;
9 stdcall;
10 function DragLeave: HResult;
11 stdcall;
12 function Drop(const dataObj: IDataObject; grfKeyState: Longint;
13 pt: TPoint; var dwEffect: Longint): HResult;
14 stdcall;
15 end;
Listing 1
A description of the methods and how we use them follows:
- DragEnter
-
Called when the user first drags an object over the window that is registered for drag and drop. Parameters are:
- dataObj
- Describes the object being dragged. We will examine this object in detail later.
- grfKeyState
- Bit mask describing which mouse buttons and "shift" (modifier) keys are pressed. We may
want to use this information to decide how to handle the operation and to help decide the value
assigned to dwEffect (see below).
- pt
- The mouse location.
- dwEffect
- Describes the cursor effect used for the operation. When the method is called it contains a bit mask detailing the permitted effects and must be set to a single value decribing the desired effect within the method. Effects are: the "no entry" cursor (meaning we can't accept a drop), the copy cursor, the move cursor or the shortcut cursor.
- DragOver
-
Called repeatedly as the user moves the mouse over our window. The parameters are the same as for DragEnter except that dataObj is not provided here. We use this method to modify the the cursor according to the mouse position and any change in the mouse button or modifier keys.
- DragLeave
-
Called to inform us that the user has dragged the object off our window without dropping the object. We use this to tidy up if necessary.
- Drop
-
Called if the user drops the object over our window. The parameters are exactly the same as for DragEnter. When this method is called we process the dropped object and tidy up. Note thatDragLeave is not called if the object is dropped. Drop is not called if the
"no entry" cursor has been requested.
Further information can be found in Delphi's Windows API help file.
When implementing IDropTarget we may need to examine the data object, the permitted drop effects and the modifier keys to answer the following questions:
- Can we accept the object?
- How should we handle the object if dropped?
We will answer these questions later in the article when we look at implementing IDropTarget.
About IDataObject
Again the declaration of IDataObject is provided in the ActiveX unit. Listing 2 replicates the definition.
1type
2 IDataObject = interface(IUnknown)
3 ['{0000010E-0000-0000-C000-000000000046}']
4 function GetData(const formatetcIn: TFormatEtc;
5 out medium: TStgMedium): HResult;
6 stdcall;
7 function GetDataHere(const formatetc: TFormatEtc;
8 out medium: TStgMedium): HResult;
9 stdcall;
10 function QueryGetData(const formatetc: TFormatEtc): HResult;
11 stdcall;
12 function GetCanonicalFormatEtc(const formatetc: TFormatEtc;
13 out formatetcOut: TFormatEtc): HResult;
14 stdcall;
15 function SetData(const formatetc: TFormatEtc;
16 var medium: TStgMedium; fRelease: BOOL): HResult;
17 stdcall;
18 function EnumFormatEtc(dwDirection: Longint; out enumFormatEtc:
19 IEnumFormatEtc): HResult;
20 stdcall;
21 function DAdvise(const formatetc: TFormatEtc;
22 advf: Longint; const advSink: IAdviseSink;
23 out dwConnection: Longint): HResult;
24 stdcall;
25 function DUnadvise(dwConnection: Longint): HResult;
26 stdcall;
27 function EnumDAdvise(out enumAdvise: IEnumStatData): HResult;
28 stdcall;
29 end;
Listing 2
IDataObject is supported by data objects dragged and dropped over our window. We don't need to implement this interface – Windows provides an instance to us via the methods of IDropTarget. Not all the methods are required when handling drag and drop. The only ones of interest to us in this article are:
- GetData
-
Unsurprisingly this method retrieves data from the drop object. We specify a desired data format, medium and target device in the method's formatetcIn parameter and, if the object supports it, the method returns the required data encapsulated in a storage medium via its medium parameter. We will discuss storage media in detail later.
- QueryGetData
-
Checks whether the data object can provide data in the required format.
- EnumFormatEtc
-
Returns an enumerator that can iterate through the various data formats, media and target devices supported by the data object.
The GetDataHere and GetCanonicalFormatEtc methods could also be of use but are not considered in this article.
As has been observed, data objects can store different versions of the same data in various formats. It is important that we have a reasonable understanding of them. In the next section we examine data formats in more detail.
Querying the Data Object
We often need to query an object to find out about the formats it supports. There are two distinct ways to do this. The first is to enquire whether the data object supports a specified format and the second is to enumerate all the supported formats. We will consider each of these in turn.
Checking for a specified data format
To check for a specific format we call the data object's QueryGetData method, passing it a TFormatEtc structure that describes the required data format. Listing 4 defines a helper routine that finds this information for a given clipboard format and storage medium.
1function HasFormat(const DataObj: IDataObject;
2 const Fmt: TClipFormat; const Tymed: Integer): Boolean;
3var
4 FmtEtc: TFormatEtc;
5begin
6 FmtEtc.cfFormat := Fmt;
7 FmtEtc.ptd := nil;
8 FmtEtc.dwAspect := DVASPECT_CONTENT;
9 FmtEtc.lindex := -1;
10 FmtEtc.tymed := Tymed;
11 Result := DataObj.QueryGetData(FmtEtc) = S_OK;
12end;
Listing 4
Here we pass a IDataObject instance to the helper function along with the required clipboard format and storage medium type. We record the clipboard format and storage medium type in a TFormatEtc structure and set its other fields to default values. Once we have set up the TFormatEtc structure we pass it to the data object's QueryGetData method and check the return value. A return of S_OK indicates the format is supported.
Enumerating all data formats
IDataObject can provide a standard Windows enumerator that iterates through all data formats supported by a data object. The skeleton code in Listing 5 show how to use the enumeration.
1procedure EnumDataFormats(const DataObj: IDataObject);
2var
3 Enum: IEnumFormatEtc;
4 FormatEtc: TFormatEtc;
5begin
6 OleCheck(DataObj.EnumFormatEtc(DATADIR_GET, Enum));
7 while Enum.Next(1, FormatEtc, nil) = S_OK do
8 begin
9
10 ...
11 end;
12end;
Listing 5
We first call the EnumFormatEtc method of the data object to get the required enumerator. We pass the DATADIR_GET flag to the method to indicate that we want to know about data formats we can read from the data object (rather the formats we could write to the object). If all goes well the method passes the enumerator out via the Enum parameter. If the call fails the OleCheck traps the error return and raises an exception.
Example code
One of the examples in this article's demo code uses this technique to list data formats
in a list view.
Once we have the enumerator we repeatedly call its Next method passing 1 as the first parameter to indicate we want one TFormatEtc structure at each iteration. For as long as there is more information available the method sets FormatEtc and returns S_OK. When the data formats are exhausted the method returns S_FALSE and the loop ends. Within the while loop we can do something with data formats, such as list them in a dialog box.
Getting Data from the Data Object
Once we know the data format we want, the easiest way to get data from the data object is via its GetData method. The skeleton code presented in Listing 6 shows how to approach this.
1procedure GetDataFromObj(const DataObj: IDataObject;
2 const Fmt: TClipFormat; const Tymed: Integer);
3var
4 FmtEtc: TFormatEtc;
5 Medium: TStgMedium;
6begin
7 FmtEtc.cfFormat := Fmt;
8 FmtEtc.ptd := nil;
9 FmtEtc.dwAspect := DVASPECT_CONTENT;
10 FmtEtc.lindex := -1;
11 FmtEtc.tymed := Tymed;
12 OleCheck(DataObj.GetData(FmtEtc, Medium));
13 try
14 Assert(Medium.tymed = FmtEtc.tymed);
15
16 ...
17 finally
18 ReleaseStgMedium(Medium);
19 end;
20end;
Listing 6
Just like in Listing 4 we must fill in the TFormatEtc structure with details of the required data format as specified by the Fmt and Tymed parameters. Next we call the data object's GetData method passing in the data format structure. If the method succeeds it returns S_OK and sets Medium to reference the required storage medium. Medium should have the same value in its tymed field as FmtEtc so we use an assertion to check this. If GetData fails it causes OleCheck to raise an exception.
Now we have the storage medium we can then do something with it, for example display the information it contains. Once we have finished with the storage medium we must release its data structures. How this is done varies depending on the medium type. Fortunately Windows provides the ReleaseStgMedium method that knows how to free the required data structures, so we simply call that routine.
Now it's all very well saying "do something with the data from Medium", but what exactly is that "something"? Well, the answer depends on both the data format and the media type. So next we'll look at a few examples of working with different data formats and data types.
Example 1: Text stored in global memory
Several data formats provide their data as ANSI text stored in global memory. Examples are:
- CF_TEXT – a standard format.
- CF_FILENAMEA – a format defined by the shell for passing filenames.
- Rich Text Format
- HTML Format
ANSI Text?
Notice how I mentioned ANSI text above. When this article was written I was using Delphi 7 and there was no Unicode support. The code presented here assumes that:
type string = AnsiString
What effect compiling this code with
type string = UnicodeString
with later compilers has not been tested.
Listing 7 shows an example of how to retrieve plain text data from global memory. The function can be passed any clipboard format that uses plain text data and will return the text as a string.
1function GetTextFromDataObj(const DataObj: IDataObject;
2 const Fmt: TClipFormat): string;
3var
4 FormatEtc: TFormatEtc;
5 Medium: TStgMedium;
6 PText: PChar;
7begin
8 FormatEtc.cfFormat := Fmt;
9 FormatEtc.ptd := nil;
10 FormatEtc.dwAspect := DVASPECT_CONTENT;
11 FormatEtc.lindex := -1;
12 FormatEtc.tymed := TYMED_HGLOBAL;
13 OleCheck(DataObj.GetData(FormatEtc, Medium));
14 try
15 Assert(Medium.tymed = TYMED_HGLOBAL);
16 PText := GlobalLock(Medium.hGlobal);
17 try
18 Result := PText;
19 finally
20 GlobalUnlock(Medium.hGlobal);
21 end;
22 finally
23 ReleaseStgMedium(Medium);
24 end;
25end;
Listing 7
The routine starts the same as Listing 6 except that we hard wire the TYMED_HGLOBAL media type. The code that retrieves the text begins after the Assert statement. We lock the global memory handle (accessed via a field of the TStgMedium structure returned by GetData in Medium) and store the resulting pointer in PText. PText now points to the start of a zero termimated string of ANSI characters, so we simply set the function result to that string. Finally we unlock the memory handle and then call ReleaseStgMedium, which in turn frees the storage medium's memory.
Example 2: File list stored in global memory
In article #11 we noted that the list of files dropped on a window was accessed via a HDROP handle and we used the DragQueryXXX API functions to get at the list of files. Well, the same principle applies for OLE drag and drop. The related clipboard format is CF_HDROP and the medium type is TYMED_HGLOBAL. The drop handle is stored in the TStgMedium.hGlobal field. Listing 8 is based on article #11 listing 7, but it gets its drop handle from the data object instead of from a Windows message.
1procedure GetFileListFromObj(const DataObj: IDataObject;
2 const FileList: TStrings);
3var
4 FmtEtc: TFormatEtc;
5 Medium: TStgMedium;
6 DroppedFileCount: Integer;
7 I: Integer;
8 FileNameLength: Integer;
9 FileName: string;
10begin
11
12 FmtEtc.cfFormat := CF_HDROP;
13 FmtEtc.ptd := nil;
14 FmtEtc.dwAspect := DVASPECT_CONTENT;
15 FmtEtc.lindex := -1;
16 FmtEtc.tymed := TYMED_HGLOBAL;
17 OleCheck(DataObj.GetData(FmtEtc, Medium));
18 try
19 try
20
21 DroppedFileCount := DragQueryFile(
22 Medium.hGlobal, $FFFFFFFF, nil, 0
23 );
24
25 for I := 0 to Pred(DroppedFileCount) do
26 begin
27
28 FileNameLength := DragQueryFile(Medium.hGlobal, I, nil, 0);
29 SetLength(FileName, FileNameLength);
30 DragQueryFile(
31 Medium.hGlobal, I, PChar(FileName), FileNameLength + 1
32 );
33
34 FileList.Add(FileName);
35 end;
36 finally
37
38
39 DragFinish(Medium.hGlobal);
40 end;
41 finally
42 ReleaseStgMedium(Medium);
43 end;
44end;
Listing 8
This routine gets a list of dropped files from the the provided data object and stores the file names in the routine's FileList string list parameter. It begins, as usual by populating a TFormatEtc with the required information (this time requesting the CF_HDROP format and TYMED_HGLOBAL storage medium). It then gets the data from the data object.
We pass the value of the storage medium's hGlobal field to DragQueryFile to get the number of dropped files. Next we loop through all the dropped files getting the name of each file name by making two more calls to the ever so flexible DragQueryFile function. The first call gets the length of the filename, to enable us to allocate a string of the required length. The second call reads the file name into the string. The file name is then added to the list. When the loop finishes a call to DragFinish finalises the drop handle. Finally, we release the storage medium's memory by means of the now familiar call to ReleaseStgMedium.
Example 3: Data stored in a stream
Most data objects you will encounter when handling drag and drop will make their data available through a memory handle accessed via TStgMedium's hGlobal field. However you will occasionally come across the other storage media types. We will take a very brief look at just one more here – data streams accessed via the IStream interface.
Get the the required medium in the usual way, but specify TYMED_ISTREAM in TFormatEtc's tymed field. Obtain the medium as normal by calling the data object's GetData method. Once you have the storage medium cast it's pstm field to IStream and use the methods of IStream to manipulate the data.
Now we have an understanding of how to manipulate data objects we are at long last in a position to look at how to implement the IDropTarget interface.
Implementing IDropTarget
We will illustrate how to implement IDropTarget by considering two examples.
In both examples we will implement IDropTarget in the main form. Since TForm implements IUnknown we don't have to bother implementing its methods. Therefore all we need to do is implement the IDropTarget methods.
Boilerplate code
Both examples share some boilerplate code, which is presented in Listing 9.
1unit Form1;
2
3interface
4
5type
6 TForm1 = class(TForm, IDropTarget)
7 ...
8 procedure FormCreate(Sender: TObject);
9 procedure FormDestroy(Sender: TObject);
10 ...
11 private
12 ...
13 protected
14
15 function IDropTarget.DragEnter = DropTargetDragEnter;
16 function DropTargetDragEnter(const dataObj: IDataObject;
17 grfKeyState: Longint; pt: TPoint; var dwEffect: Longint): HResult;
18 stdcall;
19 function IDropTarget.DragOver = DropTargetDragOver;
20 function DropTargetDragOver(grfKeyState: Longint; pt: TPoint;
21 var dwEffect: Longint): HResult;
22 stdcall;
23 function IDropTarget.DragLeave = DropTargetDragLeave;
24 function DropTargetDragLeave: HResult;
25 stdcall;
26 function IDropTarget.Drop = DropTargetDrop;
27 function DropTargetDrop(const dataObj: IDataObject; grfKeyState:
28 Longint; pt: TPoint; var dwEffect: Longint): HResult; stdcall;
29 public
30 ...
31 end;
32
33implementation
34
35function TForm1.DropTargetDragEnter(const dataObj: IDataObject;
36 grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
37begin
38 ...
39 Result := S_OK;
40end;
41
42function TForm1.DropTargetDragLeave: HResult;
43begin
44 ...
45 Result := S_OK;
46end;
47
48function TForm1.DropTargetDragOver(grfKeyState: Integer; pt: TPoint;
49 var dwEffect: Integer): HResult;
50begin
51 ...
52 Result := S_OK;
53end;
54
55function TForm1.DropTargetDrop(const dataObj: IDataObject;
56 grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
57begin
58 ...
59 Result := S_OK;
60end;
61
62procedure TForm1.FormCreate(Sender: TObject);
63begin
64 OleInitialize(nil);
65 OleCheck(RegisterDragDrop(Handle, Self));
66end;
67
68procedure TForm1.FormDestroy(Sender: TObject);
69begin
70 RevokeDragDrop(Handle);
71 OleUninitialize;
72end;
73
74end.
Listing 9
First of all notice that the class declaration for TForm1 includes IDropTarget. Further down in the protected section we declare the methods of IDropTarget. We have used Delphi's method resolution clauses to rename each of the methods. This has been done because TForm already has a method called DragOver that clashes with, and gets hidden by, the method of the same name in IDropTarget. Each of IDropTarget's methods should return S_OK to indicate a successful completion.
The remaining code are the handlers of the form's OnCreate and OnDestroy events – FormCreate and FormDestroy. In FormCreate we first initialise OLE then call the RegisterDragDrop API function to register our implementation of IDropTarget – in this case our own form – as the drag-drop handler for the main window. Finally, in FormDestroy, we unregister drag-drop for the window by calling RevokeDragDrop. Lastly we unitialise OLE.
Registering Drag-drop Implementations
You can register any object that implements IDropTarget as the drag-drop handler for any window. It doesn't have to be the main form class or its window.
Example 1: Listing data formats
In this first example we will list all the data formats supported by a data object dropped on a form. Our main window will accept any dropped object and will enumerate its data formats when dropped.
To begin, create a new project and add all the boilerplate code from Listing 9. Now drop a TListView control on the form, name it lvDisplay, set it's ViewStyle property to vsReport and add four columns titled Format, Storage Medium, Aspect and lindex.
Given that we will accept any object we will always display a copy cursor whenever an object is dragged over the form. Furthermore, we won't take any notice of modifier keys and have no need to examine the object until it is dropped. These observations lead us to implement the DropTargetDragEnter, DropTargetDragLeave and DropTargetDragOver methods as shown in Listing 10.
1function TForm1.DropTargetDragEnter(const dataObj: IDataObject;
2 grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
3begin
4 dwEffect := DROPEFFECT_COPY;
5 Result := S_OK;
6end;
7
8function TForm1.DropTargetDragLeave: HResult;
9begin
10 Result := S_OK;
11end;
12
13function TForm1.DropTargetDragOver(grfKeyState: Integer; pt: TPoint;
14 var dwEffect: Integer): HResult;
15begin
16 dwEffect := DROPEFFECT_COPY;
17 Result := S_OK;
18end;
Listing 10
All we are doing here is to get Windows to display the copy cursor by setting the dwEffect parameter of DropTargetDragEnter and DropTargetDragOver to DROPEFFECT_COPY. DropTargetDragLeave is left unchanged from the boilerplate code.
All the action takes place in DropTargetDrop as is shown in Listing 11.
1function TForm1.DropTargetDrop(const dataObj: IDataObject;
2 grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
3var
4 Enum: IEnumFormatEtc;
5 FormatEtc: TFormatEtc;
6begin
7 OleCheck(DataObj.EnumFormatEtc(DATADIR_GET, Enum));
8 lvDisplay.Clear;
9 while Enum.Next(1, FormatEtc, nil) = S_OK do
10 DisplayDataInfo(FormatEtc);
11 dwEffect := DROPEFFECT_COPY;
12 Result := S_OK;
13end;
14
15procedure TForm1.DisplayDataInfo(const FmtEtc: TFormatEtc);
16var
17 LI: TListItem;
18begin
19 LI := lvDisplay.Items.Add;
20 LI.Caption := CBFormatDesc(FmtEtc.cfFormat);
21 LI.SubItems.Add(TymedDesc(FmtEtc.tymed));
22 LI.SubItems.Add(AspectDesc(FmtEtc.dwAspect));
23 LI.SubItems.Add(IntToStr(FmtEtc.lindex));
24end;
Listing 11
First we set the cursor to DROPEFFECT_COPY and clear the list view. Next we get a data format enumerator from the data object. Using the enumerator we get each supported data format and display information from it's TFormatEtc structure using a subsidiary method, DisplayDataInfo. Finally we return S_OK.
DisplayDataInfo simply adds a new item to the list view that displays information about the FmtEtc structure passed to the method as a parameter. The following helper routines are used to get the display information:
- CBFormatDesc – returns a description of the clipboard format specified by FmtEtc.cfFormat. The function can handle both built-in and custom clipboard formats.
- TymedDesc – returns the name of the TYMED_* constant that describes the value of FmtEtc.tymed.
- AspectDesc – returns the name of the DVASPECT_* constant describing FmtEtc.dwAspect.
These routines are not listed here because they are trivial and not relevant to the main subject. However the routines are supplied with the associated demo program.
Example 2: Displaying data from selected data formats
This example is more like somethings you may need to implement in a real world application – it checks the data object being dragged and decides whether it can accept the object or not. If it can accept the object the program displays a textual representation of the data.
Our application will accept the following data object types, all of which must be provided via a global memory storage medium:
- Plain text in the CF_TEXT format. Text dropped on the application will be displayed in the main window.
- HTML code in the custom "HTML format". The HTML source code will be displayed in full in the main window. The plain text version of the code will also be displayed if it is available.
The program will also attempt to move the data from the source application if the Shift key is held down. Otherwise if either no or any other modifier key is held down the data will be copied.
To begin with, start a new application and drop two TMemo controls onto the main form. Arrange them one above the other and name the top one "edText" and the lower one "edHTML". Now add the boilerplate code from Listing 9.
Before we get started on the code proper, recall that we will be handling "HTML Format" data objects. Since this is a custom format we need to register it. We do this by declaring a global variable named CF_HTML in the form unit's implementation section and by registering the format with Windows in the initialization section, storing the format identifier in CF_TEXT. Listing 12 illustrates the code.
1...
2
3implementation
4
5...
6
7var CF_HTML: TClipFormat;
8
9...
10
11initialization
12
13CF_HTML := RegisterClipboardFormat('HTML Format');
14
15end.
Listing 12
We will begin by considering the IDropTarget.DragEnter method, implemented as DropTargetDragEnter, as shown in Listing 13 below. The listing also shows two helper methods called by DropTargetDragEnter.
1function TForm1.DropTargetDragEnter(const dataObj: IDataObject;
2 grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
3begin
4 Result := S_OK;
5 fCanDrop := CanDrop(dataObj);
6 dwEffect := CursorEffect(dwEffect, grfKeyState);
7end;
8
9function TForm1.CanDrop(const DataObj: IDataObject): Boolean;
10begin
11 Result := DataObj.QueryGetData(MakeFormatEtc(CF_TEXT)) = S_OK;
12 if not Result then
13 Result := DataObj.QueryGetData(MakeFormatEtc(CF_HTML)) = S_OK;
14end;
15
16function TForm1.CursorEffect(const AllowedEffects: Longint;
17 const KeyState: Integer): Longint;
18begin
19 Result := DROPEFFECT_NONE;
20 if fCanDrop then
21 begin
22 if (KeyState and MK_SHIFT = MK_SHIFT) and
23 (DROPEFFECT_MOVE and AllowedEffects = DROPEFFECT_MOVE) then
24 Result := DROPEFFECT_MOVE
25 else if (DROPEFFECT_COPY and AllowedEffects = DROPEFFECT_COPY) then
26 Result := DROPEFFECT_COPY;
27 end;
28end;
Listing 13
After setting the required return value in DropTargetDragEnter, the first thing we do is check if the data object can be dropped, i.e. if it supports either of the data formats we are interested in. We hand this decision off to CanDrop and store the result in the private fCanDrop field for use later (in DropTargetDragOver).
CanDrop simply queries the data object to see if it supports the CF_TEXT data format. If it doesn't the data object is queried again for the HTML format. If neither format is supported then false is returned. Note that CanDrop calls the convenience MakeFormatEtc method that simply constructs an appropriate TFormatEtc structure to pass to IDataObject.QueryGetData.
Back in DropTargetDragEnter the next thing we do is to determine the required cursor effect. Again this is handed off to a helper method, CursorEffect. DropTargetDragEnter passes the bitmask of allowed cursor effects, from its dwEffect parameter, along with the keyboard and mouse state, from the grfKeyState parameter, to CursorEffect. This method calculates the required cursor effect according to the following rules:
- If the data object does not support the required data formats return DROPEFFECT_NONE.
- If the Shift key is pressed and the DROPEFFECT_MOVE effect is allowed return DROPEFFECT_MOVE.
- If any other, or no, modifier keys are pressed, or if DROPEFFECT_MOVE is reqested but not allowed, DROPEFFECT_COPY is returned.
Listing 14 shows the next IDropTarget method, DragOver, implemented as DropTargetDragOver.
1function TForm1.DropTargetDragOver(grfKeyState: Integer; pt: TPoint;
2 var dwEffect: Integer): HResult;
3begin
4 Result := S_OK;
5 dwEffect := CursorEffect(dwEffect, grfKeyState);
6end;
Listing 14
All we do here is set the cursor effect once again, using the current key state from the grfKeyState parameter and the permitted cursor effects from dwEffect. The cursor state may change between calls to this method because the pressed modifier keys may have changed.
Next up is DragLeave, implemented as DropTargetDragLeave. As is shown by Listing 15, this method is unchanged from the boilerplate code.
1function TForm1.DropTargetDragLeave: HResult;
2begin
3 Result := S_OK;
4end;
Listing 15
The final IDropTarget method to consider is Drop, implemented as DropTargetDrop. Listing 16 shows this method along with helper methods that are used to display the data.
1function TForm1.DropTargetDrop(const dataObj: IDataObject;
2 grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
3begin
4 Result := S_OK;
5 fCanDrop := CanDrop(dataObj);
6 dwEffect := CursorEffect(dwEffect, grfKeyState);
7 DisplayData(dataObj);
8end;
9
10procedure TForm1.DisplayData(const DataObj: IDataObject);
11begin
12 edText.Text := GetTextFromObj(DataObj, CF_TEXT);
13 edHTML.Text := GetTextFromObj(DataObj, CF_HTML);
14end;
15
16function TForm1.GetTextFromObj(const DataObj: IDataObject;
17 const Fmt: TClipFormat): string;
18var
19 Medium: TStgMedium;
20 PText: PChar;
21begin
22 if DataObj.GetData(MakeFormatEtc(Fmt), Medium) = S_OK then
23 begin
24 Assert(Medium.tymed = MakeFormatEtc(Fmt).tymed);
25 try
26 PText := GlobalLock(Medium.hGlobal);
27 try
28 Result := PText;
29 finally
30 GlobalUnlock(Medium.hGlobal);
31 end;
32 finally
33 ReleaseStgMedium(Medium);
34 end;
35 end
36 else
37 Result := '';
38end;
Listing 16
DropTargetDrop starts by re-checking data object to see if we can handle it and then sets the drop cursor. Finally it calls DisplayData to display the data from the dropped data object.
DisplayData simply sets the text of the two memo controls to the strings returned from GetTextFromObj. GetTextFromObj is called twice, once to retrieve any plain text (CF_TEXT) and again to retrieve any HTML Format (CF_HTML) data as text from the data object. GetTextFromObj will return the empty string if the requested data format is not available.
GetTextFromObj extracts text data from the data object in the format specified in its Fmt parameter. It does this by calling MakeFormatEtc to create a TFormatEtc structure that describes the desired clipboard format. This structure is passed to the data object's GetData method. If the method returns a value other than S_OK then the requested format is not supported and the empty string is returned. Otherwise the now familiar code to retrieve text from a global memory handle is executed and the text is returned.
All that remains to do now is to implement the MakeFormatEtc helper method referred to above. The method is presented in Listing 17 below.
1function TForm1.MakeFormatEtc(const Fmt: TClipFormat): TFormatEtc;
2begin
3 Result.cfFormat := Fmt;
4 Result.ptd := nil;
5 Result.dwAspect := DVASPECT_CONTENT;
6 Result.lindex := -1;
7 Result.tymed := TYMED_HGLOBAL;
8end;
Listing 17
All the method does is set up and return a TFormatEtc structure that requires a TYMED_HGLOBAL medium type for the clipboard format passed in the method's Fmt parameter. The code should be familiar by now.
Our examination of how to catch data dragged and dropped from other applications is now complete.
Demo Code
A demo program to accompany this article can be found in the delphidabbler/article-demos
Git repository on GitHub.
You can view the code in the article-24
sub-directory. Alternatively download a zip file containing all the demos by going to the repository's landing page and clicking the Clone or download button and selecting Download ZIP.
The demo includes the complete source to the two examples presented in the previous section See the demo's README.md file for details.
This source code is merely a proof of concept and is intended only to illustrate this article. It is not designed for use in its current form in finished applications. The code is provided on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.
The demo is open source. See the demo's LICENSE.md file for licensing details.
Going Further
While the code presented in this article works fine, it does rather clutter up the code of the main form. A more tidy, but slightly more complex, solution is to implement IDropTarget in a separate class and to call back to the main program to determine whether a dragged object can be dropped and how to configure the drag cursor. It is also possible to wrap up the code that interogates and reads data objects into a separate class.
My GUI program for the PasHi Pascal Syntax Highlighter uses OLE drag-drop handling and isolates the IDropTarget implementation in its own class. If you are interested in this approach please feel free to download and examine the program's source code.
Summary
This article has provided an introduction to working with OLE Drag and Drop and has shown how to implement IDropTarget and how interogate and extract some kinds of data from a data object via its IDataObject interface. The article also showed how to register a window to receive OLE drag-drop notifications by associating an IDropTarget implementation with the window.
In addition, a demo providing source code of the two examples was also made available.
Finally some suggestions were made about to how to improve the code by isolating the IDropTarget code in its own class.
Feedback
I hope you found this article useful.
If you have any observations, comments, or have found any errors there are two places you can report them.
- For anything to do with the article content, but not the downloadable demo code, please use this website's Issues page on GitHub. Make sure you mention that the issue relates to "article #24".
- For bugs in the demo code see the
article-demo
project's README.md
file for details of how to report them.