Contents
Introduction
Here in Part VI of the Guide, I'll introduce a lesser-used type of shell extension, the drop handler. This type
can be used to add drag-and-drop functionality to Explorer, where the file being dropped on (the drop target)
determines the extension that gets invoked.
This article assumes that you understand the basics of shell extensions, and are familiar with the MFC classes
used to interact with the shell. If you need a refresher on the MFC classes, you should read Part
IV, since the same techniques will be used in this article.
Remember that VC 7 (and probably VC 8) users will need to change some settings before compiling. See the
README section in Part I for the details.
Back in Part IV, I talked about the drag and drop handler, which is
invoked during a right-button drag-and-drop operation. Explorer also lets us write an extension that is invoked
during a left-button drag-and-drop operation, where the extension operates on the file that is being dropped on.
For example, WinZip contains a drop handler that lets you add files to a zip
by dropping them on the zip. When you drag files over a zip, Explorer indicates that the zip is a drop target by
highlighting the zip file and changing the cursor so it has a + icon:
If there were no drop handler, then nothing would happen when you tried to drag files over a zip:
Drop handlers are really only useful in this way if you have your own file type, like WinZip does. A much more
interesting thing to do with drop handlers is add items to the Send To menu. The Send To menu shows
the contents of the SendTo
menu (on Windows 9x, this directory is under the Windows directory; on
NT-based OSes, this directory is in the user's profile directory). In older versions of Windows, the SendTo
directory contains just shortcuts, but newer versions (and other apps like the Shell Power Toys) add several other
items, as shown here:
If you don't seen how drop handlers figure into this, take a look at a directory listing of the SendTo
directory:
12/23/05 3:39a 129 3½ Floppy (A).lnk
12/28/05 1:42p 0 Desktop (create shortcut).DeskLink
12/28/05 1:42p 0 Mail Recipient.MAPIMail
12/23/05 12:31p 0 My Documents.mydocs
5/25/06 11:05a 1,267 Notepad.lnk
Notice those weird extensions like ".DeskLink". Those zero-byte files are there to cause the items
to appear in the Send To menu, and the extensions are listed in the registry. They are not normal associations,
though, since the files have no verbs like open or print. What they do have are drop handlers.
When you select one of those items on the Send To menu, Explorer invokes the associated drop handler. Here's
another look at the menu, with the drop targets indicated:
This article's sample project will be a clone of the old "Send To Any Folder" Powertoy - it will copy
or move files to any directory on your computer.
The Initialization Interface
You should be familiar with the set-up steps now, so I'll skip the instructions for going through the VC wizards.
If you're following along in the wizards, make a new ATL COM app called SendToClone, with a C++ implementation
class CSendToShlExt
.
Since a drop handler is invoked on just one file (the file being dropped on), initialization is done through
the IPersistFile
interface. (Recall that IPersistFile
is used for extensions that can
only ever operate on a single file.) IPersistFile
has a lot of methods, but in shell extensions, only
the Load()
method needs an implementation.
We'll need to add IPersistFile
to the list of interfaces that CSendToShlExt
implements.
Open SendToShlExt.h and add the lines listed here in bold:
#include <comdef.h>
#include <shlobj.h>
class CSendToShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
public IPersistFile
{
BEGIN_COM_MAP(CSendToShlExt)
COM_INTERFACE_ENTRY(IPersistFile)
END_COM_MAP()
public:
STDMETHOD(GetClassID)(LPCLSID) { return E_NOTIMPL; }
STDMETHOD(IsDirty)() { return E_NOTIMPL; }
STDMETHOD(Load)(LPCOLESTR, DWORD) { return S_OK; }
STDMETHOD(Save)(LPCOLESTR, BOOL) { return E_NOTIMPL; }
STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; }
STDMETHOD(GetCurFile)(LPOLESTR*) { return E_NOTIMPL; }
Notice that Load()
doesn't do anything besides returning S_OK
. The Load()
method receives the full path of the file that is the drop target, but for this extension we don't care about that
filename, so we can ignore it.
Taking Part in the Drag and Drop Operation
In order to do its job, our extension has to communicate with the drop source, which is Explorer itself. Our
extension gets a list of files being dragged, and it tells Explorer whether it will accept the files if the user
drops them. This communication happens through an interface, naturally: IDropTarget
. The IDropTarget
methods are:
DragEnter()
: Called when the user first drags over a file. The return value tells Explorer whether
the extension will accept the files if they are dropped.
DragOver()
: Not called in shell extensions.
DragLeave()
: Called when the user drags off of the target file without dropping.
Drop()
: Called when the user drops onto the target file. This is where the shell extension does
its work.
To add IDropTarget
to CSendToShlExt
, open SendToShlExt.h and add the lines
listed here in bold:
class CSendToShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
public IPersistFile,
public IDropTarget
{
BEGIN_COM_MAP(CSendToShlExt)
COM_INTERFACE_ENTRY(IPersistFile)
COM_INTERFACE_ENTRY(IDropTarget)
END_COM_MAP()
public:
STDMETHODIMP DragEnter(IDataObject* pDataObj, DWORD grfKeyState,
POINTL pt, DWORD* pdwEffect);
STDMETHODIMP DragOver(DWORD grfKeyState, POINTL pt, DWORD* pdwEffect)
{ return E_NOTIMPL; }
STDMETHODIMP DragLeave();
STDMETHODIMP Drop(IDataObject* pDataObj, DWORD grfKeyState,
POINTL pt, DWORD* pdwEffect);
protected:
CStringList m_lsDroppedFiles;
}
As in the earlier extensions, we'll use a list of strings to hold the names of the files being dragged. The
DragOver()
method needs no implementation, since it is not called. I'll cover the other three methods
next.
DragEnter()
The prototype for DragEnter()
is:
HRESULT IDropTarget::DragEnter (
IDataObject* pDataObj,
DWORD grfKeyState,
POINTL pt,
DWORD* pdwEffect );
pDataObj
is an IDataObject
interface with which we can enumerate the files being dragged.
grfKeyState
is a set of flags indicating which shift keys and mouse buttons are pressed. pt
is a POINTL
struct (which happens to be the same as a POINT
) that holds the mouse coordinates.
pdwEffect
is a pointer to a DWORD
in which we will return a value that tells Explorer
if we will accept the drop, and if so, what kind of icon to overlay on the mouse cursor.
As I mentioned earlier, DragEnter()
is normally called when the user first drags over a drop target.
However, it is also called when the user clicks a Send To menu item, so we can still do all our work in
DragEnter()
even though there is technically no drag and drop happening.
Our DragEnter()
implementation will fill a string list with the names of the files being dragged.
This extension will accept all files or directories, since any file system object can be copied or moved.
DragEnter()
starts off with stuff that should be familiar to you now - we attach a COleDataObject
to the IDataObject
interface, and enumerate the files being dragged.
STDMETHODIMP CSendToShlExt::DragEnter (
IDataObject* pDataObj,
DWORD grfKeyState,
POINTL pt,
DWORD* pdwEffect )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
COleDataObject dataobj;
TCHAR szItem[MAX_PATH];
UINT uNumFiles;
HGLOBAL hglobal;
HDROP hdrop;
dataobj.Attach ( pDataObj, FALSE );
hglobal = dataobj.GetGlobalData ( CF_HDROP );
if ( NULL != hglobal )
{
hdrop = (HDROP) GlobalLock ( hglobal );
uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );
for ( UINT uFile = 0; uFile < uNumFiles; uFile++ )
{
if ( 0 != DragQueryFile ( hdrop, uFile, szItem, MAX_PATH ))
m_lsDroppedFiles.AddTail ( szItem );
}
GlobalUnlock ( hglobal );
}
Now it's time to return a value in pdwEffect
. The effects we can return are:
DROPEFFECT_COPY
: Tell Explorer that the dragged files will be copied by our extension.
DROPEFFECT_MOVE
: Tell Explorer that the dragged files will be moved by our extension.
DROPEFFECT_LINK
: Tell Explorer that the dragged files will be linked to by our extension.
DROPEFFECT_NONE
: Tell Explorer that we won't accept the files being dragged.
The only effect we'll return is DROPEFFECT_COPY
. We can't return DROPEFFECT_MOVE
,
because that would make Explorer delete the files that were dropped. We could return DROPEFFECT_LINK
,
but the cursor would have the little arrow that normally indicates shortcuts, and that would be misleading to the
user. If the file list is empty, which happens if we can't read the clipboard, we return DROPEFFECT_NONE
to tell Explorer we won't accept the drop.
if ( m_lsDroppedFiles.GetCount() > 0 )
{
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
else
{
*pdwEffect = DROPEFFECT_NONE;
return E_INVALIDARG;
}
}
DragLeave()
DragLeave()
is called if the user drags away from our target file without dropping. This method
isn't used by the Send To menu, but it is called if you have an Explorer window open on the Send
To directory and drag onto the files in that directory. We don't have any cleanup tasks, so all we have to
do is return S_OK
:
STDMETHODIMP CSendToShlExt::DragLeave()
{
return S_OK;
}
Drop()
If the user selects our Send To menu item, Explorer calls Drop()
, which has this prototype:
HRESULT IDropTarget::Drop (
IDataObject* pDataObj,
DWORD grfKeyState,
POINTL pt,
DWORD* pdwEffect );
The first three parameters are the same as DragEnter()
. Drop()
should return the final
effect of the operation through the pdwEffect
parameter. Our Drop()
function creates
the main dialog and passes it the list of filenames. The dialog does all the work, and when DoModal()
returns, we set the final drop effect.
STDMETHODIMP CSendToShlExt::Drop (
IDataObject* pDataObj,
DWORD grfKeyState,
POINTL pt,
DWORD* pdwEffect )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CSendToCloneDlg dlg ( &m_lsDroppedFiles );
dlg.DoModal();
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}
The dialog looks like this:
It's a pretty straightforward MFC dialog, and you can find the source in the SendToCloneDlg.cpp file.
I did the actual moving and copying using the CShellFileOp
class from my article "CShellFileOp
- Wrapper for SHFileOperation."
As with the extension in Part II, we need to add a manifest to the DLL's
resources so our dialog gets themed on XP. However, just adding the manifest isn't enough in this case, because
the code that creates and manages the dialog is in MFC. MFC isn't compiled with the ISOLATION_AWARE_ENABLED
symbol defined, so the code doesn't use the IsolationAwareXxx
wrappers that are necessary to hook
up themes.
There is a good explanation of the situation in a post from Dave Anderson in this
newsgroup thread. To summarize, before we show the dialog, we need to use the activation context APIs
so Windows uses our manifest and uses version 6 of the common controls for our dialog. This code is encapsulated
in the CActCtx
class, and used in our Drop()
method:
STDMETHODIMP CSendToShlExt::Drop(...)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CSendToCloneDlg dlg ( &m_lsDroppedFiles );
CAxtCtx ctx;
dlg.DoModal();
*pdwEffect = DROPEFFECT_COPY;
return S_OK;
}"/shell/cshellfileop.asp">
After adding this code, the dialog becomes properly themed:
But wait -- how do we tell Explorer about our drop handler? And how do we get an item in the Send To
menu? I'll explain how to do this in the next section.
Registering the Extension
Registering a drop handler is a bit different than other types of extensions, since it requires creating a new
association under HKEY_CLASSES_ROOT
. The AppWizard-generated RGS script is listed below. Parts which
you should add or change are listed in bold.
HKCR
{
.SendToClone = s 'CLSID\{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'
NoRemove CLSID
{
ForceRemove {B7F3240E-0E29-11D4-8D3B-80CD3621FB09} = s 'Send To Any Folder Clone'
{
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
val NeverShowExt = s ''
DefaultIcon = s '%MODULE%,0'
shellex
{
DropHandler = s '{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}'
}
}
}
}
The first line is the one that makes the association. It creates a new extension, .SendToClone
,
that will be used by our drop target. Note that the default value of the .SendToClone
key is prefixed
with "CLSID\". This tells Explorer that the data that describes the association is in a key under HKCR\CLSID
.
With more conventional associations, that stuff is stored in a key right under HKEY_CLASSES_ROOT
(for
example, the .txt
key points at the HKCR\txtfile
key), but it seems to be a convention
to store a drop handler's association data under its CLSID
key, to keep all the data in one place.
The "Send To Any Folder Clone" string is the file type description that appears in Explorer if you
browse to the SendTo
directory. The NeverShowExt
value is created to tell Explorer that
it should not show the ".SendToClone" extension. The DefaultIcon
key lists the location
of the icon to use for .SendToClone
files. Finally we have the familiar shellex
key with
the DropHandler
subkey. Since there can be only one drop handler for a file type, the handler's GUID
is stored right in the DropHandler
key, instead of being listed in a subkey under DropHandler
.
The only remaining detail is to create a file in the SendTo
directory so our menu item appears.
We can do this in DllRegisterServer()
and delete the file in DllUnregisterServer()
. Here's
the code to create the file:
LPITEMIDLIST pidl;
TCHAR szSendtoPath[MAX_PATH];
HANDLE hFile;
LPMALLOC pMalloc;
if ( SUCCEEDED( SHGetSpecialFolderLocation ( NULL, CSIDL_SENDTO, &pidl ) ))
{
if ( SHGetPathFromIDList ( pidl, szSendtoPath ) )
{
PathAppend ( szSendtoPath, _T("Some other folder.SendToClone") );
hFile = CreateFile ( szSendtoPath, GENERIC_WRITE, FILE_SHARE_READ,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
CloseHandle ( hFile );
}
if ( SUCCEEDED( SHGetMalloc ( &pMalloc ) ))
{
pMalloc->Free ( pidl );
pMalloc->Release();
}
}
Here's what the new Send To menu item looks like, with our own item:
DllUnregisterServer()
has similar code that deletes the file. The code above will work on all versions
of Windows (well, all versions 4 and above). If you know your code will be run on only shell version 4.71 and higher,
you can use the SHGetSpecialFolderPath()
function instead of SHGetSpecialFolderLocation()
.
As always, on NT-based OSes, we need to add our extension to the list of "approved" extensions. The
code to do this is in the DllRegisterServer()
and DllUnregisterServer()
functions in
the sample project.
To Be Continued...
Coming up in Part VII, I'll answer some reader requests and demonstrate two new variations of context menu extensions.
Copyright and License
This article is copyrighted material, ©2000-2006 by Michael Dunn. I realize this isn't going to stop people
from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation
of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation,
I would just like to be aware of the translation so I can post a link to it here.
The demo code that accompanies this article is released to the public domain. I release it this way so that
the code can benefit everyone. (I don't make the article itself public domain because having the article available
only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own
application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are
benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not
required.
Revision History
April 20, 2000: Article first published.
June 6, 2000: Something updated. ;)
May 26, 2006: Updated to cover changes in VC 7.1, cleaned up code snippets, sample project gets themed on XP.
Series Navigation: « Part V | Part
VII »