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.