Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / ATL
Article

The Complete Idiot's Guide to Writing Shell Extensions - Part IV

Rate me:
Please Sign up or sign in to vote.
5.00/5 (23 votes)
24 May 2006 307.7K   4.3K   126   44
A tutorial on writing a shell extension that provides custom drag and drop functionality.

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:

 [Drag and drop menu - 4K]

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 menu - 6K]

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:
  // IShellExtInit
  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 #defines 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());  // init MFC
 
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 ))
  {
  // Couldn't determine file system type.
  return E_FAIL;
  }

if ( 0 != lstrcmpi ( szFileSystemName, _T("ntfs") ))
  {
  // The file system isn't NTFS, so it doesn't support hard links.
  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 ) )
      {
      // We found a directory!  Bail out.
      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) )
  {
  // Dropped files came from a different volume - bail out.
  m_lsDroppedFiles.RemoveAll();
  break;
  }

If the file passes both checks, we add it to m_lsDroppedFiles, which is a CStringList (linked list of CStrings).

  // Add the file to our list of dropped files.
  m_lsDroppedFiles.AddTail ( szFile );
  }
}   // end for

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>// IContextMenu
  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());  // init MFC
 
  // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.
  if ( uFlags & CMF_DEFAULTONLY )
    return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
 
  // Add the hard link menu item.
  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 1 to tell the shell that we added 1 top-level menu item.
  return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

Here's what the new menu item looks like:

 [Hard link menu item - 11K]

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());  // init MFC
 
TCHAR    szNewFilename[MAX_PATH+32];
CString  sSrcFile;
TCHAR    szSrcFileTitle[MAX_PATH];
CString  sMessage;
UINT     uLinkNum;
POSITION pos;
 
  // Double-check that we're getting called for our own menu item - lpVerb
  // must be 0.
  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 )
  {
  // Get the next source filename.
  sSrcFile = m_lsDroppedFiles.GetNext ( pos );

  // Remove the path - this reduces "C:\xyz\foo\stuff.exe" to "stuff.exe"
  lstrcpy ( szSrcFileTitle, sSrcFile );
  PathStripPath ( szSrcFileTitle );

  // Make the filename for the hard link - we'll first try
  // "Hard link to stuff.exe"
  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++ )
  {
  // Try another filename for the link.
  wsprintf ( szNewFilename, _T("%sHard link (%u) to %s"),
             m_szFolderDroppedIn, uLinkNum, szSrcFileTitle );

  // If the resulting filename is longer than MAX_PATH, show an
  // error message.
  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;
    }
  }  // end for

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 );
    }  // end while
 
  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.

 [Hard link to the file - 4K]

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:

// #define NOT_ON_WIN2K

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 »

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior) VMware
United States United States
Michael lives in sunny Mountain View, California. He started programming with an Apple //e in 4th grade, graduated from UCLA with a math degree in 1994, and immediately landed a job as a QA engineer at Symantec, working on the Norton AntiVirus team. He pretty much taught himself Windows and MFC programming, and in 1999 he designed and coded a new interface for Norton AntiVirus 2000.
Mike has been a a developer at Napster and at his own lil' startup, Zabersoft, a development company he co-founded with offices in Los Angeles and Odense, Denmark. Mike is now a senior engineer at VMware.

He also enjoys his hobbies of playing pinball, bike riding, photography, and Domion on Friday nights (current favorite combo: Village + double Pirate Ship). He would get his own snooker table too if they weren't so darn big! He is also sad that he's forgotten the languages he's studied: French, Mandarin Chinese, and Japanese.

Mike was a VC MVP from 2005 to 2009.

Comments and Discussions

 
QuestionIContextMenu::GetCommandString method Pin
AnjumSKhan17-Aug-16 9:38
AnjumSKhan17-Aug-16 9:38 
QuestionEquivalent to QueryContextMenu? Pin
Robert97431-Mar-11 3:42
Robert97431-Mar-11 3:42 
QuestionThis is possible, right? Pin
Sebastien Mongrain18-Jan-10 11:06
Sebastien Mongrain18-Jan-10 11:06 
AnswerRe: This is possible, right? [edited] Pin
Garth J Lancaster18-Jan-10 13:18
professionalGarth J Lancaster18-Jan-10 13:18 
GeneralRe: This is possible, right? [edited] Pin
Sebastien Mongrain19-Jan-10 5:02
Sebastien Mongrain19-Jan-10 5:02 
GeneralHardLink is not woking in Vista Pin
GrantFei1-Feb-09 14:52
GrantFei1-Feb-09 14:52 
GeneralRe: HardLink is not woking in Vista Pin
GrantFei2-Feb-09 2:57
GrantFei2-Feb-09 2:57 
GeneralHandle taskbar Contextmenu for lock/unlock toolbars. Pin
knareshkumar14-Dec-08 23:43
knareshkumar14-Dec-08 23:43 
Questionis there a way to have a shell extension not on right-drag-and-drop but on usual left-drag-and drop ? Pin
carabutnicolae123415-Oct-07 2:09
carabutnicolae123415-Oct-07 2:09 
AnswerRe: is there a way to have a shell extension not on right-drag-and-drop but on usual left-drag-and drop ? Pin
Michael Dunn21-Oct-07 14:01
sitebuilderMichael Dunn21-Oct-07 14:01 
GeneralRe: is there a way to have a shell extension not on right-drag-and-drop but on usual left-drag-and drop ? Pin
carabutnicolae123425-Oct-07 23:55
carabutnicolae123425-Oct-07 23:55 
http://msdn2.microsoft.com/en-us/library/bb776049.aspx
http://www.codeproject.com/atl/CopyHook.asp

for a while the problem was canceled, now I have it back, so I will continue asking.

From the links above I see it is works on folders only.
I need to have a way to intercept the copy for files and folders,
and when it happens to pop up a UI that will allow to do some custom things on those items.

One of the idea is to replace in the IAT for explorer.exe(all the actions happen from the explerer only) the pointer to the SHFileOperation, but in-memory patching might not work on another OS like vista where all the suspect operations can be canceled, or any antiviruses that can block this. Besides that, it is a better way to have a shell extension that will allow a more 'legal' activity of the process.

QuestionIContexMenu::InvokeCommand not called on Vista Pin
sathink25-Jul-07 1:48
sathink25-Jul-07 1:48 
AnswerRe: IContexMenu::InvokeCommand not called on Vista Pin
Michael Dunn28-Jul-07 9:28
sitebuilderMichael Dunn28-Jul-07 9:28 
AnswerRe: IContexMenu::InvokeCommand not called on Vista Pin
Hugo Caldeira13-Apr-09 23:07
Hugo Caldeira13-Apr-09 23:07 
QuestionAsync Transfers in a Namespace Extension Pin
altronome13-Apr-07 13:53
altronome13-Apr-07 13:53 
GeneralShel Drag Drop-ICopyHook::copyCallback() Not Called Pin
16141113-Nov-06 1:55
16141113-Nov-06 1:55 
GeneralRe: Shel Drag Drop-ICopyHook::copyCallback() Not Called Pin
Michael Dunn18-Nov-06 15:08
sitebuilderMichael Dunn18-Nov-06 15:08 
Generalany body help me Pin
syriast31-Jul-06 2:40
syriast31-Jul-06 2:40 
GeneralFailed to build in Visual Studio .NET Pin
Jonas A29-Aug-05 8:02
Jonas A29-Aug-05 8:02 
GeneralRe: Failed to build in Visual Studio .NET Pin
Michael Dunn29-Aug-05 11:07
sitebuilderMichael Dunn29-Aug-05 11:07 
GeneralRe: Failed to build in Visual Studio .NET Pin
belgab15-Jun-06 6:21
belgab15-Jun-06 6:21 
GeneralRe: Failed to build in Visual Studio .NET Pin
rockryan23-Jul-06 20:32
rockryan23-Jul-06 20:32 
GeneralFolder Files being linked from Pin
vaughandaly30-Jan-05 23:29
vaughandaly30-Jan-05 23:29 
GeneralRe: Folder Files being linked from Pin
Michael Dunn1-Feb-05 11:53
sitebuilderMichael Dunn1-Feb-05 11:53 
GeneralRe: Folder Files being linked from Pin
vaughandaly7-Feb-05 1:11
vaughandaly7-Feb-05 1:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.