
Introduction
This article explains how to use my self-written CShellContextMenu
class which makes it possible to use the shell contextmenu
in your own application (the one that shows if you right-click on an object in the Windows Explorer).
Why CShellContextMenu
I have a lot of projects in which I work with files/folders. So I wanted to use the common shell contextmenu
for those. Microsoft put a wonderful example on how to achieve this in their Platform SDK, called EnumDesk
. But since not all people understand Shell interfaces and the code should be reusable, I wrapped things up in a C++ class. I also did some Google-searching to get some good ways of implementing this. The class hides all the interface-related stuff; you can either use normal file system paths (for example, c:\windows) or PIDLs
to obtain the shell contextmenu
. So here it this, the CShellContextMenu
class which makes it easy as hell to use the shell contextmenu
.
CShellContextMenu scm;
scm.SetObjects (TEXT ("<a href="file:
scm.ShowContextMenu (this, point);
There's just one other important thing you have to do. In your InitInstance ()
function of your CWinApp
derived class, insert the following lines of code, that's necessary otherwise not all shell contextmenu items would be shown.
if (!AfxOleInit ())
{
AfxMessageBox (TEXT ("Unable to load OLE 2.0 libraries!"));
return (FALSE);
}
and put the following #include
statement in your project's stdafx.h file.
#include <afxole.h> // for OLE
That's all you need to pop-up the shell contextmenu
for drive C. CShellContextMenu
also supports multiple files/folders. Just pass an CStringArray
to CShellContextMenu::SetObjects ()
and you'll get a contextmenu
which refers to all the items specified in that array. That corresponds to selecting multiple objects in Windows Explorer and then right-click on the selection. Keep in mind that if you pass multiple files/folder/shell objects, they have to be all in the same folder. This is no limitation of CShellContextMenu
, rather than how the IContextMenu
interface is implemented in the Windows Shell. CShellContextMenu
also works with PIDLs
. If you don't know what PIDLs
are, then it won't matter, cause CShellContextMenu
handles the stuff for you. I would also suggest that you have a look at SetObjects (...)
and the other functions to get a better grab to shell interfaces. The source code is also heavily commented, so with MSDN at hand, there shouldn't be any problems.
How CShellContextMenu Works
Let's have an inside look in CShellContextMenu
and see what it really does under the hood to obtain that handy shell contextmenu
.
First, take a look at those SetObjects (...)
methods.
void SetObjects (CString strObject);
void SetObjects (CStringArray &strArray);
void SetObjects (LPITEMIDLIST pidl);
void SetObjects (IShellFolder * psfFolder, LPITEMIDLIST pidlItem);
void SetObjects (IShellFolder * psfFolder, LPITEMIDLIST * pidlArray,
int nItemCount);
With the SetObjects (...)
, you tell CShellContextMenu
for which objects (file/folder/shell object) you wish to have the contextmenu
. For people who don't know how to handle PIDLs
or if your program just works with usual file system paths, I implemented two overridden methods of SetObjects (...)
that accept a CString
or a CStringArray
as argument and CShellContextMenu
converts the given file system path(s) into PIDLs
and retrieves its IShellFolder
interface. That's necessary because the IContextMenu
interface is only accessible via the IShellFolder
interface which only takes PIDLs
as an argument. Now, we take some in-depth look at ShowContextMenu
which actually does the work.
UINT CShellContextMenu::ShowContextMenu(CWnd *pWnd, CPoint pt)
{
int iMenuType = 0;
LPCONTEXTMENU pContextMenu;
if (!GetContextMenu ((void**) &pContextMenu, iMenuType))
return;
if (!m_Menu)
{
delete m_Menu;
m_Menu = NULL;
m_Menu = new CMenu;
m_Menu->CreatePopupMenu ();
}
pContextMenu->QueryContextMenu (m_Menu->m_hMenu,
m_Menu->GetMenuItemCount(),0, MIN_ID, MAX_ID, CMF_EXPLORE);
WNDPROC OldWndProc;
if (iMenuType > 1)
{
OldWndProc = (WNDPROC) SetWindowLong (pWnd->m_hWnd,
GWL_WNDPROC, (DWORD) HookWndProc);
if (iMenuType == 2)
g_IContext2 = (LPCONTEXTMENU2) pContextMenu;
else
g_IContext3 = (LPCONTEXTMENU3) pContextMenu;
}
else
OldWndProc = NULL;
UINT idCommand = Menu.TrackPopupMenu (TPM_RETURNCMD | TPM_LEFTALIGN,
pt.x, pt.y, pWnd);
if (OldWndProc)
SetWindowLong (pWnd->m_hWnd, GWL_WNDPROC, (DWORD) OldWndProc);
if (idCommand >= MIN_ID && idCommand <= MAX_ID)
{
InvokeCommand (pContextMenu, idCommand - MIN_ID);
idCommand = 0;
}
pContextMenu->Release();
g_IContext2 = NULL;
g_IContext3 = NULL;
return (idCommand);
}
As you can see, ShowContextMenu
takes a pointer to a CWnd
object and a CPoint
object as arguments. The CWnd
pointer is needed for later subclassing and CPoint
is used to determine at which position the contextmenu
should be shown. Note that these are screen coordinates. So, if you have client coordinates, convert them via ScreenToClient (...)
before passing them to ShowContextMenu
. So, what is ShowContextMenu
doing? First, it calls the GetContextMenu (...)
to retrieve the IContextMenu
interface (which is then stored in pContextMenu
) associated with the objects passed in SetObjects (...)
. The GetContextMenu
is explained afterwards. What we now have to do, is to determine which version of IContextMenu
we have. That's necessary because if we have a IContextMenu
higher than version 1, we need to handle the WM_DRAWITEM
, WM_MEASUREITEM
and WM_INITMENUPOPUP
messages. These messages are sent to the window pointed to by pWnd
which is passed in ShowContextMenu
's argument list. That's the point where window subclassing comes in handy. All we have to do is to redirect the window's default window procedure (the function which handles all the messages belonging to a window). With SetWindowLong (...)
we set the new window procedure to HookWndProc (...)
which is a static
member function of CShellContextMenu
.
Let's again take a look at the code. After we have a pointer to the IContextMenu
interface, we create a popup menu with CMenu
's CreatePopuMenu ()
method. The next thing is, we let our popup menu fill with IContextMenu
's QueryContextMenu (...)
method. This method has four parameters. The first is the handle to the popupmenu
which should be filled with the shell menu items. The second is the menu position where it starts. This could be useful because before you let the menu be filled, you can insert additional menu items which are specific to your program. Therefore the 3rd and 4th parameters. They specify the lowest and highest command ID that QueryContextMenu (...)
should use to fill the menu. That means, that command IDs which are below or above that range, are for your own additional menu items. CShellContextMenu
has support for adding custom menus. Just call the GetMenu ()
method to retrieve a CMenu
pointer to the popupmenu
. With this, you can customize the menu as you like. After that, go on as usual and call ShowContextMenu (...)
. The 5th parameter uses the flag CMF_EXPLORE
to indicate that we want the same items that Window Explorer shows in its contextmenu
. Then, we subclass pWnd
and redirect all messages to HookWndProc (...)
, but only if the IContextMenu
is Version 2 or 3. With CMenu
's TrackPopupMenu (...)
we show the contextmenu
, and store the command ID of the selected menu item in idCommand
. Then we test idCommand
if it's between MIN_ID
and MAX_ID
, if so it means that a shell menu item was clicked and not one we manually inserted (btw. those constants are defined in ShellContextMenu.cpp, change them to your needs if you wish to). If it's a shell menu item, we call CShellContextMenu::InvokeCommand (...)
which executes the appropriate command that belongs to a shell menu item and release the IContextMenu
interface with pContextMenu->Release ()
.
Here's GetContextMenu
which retrieves the highest version of IContextMenu
available to the given objects. m_psfFolder
is an IShellFolder
interface, via its GetUIObjectsOf
method, we get version 1 of its IContextMenu
interface. nItems
is the number of object that were passed in SetObjects (...)
and m_pidlArray
is an array of PIDLs
that are relative to m_psfFolder
(IShellFolder
interface). Those PIDLs
were also passed in SetObjects (...)
or if you passed a file system paths, CShellContextMenu
has automatically retrieved the corresponding PIDLs
and the IShellFolder
interface. If we have a valid IContextMenu
interface, we try to get version 3, if that fails we test for version 2 and if that too fails, we stay with version 1. And that's all.
BOOL CShellContextMenu::GetContextMenu (void ** ppContextMenu,int & iMenuType)
{
*ppContextMenu = NULL;
LPCONTEXTMENU icm1 = NULL;
m_psfFolder->GetUIObjectOf (NULL, nItems, (LPCITEMIDLIST *) m_pidlArray,
IID_IContextMenu, NULL, (void**) &icm1);
if (icm1)
{
if (icm1->QueryInterface(IID_IContextMenu3, ppContextMenu) == NOERROR)
iMenuType = 3;
else if (icm1->QueryInterface (IID_IContextMenu2,
ppContextMenu) == NOERROR)
iMenuType = 2;
if (*ppContextMenu)
icm1->Release();
else
{
iMenuType = 1;
*ppContextMenu = icm1;
}
}
else
return (FALSE);
return (TRUE);
}
That's the alternative window procedure that is only used while the contextmenu
is being shown. HookWndProc
checks for menu related messages and calls the IContextMenu
's HandleMenuMsg
. g_IContext2
and g_IContext3
are global pointers, they are pointing to IContextMenu2
and IContextMenu3
interfaces of the contextmenu
that is currently being shown. It's necessary to have a global
variable because HookWndProc
is a static
member function and static
member functions have no this
pointer, therefore it cannot access its class member variables and functions. The HookWndProc
must be static
because a non-static
member function has always an additional this
pointer, and therefore its argument list wouldn't match that of a window procedure. At the end of HookWndProc
, we call the original WndProc
to avoid undefined behaviour of the associated window. The original WndProc
is retrieved via the GetProp ()
API function. Refer to MSDN for further information on this API function.
LRESULT CALLBACK CShellContextMenu::HookWndProc (HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_MENUCHAR:
if (g_IContext3)
{
LRESULT lResult = 0;
g_IContext3->HandleMenuMsg2 (message, wParam, lParam, &lResult);
return (lResult);
}
break;
case WM_DRAWITEM:
case WM_MEASUREITEM:
if (wParam)
break;
case WM_INITMENUPOPUP:
if (g_IContext2)
g_IContext2->HandleMenuMsg (message, wParam, lParam);
else
g_IContext3->HandleMenuMsg (message, wParam, lParam);
return (message == WM_INITMENUPOPUP ? 0 : TRUE);
break;
default:
break;
}
return ::CallWindowProc ((WNDPROC) GetProp ( hWnd, TEXT ("OldWndProc")),
hWnd, message, wParam, lParam);
}
This little function is also very important. Without it, the shell context menu would also show correctly with all the expected menu items, but it would do just nothing if you'd click on an item. So, all this function does is fill an CMINVOKECOMMANDINFO
, set its lpVerb
member to the idCommand
(command ID of the clicked menu item) and calls the IContextMenu
's InvokeCommand
method, which finally executes the command that belongs to the menu item that was clicked.
void CShellContextMenu::InvokeCommand (LPCONTEXTMENU pContextMenu,
UINT idCommand)
{
CMINVOKECOMMANDINFO cmi = {0};
cmi.cbSize = sizeof (CMINVOKECOMMANDINFO);
cmi.lpVerb = (LPSTR) MAKEINTRESOURCE (idCommand);
cmi.nShow = SW_SHOWNORMAL;
pContextMenu->InvokeCommand (&cmi);
}
Summary
So, that's the whole thing behind the shell contextmenu
. Wasn't that hard, was it? Shell interfaces are not that difficult like they seem to be on the first look. One problem with them is that they are not well documented in the MSDN. So with a little work and some Google-searching everything's possible. Before I began working with that shell context menu, I didn't know much about the Shell. I did use a lot of shell functions like SHGetFileInfo
and such stuff, but no real shell interfaces, PIDLs
and such. Now I'm able to produce a full Windows Explorer alternative with the shell interfaces. That's not a very hard thing to do.
I hope the article is good to understand, because English is not my native language. On the other hand, it's my first development related article ever. So hey, I think it's good enough for that.
What Comes Next?
I hope the example project covers CShellContextMenu
fairly well, so you'll know how to use it. It also demonstrates how to add custom app-specific menu item< to the contextmenu
before it is shown and shows how to imitate the right-pane listview
of Windows Explorer (in a simple way). The active project configuration is set to ANSI compiling, but everything also works in UNICODE mode, which is also included as a project configuration. I'm also considering providing CListCtrl
and CTreeCtrl
derived classes which imitate those in Windows Explorer. But this could still be a long way ahead, because while writing this article, I noticed that it's really an exhausting task, harder than actually programming :). There are already 2 or 3 articles about that on codeproject.com, but I've noticed that those examples/classes are totally overblown, and therefore the source codes of those are almost impossible to follow and understand.
History
- 29th April, 2003
- Added
GetMenu ()
method which returns a CMenu
pointer, so it is possible to freely customize the contextmenu
before it is shown. - Added example project which demonstrates the use of
CShellContextMenu
, and also shows how to add those custom menu items and imitating a Windows Explorer-like listview. - Replaced the
SHBindToParent
function with a workaround implementation that is called SHBindToParentEx
which does the same thing. That was necessary because SHBindToParent
isn't available on Windows 95/98 systems. - ANSI compiling supported. Before it only worked in UNICODE mode.
- Fixed a bug, where
CShellContextMenu
caused an error when SetObjects (...)
was called with a CStringArray
that contained more than one file/folder.
- 10th April, 2003
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.