|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionI'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 ExplorerAs 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 If you check out the data contained in the
The important format is The DROPFILES data structureSo what exactly is the
struct DROPFILES { DWORD pFiles; // offset of file list POINT pt; // drop point (client coords) BOOL fNC; // is it on NonClient area and pt is in screen coords BOOL fWide; // wide character flag }; The one thing that isn't listed in the Accepting a drag and drop from ExplorerAccepting 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 The old way - WM_DROPFILESTo 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 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
A typical void CMyDlg::OnDropFiles ( HDROP hdrop ) { UINT uNumFiles; TCHAR szNextFile [MAX_PATH]; // Get the # of files being dropped. uNumFiles = DragQueryFile ( hdrop, -1, NULL, 0 ); for ( UINT uFile = 0; uFile < uNumFiles; uFile++ ) { // Get the next filename from the HDROP info. if ( DragQueryFile ( hdrop, uFile, szNextFile, MAX_PATH ) > 0 ) { // *** // Do whatever you want with the filename in szNextFile. // *** } } // Free up memory. DragFinish ( hdrop ); }
The new way - using an OLE drop targetThe 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 Making a CView a drop target
void CMyView::OnInitialUpdate() { CView::OnInitialUpdate(); // Register our view as a drop target. // m_droptarget is a COleDropTarget member of CMyView. 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()
DROPEFFECT CView::OnDragEnter( COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point );
The parameters are:
Normally, in OnDragOver()If you return a value other than DROPEFFECT CView::OnDragOver ( COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point );
The parameters and return value are identical to As for the shift keys, you normally react to them as described below:
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 OnDragLeave()
void CView::OnDragLeave();
It has no parameters or return value - its purpose is to let you clean up any memory you allocated during OnDrop()If the user drops over your window (and you didn't return BOOL CView::OnDrop ( COleDataObject* pDataObject,
DROPEFFECT dropEffect, CPoint point );
The
Making a dialog a drop targetThings are a bit more difficult if your main window is a dialog (or anything not derived from A typical 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; // initialized in constructor }; 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 Once you have this new class, you add a drop target member variable to your dialog and call its BOOL CMyDialog::OnInitDialog()
{
// Register our dialog as a drop target.
// m_droptarget is a CMyDropTarget member of CMyDialog.
m_droptarget.Register ( this );
}
Accessing the HDROP data in a CDataObjectIf you use an OLE drop target, your drag and drop functions receive a pointer to a Here is the code to get an BOOL CMyDropTarget::OnDrop ( CWnd* pWnd, COleDataObject* pDataObject,
DROPEFFECT dropEffect, CPoint point )
{
HGLOBAL hg;
HDROP hdrop;
// Get the HDROP data from the data object.
hg = pDataObject->GetGlobalData ( CF_HDROP );
if ( NULL == hg )
return FALSE;
hdrop = (HDROP) GlobalLock ( hg );
if ( NULL == hdrop )
{
GlobalUnlock ( hg );
return FALSE;
}
// Read in the list of files here...
GlobalUnlock ( hg );
// Return TRUE/FALSE to indicate success/failure
}
Summary of the two methodsHandling
Using an OLE drop target:
How MultiFiler accepts drag and dropThe 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 dropTo have Explorer accept dragged files, all we have to do is create some MultiFiler initiates a drag and drop when you select files in the list control and drag them. The control sends an The steps in creating a
I'll go over the MultiFiler code now, so you can see exactly how to set up a 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; // For every selected item in the list, // put the filename into lsDraggedFiles. // c_FileList is our dialog's CListCtrl. pos = c_FileList.GetFirstSelectedItemPosition(); while ( NULL != pos ) { nSelItem = c_FileList.GetNextSelectedItem ( pos ); // put the filename in sFile sFile = c_FileList.GetItemText ( nSelItem, 0 ); lsDraggedFiles.AddTail ( sFile ); // Calculate the # of chars required to hold this string. uBuffSize += lstrlen ( sFile ) + 1; } At this point, // Add 1 extra for the final null char, // and the size of the DROPFILES struct. 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 HGLOBAL hgDrop;
DROPFILES* pDrop;
// Allocate memory from the heap for the DROPFILES struct.
hgDrop = GlobalAlloc ( GHND | GMEM_SHARE, uBuffSize );
if ( NULL == hgDrop )
return;
We then get direct access to the memory with pDrop = (DROPFILES*) GlobalLock ( hgDrop );
if ( NULL == pDrop )
{
GlobalFree ( hgDrop );
return;
}
Now we can start filling in the // Fill in the DROPFILES struct. pDrop->pFiles = sizeof(DROPFILES); #ifdef _UNICODE // If we're compiling for Unicode, set the Unicode flag in the struct to // indicate it contains Unicode strings. pDrop->fWide = TRUE; #endif Note that the Now we can copy all of the filenames into memory, and then unlock the buffer. TCHAR* pszBuff;
// Copy all the filenames into memory after
// the end of the DROPFILES struct.
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 datasrc;
FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
// Put the data in the data source.
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 // The global g_uCustomClipbrdFormat is set like this: // UINT g_uCustomClipbrdFormat = RegisterClipboardFormat // ( _T("MultiFiler_3BCFE9D1_6D61_4cb6_9D0B_3BB3F643CA82") ); // Add our own custom data, so we know that // the drag originated from our window. // The data will just be a dummy bool. HGLOBAL hgBool; hgBool = GlobalAlloc ( GHND | GMEM_SHARE, sizeof(bool) ); if ( NULL == hgBool ) { GlobalFree ( hgDrop ); return; } // Put the data in the data source. 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 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 Once 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 switch ( dwEffect ) { case DROPEFFECT_COPY: case DROPEFFECT_MOVE: { // The files were copied or moved. // Note: Don't call GlobalFree() because // the data will be freed by the drop target. // ** Omitted code to remove list control items. ** } break; case DROPEFFECT_NONE: { // ** Omitted code for NT/2000 that checks // if the operation actually succeeded. ** // The drag operation wasn't accepted, or was canceled, so we // should call GlobalFree() to clean up. GlobalFree ( hgDrop ); GlobalFree ( hgBool ); } break; } } Other detailsYou 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 If you look at the MultiFiler sample that uses a IDropTargetHelper* m_piDropHelper;
bool m_bUseDnDHelper;
In the view's constructor, the code creates the drop helper COM object and gets an CMultiFilerView::CMultiFilerView() : m_bUseDnDHelper(false), m_piDropHelper(NULL) { // Create an instance of the shell drag and drop helper object. 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 DROPEFFECT CMultiFilerView::OnDragEnter(COleDataObject* pDataObject,
DWORD dwKeyState, CPoint point)
{
DROPEFFECT dwEffect = DROPEFFECT_NONE;
// ** Omitted - code to determine dwEffect. **
// Call the drag and drop helper.
if ( m_bUseDnDHelper )
{
// The DnD helper needs an IDataObject interface, so get one from
// the COleDataObject. Note that the FALSE param means that
// GetIDataObject will not AddRef() the returned interface, so
// we do not Release() it.
IDataObject* piDataObj = pDataObject->GetIDataObject ( FALSE );
m_piDropHelper->DragEnter ( GetSafeHwnd(), piDataObj,
&point, dwEffect );
}
return dwEffect;
}
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 If you're wondering about going the other way - using | ||||||||||||||||||||