Contents
Introduction
In Parts I and II of the Guide,
I showed how to write context menu extensions that are invoked when the user right-clicks on certain types of files.
In this part, I'll demonstrate a different type of context menu extension, the drag and drop handler, which
adds items to the context menu displayed for a right-button drag and drop operation. I'll also give more examples
of using MFC in an extension.
Part IV assumes that you understand the basics of shell extensions, and are familiar with MFC. This particular
extension is a real utility that creates hard links on Windows 2000 and later, but you can still follow along even
if you are using an older version of Windows.
As every power user knows (and few normal users know), you can drag and drop items in Explorer using the right
mouse button. When you release the button, Explorer shows a context menu that lists all the available actions you
can take. Normally, these are move, copy, and create shortcut:
Explorer lets us add items to this menu, by using a drag and drop handler. This type of extension is
invoked when any right-drag and drop operation happens, and the extension can add menu items if it deems it should.
An example of a drag and drop handler is in WinZip. Here's what WinZip adds
to the context menu when you right-drag a compressed file:
WinZip's extension is invoked for any right-drag and drop operation, but it only adds it menu items if a compressed
file is being dragged.
This article's sample extension will use an API added in Windows 2000, CreateHardLink()
, to make
hard links to files on NTFS volumes. We'll add an item for making links to the context menu, so the user can make
hard links the same way as regular shortcuts.
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.
Using AppWizard to Get Started
Run the AppWizard and make a new ATL COM app. We'll call it HardLink. We are going to use MFC, so check
the Support MFC checkbox, and then click Finish. To add a COM object to the DLL, go to the ClassView
tree, right-click the HardLink classes item, and pick New ATL Object. (In VC 7, right-click the item
and pick Add|Add Class.) As before, choose Simple Object in the wizard, and use the name HardLinkShlExt
for the object. This will create a C++ class CHardLinkShlExt
that will implement the extension.
The Initialization Interface
As with our earlier context menu extensions, Explorer initializes us using the IShellExtInit
interface.
We first need to add IShellExtInit
to the list of interfaces that CHardLinkShlExt
implements.
Open HardLinkShlExt.h, and add the lines listed here in bold:
#include <comdef.h>
#include <shlobj.h>
class CHardLinkShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CHardLinkShlExt, &CLSID_HarkLinkShlExt>,
public IShellExtInit<FONT COLOR="red">
</FONT>{
BEGIN_COM_MAP(CHardLinkShlExt)
<FONT COLOR="red"> </FONT>COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
public:
STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
We'll also need some variables to hold a bitmap and the names of the files being dragged:
protected:
CBitmap m_bitmap;
TCHAR m_szFolderDroppedIn[MAX_PATH];
CStringList m_lsDroppedFiles;
Also, we'll need to add a some #define
s to stdafx.h to make the CreateHardLink()
and shlwapi.dll function prototypes visible:
#define WINVER 0x0500
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0400
Defining WINVER
as 0x0500 enables features specific to Win 98 and 2000, and defining _WIN32_WINNT
as 0x0500 enables NT features specific to Win 2000. Defining _WIN32_IE
as 0x0400 enables features
introduced with IE 4.
Now, on to the Initialize()
method. This time, I'll show how to use MFC to access the list of files
being dragged. MFC has a class, COleDataObject
, that wraps the IDataObject
interface.
Previously, we had to call IDataObject
methods directly. But fortunately, MFC makes the job a bit
easier for us. Here's the prototype for Initialize()
, to refresh your memory:
HRESULT IShellExtInit::Initialize (
LPCITEMIDLIST pidlFolder,
LPDATAOBJECT pDataObj,
HKEY hProgID );
For drag and drop extensions, pidlFolder
is the PIDL of the folder where the items were dropped.
(I'll have more to say about the PIDL later.) pDataObj
is an IDataObject
interface with
which we can enumerate all of the items that were dropped. hProgID
is an open HKEY
on
our shell extension's key under HKEY_CLASSES_ROOT
.
Our first step is to load a bitmap for our menu item. Then, we attach a COleDataObject
variable
to the IDataObject
interface.
HRESULT CHardLinkShlExt::Initialize (
LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj,
HKEY hProgID )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
COleDataObject dataobj;
HGLOBAL hglobal;
HDROP hdrop;
TCHAR szRoot[MAX_PATH];
TCHAR szFileSystemName[256];
TCHAR szFile[MAX_PATH];
UINT uFile, uNumFiles;
m_bitmap.LoadBitmap ( IDB_LINKBITMAP );
dataobj.Attach ( pDataObj, FALSE );
Passing FALSE
as the second parameter to Attach()
means to not release the IDataObject
interface when the dataobj
variable is destructed. The next step is to get the directory where the
items were dropped. We have the PIDL of this directory, but how do we get the path? Time for a little sidebar...
"PIDL" is an acronym for pointer to an ID list. A PIDL is a way of uniquely identifying
any object within the hierarchy presented by Explorer. Every object in the shell, whether it's part of the file
system or not, has a PIDL. The exact structure of a PIDL depends on where the object is, but unless you are writing
your own namespace extension, you don't (and shouldn't) have to worry about the internal structure of a PIDL.
For our purposes, we can use the shell API SHGetPathFromIDList()
to translate the PIDL into a conventional
path. If the target folder isn't a directory in the file system (for example, the Control Panel), SHGetPathFromIDList()
will fail and we can bail out.
if ( !SHGetPathFromIDList(pidlFolder, m_szFolderDroppedIn) )
return E_FAIL;
Next, we have to check if the target directory is on an NTFS volume. We get the root component of the path (for
example, E:\
), and get the info about that volume. If the file system is not NTFS, we can't make any
links, so we can return.
lstrcpy ( szRoot, m_szFolderDroppedIn );
PathStripToRoot ( szRoot );
if ( !GetVolumeInformation ( szRoot, NULL, 0, NULL, NULL, NULL,
szFileSystemName, 256 ))
{
return E_FAIL;
}
if ( 0 != lstrcmpi ( szFileSystemName, _T("ntfs") ))
{
return E_FAIL;
}
Next, we get an HDROP
handle from the data object, which we'll use to enumerate the files that
were dropped. This is similar to the method in Part III, except we're using the MFC class to access the data. COleDataObject
handles setting up the FORMATETC
and STGMEDIUM
structs for us.
hglobal = dataobj.GetGlobalData ( CF_HDROP );
if ( NULL == hglobal )
return E_INVALIDARG;
hdrop = (HDROP) GlobalLock ( hglobal );
if ( NULL == hdrop )
return E_INVALIDARG;
We then use the HDROP
handle to enumerate the dropped files. For each one, we check if it is a
directory. Directories cannot be linked to, so if we find a directory, we can return.
uNumFiles = DragQueryFile ( hdrop, 0xFFFFFFFF, NULL, 0 );
for ( uFile = 0; uFile < uNumFiles; uFile++ )
{
if ( DragQueryFile ( hdrop, uFile, szFile, MAX_PATH ) )
{
if ( PathIsDirectory ( szFile ) )
{
m_lsDroppedFiles.RemoveAll();
break;
}
We also have to check that the dropped files reside on the same volume as the target directory. What I did was
compare the root components of each file and the target directory, and return if they are different. This is not
a complete solution, though, since on NTFS volumes, you can mount a volume in the middle of another. For example,
you could have a C: volume, and mount another volume as C:\dev. This code will not reject an attempt to make a
link from C:\dev to somewhere else on C:.
Here's the check that compares the root components:
if ( !PathIsSameRoot(szFile, m_szFolderDroppedIn) )
{
m_lsDroppedFiles.RemoveAll();
break;
}
If the file passes both checks, we add it to m_lsDroppedFiles
, which is a CStringList
(linked list of CString
s).
m_lsDroppedFiles.AddTail ( szFile );
}
}
After the for loop, we release resources and return back to Explorer. If the string list contains any filenames,
we return S_OK
to indicate we need to modify the context menu. Otherwise, we return E_FAIL
so that we won't be called again for this drag and drop event.
GlobalUnlock ( hglobal );
return (m_lsDroppedFiles.GetCount() > 0) ? S_OK : E_FAIL;
}
Modifying the Context Menu
Like other context menu extensions, a drag and drop handler implements the IContextMenu
interface
with which it interacts with the context menu. To add IContextMenu
to our extension, open HardLinkShlExt.h
again and add the lines listed in bold:
class CHardLinkShlExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CHardLinkShlExt, &CLSID_HardLinkShlExt>,
public IShellExtInit,
public IContextMenu
{
BEGIN_COM_MAP(CHardLinkShlExt)
COM_INTERFACE_ENTRY(IShellExtInit)
<FONT COLOR="red"> </FONT>COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()
public:
<FONT COLOR="red"> </FONT>
STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT)
{ return E_NOTIMPL; }
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
Note that we don't need any code in GetCommandString()
, because that method is not called in drag
and drop handlers.
Explorer calls our QueryContextMenu()
function to let us modify the context menu. There's nothing
here you haven't seen before; we just add one menu item and set its bitmap.
HRESULT CHardLinkShlExt::QueryContextMenu (
HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd,
UINT uidLastCmd, UINT uFlags )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
if ( uFlags & CMF_DEFAULTONLY )
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
InsertMenu ( hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION,
uidFirstCmd, _T("Create hard link(s) here") );
if ( NULL != m_bitmap.GetSafeHandle() )
{
SetMenuItemBitmaps ( hmenu, uMenuIndex, MF_BYPOSITION,
(HBITMAP) m_bitmap.GetSafeHandle(), NULL );
}
return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}
Here's what the new menu item looks like:
Making the Link
Explorer calls InvokeCommand()
when the user clicks our menu item. We'll create links to all the
files that were dropped. The names of the links will be "Hard link to <filename>"
,
or, if that name is already in use, "Hard link (2) to <filename>"
. The number will
go up to 99, an arbitrary limit.
First, the locals and a check of the lpVerb
parameter, which must be 0 since we only have 1 menu
item.
HRESULT CHardLinkShlExt::InvokeCommand (
LPCMINVOKECOMMANDINFO pInfo )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
TCHAR szNewFilename[MAX_PATH+32];
CString sSrcFile;
TCHAR szSrcFileTitle[MAX_PATH];
CString sMessage;
UINT uLinkNum;
POSITION pos;
if ( 0 != pInfo->lpVerb )
return E_INVALIDARG;
Next, we get a POSITION
value pointing at the beginning of the string list. A POSITION
is an opaque data type which you don't use directly, but instead you pass it to other methods of the CStringList
class. To get the POSITION
of the head of the list, we call GetHeadPosition()
:
pos = m_lsDroppedFiles.GetHeadPosition();
ASSERT ( NULL != pos );
pos
will be NULL if the list is empty, but the list shouldn't be empty, ever, so I added an ASSERT
to check for that case. Next up is the beginning of the loop that will iterate through the filenames in the list
and make a link to each one.
while ( NULL != pos )
{
sSrcFile = m_lsDroppedFiles.GetNext ( pos );
lstrcpy ( szSrcFileTitle, sSrcFile );
PathStripPath ( szSrcFileTitle );
wsprintf ( szNewFilename, _T("%sHard link to %s"), m_szFolderDroppedIn,
szSrcFileTitle );
GetNext()
returns the CString
at the position indicated by pos
, and increments
pos
to point at the next string. If pos
was at the end of the list, pos
becomes NULL (so that's how the while loop will end).
At this point, szNewFilename
holds the full path of the hard link. We check if a file with this
name exists, and if so, we'll try adding numbers 2 through 99, looking for a name that's not already in use. We
also make sure the length of the link name (including the terminating null) doesn't exceed MAX_PATH characters.
for ( uLinkNum = 2;
PathFileExists(szNewFilename) && uLinkNum < 100;
uLinkNum++ )
{
wsprintf ( szNewFilename, _T("%sHard link (%u) to %s"),
m_szFolderDroppedIn, uLinkNum, szSrcFileTitle );
if ( lstrlen(szNewFilename) >= MAX_PATH )
{
sMessage.Format ( _T("Failed to make a link to %s.\nDo you want to continue making links?"),
(LPCTSTR) sSrcFile );
if (IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
MB_ICONQUESTION | MB_YESNO) )
break;
else
continue;
}
}
The message box lets you abort the entire operation if you want. Next, we check to see if we hit the limit of
99 links. Again, we let the user abort the whole operation.
if ( 100 == uLinkNum )
{
sMessage.Format ( _T("Failed to make a link to %s.\nDo you want to continue making links?"),
(LPCTSTR) sSrcFile );
if (IDNO == MessageBox ( pInfo->hwnd, sMessage, _T("Create Hard Links"),
MB_ICONQUESTION | MB_YESNO) )
break;
else
continue;
}
All that's left is to make the hard link. I've omitted the error handling code for clarity.
CreateHardLink ( szNewFilename, sSrcFile, NULL );
}
return S_OK;
}
The hard link doesn't look any different in Explorer, it just looks like any other ordinary file. But if you
modify one copy, the changes will be reflected in the other copy.
Registering the Shell Extension
Registering a drag and drop handler is simpler than other context menu extensions. All handlers are registered
under the HKCR\Directory
key, since that's where the drop happens, in a directory. However, what the
docs don't say is that registering under HKCR\Directory
isn't enough to handle all cases. You also
need to register under HKCR\Folder
to handle drops on the desktop, and HKCR\Drive
to
handle drops in root directories.
Here is the RGS script to handle all three of the above situations:
HKCR
{
NoRemove Directory
{
NoRemove shellex
{
NoRemove DragDropHandlers
{
ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
}
}
}
NoRemove Folder
{
NoRemove shellex
{
NoRemove DragDropHandlers
{
ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
}
}
}
NoRemove Drive
{
NoRemove shellex
{
NoRemove DragDropHandlers
{
ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
}
}
}
}
As with our previous extensions, 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.
If You Don't Have Windows 2000/NTFS
You can still build and run the sample project on earlier versions of Windows, or if you don't have an NTFS
volume available. Just open the stdafx.h file, and uncomment the line that reads:
That will make the extension skip the file system check (so it will run on anything, not just NTFS), and display
message boxes instead of actually making links.
To Be Continued
Coming up in Part V, we'll see a new type of extension, the property sheet handler, which adds pages to the
properties dialog for files.
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 3, 2000: Article first published.
June 6, 2000: Something updated. ;)
May 24, 2006: Updated to cover changes in VC 7.1, cleaned up code snippets.
Series Navigation: « Part III | Part
V »