Skip over navigation

Welcome to the all new DelphiDabbler website.

I hope you like it.

There are going to be bugs. Please report any you find on the website's issues page (GitHub account required). Thanks.

Click the close button and you won't see this message again.

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:

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

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

procedure TForm1.WMDropFiles(var Msg: TWMDropFiles);
var
  DropH: HDROP;               // drop handle
  DroppedFileCount: Integer;  // number of files dropped
  FileNameLength: Integer;    // length of a dropped file name
  FileName: string;           // a dropped file name
  I: Integer;                 // loops thru all dropped files
  DropPoint: TPoint;          // point where files dropped
begin
  inherited;
  // Store drop handle from the message
  DropH := Msg.Drop;
  try
    // Get count of files dropped
    DroppedFileCount := DragQueryFile(DropH, $FFFFFFFF, nil, 0);
    // Get name of each file dropped and process it
    for I := 0 to Pred(DroppedFileCount) do
    begin
      // get length of file name
      FileNameLength := DragQueryFile(DropH, I, nil, 0);
      // create string large enough to store file
      SetLength(FileName, FileNameLength);
      // get the file name
      DragQueryFile(DropH, I, PChar(FileName), FileNameLength + 1);
      // process file name (application specific)
      // ... processing code here
    end;
    // Optional: Get point at which files were dropped
    DragQueryPoint(DropH, DropPoint);
    // ... do something with drop point here
  finally
    // Tidy up - release the drop handle
    // don't use DropH again after this
    DragFinish(DropH);
  end;
  // Note we handled message
  Msg.Result := 0;
end;
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:

procedure TForm1.FormDestroy(Sender: TObject);
begin
  // ... other code here
  DragAcceptFiles(Self.Handle, False);
  // ... other code here
end;
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.

type
  TFileCatcher = class(TObject)
  private
    fDropHandle: HDROP;
    function GetFile(Idx: Integer): string;
    function GetFileCount: Integer;
    function GetPoint: TPoint;
  public
    constructor Create(DropHandle: HDROP);
    destructor Destroy; override;
    property FileCount: Integer read GetFileCount;
    property Files[Idx: Integer]: string read GetFile;
    property DropPoint: TPoint read GetPoint;
  end;
Listing 9
constructor TFileCatcher.Create(DropHandle: HDROP);
begin
  inherited Create;
  fDropHandle := DropHandle;
end;
 
destructor TFileCatcher.Destroy;
begin
  DragFinish(fDropHandle);
  inherited;
end;
 
function TFileCatcher.GetFile(Idx: Integer): string;
var
  FileNameLength: Integer;
begin
  FileNameLength := DragQueryFile(fDropHandle, Idx, nil, 0);
  SetLength(Result, FileNameLength);
  DragQueryFile(fDropHandle, Idx, PChar(Result), FileNameLength + 1);
end;
 
function TFileCatcher.GetFileCount: Integer;
begin
  Result := DragQueryFile(fDropHandle, $FFFFFFFF, nil, 0);
end;
 
function TFileCatcher.GetPoint: TPoint;
begin
  DragQueryPoint(fDropHandle, Result);
end;
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:

procedure TForm1.WMDropFiles(var Msg: TWMDropFiles);
var
  I: Integer;                 // loops thru all dropped files
  DropPoint: TPoint;          // point where files dropped
  Catcher: TFileCatcher;      // file catcher class
begin
  inherited;
  Catcher := TFileCatcher.Create(Msg.Drop);
  try
    for I := 0 to Pred(Catcher.FileCount) do
    begin
      // ... code to process file here
    end;
    DropPoint := Catcher.DropPoint;
    // ... do something with drop point
  finally
    Catcher.Free;
  end;
  Msg.Result := 0;
end;
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.txt 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 code is open source – see the source files for any licensing terms.