Skip over navigation

How to receive data dragged from other applications

Contents

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.

Understanding Data Formats

In this section we will investigate various data formats used to transfer data from IDataObject instances. There are four different attributes of data formats to be considered:

  1. The data format

    Just like the clipboard a data object can store data in numerous different formats. In fact, data objects use the same way of describing data formats as that used by the clipboard. A format may be one of the built-in ones such as CF_TEXT or an application defined format such as "Rich Text Format".

  2. The storage medium used to transfer the data

    Data can be delivered via various different mechanisms. These mechanisms are described by the TYMED_* enumeration and are:

    TYMED_HGLOBAL
    The data is transferred via global memory accessed via a global memory handle of type HGLOBAL.
    TYMED_FILE
    The data is transferred via a disk file.
    TYMED_ISTREAM
    The data is transferred as a stream accessed through an IStream interface.
    TYMED_ISTORAGE
    The data is transferred in a Windows storage object accessed through an IStorage interface.
    TYMED_GDI
    The data is specified by a GDI component accessed via a HBITMAP handle.
    TYMED_MFPICT
    The data is a metafile accessed via a HMETAFILEPICT handle.
    TYMED_ENHMF
    The data is an enhanced metafile accessed via a HENHMETAFILE handle.

    The way we access the data depends on the storage medium. By far the most common storage medium is the global memory handle and we will concentrate mainly on that mechanism in this article.

  3. The "Aspect", or view of the data

    This indicates how much detail should be provided when rendering the data. The possible aspects are:

    • a full representation as an embedded object,
    • a thumbnail view,
    • an icon view,
    • a print preview.

    The first of these is the norm and the only one considered in this article.

  4. The target device

    The type of device the data is targetted at can also be specified. However, the usual case is that the data is device independent and this is the only case considered by this article.

Windows encapsulates all this information inside a TFormatEtc structure, defined in the ActiveX unit. Listing 3 reproduces the definitions.

  1type
  2  tagFORMATETC = record
  3    cfFormat: TClipFormat;
  4    ptd: PDVTargetDevice;
  5    dwAspect: Longint;
  6    lindex: Longint;
  7    tymed: Longint;
  8  end;
  9  TFormatEtc = tagFORMATETC;
Listing 3

The fields are discussed briefly below:

cfFormat

Tells us about the data format and is a clipboard format identifier.

ptd

Informs about the target device and is a structure of type DVTARGETDEVICE. The value of this field is nil if the data is device independent. We will always set this value to nil in this article.

dwAspect

The view aspect. In all the cases we will consider, this value will be DVASPECT_CONTENT meaning we should represent the data as an embedded object. Other possible values are DVASPECT_THUMBNAIL, DVASPECT_ICON and DVASPECT_DOCPRINT.

lindex

Tells us when the view aspect must be split across page boundaries. Often, and in all cases considered here, lindex is -1 indicating that all the data should be displayed.

tymed

Describes the media type of the storage medium. This is one of the TYMED_* enumeration values discussed above.

See Delphi's Windows API help file for more detailed information.

We will use TFormatEtc structures in one of two ways:

  1. We will examine structures filled in by Windows to learn about a data object's format.
  2. We will set up our own structures to request objects in a required format.

That concludes the discussion of data objects. Armed with this knowledge we can move on to discuss how to interogate, and extract data from, data objects.

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;                    // device independent
  8  FmtEtc.dwAspect := DVASPECT_CONTENT;  // full detail
  9  FmtEtc.lindex := -1;                  // all the data
 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    // Do something with FormatEtc here
 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    // Do something with the data from Medium
 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;         // specifies required data format
  5  Medium: TStgMedium;         // storage medium containing file list
  6  DroppedFileCount: Integer;  // number of dropped files
  7  I: Integer;                 // loops thru dropped files
  8  FileNameLength: Integer;    // length of a dropped file name
  9  FileName: string;           // name of a dropped file
 10begin
 11  // Get required storage medium from data object
 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      // Get count of files dropped
 21      DroppedFileCount := DragQueryFile(
 22        Medium.hGlobal, $FFFFFFFF, nil, 0
 23      );
 24      // Get name of each file dropped and process it
 25      for I := 0 to Pred(DroppedFileCount) do
 26      begin
 27        // get length of file name, then name itself
 28        FileNameLength := DragQueryFile(Medium.hGlobal, I, nil, 0);
 29        SetLength(FileName, FileNameLength);
 30        DragQueryFile(
 31          Medium.hGlobal, I, PChar(FileName), FileNameLength + 1
 32        );
 33        // add file name to list
 34        FileList.Add(FileName);
 35      end;
 36    finally
 37      // Tidy up - release the drop handle
 38      // don't use DropH again after this
 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    { IDropTarget methods }
 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; // identifier for HTML clipboard format
  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.

  1. 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".
  2. For bugs in the demo code see the article-demo project's README.md file for details of how to report them.