Cascading and Bitmapped Context Menus

There are things in software development that seem to scare otherwise intrepid and bold programmers. Although it's definitely an obvious and straightforward technique, when confronted with it you're instantly suspicious and mistrustful. This is the common feeling that programmers worldwide share about using cascading and bitmapped shell extension context menus. Showing a custom child pop-up menu from a context menu requires you add a few more lines of code than showing a custom menu item. Actually, it is as easy as creating and inserting a pop-up menu into the menu handle the shell passes you. The typical example of a cascading shell context menu is the SendTo menu. (See Figure 1)


Figure 1 - Shell extensions such as the standard 'Send To' menu option enable users to interact with objects in the shell without having to run a separate application

There's another interesting characteristics in that menu, though. It also features bitmapped menu items. Obtaining this, far from being impossible or particularly hard, is only a bit more boring. In this article, I'll show you just how to create cascading and bitmapped shell context menus. I'll also assume that you are familiar with shell extension development so you won't find here a primer on shell extensions. If you need it, check out my Visual C++ Windows Shell Programming book from Wrox Press or dig into MSDN documentation.

Cascading Menus

When you write a context menu shell extension, you usually create a COM object that exposes the IContextMenu and IShellExtInit interfaces. With ATL this is really a snap. Using a progID of CascMenu.GotoList, the main class would look like this:
class ATL_NO_VTABLE CAppList : 
 public CComObjectRootEx<CComSingleThreadModel>,
 public CComCoClass<CGotoList, &CLSID_GotoList>,
 public IShellExtInitImpl,
 public IContextMenuImpl,
 public IDispatchImpl<IGotoList, 
 &IID_IGotoList, &LIBID_CASCMENULib>

I assume you already have a header file with a minimal implementation of IShellExtInit and IContextMenu. Normally, you don't need to do as many things through IshellExtInit so the following implementation may suit you more often than you think.
class ATL_NO_VTABLE IShellExtInitImpl : 
public IShellExtInit
{
public:
 STDMETHOD(QueryInterface)(REFIID, void**) = 0;
 _ATL_DEBUG_ADDREF_RELEASE_IMPL( IShellExtInitImpl )
 STDMETHOD(Initialize)(LPCITEMIDLIST, LPDATAOBJECT, HKEY)
 { return S_FALSE; };
};

Instead, you need to pay more attention to the IContextMenu's methods since it actually provides the core behavior of the shell extension. In its simplest encarnation, IContextMenu requires you to implement three methods: QueryContextMenu, InvokeCommand, and GetCommandString.

The first method receives the context menu handle (HMENU) from the shell. It is expected to manipulate the shell by adding all the new items the extension wants to support. InvokeCommand is the method that gets into the game when the user clicks on any of the items the shell extension manages. It must detect which item the user selects and run any code it reckons to be appropriate. GetCommandString helps the shell retrieve either the command's verb or a descriptive text to be shown on the Explorer's status bar. The verb is a language-independent name for the command. If specified, this name will be passed to InvokeCommand to help identify the clicked item. Otherwise, InvokeCommand can only determine the selected item through an offset ID -- 0 for the first item added, 1 for the second, and so on.

Arranging a Goto Context Menu

If you already love the SendTo menu, you'll love the new Goto menu I'm about to add to the folders and file system directories. It will show you a pop-up menu with a list of favorite folders you want to be just one click away from your mouse. As a first step, let's see how to add a pop-up menu with a few fixed links. Modify the coclass constructor by filling a couple of arrays, one for the items and one for the respective help text.
CGotoList() {
 menuItems.Add((CComBSTR) L"Web Site");
 menuItems.Add((CComBSTR) L"Windows System");

 menuItemsHelp.Add((CComBSTR) 
  L"Go to the root of the local Web server");
 
 menuItemsHelp.Add((CComBSTR) 
  L"Go to the Windows system folder");
}

In this sample source code I'm using the CSimpleArray class, but feel free to use any other container class you think works.

protected:
 CSimpleArray<CComBSTR> menuItems;
 CSimpleArray<CComBSTR> menuItemsHelp;

The first IContextMenu's method that gets called once the shell extension is properly compiled and registered is QueryContextMenu.

HRESULT CGotoList::QueryContextMenu(
HMENU hmenu, 
UINT indexMenu, 
UINT idCmdFirst, 
UINT idCmdLast, 
UINT uFlags)

The HMENU argument represents the shell context menu handle. Use regular Win32 API functions on this handle to append new pop-ups or individual items. The idCmdFirst argument tells you the offset to use for the first item. You usually assign this value to a local variable that increases item after item. UINT idCmd = idCmdFirst;

If you want to insert individual items, just call InsertMenu as many times as you need.

InsertMenu(hmenu, indexMenu++, MF_STRING|MF_BYPOSITION,
           idCmd++, szItemText);

The indexMenu argument lets you know which is the position where to add the first menu item. If you want to add a pop-up menu, start by creating an empty one and fill it with child items. For example,

HMENU hmnuPopup = CreatePopupMenu();
InsertMenu(hmnuPopup, 0, MF_STRING|MF_BYPOSITION,
           idCmd++, OLE2T(menuItems[0]));

InsertMenu(hmnuPopup, 1, MF_STRING|MF_BYPOSITION,
           idCmd++, OLE2T(menuItems[1]));

Don't use indexMenu here as this is an internal menu you're creating from scratch. The first item must be added at position 0. The final step just appends the pop-up to the shell menu handle:

InsertMenu(hmenu, indexMenu++, MF_POPUP|MF_BYPOSITION,
           (UINT)hmnuPopup, _T("Goto"));

Since you're inserting new items into the shell menu, use the indexMenu argument to indicate the position. As the Win32 API recommends, specify the pop-up handle instead of the element ID to set the fourth parameter. The final argument is the text that will characterize the pop-up. What you've built so far is shown in Figure 2. Notice the two sub-items and the help text on the status bar. Remember, this text must be shorter than 40 characters. The GetCommandString method should be ready to handle both ASCII and Unicode text.


Figure 2 - Keeping the text short ensures that users can quickly see what actions they can use with a given object in the shell space

if (uFlags == GCS_HELPTEXTA) {
 lstrcpy(pszText, OLE2T(menuItemsHelp[idCmd]));
}
It receives the offset ID of the element and a flag indicating whether you should return the help text or the verb, and in which character set. The method's signature declares the output buffer for the text (pszText) as a LPSTR. However, you can safely cast it to LPWSTR in case a Unicode text is requested.

When the User Clicks...

The method InvokeCommand takes care of handling all the users clicking on the items the shell extension added to the menu. It receives a structure named CMINVOKECOMMANDINFO with all the information available about the context. You must decide whether you want to identify items through verbs or IDs. In case you want to use IDs, apply the following filter in InvokeCommand:
if (HIWORD(lpcmi->lpVerb) == 0) {
 UINT idCmd = LOWORD(lpcmi->lpVerb);
}

What you do next depends upon what functionality your shell extension is supposed to provide. The sample GotoList extension is intended to supply shortcuts to open frequently accessed folders more quickly. The code within InvokeCommand could just open a new copy of Explorer on the specified path.

ShellExecute(NULL, _T("explore"), 
             OLE2T(menuItemsData[idCmd]), NULL, NULL, 
			 SW_SHOW);

To keep track of the path associated with each item you can use yet another array - say menuItemsData.

It would be great if you could reuse the current instance of Explorer to navigate. Unfortunately, connecting to a running instance of Explorer is no picnic. Or better yet, you could connect but then you have the problem to make it navigate to the specified folder. If Explorer exposed the IWebBrowser2 interface, as Internet Explorer does, it would be extremely handy and as easy as implementing IObjectWithSite. There's no known way to get the Explorer's IWebBrowser2 interface. The only way to go is the hard one: get the handle of the address bar edit control and subclass it.

Owner-draw Menus

At this point we have a cascading context menus that makes a couple of folders only a click away. Of course, the shell extension could be enhanced to read the menu items from a text file or, why not, to mirror the Favorites folder. But this is good stuff for another article. In the meantime, let's see how to add little bitmaps to the items just like the inspiring SendTo menu does.

The shell provides special support for owner-draw menus. In fact, the SendTo or the New menu - two of the ones which feature little bitmaps near the text - simply rely on a very basic feature of Windows: owner-draw menus. To make a normal shell context menu into a bitmapped context menu, use IContextMenu3 instead of IContextMenu as your base class.

Actually, between IContextMenu and IContextMenu3 there's an intermediate interface called IContextMenu2. Version 2 of the interface adds the minimum support needed for bitmapped menus as it exposes an additional method called HandleMenuMsg.

HRESULT HandleMenuMsg(
 UINT uMsg, 
 WPARAM wParam,
 LPARAM lParam
); 

The method allows the shell extension to manage menu-related Windows messages such as WM_INITPMENUPOPUP, WM_DRAWITEM, and WM_MEASUREITEM. The last two messages are directly involved with the workings of an owner-draw menu. In IContextMenu3 you have a newer version of this method called HandleMenuMsg2. It handles WM_MENUCHAR and enables keyboard accelerator support for the menu. IContextMenu2 works properly with Windows 9x, Windows NT 4.0, and higher. On the other hand, IContextMenu3 needs shell version 4.71, which means Windows 98 or Windows 2000, and Windows 95 and Windows NT 4.0 provided that Active Desktop is installed.

Expose the new context menu interfaces and change the shell extension header file like this:

STDMETHOD(HandleMenuMsg)(UINT, WPARAM, LPARAM);
STDMETHOD(HandleMenuMsg2)(UINT, WPARAM, LPARAM, LRESULT*);

HandleMenuMsg just defaults to HandleMenuMsg2 where all the job is done.

HRESULT CGotoList::HandleMenuMsg(
UINT uMsg,WPARAM wParam, LPARAM lParam) {
 return HandleMenuMsg2(uMsg, wParam, lParam, NULL);
} 

If you want it to be fully backwards compatible, move the code I'm going to show for HandleMenuMsg2 in the IContextMenu2:: HandleMenuMsg method.

HRESULT CGotoList::HandleMenuMsg2(
UINT uMsg, WPARAM wParam, 
LPARAM lParam, LRESULT *plResult)
{
 switch(uMsg)
 {
  case WM_INITMENUPOPUP:
   g_hMenu = (HMENU) wParam;
  break;

  case WM_DRAWITEM:
   DrawMenuItem((LPDRAWITEMSTRUCT) lParam);
  break;

  case WM_MEASUREITEM:
   MeasureItem((LPMEASUREITEMSTRUCT) lParam);
  break;
 }

 return S_OK;
}

In this code, both MeasureItem and DrawMenuItem are private members of the CGotoList coclass. MeasureItem provides the expected width and height of the menu item. You normally would use constants values. DrawMenuItem takes care of the actual drawing process. It has three things to do. Firstly, it must ascertain why the menu is being drawn. It could be for selection, unselection, or focus. Secondly, it must figure out what is the bitmap and how to get it. Thirdly, it must draw both the bitmap and the companion text and handle fonts, brushes, and colors. The function is passed the handle of the physical device context of the menu window. Notice that you are completely free of deciding how the item appears. Putting the bitmap on the left side with a certain offset is only a convention. You could do the opposite if you want. Also notice that, while in this example I'm customizing the painting of pop-up menu items, there's really nothing to prevent you from customizing top-level menu items, too. Drawing customized menu items requires lots of GDI calls. Figure 3 shows the final result. Please refer to the available source code for the complete project.


Figure 3 - While not mandatory, you can even draw customized menu items using images to create professional looking shell extensions

As you may have noticed, the icon displayed is just the icon that Explorer utilizes for the folder. This is the information you can get from the SHGetFileInfo function:

SHFILEINFO sfi;
SHGetFileInfo(OLE2T(menuItemsData[nIndex]), 0, &sfi, 
              sizeof(SHFILEINFO), SHGFI_SMALLICON|SHGFI_ICON);

SHGetFileInfo returns a HICON handle which you can convert to a HBITMAP simply calling GetIconInformation

ICONINFO ii;
GetIconInfo(sfi.hIcon, &ii);
hbmItem = ii.hbmColor;

If you have a bitmap you can take advantage of new GDI functions in msimg32.dll like TransparentBlt. You need transparency in owner-draw menus in order to use one bitmap for both the selected and unselected case. If you use TransparentBlt you need to specify a transparent color the function will replace with the current HDC background color. This is information you normally know only if you're providing the bitmap yourself. In this case, icons are mostly part of system imagelist so using DrawIconEx gives you transparency over plain HICONs.

Keep in mind that in order to make a menu item an owner-draw item you must turn on the proper bit (MF_OWNERDRAW) while calling InsertMenu.

InsertMenu(hmnuPopup, 0, 
           MF_STRING|MF_BYPOSITION|MF_OWNERDRAW,
           idCmd++, OLE2T(menuItems[0]));

If you do this, Windows will skip painting that item for you.

The File Folder Background Menu

A context menu shell extension must be registered under the domain of a certain file class -- that is all the files with a certain extension. You can also associate it with system object such as directories or folders. An extra and quite interesting possibility is extending the file folder background context menu. This menu gets displayed whenever you right-click on the Explorer's right pane, but outside a folder item. In Figure 4 you can see such a menu extended with the Goto menu.


Figure 4 - Not only can you write extensions for your custom objects, you can also write them for system objects such as folders

To make a context menu shell extension affect this menu just register it also under:

HKCR  {
 NoRemove Directory {
 NoRemove Background {
 NoRemove Shellex {
 NoRemove ContextMenuHandlers {
 ForceRemove {882565E4-41BC-4D85-80DF-CBB0099B07AA}
}}}}}

Notice that this feature is not supported for non-file system folders like Printers or My Network Places.

Downloads

Download demo code - 28 Kb


Comments

  • Set Icon at 'Goto'

    Posted by rahul123 on 05/26/2010 11:23am

    Hi, In the above sample, I want to set image in 'Goto' similar to 'Add To Zip'. I tried to add MF_OWNERDRAW in InsertMenu of 'Goto' . But it becomes empty. for eg, InsertMenu(hmenu, indexMenu++, MF_POPUP|MF_BYPOSITION|MF_OWNERDRAW, (UINT)hmnuPopup, _T("Goto")); Could any one update me how to do this. Thanks in Advance.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Wednesday, September 24, 2014 8:00 AM - 9:00 AM PDT According to a recent Forrester Research report, many companies are choosing low-code platforms over traditional programming platforms, due to the speed with which low-code apps can be assembled and tested. With customer-facing applications on the rise, traditional programming platforms simply can't keep up with the "short schedules and rapid change cycles" required to develop these applications. Check out this upcoming webinar and join Clay Richardson from …

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds