Introduction
I've seen several questions around the CodeProject boards lately asking about doing drag and drop between a program and Explorer windows. Like many things in Windows, it seems easy once you know how it's done, but finding the answer can be quite a chore. In this article, I demonstrate how to hook up drag and drop so your program can accept drops from an Explorer window, and be a drag source so your users can drag files into an Explorer window.
The sample project is an MFC app, and the article assumes you're familiar with C++, MFC, and using COM objects and interfaces. If you need help with COM objects and interfaces, check out my Intro to COM article. The program is MultiFiler, a little utility that acts like a drag and drop "staging area". You can drag any number of files into MultiFiler, and it shows them all in a list. You can then drag files back out to Explorer windows, using the Shift or Ctrl keys to tell Explorer to move or copy the original files, respectively.
Drag and Drop with Explorer
As you know, Explorer lets you drag files among Explorer windows and the desktop. When you begin a drag operation, the Explorer window you drag from (the drop source), creates a COM object implementing the IDataObject
interface, and puts some data into the object. The window you drag into (the drop target), then reads that data using IDataObject
methods; that's how it knows what files are being dropped.
If you check out the data contained in the IDataObject
with a viewer like ClipSpy, you'll see that Explorer puts several data formats in the data object:
The important format is CF_HDROP
. The other formats are custom formats registered by Explorer for its own use. If we write an app that registers its window as a drop target, and if we know how to read CF_HDROP
data, we'll be able to accept dropped files. Similarly, if we can fill a data object with CF_HDROP
data, Explorer will let our app be a drag source. So, what's contained in that CF_HDROP
format? Read on...
The DROPFILES data structure
So what exactly is the CF_HDROP
format? It turns out that it's just a DROPFILES
struct
. There's also the HDROP
type, which is simply a pointer to a DROPFILES
struct
.
DROPFILES
isn't a very complex structure. Here is its definition:
struct DROPFILES
{
DWORD pFiles;
POINT pt;
BOOL fNC;
BOOL fWide;
};
The one thing that isn't listed in the struct
definition is the list of filenames. The list is formatted as a double-null terminated list of strings. But where is it stored? It is actually stored right after the fWide
member, and pFiles
holds the offset (relative to the beginning of the struct
) where the list is located in memory. The only other member that's used in drag and drop is fWide
, which indicates whether the filenames are in ANSI or Unicode characters.
Accepting a drag and drop from Explorer
Accepting a drag and drop is much easier than initiating one, so I'll cover accepting first.
There are two ways for your window to accept drag and drop. The first way is a holdover from Windows 3.1 and uses the WM_DROPFILES
message. The other way is to register your window as an OLE drop target.
The old way - WM_DROPFILES
To use the old method, you first set the "accept files" style in your window. For dialogs, this is on the "Extended Styles" page, as shown here:
If you want to set this style at runtime, call the DragAcceptFiles()
API, which takes two parameters. The first is your main window handle, and the second is TRUE
to indicate you can accept drag and drop. If your main window is a CView
instead of a dialog, you'll need to set this style at runtime.
No matter which of the two methods you use, your window becomes a drop target. When you drag files or folders from an Explorer window and drop them in your window, the window receives a WM_DROPFILES
message. The WPARAM
of a WM_DROPFILES
message is an HDROP
that lists what files are being dropped. There are three APIs you use to get the file list out of the HDROP
: DragQueryFile()
, DragQueryPoint()
, and DragFinish()
.
DragQueryFile()
does two things: returns the number of files being dragged, and enumerates through the list of files. DragQueryPoint()
returns the pt
member of the DROPFILES
struct
. DragFinish()
frees up memory allocated during the drag and drop process.
DragQueryFile()
takes four parameters: The HDROP
, the index of the filename to return, a buffer allocated by the caller to hold the name, and the size of the buffer in characters. If you pass -1 as the index, DragQueryFile()
returns the number of files in the list. Otherwise, it returns the number of characters in the filename. You can test this return against 0 to tell if the call succeeded.
DragQueryPoint()
takes two parameters, the HDROP
and a pointer to a POINT
struct
that receives the value in the pt
member of the DROPFILES
struct
. DragFinish()
just takes one parameter, the HDROP
.
A typical WM_DROPFILES
handler would look like this:
void CMyDlg::OnDropFiles ( HDROP hdrop )
{
UINT uNumFiles;
TCHAR szNextFile [MAX_PATH];
uNumFiles = DragQueryFile ( hdrop, -1, NULL, 0 );
for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
{
if ( DragQueryFile ( hdrop, uFile, szNextFile, MAX_PATH ) > 0 )
{
}
}
DragFinish ( hdrop );
}
DragQueryPoint()
isn't necessary if all you want is the list of files. (Actually, I've never had to use it myself.)
The new way - using an OLE drop target
The other method of accepting drag and drop is to register your window as an OLE drop target. Normally, doing so would require that you write a C++ class that implements the IDropTarget
interface. However, MFC has a COleDropTarget
class that takes care of that for us. The process is a bit different depending on whether your main window is a dialog or a CView
, so I'll cover both below.
Making a CView a drop target
CView
already has some drag and drop support built-in, however it's not normally activated. To activate it, you add a COleDropTarget
member variable to the view, and then call its Register()
function in your view's OnInitialUpdate()
to make the view a drop target, as shown below:
void CMyView::OnInitialUpdate()
{
CView::OnInitialUpdate();
m_droptarget.Register ( this );
}
Once that's done, you then override four virtual functions that are called when the user drags over your view:
OnDragEnter()
: Called when the cursor enters your window.
OnDragOver()
: Called when the cursor moves inside your window.
OnDragLeave()
: Called when the cursor leaves your window.
OnDrop()
: Called when the user drops in your window.
OnDragEnter()
OnDragEnter()
is the first function called. Its prototype is:
DROPEFFECT CView::OnDragEnter( COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point );
The parameters are:
pDataObject
: Pointer to a COleDataObject
that contains the data being dragged.
dwKeyState
: A set of flags indicating which mouse button is clicked and which shift keys (if any) are pressed. The flags are MK_CONTROL
, MK_SHIFT
, MK_ALT
, MK_LBUTTON
, MK_MBUTTON
, and MK_RBUTTON
.
point
: The cursor position, expressed in the view's client coordinates.
OnDragEnter()
returns a DROPEFFECT
value, which tells OLE whether the drop will be accepted, and if so, what cursor should be displayed. The values and their meanings are:
DROPEFFECT_NONE
: The drop will not be accepted. The cursor changes to: .
DROPEFFECT_MOVE
: The data will be moved by the drop target. The cursor changes to: .
DROPEFFECT_COPY
: The data will be copied by the drop target. The cursor changes to:
DROPEFFECT_LINK
: The data will be linked to by the drop target. The cursor changes to: .
Normally, in OnDragEnter()
you examine the data being dragged and see if it meets your criteria. If not, you return DROPEFFECT_NONE
to reject the drag and drop. Otherwise, you can return one of the other values depending on what you intend to do with the data.
OnDragOver()
If you return a value other than DROPEFFECT_NONE
from OnDragEnter()
, OnDragOver()
is called whenever the mouse cursor moves within your window. The prototype of OnDragOver()
is:
DROPEFFECT CView::OnDragOver ( COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point );
The parameters and return value are identical to OnDragEnter()
. OnDragOver()
lets you return different DROPEFFECT
values depending on the cursor position and the shift key state. For instance, if your main view window has several areas, displaying different lists of information, and you want to allow drops only in one part, you'd check the cursor position in the point
parameter, and return DROPEFFECT_NONE
if the cursor is not in that area.
As for the shift keys, you normally react to them as described below:
- SHIFT pressed (
MK_SHIFT
in dwKeyState
): Return DROPEFFECT_MOVE
.
- CONTROL pressed (
MK_CONTROL
): Return DROPEFFECT_COPY
.
- Both pressed (
MK_SHIFT | MK_CONTROL
): Return DROPEFFECT_LINK
.
These are only guidelines, although it's best to adhere to them, since they are what Explorer uses. But if some of the actions (copy, move, or link) doesn't make sense for your app, you don't have to return the corresponding DROPEFFECT
. For instance, in MultiFiler (I'll get to it, I promise!) OnDragOver()
always returns DROPEFFECT_COPY
. Just be sure to return the right value, so that the cursor accurately indicates to the user, what will happen if he drops in your window.
OnDragLeave()
OnDragLeave()
is called if the user drags out of your window without dropping. The prototype is:
void CView::OnDragLeave();
It has no parameters or return value - its purpose is to let you clean up any memory you allocated during OnDragEnter()
and OnDragOver()
.
OnDrop()
If the user drops over your window (and you didn't return DROPEFFECT_NONE
from the most recent call to OnDragOver()
), then OnDrop()
is called so you can act on the drag and drop. The prototype of OnDrop()
is:
BOOL CView::OnDrop ( COleDataObject* pDataObject,
DROPEFFECT dropEffect, CPoint point );
The dropEffect
parameter is equal to the last return value from OnDragOver()
, and the others are the same as OnDragEnter()
. The return value is TRUE
if the drop is completed successfully (it's up to you to define what a "successful" completion is), or FALSE
if not.
OnDrop()
is where all the action happens - you can act on the dropped data in whatever way makes sense for your app. In MultiFiler, the dropped files are added to the main window's list control.
Making a dialog a drop target
Things are a bit more difficult if your main window is a dialog (or anything not derived from CView
). Since the base COleDropTarget
implementation is designed to work only with a CView
-derived window, you need to derive a new class from COleDropTarget
and override the four methods outlined above.
A typical COleDropTarget
-derived class declaration looks like this:
class CMyDropTarget : public COleDropTarget
{
public:
DROPEFFECT OnDragEnter ( CWnd* pWnd, COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point );
DROPEFFECT OnDragOver ( CWnd* pWnd, COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point );
BOOL OnDrop ( CWnd* pWnd, COleDataObject* pDataObject,
DROPEFFECT dropEffect, CPoint point );
void OnDragLeave ( CWnd* pWnd );
CMyDropTarget ( CMyDialog* pMainWnd );
virtual ~CMyDropTarget();
protected:
CMyDialog* m_pParentDlg;
};
In this example, the constructor is passed a pointer to the main window, so the drop target methods can send messages and do other stuff in the dialog. You can change this to whatever best suits your needs. You then implement the four drag and drop methods as described in the previous section. The only difference is the additional CWnd*
parameter, which is a pointer to the window that the cursor is over at the time of the call.
Once you have this new class, you add a drop target member variable to your dialog and call its Register()
function in OnInitDialog()
:
BOOL CMyDialog::OnInitDialog()
{
m_droptarget.Register ( this );
}
Accessing the HDROP data in a CDataObject
If you use an OLE drop target, your drag and drop functions receive a pointer to a COleDataObject
. This is an MFC class that implements IDataObject
and contains all of the data that the drag source created when the drag began. You'll need a bit of code to look for CF_HDROP
data in the data object and get an HDROP
handle. Once you have an HDROP
, you can use DragQueryFile()
, as shown earlier, to read the list of dropped files.
Here is the code to get an HDROP
from a COleDataObject
:
BOOL CMyDropTarget::OnDrop ( CWnd* pWnd, COleDataObject* pDataObject,
DROPEFFECT dropEffect, CPoint point )
{
HGLOBAL hg;
HDROP hdrop;
hg = pDataObject->GetGlobalData ( CF_HDROP );
if ( NULL == hg )
return FALSE;
hdrop = (HDROP) GlobalLock ( hg );
if ( NULL == hdrop )
{
GlobalUnlock ( hg );
return FALSE;
}
GlobalUnlock ( hg );
}
Summary of the two methods
Handling WM_DROPFILES
:
- Holdover from Windows 3.1; it could conceivably be removed in the future.
- No customization of the drag and drop process possible - you can only act after the drop occurs.
- Doesn't let you inspect the raw data being dropped.
- If you don't need any fancy customization, this method is much easier to code.
Using an OLE drop target:
- Uses a COM interface, which is a modern and better-supported mechanism.
- Good MFC support through
CView
and COleDropTarget
.
- Allows full control over the drag and drop operation.
- Gives you access to the raw
IDataObject
so you can access any data format.
- Requires a bit more code, but once you write it once, you can cut and paste it to new projects.
How MultiFiler accepts drag and drop
The demo project ZIP file actually contains three MultiFiler projects. Each uses one of the techniques I've described to accept drag and drop from Explorer windows. When the user drops over the MultiFiler window, all of the dropped files are added to a list control, as shown here:
MultiFiler automatically eliminates duplicate files, so any file will only appear once in the list.
If you are using Windows 2000 and run a MultiFiler that uses an OLE drop target, you'll notice that the drag image is the new-fangled faded style:
This doesn't come for free, but it's a nice example of the customization you can do when you use an OLE drop target. I'll explain how to do this at the end of the article.
Initiating a drag and drop
To have Explorer accept dragged files, all we have to do is create some CF_HDROP
data and put it in a data object. Of course, if it were that simple, I wouldn't have anything to write about. The DROPFILES
struct
is a bit tricky to create (since it's not always the same size), but again, after you write the code once (or, after I write it once!) you can reuse it everywhere.
MultiFiler initiates a drag and drop when you select files in the list control and drag them. The control sends an LVN_BEGINDRAG
notification message when this happens, so that's when MultiFiler creates the data object and hands it off to OLE to begin the drag and drop operation.
The steps in creating a DROPFILES
are:
- Enumerate all of the selected items in the list control, putting them in a string list.
- Keep track of the length of each string as it's added to the string list.
- Allocate memory for the
DROPFILES
itself and the list of filenames.
- Fill in the
DROPFILES
members.
- Copy the list of filenames into the allocated memory.
I'll go over the MultiFiler code now, so you can see exactly how to set up a DROPFILES
. If you feel like checking out the source in the MultiFiler projects, all three do it the same way, so you can look at any of the three.
The first step is to put all of the selected filenames in a list, and keep track of the memory needed to hold all of the strings.
void CMultiFilerDlg::OnBegindragFilelist(NMHDR* pNMHDR, LRESULT* pResult)
{
CStringList lsDraggedFiles;
POSITION pos;
CString sFile;
UINT uBuffSize = 0;
pos = c_FileList.GetFirstSelectedItemPosition();
while ( NULL != pos )
{
nSelItem = c_FileList.GetNextSelectedItem ( pos );
sFile = c_FileList.GetItemText ( nSelItem, 0 );
lsDraggedFiles.AddTail ( sFile );
uBuffSize += lstrlen ( sFile ) + 1;
}
At this point, uBuffSize
holds the total length of all the strings, including nulls, in characters. We add 1 for the final null to terminate the list, then multiply that by sizeof(TCHAR)
to convert from characters to bytes. We then add sizeof(DROPFILES)
to get the final required buffer size.
uBuffSize = sizeof(DROPFILES) + sizeof(TCHAR) * (uBuffSize + 1);
Now that we know how much memory we need, we can allocate it. When doing drag and drop, you allocate memory from the heap with GlobalAlloc()
:
HGLOBAL hgDrop;
DROPFILES* pDrop;
hgDrop = GlobalAlloc ( GHND | GMEM_SHARE, uBuffSize );
if ( NULL == hgDrop )
return;
We then get direct access to the memory with GlobalLock()
:
pDrop = (DROPFILES*) GlobalLock ( hgDrop );
if ( NULL == pDrop )
{
GlobalFree ( hgDrop );
return;
}
Now we can start filling in the DROPFILES
. The GHND
flag in the GlobalAlloc()
call initializes the memory to zero, so we only have to set a couple of members:
pDrop->pFiles = sizeof(DROPFILES);
#ifdef _UNICODE
pDrop->fWide = TRUE;
#endif
Note that the pFiles
member doesn't indicate the size of the DROPFILES
struct
; it's the offset of the file list. But since the file list is located right after the end of the struct
, its offset is the same as the size of the struct
.
Now we can copy all of the filenames into memory, and then unlock the buffer.
TCHAR* pszBuff;
pos = lsDraggedFiles.GetHeadPosition();
pszBuff = (TCHAR*) (LPBYTE(pDrop) + sizeof(DROPFILES));
while ( NULL != pos )
{
lstrcpy ( pszBuff, (LPCTSTR) lsDraggedFiles.GetNext ( pos ) );
pszBuff = 1 + _tcschr ( pszBuff, '\0' );
}
GlobalUnlock ( hgDrop );
The next step is to construct a COleDataSource
object and put our data into it. We also need a FORMATETC
struct
that describes the clipboard format (CF_HDROP
) and how the data is stored (an HGLOBAL
).
COleDataSource datasrc;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
datasrc.CacheGlobalData ( CF_HDROP, hgDrop, &etc );
Now, this would be sufficient to initiate a drag and drop, but there's one more detail to take care of. Since MultiFiler accepts drag and drop, it will happily accept the drag and drop we are about to initiate ourselves. While that's not disastrous, it's not very neat either. So we will add another bit of data to the data source, in a custom clipboard format that we register. Our OnDragEnter()
and OnDragOver()
functions will check for this format, and if it's present, they will not accept the drop.
HGLOBAL hgBool;
hgBool = GlobalAlloc ( GHND | GMEM_SHARE, sizeof(bool) );
if ( NULL == hgBool )
{
GlobalFree ( hgDrop );
return;
}
etc.cfFormat = g_uCustomClipbrdFormat;
datasrc.CacheGlobalData ( g_uCustomClipbrdFormat, hgBool, &etc );
Note that we don't have to set the data to any particular value - the fact that the data is in the data source is the important part.
Now that we've put together the data, we can start the drag and drop operation! We call the DoDragDrop()
method of COleDataSource
, which does not return until the drag and drop is completed. The only parameter is one or more DROPEFFECT
values that indicate what operations we will allow the user to do. It returns a DROPEFFECT
value indicating what the user wants to do with the data, or DROPEFFECT_NONE
if the drop was aborted or not accepted by the target.
DROPEFFECT dwEffect;
dwEffect = datasrc.DoDragDrop ( DROPEFFECT_COPY | DROPEFFECT_MOVE );
In our case, we only allow copying and moving. During the drag and drop, the user can hold down the Control or Shift key to change the operation. For some reason, passing DROPEFFECT_LINK
does not make Explorer create shortcuts, so that's why I didn't include DROPEFFECT_LINK
in the call above.
Once DoDragDrop()
returns, we check the return value. If it's DROPEFFECT_MOVE
or DROPEFFECT_COPY
, the drag and drop completed successfully, so we remove all of the selected files from the main window's list control. If it's DROPEFFECT_NONE
, things are a bit tricky. On Windows 9x, it means the operation was canceled. However, on NT/2000, the shell returns DROPEFFECT_NONE
for move operations as well. (This is by design! See KB article Q182219 for details.) So on NT, we have to manually check whether the files were moved, and if so, we remove them from the list control. The code is a bit long, so I won't repeat it here. Check out the MultiFiler source if you're interested in how it works.
The last thing we do is free the allocated memory if the drag and drop was canceled. If it completed, then the drop target owns the memory, and we must not free it. Below is the code to check the return value from DoDragDrop()
, just without the NT/2000 code I just mentioned.
switch ( dwEffect )
{
case DROPEFFECT_COPY:
case DROPEFFECT_MOVE:
{
}
break;
case DROPEFFECT_NONE:
{
GlobalFree ( hgDrop );
GlobalFree ( hgBool );
}
break;
}
}
Other details
You can also right-click the MultiFiler list control to get a context menu with four commands. They are pretty simple, and deal with managing the selection in the list and clearing the list.
Oh, and I promised to explain how to get the cool drag image on Windows 2000! It's actually pretty simple. There is a new coclass supported by the shell called CLSID_DragDropHelper
with two interfaces, IDragSourceHelper
and IDropTargetHelper
. IDropTargetHelper
is the one that draws the drag image. It has four methods whose names should be familiar: DragEnter()
, DragOver()
, DragLeave()
, and Drop()
. All you have to do is do your normal drag and drop processing, determine what DROPEFFECT
you will return, and then call the IDropTargetHelper
method that corresponds to the COleDropTarget
method. The IDropTargetHelper
methods need the DROPEFFECT
to properly draw the drag image, so that's why you need to determine it first.
If you look at the MultiFiler sample that uses a CView
, you'll see two member variables:
IDropTargetHelper* m_piDropHelper;
bool m_bUseDnDHelper;
In the view's constructor, the code creates the drop helper COM object and gets an IDropTargetHelper
interface. Based on whether this succeeds, m_bUseDnDHelper
is set so that other functions will know whether the COM object is available.
CMultiFilerView::CMultiFilerView() : m_bUseDnDHelper(false),
m_piDropHelper(NULL)
{
if ( SUCCEEDED( CoCreateInstance ( CLSID_DragDropHelper, NULL,
CLSCTX_INPROC_SERVER,
IID_IDropTargetHelper,
(void**) &m_piDropHelper ) ))
{
m_bUseDnDHelper = true;
}
}
Then the four drag and drop functions call IDropTargetHelper
methods. Here's an example:
DROPEFFECT CMultiFilerView::OnDragEnter(COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point)
{
DROPEFFECT dwEffect = DROPEFFECT_NONE;
if ( m_bUseDnDHelper )
{
IDataObject* piDataObj = pDataObject->GetIDataObject ( FALSE );
m_piDropHelper->DragEnter ( GetSafeHwnd(), piDataObj,
&point, dwEffect );
}
return dwEffect;
}
GetIDataObject()
is an undocumented function in COleDataObject
that returns an IDataObject
interface. (You can find some useful stuff by looking through the MFC header files sometimes!)
And finally, the view's destructor releases the COM object.
CMultiFilerView::~CMultiFilerView()
{
if ( NULL != m_piDropHelper )
m_piDropHelper->Release();
}
By the way, if you don't have a recent Platform SDK installed, you may not have the definition of the IDropTargetHelper
interface and the associated GUIDs. I've included the necessary definitions in each of the MultiFiler samples; just uncomment them and you should be good to go.
If you're wondering about going the other way - using IDragSourceHelper
to draw the neat drag image when MultiFiler is the drag source - the documentation is less clear on this topic, and it certainly looks harder than using IDropTargetHelper
, so I haven't worked on it yet.