Submenus in a context menu extension
In this article, I'll cover a tricky aspect of context menu extensions - submenus. The approach most people take at first to creating submenus leads to odd behavior in Explorer, but once you know the trick to make Explorer manage the menu correctly, it's easy! This article assumes you have a good grasp of context menu extensions. If you need a refresher, see part 1 and part 2 of my shell extension series.
Adding a submenu
This article's extension is a simple Open With... submenu, which has two items, Notepad and Internet Explorer. It behaves like the enhanced Open With menu in XP, and opens the selected file in the program that you pick. This Open With menu will demonstrate how to properly create a submenu in an extension.
What you might try first
The first thing that comes to mind is to create a new menu with CreatePopupMenu()
and insert it into the menu provided by Explorer.
HRESULT COpenWithCtxMenuExt::QueryContextMenu (
HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags )
{
if ( uFlags & CMF_DEFAULTONLY )
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
HMENU hSubmenu = CreatePopupMenu();
UINT uID = uidFirstCmd;
InsertMenu ( hSubmenu, 0, MF_BYPOSITION, uID++, _T("&Notepad") );
InsertMenu ( hSubmenu, 1, MF_BYPOSITION, uID++, _T("&Internet Explorer") );
InsertMenu ( hmenu, uMenuIndex, MF_BYPOSITION | MF_POPUP,
(UINT_PTR) hSubmenu, _T("C&P Open With") );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, uID - uidFirstCmd );
}
This actually works fine for the context menu, however the context menu items you add are duplicated on Explorer's File
menu. If you invoke the context menu repeatedly, you'll see leftover popups on the File
menu:
The cause is related to how Explorer cleans up its menus after extensions are invoked. The return value of QueryContextMenu()
tells Explorer how many items we add, and Explorer cleans up by calculating the IDs of our items and deleting them. Explorer knows the ID of the first item (since it passes us the value as uidCmdFirst
) and can calculate the IDs of the others since the IDs are assumed to be consecutive. However, the popup menu has no ID, so Explorer doesn't delete it.
The correct way
The secret is to insert the submenu using the InsertMenuItem()
API. What's different about using InsertMenuItem()
is that you can give the submenu an ID, which isn't possible when you use InsertMenu()
. Here's the corrected QueryContextMenu()
code.
HRESULT COpenWithCtxMenuExt::QueryContextMenu (
HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags )
{
if ( uFlags & CMF_DEFAULTONLY )
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, 0 );
HMENU hSubmenu = CreatePopupMenu();
UINT uID = uidFirstCmd;
InsertMenu ( hSubmenu, 0, MF_BYPOSITION, uID++, _T("&Notepad") );
InsertMenu ( hSubmenu, 1, MF_BYPOSITION, uID++, _T("&Internet Explorer") );
MENUITEMINFO mii = { sizeof(MENUITEMINFO) };
mii.fMask = MIIM_SUBMENU | MIIM_STRING | MIIM_ID;
mii.wID = uID++;
mii.hSubMenu = hSubmenu;
mii.dwTypeData = _T("C&P Open With");
InsertMenuItem ( hmenu, uMenuIndex, TRUE, &mii );
return MAKE_HRESULT ( SEVERITY_SUCCESS, FACILITY_NULL, uID - uidFirstCmd );
}
The return value this time is 3, which tells Explorer that we inserted 3 items (the two Open With items, and the submenu itself). Since all 3 items have IDs, Explorer can delete them all and completely remove our items from its menu.