Context menu shell extensions using COM

By: Clayton Todd

Abstract: Context menus are the menus that pop up in Windows when you right-click on a file or object. This project shows how you can add your own items to these menus using COM. By Clayton Todd.

Note: The code for this project may be downloaded from Code Central.

A context menu is the menu that pops up when you right-click on a file or object in Windows. What we want to do is extend that menu and add some items of our own. This article shows how to build a COM object that will allow us to add menu items to the context menu that pops up when you right-click on a file with a .cpp extension. You can easily change it to work with any type of extension or any type of Windows object.

Start like this:

  • Start C++Builder.
  • File | New, select ActiveX tab, and select Active Library.
  • File | New, select ActiveX tab, and select COM Object.
  • The New COM Object Dialog pops up. Enter a name for the CoClass -- in this case I used MyContextMenuExention.
  • Enter a description.

Be sure to include the following header in the COM object header file:

#include <shlobj.h>

Now you find out if you need to makes some changes before continuing. Hit build and you may likely be greeted with some error messages.

> [C++ Error] shlobj.h(1762): E2238 Multiple declaration for 'FVSHOWINFO'
> [C++ Error] shlobj.h(1936): E2238 Multiple declaration for 'FOLDERSETTINGS'
> [C++ Error] shlobj.h(3717): E2238 Multiple declaration for 'DESKBANDINFO'
> [C++ Error] shlobj.h(4808): E2238 Multiple declaration for 'SHELLFLAGSTATE'

This problem is addressed in the upd1rdme.txt file in the builder directory, which appears after applying the service pack. For what it's worth, I have never tried the solution suggested in the upd1rdme.txt file. I simply commented the areas out of the ShlObj.hpp. Later I would read a post by Alex Bakaev [Team B] in borland.public.cppbuilder.activex that suggested commenting out the offending parts from the ShlObj.hpp file also. So there you have it. Someone in the know also suggests the "comment it out" solution. Great minds think alike, and all that.

The following code was all that I commented out of ShlObj.hpp to get the project to build.

//typedef FVSHOWINFO *PFVShowInfo;
//typedef FVSHOWINFO  TFVShowInfo;
//typedef FOLDERSETTINGS *PFolderSettings;
//typedef FOLDERSETTINGS  TFolderSettings;
...
//typedef DESKBANDINFO *PDeskBandInfo;
//typedef DESKBANDINFO  TDeskBandInfo;
...
//typedef SHELLFLAGSTATE *PShellFlagState;
//typedef SHELLFLAGSTATE  TShellFlagState;

As I said, I commented the areas out of the ShlObj.hpp file. You can do the same, or you can try the solution in the upd1rdme.txt file. I know at least one of them works for sure.

After you have made the changes the project should now build. Of course , it does nothing of any use to us yet. But building the project is always an accomplishment in C++, so you might as well celebrate this small victory.

In the header file of your COM object, remove the line public IMyContextMenuExtension, and replace it with public IShellExtInit, public IContextMenu:

class ATL_NO_VTABLE TMyContextMenuExtensionImpl : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TMyContextMenuExtensionImpl, &CLSID_MyContextMenuExtension>,
  public IMyContextMenuExtension	//Remove this line

So we should now have something like the following. Since we are planning to implement a ContextMenu ShellExtension it would be really helpful to add them both here:

class ATL_NO_VTABLE TMyContextMenuExtensionImpl :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<TMyContextMenuExtensionImpl, &CLSID_MyContextMenuExtension>,
  public IShellExtInit,
  public IContextMenu

Next, find the section below in the header file and delete the COM_INTERFACE_ENTRY(IMyContextMenuExtension) line.

BEGIN_COM_MAP(TMyContextMenuExtensionImpl)
  COM_INTERFACE_ENTRY(IMyContextMenuExtension) //Remove this line
END_COM_MAP()

Change it to look like the following:

BEGIN_COM_MAP(TMyContextMenuExtensionImpl)
   COM_INTERFACE_ENTRY(IShellExtInit)
   COM_INTERFACE_ENTRY(IContextMenu)
END_COM_MAP()

Now somewhere in the public section add the following:

// IShellExtInit method
STDMETHODIMP Initialize(LPCITEMIDLIST,LPDATAOBJECT,HKEY);

// IContextMenu
STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT);
STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);

C++Builder will add some code to the header file when you save and reload the project. DUALINTERFACE_IMPL(MyContextMenuExtension, IMyContextMenuExtension) will appear under public IContextMenu at the top. DUALINTERFACE_ENTRY(IMyContextMenuExtension) will appear in the BEGIN_COM_MAP section. This feature saved me some keystrokes so I'm not complaining.

Now we need to implement the methods.

IShellExtInit calls its Initialize method. Here we want to get the name of the file that has been selected.
STDMETHODIMP TMyContextMenuExtensionImpl::Initialize(LPCITEMIDLIST pidlFolder,
   LPDATAOBJECT lpdobj, HKEY hkeyProgID)
{
   HDROP hDrop;
   HRESULT hResult;
   FORMATETC fmtEtc;
   STGMEDIUM medium;

   fmtEtc.cfFormat = CF_HDROP;
   fmtEtc.ptd = NULL;
   fmtEtc.dwAspect = DVASPECT_CONTENT;
   fmtEtc.lindex = -1;
   fmtEtc.tymed = TYMED_HGLOBAL;

   medium.tymed = TYMED_HGLOBAL;

   // Get the CF_HDROP data
   if((hResult = lpdobj->GetData(&fmtEtc,&medium)) < 0)
      return E_INVALIDARG;

   if((hDrop = (HDROP)GlobalLock(medium.hGlobal))==NULL)
      return E_INVALIDARG;

   // Get the name of the file
   if(DragQueryFile(hDrop,0,FileName,MAX_PATH) == 0)
      hResult = E_INVALIDARG;
   else
      hResult = S_OK;

   // Clean up
   GlobalUnlock(medium.hGlobal);
   ReleaseStgMedium(&medium);

   return hResult;
};

When you are in the Windows Explorer and you run the mouse over menu items a short help message is displayed in the bottom left hand of the window. The GetCommandString returns a string for Explorer to display in the corner of the window when the mouse moves over our menu items we insert:

STDMETHODIMP TMyContextMenuExtensionImpl::GetCommandString(UINT idCmd, UINT uFlags, UINT *pwReserved,
   LPSTR pszName, UINT cchMax )
{
   HRESULT hResult;
   USES_CONVERSION;
   // Check idCmd, it must be 0 or 1 for the first and second menu items we added
   if (idCmd > 1)
      return E_INVALIDARG;
   // Why switch when I check only one case, there are others like GCS_VERB
   // that uFlags can represent, easier to add other cases later
   switch(uFlags)
   {
      case GCS_HELPTEXT:
      {
         /*
         If you are in the Windows Explorer and you right-click a file which this
         object is working on, you will see the help message in the Explorer status
         bar on the bottom left.
         */
         LPCTSTR HelpMessage;

         // Since you have two menu items we need two help messages
         if(idCmd == 0)
            HelpMessage = _T("I am a helpfull message.");
         if(idCmd == 1)
            HelpMessage = _T("Thanks for the help.");

         // We can handle either ASCII and Unicode text.
         if (uFlags & GCS_UNICODE )
            // Cast to LPWSTR and handle Unicode text
            lstrcpynW((LPWSTR)pszName,T2CW(HelpMessage),cchMax);
         else
            // Handle ANSI text
            lstrcpynA(pszName,T2CA(HelpMessage),cchMax);
         hResult = S_OK;
      }
      break;
   }
   return hResult;
};

Now to the fun part -- we get to modify the menu. At the bottom of the article I have some comments about adding bitmaps to your menu items, and how to move the menu items to a submenu, and other owner-draw stuff that would take too long to add here.

STDMETHODIMP TMyContextMenuExtensionImpl::QueryContextMenu(HMENU hmenu, UINT indexMenu, UINT idFirstCmd,
   UINT idLastCmd, UINT uFlags)
{
   UINT cmd = idFirstCmd;
   //  Make sure we do nothing if default is in uFlags.
   if (uFlags & CMF_DEFAULTONLY)
      return MAKE_HRESULT(SEVERITY_SUCCESS,FACILITY_NULL,0);

   InsertMenu (hmenu,indexMenu,MF_BYPOSITION,cmd++,"Add File to Source Control");
   indexMenu++;
   InsertMenu (hmenu,indexMenu,MF_BYPOSITION,cmd++,"Open in Source Control");

   return MAKE_HRESULT(SEVERITY_SUCCESS,FACILITY_NULL, 2);
};

If one of the menu items we added gets clicked then the InvokeCommand is called.

STDMETHODIMP TMyContextMenuExtensionImpl::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo)
{
   HRESULT hResult;
   WORD cmdId;
   // We want to identify items using IDs and not Verbs.
   // So if lpVerb is pointing at a string, lets use the ID.
   if(HIWORD(pCmdInfo->lpVerb)==0)
      cmdId = LOWORD(pCmdInfo->lpVerb);

   if(cmdId > 1)
      return E_INVALIDARG;

   switch(cmdId)
   {
   case 0:
   case 1:
      {
	   // Find out which item was clicked and call the correct method
         AnsiString Operation = cmdId ? "Open in Source Control" : "Add File to Source Control";
         if(Operation == "Add File to Source Control")
            AddFileToSourceControl(pCmdInfo);
         else
            OpenFileInSourceControl();
         hResult = S_OK;
      }
      break;
   default:
      hResult = E_INVALIDARG;
      break;
   }

   return hResult;
}

I went ahead and added some filler code so that you can see what happens when you select a menu item and either the AddFileToSourceControl and OpenFileInSourceControl methods are executed. The names of the methods are misleading but this is just an example.

Add the methods to the header file:

private:
   void AddFileToSourceControl(LPCMINVOKECOMMANDINFO);
   void OpenFileInSourceControl();
void TMyContextMenuExtensionImpl::AddFileToSourceControl(LPCMINVOKECOMMANDINFO pCmdInfo)
{
   //  Just displays some info about the selected file to show you it's working
   AnsiString Message;
   Message = "File Name: "+ExtractFileName(FileName)+"n";
   Message += "Path: "+ExtractFilePath(FileName)+"n";
   Message += "File Attributes: ";
   int Attrs = FileGetAttr(FileName);
   if(Attrs & faHidden)
      Message += "Hidden ";
   if(Attrs & faReadOnly)
      Message += "Read Only ";
   if(Attrs & faSysFile)
      Message += "System File ";
   if(Attrs & faArchive)
      Message += "Archive ";
   MessageBox (pCmdInfo->hwnd, Message.c_str(),"Example",MB_ICONINFORMATION );
}
void TMyContextMenuExtensionImpl::OpenFileInSourceControl()
{
   //  This will just open the file in the program it is associated with.
   char path[MAX_PATH];
   GetWindowsDirectory(path,MAX_PATH);
   ShellExecute(NULL,"open",FileName,NULL,path,SW_SHOW);
}

To install the new menu:

  • Build the project.
  • Then under Run | Register ActiveX Server you should get a message letting you know that the server is registered.

Now you need to set it up in the registry.

If you kept the default names, look in the Project1_TLB.cpp file you will find the GUIDS. You can find the same thing by running regedit and searching for MyContextMenuExtension

Now you need to associate it with an extension so Explorer will know to use our new menu. I will use the .cpp extension.

  • Find the .cpp extension and check out the default value for the key.

I see BCBUnit. If it is a different name, continue -- but place the name you see in place of BCBUnit.

  • Find BCBUnit in the registry.
  • Add a key named Shellex.
  • Under that key, add a key named ContextMenuHandlers.

Set the key value to the value in the next section only if you downloaded the project from Code Central and you are using it. If you created a new project then you will need to set the key value to the GUID value found in your Project1_TLB.cpp file.

  • Under that key, add the key name MyContextMenuExtension and set its default value to 0ABC22EB-602A-449D-A2D2-0D847869F67D.
  • Go find the nearest .cpp file and see if it worked.

Adding bitmaps for menu items and other advanced features

Owner-draw menus could take up another article so I just tell you where to look to get going. Take a look at IContextMenu2 and the HandleMenuMsg method. This will allow you to intercept some messages like WM_MEASUREITEM and WM_DRAWITEM. Be sure to add the MF_OWNERDRAW flag to any items you insert.

Placing the menu items in a submenu is a good idea if you are adding numerous menu items. This way the main menu does not grow really long. Create a temporary popup menu and insert your menu items into that. Then insert the popup menu into the main menu. You will get something similar to the Send To... menu. I'll cover that technique in a future article.


Server Response from: ETNASC02