Skip over navigation

How to catch files dragged and dropped on an application from Explorer

Contents

Why do it?

Many programs allow the user to open files by dragging them from Explorer and dropping them on the program's window. This is often more convenient to the user than using the normal File | Open menu option or toolbar button, since it misses out the step of navigating an Open File dialog box. Adding similar support to your program makes it appear more professional.

How it's done

There are two main ways to add support to a Windows application. The first is to use Windows drag and drop API functions and handle a related Windows message. The second method uses OLE (COM), and additionally supports inter and intra-application drag and drop. We'll explore the first, and most simple, of these methods here.

Overview

File drag and drop is not enabled in Windows applications by default. To support it we need to tell Windows we want to be notified about file drops, and nominate a window to receive the notifications. The nominated window has to handle the WM_DROPFILES message that Windows sends us when a drop occurs. The message supplies us with a "drop handle" that we pass to various API routines to get information about what was dropped. When we've finished good manners require that we tell Windows we no longer need to be told about drops.

Here is a step by step guide to what we need to do in a Delphi application to handle dropped files:

  1. Use the ShellAPI unit to get access to the required API routines:

    uses ShellAPI
    Listing 1
  2. Call DragAcceptFiles, passing the handle of the window that is to receive WM_DROPFILES messages, along with a flag to indicate we want to receive notifications. For example, for our main program form to receive the messages use:

    DragAcceptFiles(Form1.Handle, True);
    Listing 2
  3. Handle the WM_DROPFILES message. In Delphi we can declare a message handler for this event in the main form's class declaration:

    procedure WMDropFiles(var Msg: TWMDropFiles); message WM_DROPFILES;
    Listing 3
  4. In the WM_DROPFILES message handler use the DragQueryXXX API functions to retrieve information about the file drop – see the explanation below.

  5. Notify Windows we have finished with the supplied drop handle. We do this by calling DragFinish, passing it the drop handle we retrieved from the WM_DROPFILES message. This frees the resources used to store information about the drop:

    DragFinish(DropH);
    Listing 4
  6. When we are closing down our application we call DragAcceptFiles again, but this time with a final parameter of False to let Windows know we're no longer interested in handling file drops:

    DragAcceptFiles(Form1.Handle, False);
    Listing 5

Getting information about dropped files

We use two API function to get information about dropped files – DragQueryFile and DragQueryPoint. Both of these require the drop handle from the WM_DROPFILE messages.

DragQueryFile – jack of all trades

Like many Windows API functions, DragQueryFile can perform several functions depending on the parameters passed to it. This makes it hard to remember exactly how to use it. Its parameters (in Delphi-speak) are:

  • DropHandle: HDROP – the drop handle provided by the WM_DROPFILES message.
  • Index: Integer – the index of the file to query in the list of dropped files.
  • FileName: PChar – pointer to a dropped file name.
  • BufSize: Integer – size of buffer for the above.

The three tasks the function performs are:

  1. Finding the number of files dropped.

    We get this information by passing $FFFFFFFF as the Indexparameter, nil as the file name parameter and 0 as the BufSize parameter. The return value is the number of files dropped. Obvious isn't it!

  2. Finding the size of buffer needed to store the file name at a specific (zero based) index.

    We pass the file's index number in the Index parameter, nil for the file name and 0 for the buffer size. The function then returns the buffer size required.

  3. Fetching the name of the file at a given index.

    We pass the index number, a PChar buffer large enough to hold the file name (with #0 terminator), and the actual buffer size.

All this makes for extremely unreadable code. Later in the article we'll hide it all away in a class.

DragQueryPoint

This function is quite an anti-climax after DragQueryFile – it simply does what it says and provides the mouse cursor position where the files were dropped. The parameters are:

  • DropHandle: HDROP – the drop handle from WM_DROPFILES.
  • var Point: TPoint – reference to a TPoint structure that receives the required point. The function returns non-zero if the drop was in the window's client area and zero if it wasn't.

Putting it all together

Let's pull all we've discovered together in a skeleton Delphi application whose main form, Form1, needs to be able to catch and process dropped files.

In our form creation event handler we register our interest with Windows:

  1procedure TForm1.FormCreate(Sender: TObject);
  2begin
  3  // ... other code here
  4  DragAcceptFiles(Self.Handle, True);
  5  // ... other code here
  6end;
Listing 6

The meat of the processing comes in the WM_DROPFILES event handler:

  1procedure TForm1.WMDropFiles(var Msg: TWMDropFiles);
  2var
  3  DropH: HDROP;               // drop handle
  4  DroppedFileCount: Integer;  // number of files dropped
  5  FileNameLength: Integer;    // length of a dropped file name
  6  FileName: string;           // a dropped file name
  7  I: Integer;                 // loops thru all dropped files
  8  DropPoint: TPoint;          // point where files dropped
  9begin
 10  inherited;
 11  // Store drop handle from the message
 12  DropH := Msg.Drop;
 13  try
 14    // Get count of files dropped
 15    DroppedFileCount := DragQueryFile(DropH, $FFFFFFFF, nil, 0);
 16    // Get name of each file dropped and process it
 17    for I := 0 to Pred(DroppedFileCount) do
 18    begin
 19      // get length of file name
 20      FileNameLength := DragQueryFile(DropH, I, nil, 0);
 21      // create string large enough to store file
 22      SetLength(FileName, FileNameLength);
 23      // get the file name
 24      DragQueryFile(DropH, I, PChar(FileName), FileNameLength + 1);
 25      // process file name (application specific)
 26      // ... processing code here
 27    end;
 28    // Optional: Get point at which files were dropped
 29    DragQueryPoint(DropH, DropPoint);
 30    // ... do something with drop point here
 31  finally
 32    // Tidy up - release the drop handle
 33    // don't use DropH again after this
 34    DragFinish(DropH);
 35  end;
 36  // Note we handled message
 37  Msg.Result := 0;
 38end;
Listing 7

First we record the drop handle in a local variable for convenience and then make the first of those confusing calls to DragQueryFile to get the number of files dropped. Now we loop through each file by index (zero based, remember) and get the file names. For each file name we first get the required buffer size using the second form of DragQueryFile, then create a sufficiently large string by calling SetLength. We finally read the string into the buffer using the third form of DragQueryFile.

After reading all the file names we then get the drop point. Once all the processing is done we call DragFinish to release the drop handle. Notice we do this in a finally section since the drop handle is a resource that we need to ensure is freed, even if there is an exception. We end by settting the message result to 0 to indicate to Windows that we have handled it.

Finally, we unregister our interest in file drops in the form destruction event handler:

  1procedure TForm1.FormDestroy(Sender: TObject);
  2begin
  3  // ... other code here
  4  DragAcceptFiles(Self.Handle, False);
  5  // ... other code here
  6end;
Listing 8

The Delphi Way

If you agree with me that all the messing about with API routines rather spoils our Delphi code, what better than to make a helper class to hide a lot of the mess. Here's a small class that can be created and destroyed within the WM_DROPFILES message handler. The class hides away a lot of the code, although we must still handle WM_DROPFILES ourselves. We must also notify Windows of our intention to accept drag drop.

The class's declaration appears in Listing 9 while Listing 10 has the implementation.

  1type
  2  TFileCatcher = class(TObject)
  3  private
  4    fDropHandle: HDROP;
  5    function GetFile(Idx: Integer): string;
  6    function GetFileCount: Integer;
  7    function GetPoint: TPoint;
  8  public
  9    constructor Create(DropHandle: HDROP);
 10    destructor Destroy; override;
 11    property FileCount: Integer read GetFileCount;
 12    property Files[Idx: Integer]: string read GetFile;
 13    property DropPoint: TPoint read GetPoint;
 14  end;
Listing 9
  1constructor TFileCatcher.Create(DropHandle: HDROP);
  2begin
  3  inherited Create;
  4  fDropHandle := DropHandle;
  5end;
  6 
  7destructor TFileCatcher.Destroy;
  8begin
  9  DragFinish(fDropHandle);
 10  inherited;
 11end;
 12 
 13function TFileCatcher.GetFile(Idx: Integer): string;
 14var
 15  FileNameLength: Integer;
 16begin
 17  FileNameLength := DragQueryFile(fDropHandle, Idx, nil, 0);
 18  SetLength(Result, FileNameLength);
 19  DragQueryFile(fDropHandle, Idx, PChar(Result), FileNameLength + 1);
 20end;
 21 
 22function TFileCatcher.GetFileCount: Integer;
 23begin
 24  Result := DragQueryFile(fDropHandle, $FFFFFFFF, nil, 0);
 25end;
 26 
 27function TFileCatcher.GetPoint: TPoint;
 28begin
 29  DragQueryPoint(fDropHandle, Result);
 30end;
Listing 10

There is not much to explain, given the code we have already seen. The constructor takes a drop handle and records it. The drop handle is "freed" in the destructor with a call to DragFinish. The list of dropped files, the number of files dropped and the drop point are all provided as properties. The accessor methods for the properties are simply wrappers round the DragQueryFile and DragQueryPoint API functions.

Let us rewrite our original WM_DROPFILES message handler to use the new class:

  1procedure TForm1.WMDropFiles(var Msg: TWMDropFiles);
  2var
  3  I: Integer;                 // loops thru all dropped files
  4  DropPoint: TPoint;          // point where files dropped
  5  Catcher: TFileCatcher;      // file catcher class
  6begin
  7  inherited;
  8  Catcher := TFileCatcher.Create(Msg.Drop);
  9  try
 10    for I := 0 to Pred(Catcher.FileCount) do
 11    begin
 12      // ... code to process file here
 13    end;
 14    DropPoint := Catcher.DropPoint;
 15    // ... do something with drop point
 16  finally
 17    Catcher.Free;
 18  end;
 19  Msg.Result := 0;
 20end;
Listing 11

Going further

The TFileCatcher class could be extended to encapsulate all of the drop files functionality, hiding away all the API calls. To do this would require access to the form's window so we could intercept its messages and respond to WM_DROPFILES. One way to do this is to subclass the form's window. It is beyond the scope of this article to look at how that can be done. However, if you want to invetigate further, please check out my Drop Files components.

Demo Application

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-11 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 is described in the README.md file that is included with the source code.

The code has been tested with Delphi 4 and Delphi 7.

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.

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