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:
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>
class CBmpCtxMenuExt :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
public IShellExtInit
{
BEGIN_COM_MAP(CBmpCtxMenuExt)
COM_INTERFACE_ENTRY(IShellExtInit)
END_COM_MAP()
public:
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;
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 );
hglobal = dataobj.GetGlobalData ( CF_HDROP );
if ( NULL == hglobal )
return E_INVALIDARG;
hdrop = (HDROP) GlobalLock ( hglobal );
if ( NULL == hdrop )
return E_INVALIDARG;
if ( DragQueryFile ( hdrop, 0, m_szFile, MAX_PATH ))
{
if ( PathMatchSpec ( m_szFile, _T("*.bmp") ))
{
HBITMAP hbm = (HBITMAP) LoadImage ( NULL, m_szFile, IMAGE_BITMAP,
0, 0, LR_LOADFROMFILE );
if ( NULL != hbm )
{
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:
STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, LPSTR, UINT);
STDMETHODIMP HandleMenuMsg(UINT, WPARAM, LPARAM);
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 ( 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 )
{
mii.dwTypeData = (LPTSTR) m_bmp.GetSafeHandle();
}
InsertMenuItem ( hmenu, uIndex, TRUE, &mii );
m_uOurItemID = uidCmdFirst;
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.");
if ( 0 != uCmd )
return E_INVALIDARG;
if ( uFlags & GCS_HELPTEXT )
{
if ( uFlags & GCS_UNICODE )
{
lstrcpynW ( (LPWSTR) pszName, T2CW(szHelpString), cchMax );
}
else
{
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 ( 0 != HIWORD(pInfo->lpVerb) )
return E_INVALIDARG;
if ( 0 != LOWORD(pInfo->lpVerb) )
return E_INVALIDARG;
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());
LRESULT res;
return MenuMessageHandler ( uMsg, wParam, lParam, &res );
}
STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg2 (
UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT* pResult )
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
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;
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.
lThumbWidth = (m_lBmpWidth <= m_lMaxThumbnailSize) ? m_lBmpWidth :
m_lMaxThumbnailSize;
lThumbHeight = (m_lBmpHeight <= m_lMaxThumbnailSize) ? m_lBmpHeight :
m_lMaxThumbnailSize;
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;
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 );
CRect rcDraw;
if ( m_uOurItemID != pdis->itemID )
return S_OK;
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;
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.
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.
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.
CBitmap* pOldBmp;
dcBmpSrc.CreateCompatibleDC ( &dc );
pOldBmp = dcBmpSrc.SelectObject ( &m_bmp );
pdcMenu->StretchBlt ( rcDraw.left, rcDraw.top, rcDraw.Width(), rcDraw.Height(),
&dcBmpSrc, 0, 0, m_lBmpWidth, m_lBmpHeight, SRCCOPY );
dcBmpSrc.SelectObject ( pOldBmp );
*pResult = TRUE;
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.
And here's the menu on shell version 4.00. Notice how the selected state inverts all the colors, making it kinda
ugly.
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:
- The parameters to
IShellExtInit::Initialize()
are used differently.
- 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 )
{
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");
if ( uCmd > 1 )
return E_INVALIDARG;
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. ;)
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.