|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
IntroductionIn 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 ItemIn 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 InterfaceYou 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 As in the previous context menu extensions, this extension will implement the #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 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:
Also, add the definition of the 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 Interacting With the Context MenuAs before, if There is an
Since 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 MenuAs before, we do our work in the three First, the code to check the shell version. It calls the 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, 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 Showing Fly-by Help in the Status BarDisplaying fly-by help is no different than in the earlier extensions. Explorer calls #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 SelectionThe last method in 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 ItemOK, 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 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 );
}
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 Handling WM_MEASUREITEMThe shell sends our extension a 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 pmis->itemWidth = m_lItemWidth; pmis->itemHeight = m_lItemHeight; *pResult = TRUE; // we handled the message return S_OK; } Handling WM_DRAWITEMWhen we receive a 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 // 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 // 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.
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 extensionRegistering 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 Extension 2 - Handling a Right-Click in a Directory WindowIn 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:
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()
Here's the implementation of 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 Differences in IShellExtInit::GetCommandString()Starting with XP, Explorer checks the verb names returned from 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 RegistrationThis type of extension is registered under a different key: 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 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
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 LicenseThis 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 HistorySeptember 1, 2000: Article first published. Series Navigation: « Part VI | Part VIII » End Notes[1] PicaView was made by ACD Systems, but is no longer available. | ||||||||||||||||||||