您的位置:首页 > 运维架构 > Shell

Use Shell ContextMenu in your applications

2015-11-30 23:42 876 查看


Use Shell ContextMenu in your applications

Roman
Engels, 15 May 2003





4.94 (40 votes)
Rate this:
A class that makes it easy to use the Shell Context Menu (aka Explorer Contextmenu) in your own applications

Download source files - 5 Kb

Download Demo files - 59 Kb

<!-- Article image -->


<!-- Add the
rest of your HTML here -->


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 (f. ex. 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;
// instantiate class object
scm.SetObjects (TEXT ("<A href="file:///c://">c:\\</A>"));
// we whant shellcontext menu for drive c:\
scm.ShowContextMenu (this, point);
// point is a CPoint objects which indicates where the contextmenu should
// be shown this refers to a MFC-window class (showcontextmenu needs that
// to set the owner window)

There's just one other importing thing you have to do. In your
InitInstance ()
function of your
CWinApp
derived
class insert the following lines of code, that's neccessary otherwise not all shell contextmenu items would be shown.

// Initialize OLE 2.0 libraries
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 than 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 then 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);
// one file system path (file/folder)
void SetObjects (CStringArray &strArray);
// array of multiple file system paths (files/folders)
void SetObjects (LPITEMIDLIST pidl);
// full qualified PIDL of shell object
void SetObjects (IShellFolder * psfFolder, LPITEMIDLIST pidlItem);
// relative PIDL and its parent IShellFolder interface
void SetObjects (IShellFolder * psfFolder, LPITEMIDLIST * pidlArray,
int nItemCount);
// array of multiple relative PIDLs and their parent IShellFolder interface

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 overriden 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 neccessary because the
IContextMenu
interface
is only accessable via the
IShellFolder
interface which only takes
PIDLs
as
an argument. Now we take some in-depths look at
ShowContextMenu
which actually does the work.

UINT CShellContextMenu::ShowContextMenu(CWnd *pWnd, CPoint pt)
{
int iMenuType = 0;
// to know which version of IContextMenu is supported
LPCONTEXTMENU pContextMenu;
// common pointer to IContextMenu and higher version interface

if (!GetContextMenu ((void**) &pContextMenu, iMenuType))
return;    // something went wrong

if (!m_Menu)
{
delete m_Menu;
m_Menu = NULL;
m_Menu = new CMenu;
m_Menu->CreatePopupMenu ();
}

// lets fill the our popupmenu
pContextMenu->QueryContextMenu (m_Menu->m_hMenu,
m_Menu->GetMenuItemCount(),0, MIN_ID, MAX_ID, CMF_EXPLORE);

// subclass window to handle menurelated messages in CShellContextMenu
WNDPROC OldWndProc;
if (iMenuType > 1)    // only version 2 and 3 supports menu messages
{
OldWndProc = (WNDPROC) SetWindowLong (pWnd->m_hWnd,
GWL_WNDPROC, (DWORD) HookWndProc);
if (iMenuType == 2)
g_IContext2 = (LPCONTEXTMENU2) pContextMenu;
else	// version 3
g_IContext3 = (LPCONTEXTMENU3) pContextMenu;
}
else
OldWndProc = NULL;

UINT idCommand = Menu.TrackPopupMenu (TPM_RETURNCMD | TPM_LEFTALIGN,
pt.x, pt.y, pWnd);

if (OldWndProc) // unsubclass
SetWindowLong (pWnd->m_hWnd, GWL_WNDPROC, (DWORD) OldWndProc);

// see if returned idCommand belongs to shell menu entries
if (idCommand >= MIN_ID && idCommand <= MAX_ID)
{   //executes related command
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 neccessary because if we have a
IContextMenu
higher than version 1, we need to handle the WM_DRAWITEM,
WM_MEASUREITEM and WM_INITMENUPOPUP messages. These message are send 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 you 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 its 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 its a shell menu item we call
CShellContextMenu::InvokeCommand
(...)
which executes the appriorate 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;

// first we retrieve the normal IContextMenu
// interface (every object should have it)
m_psfFolder->GetUIObjectOf (NULL, nItems, (LPCITEMIDLIST *) m_pidlArray,
IID_IContextMenu, NULL, (void**) &icm1);

if (icm1)
{    // since we got an IContextMenu interface we can
// now obtain the higher version interfaces via that
if (icm1->QueryInterface(IID_IContextMenu3, ppContextMenu) == NOERROR)
iMenuType = 3;
else if (icm1->QueryInterface (IID_IContextMenu2,
ppContextMenu) == NOERROR)
iMenuType = 2;

if (*ppContextMenu)
icm1->Release();     // we can now release version 1 interface,
// cause we got a higher one
else
{
iMenuType = 1;
*ppContextMenu = icm1;    // since no higher versions were found
}  // redirect ppContextMenu to version 1 interface
}
else
return (FALSE);    // something went wrong

return (TRUE); // success
}


That's the alternative window procedure that is only used while the contextmenu is being showed.
HookWndProc
checks
for menu reletad 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 showed. It's neccessary 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 the MSDN for further informations on this API function.

LRESULT CALLBACK CShellContextMenu::HookWndProc (HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_MENUCHAR:	// only supported by IContextMenu3
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; // if wParam != 0 then the message is not menu-related

case WM_INITMENUPOPUP:
if (g_IContext2)
g_IContext2->HandleMenuMsg (message, wParam, lParam);
else	// version 3
g_IContext3->HandleMenuMsg (message, wParam, lParam);
return (message == WM_INITMENUPOPUP ? 0 : TRUE); // inform caller that
// we handled WM_INITPOPUPMENU by ourself
break;

default:
break;
}

// call original WndProc of window to prevent undefined bevhaviour
// of window
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 is already 2 or 3 article 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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: