Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
30 May 2006 3  
A tutorial on using owner-drawn menus in a context menu shell extensions, and on making a context menu extension that responds to a right-click in a directory background.

Contents

Introduction

In this part of the Idiot's Guide, I'll answer some reader requests and write about two topics: Using owner-drawn menus in a context menu extension, and making a context menu extension that's invoked when the user right-clicks the background of a directory window. You should read and understand Part I and Part II, so you understand the fundamentals of context menu extensions.

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.

Extension 1 - An Owner-Drawn Menu Item

In this section, I'll just cover the additional work required to implement owner-drawn menus.

Since this extension will have an owner-drawn menu, it's got to be something graphical. I decided to copy something done by the program PicaView[1]: Showing a thumbnail of a BMP file in its context menu. Here's what PicaView's menu items look like:

 [PicaView owner-drawn menu item - 23K]

This extension will create a thumbnail for BMP files, although to keep the code simple, I won't worry about getting the proportions or the colors exactly right. Fixing that up is left as an exercise for the reader. ;)

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 (with MFC support enabled) called BmpViewerExt, with a C++ implementation class CBmpCtxMenuExt.

As in the previous context menu extensions, this extension will implement the IShellExtInit interface. To add IShellExtInit to our COM object, open BmpCtxMenuExt.h and add the lines listed here in bold. There are also several member variables listed, which we'll use later while drawing our menu item.

#include <comdef.h>

 
/////////////////////////////////////////////////////////////////////////////

// CBmpCtxMenuExt

 
class CBmpCtxMenuExt : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
  public IShellExtInit
{
  BEGIN_COM_MAP(CBmpCtxMenuExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
  END_COM_MAP()
 
public:
  // IShellExtInit

  STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
 
protected:
  TCHAR   m_szFile[MAX_PATH];
  CBitmap m_bmp;
  UINT    m_uOurItemID;
 
  LONG m_lItemWidth, m_lItemHeight;
  LONG m_lBmpWidth, m_lBmpHeight;
 
  static const LONG m_lMaxThumbnailSize;
  static const LONG m_l3DBorderWidth;
  static const LONG m_lMenuItemSpacing;
  static const LONG m_lTotalBorderSpace;
 
  // Helper functions for handling the menu-related messages.

  STDMETHODIMP MenuMessageHandler(UINT, WPARAM, LPARAM, LRESULT*);
  STDMETHODIMP OnMeasureItem(MEASUREITEMSTRUCT*, LRESULT*);
  STDMETHODIMP OnDrawItem(DRAWITEMSTRUCT*, LRESULT*);
};

What we'll do in IShellExtInit::Initialize() is get the name of the file that was right-clicked, and if its extension is .BMP, create a thumbnail for it.

In the BmpCtxMenuExt.cpp file, add declarations for those static variables. These control visual aspects of the thumbnail and its borders. Feel free to change these around, and watch how they affect the final result in the menu.

const LONG CBmpCtxMenuExt::m_lMaxThumbnailSize = 64;
const LONG CBmpCtxMenuExt::m_l3DBorderWidth    = 2;
const LONG CBmpCtxMenuExt::m_lMenuItemSpacing  = 4;
const LONG CBmpCtxMenuExt::m_lTotalBorderSpace = 
               2*(m_lMenuItemSpacing+m_l3DBorderWidth);

These constants have the following meanings:

  • m_lMaxThumbnailSize: If the bitmap is larger (in either dimension) than this number, it will be compressed down to fit in a square, where each edge of the square is m_lMaxThumbnailSize pixels long. If the bitmap is smaller (in both dimensions), it will be drawn unchanged.
  • m_l3DBorderWidth: The thickness of the 3D border to be drawn around the thumbnail, in pixels.
  • m_lMenuItemSpacing: The number of pixels to leave blank around the 3D border. This provides a bit of space between the thumbnail and the surrounding menu items.

Also, add the definition of the IShellExtInit::Initialize() function:

STDMETHODIMP CBmpCtxMenuExt::Initialize (
  LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDO,
  HKEY hkeyProgID )
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());
 
COleDataObject dataobj;
HGLOBAL        hglobal;
HDROP          hdrop;
bool           bOK = false;
 
  dataobj.Attach ( pDO, FALSE );
 
  // Get the first selected file name.  I'll keep this simple and just check

  // the first name to see if it's a .BMP.

  hglobal = dataobj.GetGlobalData ( CF_HDROP );
 
  if ( NULL == hglobal )
    return E_INVALIDARG;
 
  hdrop = (HDROP) GlobalLock ( hglobal );
 
  if ( NULL == hdrop )
    return E_INVALIDARG;
 
  // Get the name of the first selected file.

  if ( DragQueryFile ( hdrop, 0, m_szFile, MAX_PATH ))
    {
    // Is its extension .BMP?

    if ( PathMatchSpec ( m_szFile, _T("*.bmp") ))
      {
      // Load the bitmap and attach our CBitmap object to it.

      HBITMAP hbm = (HBITMAP) LoadImage ( NULL, m_szFile, IMAGE_BITMAP,
                                          0, 0, LR_LOADFROMFILE );
 
      if ( NULL != hbm )
        {
        // We loaded the bitmap, so attach the CBitmap to it.

        m_bmp.Attach ( hbm );
        bOK = true;
        }
      }
    }
 
  GlobalUnlock ( hglobal );
  return bOK ? S_OK : E_FAIL;
}

Pretty straightforward stuff here. We load the bitmap from the file and attach a CBitmap object to it, for later use.

Interacting With the Context Menu

As before, if IShellExtInit::Initialize() returns S_OK, Explorer queries our extension for the IContextMenu interface. To give our extension the ability to do its owner-draw menu items, it also queries for IContextMenu3. IContextMenu3 adds a method to IContextMenu that we'll use to do our owner-drawing.

There is an IContextMenu2 interface, which the documentation claims is supported by shell version 4.00. However, when I tested on Windows 95, the shell never queried for IContextMenu2, so the end result is you just can't do an owner-drawn menu item on shell version 4.00. (NT 4 might be different; I don't have access to an NT 4 system to check.) Instead, you can make the menu item display a bitmap, but that results in a less-than-optimal appearance when the item is selected. (This is what PicaView does on version 4.00.)

IContextMenu3 derives from IContextMenu2, and adds the HandleMenuMsg2() method to IContextMenu. This method gives us a chance to respond to two menu messages: WM_MEASUREITEM and WM_DRAWITEM. The docs say that HandleMenuMsg2() is called so it can handle WM_INITMENUPOPUP and WM_MENUCHAR as well, however during my testing, HandleMenuMsg2() never received either of those messages.

Since IContextMenu3 inherits from IContextMenu2 (which itself inherits from IContextMenu), we just need to have our C++ class derive from IContextMenu3. Open up BmpCtxMenuExt.h and add the lines listed here in bold:

class CBmpCtxMenuExt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
  public IShellExtInit,
  public IContextMenu3
{
  BEGIN_COM_MAP(CSimpleShlExt)
    COM_INTERFACE_ENTRY(IShellExtInit)
    COM_INTERFACE_ENTRY(IContextMenu)
    COM_INTERFACE_ENTRY(IContextMenu2)
    COM_INTERFACE_ENTRY(IContextMenu3)
  END_COM_MAP()
  
public:
  // IContextMenu

  STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
  STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
  STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, LPSTR, UINT);
 
  // IContextMenu2

  STDMETHODIMP HandleMenuMsg(UINT, WPARAM, LPARAM);
 
  // IContextMenu3

  STDMETHODIMP HandleMenuMsg2(UINT, WPARAM, LPARAM, LRESULT*);

Modifying the Context Menu

As before, we do our work in the three IContextMenu methods. We add our menu item in QueryContextMenu(). We first check the shell version. If it's 4.71 or greater, then we can add an owner-drawn menu item. Otherwise, we add an item that shows a bitmap. In the latter case, adding the item is all we have to do; the menu takes care of showing the bitmap.

First, the code to check the shell version. It calls the DllGetVersion() exported function to retrieve the version. If that export is not present, then the DLL is version 4.00, since DllGetVersion() wasn't present in version 4.00.

STDMETHODIMP CBmpCtxMenuExt::QueryContextMenu (
  HMENU hmenu, UINT uIndex, UINT uidCmdFirst,
  UINT uidCmdLast, UINT uFlags )
{
  // If the flags include CMF_DEFAULTONLY then we shouldn't do anything.

  if ( uFlags & CMF_DEFAULTONLY )
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
 
bool bUseOwnerDraw = false;
HINSTANCE hinstShell;
 
  hinstShell = GetModuleHandle ( _T("shell32") );
 
  if ( NULL != hinstShell )
    {
    DLLGETVERSIONPROC pProc;
 
    pProc = (DLLGETVERSIONPROC) GetProcAddress(hinstShell, "DllGetVersion");
 
    if ( NULL != pProc )
      {
      DLLVERSIONINFO rInfo = { sizeof(DLLVERSIONINFO) };
 
      if ( SUCCEEDED( pProc ( &rInfo ) ))
        {
        if ( rInfo.dwMajorVersion > 4 ||
             (rInfo.dwMajorVersion == 4 &&  
              rInfo.dwMinorVersion >= 71) )
          bUseOwnerDraw = true;
        }
      }
    }

At this point, bUseOwnerDraw indicates whether it's OK to use an owner-drawn item. If it is true, we insert an owner-drawn item (see the line that sets mii.fType). If it's false, we add a bitmap item, and then tell the menu the handle of the bitmap to show.

MENUITEMINFO mii = {0};
 
  mii.cbSize = sizeof(MENUITEMINFO);
  mii.fMask  = MIIM_ID | MIIM_TYPE;
  mii.fType  = bUseOwnerDraw ? MFT_OWNERDRAW : MFT_BITMAP;
  mii.wID    = uidCmdFirst;
 
  if ( !bUseOwnerDraw )
    {
    // NOTE: This will put the full-sized bitmap in the menu, which is

    // obviously a bit less than optimal.  Scaling the bitmap down

    // to a thumbnail is left as an exercise.

    mii.dwTypeData = (LPTSTR) m_bmp.GetSafeHandle();
    }
 
  InsertMenuItem ( hmenu, uIndex, TRUE, &mii );
 
  // Store the menu item's ID so we can check against it later when

  // WM_MEASUREITEM/WM_DRAWITEM are sent.

  m_uOurItemID = uidCmdFirst;
 
  // Tell the shell we added 1 top-level menu item.

  return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

We store the menu item ID in m_uOurItemID so we will know the ID later on when other messages arrive. This isn't strictly necessary since we only have one menu item, but it's good practice nonetheless, and it's absolutely required if you have multiple items.

Showing Fly-by Help in the Status Bar

Displaying fly-by help is no different than in the earlier extensions. Explorer calls GetCommandString() to retrieve a string that it shows in the status bar.

#include <atlconv.h>  // for ATL string conversion macros

  
STDMETHODIMP CBmpCtxMenuExt::GetCommandString (
  UINT uCmd, UINT uFlags, UINT* puReserved,
  LPSTR pszName, UINT cchMax )
{
USES_CONVERSION;
static LPCTSTR szHelpString =
         _T("Select this thumbnail to view the entire picture.");
 
  // Check idCmd, it must be 0 since we have only one menu item.

  if ( 0 != uCmd )
      return E_INVALIDARG;
 
  // If Explorer is asking for a help string, copy our string into the

  // supplied buffer.

  if ( uFlags & GCS_HELPTEXT )
    {
    if ( uFlags & GCS_UNICODE )
      {
      // We need to cast pszName to a Unicode string, and then use the

      // Unicode string copy API.

      lstrcpynW ( (LPWSTR) pszName, T2CW(szHelpString), cchMax );
      }
    else
      {
      // Use the ANSI string copy API to return the help string.

      lstrcpynA ( pszName, T2CA(szHelpString), cchMax );
      }
    }
 
  return S_OK;
}

Carrying Out The User's Selection

The last method in IContextMenu is InvokeCommand(). This method is called if the user clicks on the menu item we added. The extension will call ShellExecute() to bring up the bitmap in the program that is associated with .BMP files.

STDMETHODIMP CBmpCtxMenuExt::InvokeCommand (
  LPCMINVOKECOMMANDINFO pInfo )
{
  // If lpVerb really points to a string, ignore this

  // call and bail out.

  if ( 0 != HIWORD(pInfo->lpVerb) )
    return E_INVALIDARG;
 
  // The command ID must be 0 since we only have one menu item.

  if ( 0 != LOWORD(pInfo->lpVerb) )
    return E_INVALIDARG;
 
  // Open the bitmap in the default paint program.

int nRet;
 
  nRet = (int) ShellExecute ( pInfo->hwnd, _T("open"), m_szFile,
                              NULL, NULL, SW_SHOWNORMAL );
 
  return (nRet > 32) ? S_OK : E_FAIL;
}

Drawing the Menu Item

OK, so I bet you're bored by now with all the code you've seen before. Time to get to the new and interesting stuff! The two methods added by IContextMenu2 and IContextMenu3 are listed below. They just call through to a single helper function, which in turn calls a message handler. This is the method I devised to keep from having two different versions of the message handlers (one for IContextMenu2 and IContextMenu3). There's a bit of strangeness with HandleMenuMsg2() regarding the LRESULT* parameter, explained below in the comments.

STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg (
  UINT uMsg, WPARAM wParam, LPARAM lParam )
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());  // init MFC

 
  // res is a dummy LRESULT variable.  It's not used (IContextMenu2::HandleMenuMsg()

  // doesn't provide a way to return values), but it's here so that the code 

  // in MenuMessageHandler() can be the same regardless of which interface it

  // gets called through (IContextMenu2 or 3).

LRESULT res;
 
  return MenuMessageHandler ( uMsg, wParam, lParam, &res );
}
 
STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg2 (
  UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult )
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState());  // init MFC

 
  // For messages that have no return value, pResult is NULL.

  // If it is NULL, I create a dummy LRESULT variable, so the code in

  // MenuMessageHandler() will always have a valid pResult pointer.

  if ( NULL == pResult )
    {
    LRESULT res;
    return MenuMessageHandler ( uMsg, wParam, lParam, &res );
    }
  else
    return MenuMessageHandler ( uMsg, wParam, lParam, pResult );
}

MenuMessageHandler() just dispatches WM_MEASUREITEM and WM_DRAWITEM to separate message handler functions.

HRESULT CBmpCtxMenuExt::MenuMessageHandler (
  UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult )
{
  switch ( uMsg )
    {
    case WM_MEASUREITEM:
      return OnMeasureItem ( (MEASUREITEMSTRUCT*) lParam, pResult );
    break;
 
    case WM_DRAWITEM:
      return OnDrawItem ( (DRAWITEMSTRUCT*) lParam, pResult );
    break;
    }
 
  return S_OK;
}

As I noted earlier, the docs say that the shell should let our extension handle WM_INITMENUPOPUP and WM_MENUCHAR, but I have never seen those messages arrive during my testing.

Handling WM_MEASUREITEM

The shell sends our extension a WM_MEASUREITEM message to request the dimensions of our menu item. We start by checking that we're being called for our own menu item. If that test passes, we get the dimensions of the bitmap, then calculate the size of the entire menu item.

First the bitmap size:

HRESULT CBmpCtxMenuExt::OnMeasureItem (
  MEASUREITEMSTRUCT* pmis, LRESULT* pResult )
{
BITMAP bm;
LONG   lThumbWidth, lThumbHeight;
 
  // Check that we're getting called for our own menu item.

  if ( m_uOurItemID != pmis->itemID )
    return S_OK;
 
  m_bmp.GetBitmap ( &bm );
 
  m_lBmpWidth = bm.bmWidth;
  m_lBmpHeight = bm.bmHeight;

Next, we calculate the thumbnail size, and from that, the size of the entire menu item. If the bitmap is smaller than the maximum thumbnail size (which, in the sample project, is 64x64) then it will be drawn as-is. Otherwise, it will be drawn at 64x64 resolution. This may distort the bitmap's appearance a bit; making the thumbnail look nice is left as an exercise.

  // Calculate the bitmap thumbnail size.

  lThumbWidth = (m_lBmpWidth <= m_lMaxThumbnailSize) ? m_lBmpWidth : 
                 m_lMaxThumbnailSize;
 
  lThumbHeight = (m_lBmpHeight <= m_lMaxThumbnailSize) ? m_lBmpHeight :
                  m_lMaxThumbnailSize;
 
  // Calculate the size of the menu item, which is the size of the thumbnail +

  // the border and padding (which provides some space at the edges of the item).

  m_lItemWidth = lThumbWidth + m_lTotalBorderSpace;
  m_lItemHeight = lThumbHeight + m_lTotalBorderSpace;

Now that we have the size of the menu item, we store the dimensions in the MENUITEMSTRUCT that we received with the message. Explorer will reserve enough space in the menu for our item.

  pmis->itemWidth = m_lItemWidth;
  pmis->itemHeight = m_lItemHeight;
 
  *pResult = TRUE;  // we handled the message

  return S_OK;
}

Handling WM_DRAWITEM

When we receive a WM_DRAWITEM message, Explorer is requesting that we actually draw our menu item. We start off by calculating the RECT in which we'll draw the 3D border around the thumbnail. This RECT is not necessarily the same size as what we returned from the WM_MEASUREITEM handler, because the menu might be wider than that size if there are other, larger items in the menu.

HRESULT CBmpCtxMenuExt::OnDrawItem (
  DRAWITEMSTRUCT* pdis, LRESULT* pResult )
{
CDC   dcBmpSrc;
CDC*  pdcMenu = CDC::FromHandle ( pdis->hDC );
CRect rcItem ( pdis->rcItem );  // RECT of our menu item

CRect rcDraw;                   // RECT in which we'll be drawing

 
  // Check that we're getting called for our own menu item.

  if ( m_uOurItemID != pdis->itemID )
    return S_OK;
            
  // rcDraw will first be set to the RECT that we set in WM_MEASUREITEM.

  // It will get deflated as we go along.

  rcDraw.left = rcItem.CenterPoint().x - m_lItemWidth/2;
  rcDraw.top = rcItem.CenterPoint().y - m_lItemHeight/2;
  rcDraw.right = rcDraw.left + m_lItemWidth;
  rcDraw.bottom = rcDraw.top + m_lItemHeight;
 
  // Shrink rcDraw to account for the padding space around

  // the thumbnail.

  rcDraw.DeflateRect ( m_lMenuItemSpacing, m_lMenuItemSpacing );

The first drawing step is to color the background of the menu item. The itemState member of the DRAWITEMSTRUCT indicates whether our item is selected or not. This determines what color we use for the background.

  // Fill in the background of the menu item.

  if ( pdis->itemState & ODS_SELECTED )
    pdcMenu->FillSolidRect ( rcItem, GetSysColor(COLOR_HIGHLIGHT) );
  else
    pdcMenu->FillSolidRect ( rcItem, GetSysColor(COLOR_MENU) );

Next, we draw a sunken border to make the thumbnail appear recessed into the menu.

  // Draw the sunken 3D border.

  for ( int i = 1; i <= m_l3DBorderWidth; i++ )
    {
    pdcMenu->Draw3dRect ( rcDraw, GetSysColor ( COLOR_3DDKSHADOW ),
                          GetSysColor(COLOR_3DHILIGHT) );
 
    rcDraw.DeflateRect ( 1, 1 );
    }

The last step is to draw the thumbnail itself. I took the simple route and did a simple StretchBlt() call. The result isn't always pretty, but my goal was to keep the code simple.

  // Create a new DC and select the original bitmap into it.

CBitmap* pOldBmp;
 
  dcBmpSrc.CreateCompatibleDC ( &dc );
  pOldBmp = dcBmpSrc.SelectObject ( &m_bmp );
 
  // Blit the bitmap to the menu DC.

  pdcMenu->StretchBlt ( rcDraw.left, rcDraw.top, rcDraw.Width(), rcDraw.Height(),
                        &dcBmpSrc, 0, 0, m_lBmpWidth, m_lBmpHeight, SRCCOPY );
 
  dcBmpSrc.SelectObject ( pOldBmp );
 
  *pResult = TRUE;  // we handled the message

  return S_OK;
}

Note that in a real shell extension, it would be a good idea to use a flicker-free drawing class, so that the menu item won't flicker as you move the mouse over it.

Here are some screen shots of the menus! First, the owner-drawn menu, in the unselected and selected states.

 [Owner-drawn menu - 18K]  [Owner-drawn menu, selected - 18K]

And here's the menu on shell version 4.00. Notice how the selected state inverts all the colors, making it kinda ugly.

 [Bitmap menu item - 4K]  [Bitmap menu item, selected - 4K]

Registering the shell extension

Registering our bitmap viewer is done the same way as our other context menu extensions. Here's the RGS script to do the job:

HKCR
{
  NoRemove Paint.Picture
  {
    NoRemove ShellEx
    {
      NoRemove ContextMenuHandlers
      {
        BitmapPreview = s '{D6F469CD-3DC7-408F-BB5F-74A1CA2647C9}'
      }
    }
  }
}

Note that the "Paint.Picture" file type is hard-coded here. If you don't use Paint as your default viewer for .BMP files, you'll need to change "Paint.Picture" to whatever is stored in the default value in the key HKCR\.bmp. Needless to say, in production code, you should do this registration in DllRegisterServer(), so you can check whether "Paint.Picture" is the right key to write to. I have more to say about this subject in Part I.

Extension 2 - Handling a Right-Click in a Directory Window

In shell version 4.71 and later, you can modify the context menu displayed when you right-click the desktop or any Explorer window that is viewing a file system directory. Programming this kind of extension is similar to other context menu extensions. The two major differences are:

  1. The parameters to IShellExtInit::Initialize() are used differently.
  2. The extension is registered under a different key.

I won't go through all the steps again required for the extension. Check out the sample project if you want to see the whole thing.

Differences in IShellExtInit::Initialize()

Initialize() has a pidlFolder parameter which, until now, we've ignored because it's been NULL. Now, finally, that parameter has some use! It is the PIDL of the directory where the right-click happened. The second parameter (the IDataObject*) is NULL, because there are no selected files to operate on.

Here's the implementation of Initialize() from the sample project:

STDMETHODIMP CBkgndCtxMenuExt::Initialize (
  LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDO,
  HKEY hkeyProgID )
{
  // We get the conventional path of the directory from its PIDL with the

  // SHGetPathFromIDList() API.

  return SHGetPathFromIDList(pidlFolder, m_szDirClickedIn) ? S_OK : E_INVALIDARG;
}

The SHGetPathFromIDList() function returns the full path of the folder, which we then store for later use. It returns a BOOL indicating success or failure.

Differences in IShellExtInit::GetCommandString()

Starting with XP, Explorer checks the verb names returned from GetCommandString(), and removes any items that have identical verbs. Our GetCommandString() has to check the flags for GCS_VERB, and return a different verb name for each of our menu items:

STDMETHODIMP CBkgndCtxMenuExt::GetCommandString (
    UINT uCmd, UINT uFlags, UINT* puReserved,
    LPSTR pszName, UINT cchMax )
{
USES_CONVERSION;
static LPCTSTR szItem0Verb = _T("CPBkgndExt0");
static LPCTSTR szItem1Verb = _T("CPBkgndExt1");
 
  // Check idCmd, it must be 0 or 1 since we have two menu items.

  if ( uCmd > 1 )
    return E_INVALIDARG;
 
  // Omitted: code to return a fly-by help string

 
  // Return verb names for our two items.

  if ( GCS_VERBA == uFlags )
    lstrcpynA ( pszName, T2CA((0 == uCmd) ? szItem0Verb : szItem1Verb), cchMax );
  else if ( GCS_VERBW == uFlags )
    lstrcpynW ( (LPWSTR) pszName, T2CW((0 == uCmd) ? szItem0Verb : szItem1Verb), cchMax );
 
  return S_OK;
}

Differences in Registration

This type of extension is registered under a different key: HKCR\Directory\Background\ShellEx\ContextMenuHandlers. Here's the RGS script to do it:

HKCR
{
  NoRemove Directory
  {
    NoRemove Background
    {
      NoRemove ShellEx
      {
        NoRemove ContextMenuHandlers
        {
          ForceRemove SimpleBkgndExtension = s '{9E5E1445-6CEA-4761-8E45-AA19F654571E}'
        }
      }
    }
  }
}

Aside from these two differences, the extension works just like other context menu extensions. There is one catch in IContextMenu::QueryContextMenu(), though. On Windows 98 and 2000, the uIndex parameter seems to always be -1 (0xFFFFFFFF). Passing -1 as the index to InsertMenu() means the new item goes at the bottom of the menu. However, if you increment uIndex, it will become to zero, meaning that if you pass uIndex again to InsertMenu(), the second item will appear at the top of the menu. Check out the sample project's code for QueryContextMenu() to see how to properly add items in the correct place.

Here's what the modified context menu looks like, with two items added at the end. Note that IMHO, adding items to the end of the menu in this way has major usability problems. When a user wants to select the Properties item, it is a habitual action to right-click and then pick the last menu item. When our extension comes along and adds items after Properties, we render the user's habit invalid, which can cause frustration and generate some nasty emails. ;)

 [modified context menu - 42K]

To Be Continued...

Coming up in Part VIII, I'll demonstrate a column handler extension, which can add columns to Explorer's details view.

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

September 1, 2000: Article first published.
September 14, 2000: Something updated. ;)
May 30, 2006: Updated to cover changes in VC 7.1, cleaned up code snippets, sample project gets themed on XP.

Series Navigation: « Part VI | Part VIII »

End Notes

[1] PicaView was made by ACD Systems, but is no longer available.

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