Shell context menu support

The context menu is perhaps the sweetest thing about shell and the Windows OS in general. Just right-click on something to get a list of commands that are relevant to the particular item under the cursor. As the name suggests, the menu differs depending on the context, which in shell terms boils down to the file type in question. For example you'll get different options if you right click on My Computer compared to a simple file.

Exactly what do you get on a context menu? First of all you get all the verbs defined for the file type in question, like open, print etc. Then shell adds a few standard verbs of it's own like cut, copy, properties, and if applicable paste, delete and rename. A less obvious article is those extended verbs like "open with..." which only appear if <SHIFT> is pressed at the moment of the right-click. Finally, shell allows special COM objects called context memu handlers to insert extra menu items, if appropriate.

Context menu handlers are associated with a particular file type, or perhaps with a more generic "type"; for instance, the sendto extension is registered under HKCR\AllFilesystemObjects\shellex\ContextMenuHandlers, and that is the reason why you see it popping up almost all the time. Context menu handlers can add verbs dynamically depending on the context, as opposed to static verbs which are hard-coded in the registry. Why use a dynamic verb? Take for example winZip; it's context handler can add an icon to the menu as well as change the text to include the filename under the mouse cursor.

Building and showing the context menu is just half the story. The most important step is carrying out the command a user may select. Regarding static verbs, it's all too easy since shell simply executes the command registered under the verb, e.g. c:\winNT\notepad.exe "%1" for opening a text file, where %1 is replaced by the selected filename. Context menu handlers on the other hand learn about the files selected at the moment of their instantiation, just before the menu is shown. A fine yet important detail regarding selections involving multiple files: if a static verb is selected then the associated command will be executed multiple times, e.g. 10 notepad windows will open one for each filename. Dynamic verbs on the other hand treat the selected bunch as a single list of arguments to be handled by one instance of the command handler; if this is e.g. winZip, just one archive file will be created (as opposed to 10) containing all the files in the original selection.

That's how explorer manages context menus. Obviously you'd want to know how to duplicate this functionality in your programs, so that you're not the only one left alone to sleep in the barn <g>. Fear not, because this is the very subject of this whole section. Note that shell context menu is one of the trickiest "simple" things you can do; except for the basic shell COM plumbing, you also need to be familiar with menu handles and command routing. That's why I saw it fit to devote a large section solely on the subject.

Topics: Get the menu | Execute commands | Intercepting messages

Obtain the context menu

A single interface pointer IContextMenu can deliver all the functionality mentioned above, and more. Although exported by folder objects, I have this hunch that when you call GetUIObjectOf requesting a context menu, a new COM object is created, distinct from the folder object itself. Each IContextMenu is closely linked with the folder items used to initialize it, which are eventually used as the arguments of whatever command is to be executed. We'll see a code sample of the procedure shortly.

In the documentation there's mention about another two context menu interfaces, namely IContextMenu2 and IContextMenu3. You are beginning to get a taste of COM hell here, where developers have had second (and triple) thoughts after releasing the original IContextMenu specification. Once released, a COM interface is a binding contract the object must fulfil; if you want to add some new functionality you need a brand new interface. That's the story with context menus too. Microsoft though of owner drawn menus so they released IContextMenu2. Then somebody remembered that they forgot (!) about keyboard shortcuts and so IContextMenu3 was born.

Fortunately each "higher-version" interface derives from the previous in the list, so if you obtain a pointer to the highest IContextMenu3 then you can do all the tricks with just this one interface. The only complication is that you cannot ask the folder for anything other than the plain vanilla IContextMenu. Once this is successfully obtained, you need to query the context menu object for the highest version it supports. Here's the drill:
HRESULT GetSHContextMenu(LPSHELLFOLDER psfFolder, LPCITEMIDLIST localPidl,
                         void** ppCM, int* pcmType)
{
   *ppCM = NULL;
   LPCONTEXTMENU pICv1 = NULL; // plain version
   // try to obtain the lowest possible IContextMenu
   HRESULT hr = psfFolder->GetUIObjectOf(NULL, 1, &localPidl, 
                   IID_IContextMenu, NULL, (void**)&pICv1);
   if(pICv1) { // try to obtain a higher level pointer, first 3 then 2
      hr = pICv1->QueryInterface(IID_IContextMenu3, ppCM);
      if(NOERROR == hr) *pcmType = 3;
      else {
         hr = pICv1->QueryInterface(IID_IContextMenu2, ppCM);
         if(NOERROR == hr) *pcmType = 2;
      }

      if(*ppCM) pICv1->Release(); // free initial "v1.0" interface
      else { // no higher version supported
         *pcmType = 1;
         *ppCM = pICv1;
         hr = NOERROR; // never mind the query failures, this'll do
      }
   }

   return hr;
}

This function expects a folder object and a local PIDL for the item whose context menu is to be generated. If successful, it returns two things, the context menu itself in ppCM and pcmType which is an integer from 1—3, indicating the version of the menu pointer. Once the base version pointer is obtained, QueryInterface is used to go as high as possible. If any query succeeds, we'll end up with two valid pointers to the same object, so we Release the crappo one. This code can be easily modified to accommodate more than one objects.

The next step is to get an actual menu filled with items relevant to the selected object. QueryContextMenu does that for you. From the multitude of uFlags values mentioned in the documentation you'd seldom need anything other than CMF_EXPLORE which asks for the standard full menu version, and perhaps CMF_CANRENAME if this operation is possible within the folder in question.

A rather confusing issue with QueryContextMenu is the windows menu itself, and the allocation of command identifiers for each menu item. Usually you create an empty popup menu using CreatePopupMenu and pass this fresh handle to shell. In such a case the indexMenu argument to QueryContextMenu is 0, indicating that new menu items can be inserted right at the beginning. However, when it is high time to execute some command, the command identifier (ID) is used instead of the subitem offset in the menu.

This information is conveyed in the arguments idCmdFirst and idCmdLast, which prescribe the range of allowable IDs the context menu handlers may use for their commands. You should select this range carefully especially if you are planning to insert your own commands in the same menu, since then you'd need to know if the user opted for a shell command or one of yours. The good news is that QueryContextMenu is quite flexible and works with any reasonable command ID range. Usually you would set the minimum to 1 (ID==0 would be misinterpreted as error) and maximum to something like 30000, which should be more than enough, leaving all the remaining IDs up to 0xFFFF for your own internal consumption. Here's a small sample:
#define MIN_SHELL_ID 1
#define MAX_SHELL_ID 30000
CMenu menu;
menu.CreatePopupMenu();
int cmType; // we don't need this here
LPCONTEXTMENU pCM;
// assume that psfFolder and pidl are valid
HRESULT hr = GetSHContextMenu(psfFolder, pidl, (void**)&pCM, &cmType);
// fill the menu with the standard shell items
hr = pCM->QueryContextMenu(menu, 0, MIN_SHELL_ID, MAX_SHELL_ID, CMF_EXPLORE);
// show the menu and retrieve the selected command ID
int cmdID = menu.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN, cx, cy, this);

We have applied all the issues discussed above, using TrackPopupMenu to show the menu eventually. The use of the TPM_RETURNCMD flag is very important here, instructing TrackPopupMenu to return the selected command ID instead of directly sending the respective WM_COMMAND message itself. We don't really want the command to take the normal route, since we're going to delegate the command to IContextMenu. Just keep on reading <g>.

Execute the selected command

You need InvokeCommand to actually execute a command selected from the shell context menu. It accepts only one parameter of the CMINVOKECOMMANDINFO type, which neatly assembles all the necessary information. The data members are similar to those accepted by ShellExecuteEx. The most important among them is lpVerb which specifies exactly what command we want executed on the shell items (i.e. files) in question.

There is a number of canonical language-independent verbs you can use like cut, properties etc. Actually you don't have to show the menu, or even fill it in using QueryContextMenu if you just want to execute one of those special verbs on the selected bunch of items. You just obtain a valid IContextMenu and use InvokeCommand straight away. On the other hand, if you've already gone down the TrackPopupMenu avenue, you'd want to use the returned command ID instead. Allow me to demonstrate:
HRESULT ProcessCMCommand(LPCONTEXTMENU pCM, UINT idCmdOffset)
{
   CMINVOKECOMMANDINFO ici;
   ZeroMemory(&ici, sizeof(ici));
   ici.cbSize = sizeof(CMINVOKECOMMANDINFO);
   ici.lpVerb = MAKEINTRESOURCE(idCmdOffset);
   ici.nShow = SW_SHOWNORMAL;

   return pCM->InvokeCommand(&ici);
}

Instead of a text string we pass the command ID offset in the lpVerb member. The MAKEINTRESOURCE macro just casts the integer to a string; when InvokeCommand receives the data, it detects that the high word of the verb "pointer" is zero, and correctly interprets it as a command offset.

Note that I'm not talking about command IDs but offsets thereof. This is the final twist in the big ball of confusion which is the shell context menu. TrackPopupMenu has returned the ID associated with the menu command, but you'll need to subtract the MIN_SHELL_ID constant you passed with idCmdFirst to QueryContextMenu. Why? well folks it's quite complicated to explain this — you'd have to look at the source code of a context menu extension handler to understand the logic. But trust me, if you pass the zero-based offset all will be swell and Bob is your uncle <g>.

There is one last exception to keep in mind. If you enable the rename verb by passing CMF_CANRENAME in QueryContextMenu, you must be prepared to handle the renaming yourself, using SetNameOf. InvokeCommand couldn't help you since it only knows of the original item name, not the new one. What I recommend is using GetCommandString on the command ID offset, requesting the verb using GCS_VERB. If this turns out to be "rename", then ask the user for the new name and change it yourselves; otherwise proceed with InvokeCommand as normal.

ADVANCED: Unorthodox uses of context menu
The existence of canonical verbs means that you can use IContextMenu as an alternative means of file management, using verbs like "cut", "paste", "delete", even "pastelink" for creating shortcuts. This may seem an awkward way to deal with file management at first sight, but the advantage is that it could be the only viable option to manage virtual folders. The disadvantage is that you lose the fine control that SHFileOperation offers, with its various FOF_xxx options. For instance using the "delete" verb you cannot control whether a file ends up in the recycle bin or if it's obliterated. Still the technique could prove useful in certain circumstances.

Intercepting menu messages and commands

The information presented up to now is adequate for most tasks, but we haven't reached the end of our little trek yet. Try as you might, the "Send To" submenu will remain devoid of content, unless you release the last ace up your sleeve. Note that there wouldn't be a problem if this was a fixed submenu; but alas its contents are created on the fly when the menu is about to be shown. The only way round this problem is to allow the shell context menu to process menu-related messages like WM_INITMENUPOPUP.

How unintuitive is that then? I for one couldn't get my head round it the first time I heard about the trick. When there's a live shell context menu, you'll have to modify your window's regular message loop, allowing shell to have a crack at the messages calling HandleMenuMsg instead of your regular processing. This is a new member function introduced with IContextMenu2 and later versions. If you only have a plain IContextMenu pointer, then there's nothing you can do, but nowadays almost all shell folders expose at least IContextMenu2 — the only exception I am aware of is "Scheduled Tasks" which seems to have remained stuck in the ice age <g>.

This dual message processing mode can result to some ugly code for your menu handlers, but there's a sleeker alternative: temporarily subclass your window while the shell context menu is shown, redirecting relevant messages to HandleMenuMsg. This way you can maintain a readable main window message processing and tackle the SendTo problem at the same time. I got the idea from reading an article by Paul DiLascia, but I had to discard his overly intricate and complicated implementation of CSubclassWnd (somewhere in the middle of that article :) in favour of the following code:
// global variables used for passing data to the subclassing wndProc
WNDPROC g_pOldWndProc; // regular window proc
LPCONTEXTMENU2 g_pIContext2; // active shell context menu

void CMainFrame::OnContextMenu(CWnd* pWnd, CPoint pt) 
{
   CMenu menu;
   menu.CreatePopupMenu();
   int cmType; // "version" # of context menu
   LPCONTEXTMENU pCMx;
   // m_psfFolder: active IShellFolder; m_localPidl: selected item
   HRESULT hr = GetSHContextMenu(m_psfFolder, m_localPidl, (void**)&pCMx, 
                                 &cmType);
   // fill the menu with the standard shell items
   hr = pCMx->QueryContextMenu(menu, 0, MIN_SHELL_ID, MAX_SHELL_ID, 
                               CMF_EXPLORE);
   // insert a single item of our own for demonstration
   menu.InsertMenu(0, MF_BYPOSITION | MF_STRING, ID_APP_ABOUT, "&Custom");

   // install the subclassing "hook", for versions 2 or 3
   if(cmType > 1) {
      g_pOldWndProc = (WNDPROC)
         SetWindowLong(this->m_hWnd, GWL_WNDPROC, (DWORD)HookWndProc);
      g_pIContext2 = (LPCONTEXTMENU2)pCMx; // cast ok for ICMv3
   }
   else g_pOldWndProc = NULL;

   // show the menu and get the command
   UINT cmdID = menu.TrackPopupMenu(TPM_RETURNCMD | TPM_LEFTALIGN, 
                                    pt.x, pt.y, this);
   if(g_pOldWndProc) // restore old wndProc
      SetWindowLong(this->m_hWnd, GWL_WNDPROC, (DWORD)g_pOldWndProc);

   if(cmdID >= MIN_SHELL_ID && cmdID <= MAX_SHELL_ID)
      ProcessCMCommand(pCMx, cmdID-MIN_SHELL_ID /*offset passed*/ );
   else if(cmdID) PostMessage(WM_COMMAND, cmdID); // our command
   // else ID==0, no menu selection

   pCMx->Release();
   g_pIContext2 = NULL; // prevents accidental use
}

LRESULT CALLBACK HookWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
   UINT uItem;
   TCHAR szBuf[MAX_PATH];

   switch (msg) { 
   case WM_DRAWITEM:
   case WM_MEASUREITEM:
      if(wp) break; // not menu related
   case WM_INITMENUPOPUP:
      g_pIContext2->HandleMenuMsg(msg, wp, lp);
      return (msg==WM_INITMENUPOPUP ? 0 : TRUE); // handled

   case WM_MENUSELECT:
      // if this is a shell item, get it's descriptive text
      uItem = (UINT) LOWORD(wp);   
      if(0 == (MF_POPUP & HIWORD(wp)) && 
            uItem >= MIN_SHELL_ID && uItem <=  MAX_SHELL_ID) {
         g_pIContext2->GetCommandString(uItem-MIN_SHELL_ID, GCS_HELPTEXT,
            NULL, szBuf, sizeof(szBuf)/sizeof(szBuf[0]) );

         // set the status bar text
         ((CFrameWnd*)(AfxGetApp()->m_pMainWnd))->SetMessageText(szBuf);
         return 0;
      }
      break;

   default:
      break;
   }

   // for all untreated messages, call the original wndproc
   return ::CallWindowProc(g_pOldWndProc, hWnd, msg, wp, lp);
}

That sure was a big lump of code, so let's break it down in digestible portions. There's nothing strange about the way the context menu is initialised and filled, reusing the previously developed GetSHContextMenu. It's after this that things get interesting. First of all we inserted a new item of our own using InsertMenu, after shell filled the bulk of the menu. Our command is made unique by assigning it with an ID outside (over) the MAX_SHELL_ID limit we imposed on shell. The procedure for adding a custom popup submenu, if required, would be similar.

The crucial step is the subclassing of the main frame window just before the menu is shown. If our context menu pointer is IContextMenu2 or higher, we replace the default MFC message loop with our global HookWndProc() using SetWindowLong. This is "old-fashioned" subclassing, but nevertheless ideal for our lightweight requirements. Note that we store the old window procedure pointer in the global variable g_pOldWndProc so that our handler can redirect all unhandled messages to it for default processing. We also make the active context menu interface available in g_pIContext2.

While the context menu is shown, all messages go through HookWndProc. Messages that can be treated by HandleMenuMsg are eaten, whereas all the rest are sent to the previous window procedure. Note that I am not bothered with WM_MENUCHAR which is the reason d'etre of HandleMenuMsg2, since I don't really see the point — menu shortcuts work perfectly without any intervention. This also simplifies processing since everything can be treated with IContextMenu2.

Our override also intercepts WM_MENUSELECT messages for managing the help texts for each command. I am very partial to showing explanations of menu commands on the status bar (using SetMessageText), since it is both user-friendly and provides a good excuse for the lack of proper help files, which are so boring to compile <g>. If a particular command was inserted by shell, GetCommandString will provide a descriptive text. Now guys, believe it or not, this single item was responsible for a vast number of GPFs in early 2xExplorer versions, so take heed. It's all down to these cowboys I've warned you before, writing shell extensions. The documentation recommends keeping descriptive texts "reasonably short" (under 40 characters). There is also the extra security measure of cchMax parameter, stating clearly the maximum size of the supplied buffer. But would anybody comply? would they buggery...

Not only would certain individuals completely disregard the 40-character limit, opting for what amounts to a complete "manual" appearing on the status line, they would not even take any notice of the supplied buffer size, forcing whatever they had in mind down your buffer's throat. Stack overflow, followed by sudden death, possibly BSOD too. The obvious solution is to provide a large enough buffer to keep such extremists at bay, but still this wasn't enough to fend off the right-wing extremists. I can vouch for this geezer who would insist on writing a zero byte on your buffer, right past its end, even if his extension would only add 10—20 characters or so. How daft is that then? The poor daftian would actually take notice of the cchMax length argument but for all the wrong reasons, plus he would always presume a wide string buffer (i.e. UNICODE) hence where he though the end of the buffer was coincided with some arbitrary address down the road. Zero inserted there, division by zero, the end, RIP. I won't embarrass individuals revealing names/addresses, but I know who yer cack-handed cowboys are!... <g>

Another source of confusion surrounding GetCommandString is the type of the text requested, UNICODE or multibyte. This is something that even big guns in the industry, the rodeo masters, mess up totally. They would insist on returning a UNICODE string even if you explicitly request otherwise using GCS_HELPTEXTA — which is actually what GCS_HELPTEXT stands for when UNICODE is not defined. The symptom here is a help text exactly one character long, which isn't a threat per se, but it sure has a fruitcake quality. RTFM!. Anyway folks, I hope my scaremongery has alerted you sufficiently to be extra vigilant when working with context menus and shell extensions in general.

Back to the code description though. When a command is eventually selected, TrackPopupMenu returns its ID. The menu cycle is now over, so we reinstate the original window procedure straight away. The command processing depends on its ID: if this is within the shell range, ProcessCMCommand is used as before. Any other valid ID corresponds to a custom command managed by our program, and is processed as a regular WM_COMMAND message we PostMessage to ourselves.

Additional information

ARTICLE: - Shell Context Menu, Wicked Code, MSJ April 1997
Q105169 - Building a Dynamic Menu for TrackPopupMenu
Q139469 - HOWTO: How to Use TrackPopupMenu() and Update UI Handlers



Shell information Clipboard Contents