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:
-
Use the ShellAPI
unit to get access to the required API routines:
-
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
-
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
-
In the WM_DROPFILES message handler use the DragQueryXXX API functions to retrieve information about the file drop – see the explanation below.
-
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:
-
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:
-
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!
-
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.
-
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
4 DragAcceptFiles(Self.Handle, True);
5
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;
4 DroppedFileCount: Integer;
5 FileNameLength: Integer;
6 FileName: string;
7 I: Integer;
8 DropPoint: TPoint;
9begin
10 inherited;
11
12 DropH := Msg.Drop;
13 try
14
15 DroppedFileCount := DragQueryFile(DropH, $FFFFFFFF, nil, 0);
16
17 for I := 0 to Pred(DroppedFileCount) do
18 begin
19
20 FileNameLength := DragQueryFile(DropH, I, nil, 0);
21
22 SetLength(FileName, FileNameLength);
23
24 DragQueryFile(DropH, I, PChar(FileName), FileNameLength + 1);
25
26
27 end;
28
29 DragQueryPoint(DropH, DropPoint);
30
31 finally
32
33
34 DragFinish(DropH);
35 end;
36
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
4 DragAcceptFiles(Self.Handle, False);
5
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;
4 DropPoint: TPoint;
5 Catcher: TFileCatcher;
6begin
7 inherited;
8 Catcher := TFileCatcher.Create(Msg.Drop);
9 try
10 for I := 0 to Pred(Catcher.FileCount) do
11 begin
12
13 end;
14 DropPoint := Catcher.DropPoint;
15
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.