Custom WebBrowser Context Menus

In my Advanced CHtmlView Hosting article, I showed a way to disable the context menu for the WebBrowser control.  Inside the ShowContextMenu( ) handler, you can display your own context menu using ::TrackPopupMenu( ).  This requires building your own menu using the Win32 API, or creating a menu resource, and loading it.  However, there are disadvantages in this approach.  First of all, you have to implement the functionality of every menu item yourself, which could be a tedious task.  Other than it, maybe you only want to disable one item of the context menu, and show the rest.  Of course you might say that one can create a menu resource that has all the items as the WebBrowser’s default context menu, except those elements you want to remove.  However, this is not the best solution, since additional context menus can be added to IE through the registry.  In this article, I’m going to show several ways to provide a customized context menu for the WebBrowser control.

Scenario 1:  Adding a new menu item to all applications using the WebBrowser control

This method is capable of adding a menu item to the context menu shown in any application that uses the WebBrowser control, including Internet Explorer itself, unless an application shows its own custom menu, of course.  This is the simplest approach, and involves only writing a piece of script.  For doing this, you should add a Registry key under this key:  HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt.  The name of the key you create under this key is the text shown in the context menu.  This text can contain an ampersand (&) character, which, like the text of any menu, will cause the letter after it to be underlined, and used as a shortcut key.  The default value of this key should be set to a URL (either local, or a web link) of a page, containing the script you want to run.  This page will not be shown to the user.  It will be opened inside an invisible window, the scripts on it will be executed, and it will be closed automatically.  Inside this page, the window.external.menuArguments property is actually the window object of the parent window (the window on which the user has right clicked).  Using this property, you can access the whole object model of that window, and use it to perform some action in your script.

There are also a set of optional values which can extend the functionality of the context menu item.  The "Contexts" value (which should be a DWORD value) determines when the context menu will appear.  It can be an ORed value of the following bit masks:

Context Value
Default 0×01
Image 0×02
Control 0×04
Table 0×08
Text Selection 0×10
Link 0×20
Unknown 0×40

For example, if an item is to be shown only when the user clicks on an image or a link, the Contexts value should be 0×22.

The other optional value is the "Flags" value, which is also a DWORD.  If this value equals 0×1, the script will be executed inside a visible dialog window, and it won’t close after script execution.  This provides a behavior exactly similar to the case in which the dialog was shown by the window.showModalDialog( ) function.

Sample
The Scenario1.zip file which can be downloaded at the bottom of the article contains a sample of this technique.  Extract the newwindow.html file to C:\, and then double click on the newwindow.reg file.  Click Yes, and then try to open up a new instance of Internet Explorer.  You’ll see a new context menu item which allows you to open the current page in a new window.  This context menu item will appear only when you click on an empty part or an unselected text section of the pages.

Scenario 2:  Showing a whole customized WebBrowser context menu inside your application

This scenario is of use when you want to display custom context menus in your own application, and also where you not only want to display customized menu items, but also you want your menu to be totally different to the WebBrowser’s default menu.  This case is also relatively simple to implement.  You need to review my Advanced CHtmlView Hosting article before going on.

The approach you should take is providing an implementation of the IDocHostUIHandler interface inside your application, and throw your focus onto the ShowContextMenu( ) function.  In this function, you should call ::TrackPopupMenu( ), and somehow intercept the result of displaying the menu, that is, if the menu is dismissed without selecting a menu item, or figuring out which item has been selected.  Here’s a typical implementation of the ShowContextMenu( ) function that serves this purpose.

HRESULT CDocHostUIHandlerImpl::ShowContextMenu(
DWORD /*dwID*/,
POINT* pptPosition,
IUnknown* pCommandTarget,
IDispatch* /*pDispatchObjectHit*/)
{
METHOD_PROLOGUE(CCustomControlSite, DocHostUIHandler)

// get a IOleWindow pointer for retrieving the HWND

IOleWindow * pOleWnd = NULL;
HRESULT hr = pCommandTarget->QueryInterface( IID_IOleWindow,
reinterpret_cast< void ** > (&pOleWnd) );

if (SUCCEEDED(hr))
{
HWND hwndWindow = NULL;
// hwndWindow can be used inside non-MFC apps instead of

// ::AfxGetMainWnd()->m_hWnd in calls to TrackPopupMenuEx( ) and SendMessage( )

if (SUCCEEDED(pOleWnd->GetWindow( &hwndWindow )))
{
// load the main frame menu
HMENU hMenuParent = ::LoadMenu( ::AfxGetInstanceHandle(),
MAKEINTRESOURCE(IDR_MAINFRAME) );

if (hMenuParent)
{
HMENU hMenu = ::GetSubMenu( hMenuParent, 0 ); // the File sub menu

// enable all the menu items
UINT ui = 0;
while (-1 != ::EnableMenuItem( hMenu, ui ++, MF_ENABLED | MF_BYPOSITION ))
;

if (hMenu)
{
// display the popup menu

long lResult = ::TrackPopupMenuEx( hMenu, TPM_LEFTALIGN | TPM_TOPALIGN |
TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_HORPOSANIMATION |
TPM_VERPOSANIMATION, pptPosition->x, pptPosition->y,
::AfxGetMainWnd()->m_hWnd, NULL );

if (lResult) // if an item is chosen

{
// send a command message
::SendMessage( ::AfxGetMainWnd()->m_hWnd, WM_COMMAND,
MAKEWPARAM(LOWORD(lResult), 0x0), 0 );
}
}

::DestroyMenu( hMenuParent );
}
}

pOleWnd->Release();
}

return S_OK;
}

Applications can also customize their behavior according to the context on which the menu is to be displayed.  The first parameter to ShowContextMenu( ) specifies the DWORD ID that identifies the context.  Its possible values (which are defined in mshtmhst.h) are shown in the below table:

Context Value
Default CONTEXT_MENU_DEFAULT
Image CONTEXT_MENU_IMAGE
Control CONTEXT_MENU_CONTROL
Table

CONTEXT_MENU_TABLE

Text Selection CONTEXT_MENU_TEXTSELECT
Link CONTEXT_MENU_ANCHOR
Unknown CONTEXT_MENU_UNKNOWN

With IE 5 and above, the last parameter points to a valid IDispatch pointer which belongs to the item which has been under the mouse cursor at right click time.  You can call QueryInterface( ) on this pointer and see if it supports a particular interface or not.  For example, if you want to show the HREF attribute of anchors, you can QueryInterface( ) it for a IHTMLAnchorElement interface, and if it succeeds, call get_href( ) on it.

Sample
The Scenario2.zip file which can be downloaded at the bottom of this article is a sample of this kind of customized context menu.  It shows the File submenu items inside the context menu.  Of course, usually it doesn’t make much sense to show these items as a context menu, but this is only a sample after all!

This sample uses the CDocHostHtmlView class I presented in my Advanced CHtmlView Hosting article.  It overrides the CDocHostHtmlView::OnShowContextMenu( ) function to display its own context menu, which is the File menu of the main frame window.

Scenario 3:  Customizing the standard WebBrowser context menu

The most likely scenario would be the case in which you just want to customize the standard context menu, namely remove a set of menu items from the standard menu and/or adding items to the standard menu, and you don’t want to affect the menu displayed in other applications.  This is the most tricky case, and it’s hard to figure out how you should do it on your own.

The WebBrowser control loads its context menus from SHDOCLC.DLL.  If you open this DLL as a resource inside Developer Studio, you can see the context menus there.  Open the menu resource ID 24641.  This is a set of popup menus, with submenus that correspond to the CONTEXT_MENU constants defined inside mshtmsht.h.  For example, the first submenu (with the index 0, or CONTEXT_MENU_DEFAULT) is the default context menu.  So, in your ShowContextMenu( ) you can load this menu, remove the items you don’t like to be displayed and/or add any additional items using the Win32 menu API, and then pass the handle of the menu to ::TrackPopupMenu( ).

There is only one other issue remaining.  As you remember from Scenario 1, there may be additional menu items that are loaded dynamically from the registry.  You should either write the code for loading them from the registry (what a pain!) or let the WebBrowser control handle this part.  I would take the latter approach.  The trick for forcing the WebBrowser control to do this is to obtain a IOleCommandTarget interface pointer from the third parameter of OnShowContextMenu( ), and execute a certain command on it to add the menu extensions from the registry.  Another task will be adding the submenu items for the Encoding menu item.  This is also done by executing a command on the IOleCommandTarget interface.  Note that both these commands are undocumented commands, so I had to declare their IDs as a normal const variable.

The following code wraps up what I said above, and uses the above techniques for showing a context menu exactly similar to the standard context menu.  Of course if you want to show the default menu, it doesn’t make much sense to write this amount of code, but this sample shows how various tasks should be done and you can modify this code to suit your own needs.

#include <shlguid.h>   // declaration of CGID_ShellDocView
#include <mshtmcid.h> // definition of IDM_xxx constants

#ifdef VT_INT_PTR
#undef VT_INT_PTR
#endif
#define VT_INT_PTR 37 // This fixes the problem with old Platform SDK headers
HRESULT CDocHostUIHandlerImpl::ShowContextMenu(
DWORD dwID,
POINT* pptPosition,
IUnknown* pCommandTarget,
IDispatch* /*pDispatchObjectHit*/)
{
METHOD_PROLOGUE(CCustomControlSite, DocHostUIHandler)

// The resource ID of the context menu inside SHDOCLC.DLL

const UINT WebBrowserContextMenu = 24641;

// The undocumented command IDs
const UINT CmdID_GetMimeSubMenu = 27; // Command ID for getting the Encoging submenu
const UINT CmdID_AddMenuExtensions = 53; // Command ID for loading extension menus

HINSTANCE hinstSHDOCLC = NULL;
bool bFreeLibrary = false;

// first, try to get a handle to the DLL if it's already loaded
hinstSHDOCLC = ::GetModuleHandle( _T("SHDOCLC.DLL") );

if (!hinstSHDOCLC) // if it's not already loaded

{
hinstSHDOCLC = ::LoadLibrary( _T("SHDOCLC.DLL") );
if (hinstSHDOCLC)
bFreeLibrary = true;
}

HRESULT hr;
IOleCommandTarget * pCmdTarget = NULL;
IOleWindow * pOleWnd = NULL;

// Get an IOleCommandTarget pointer

hr = pCommandTarget->QueryInterface( IID_IOleCommandTarget,
reinterpret_cast< void ** > (&pCmdTarget) );

if (SUCCEEDED(hr))
{
// Get an IOleWindow pointer

hr = pCommandTarget->QueryInterface( IID_IOleWindow,
reinterpret_cast< void ** > (&pOleWnd) );

if (SUCCEEDED(hr))
{
HWND hwndWindow = NULL;

// Get the HWND of the WebBrowser control

if (SUCCEEDED(pOleWnd->GetWindow( &hwndWindow )))
{
// Load the context menu from SHDOCLC.DLL
HMENU hMenuParent = ::LoadMenu( hinstSHDOCLC,
MAKEINTRESOURCE(WebBrowserContextMenu) );

if (hMenuParent)
{
// Get the dwID submenu item

HMENU hMenu = ::GetSubMenu( hMenuParent,
static_cast< int > (dwID) );

VARIANT varEncSubMenu;
::VariantInit( &varEncSubMenu );

// Get the Encoding submenu
hr = pCmdTarget->Exec( &::CGID_ShellDocView, CmdID_GetMimeSubMenu,
OLECMDEXECOPT_DODEFAULT, NULL, &varEncSubMenu );

if (SUCCEEDED(hr))
{
// Add the submenu

MENUITEMINFO mii;
::memset( &mii, 0, sizeof(MENUITEMINFO) );

mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_SUBMENU;
mii.hSubMenu = reinterpret_cast< HMENU > (varEncSubMenu.byref);

::SetMenuItemInfo( hMenu, IDM_LANGUAGE, FALSE, &mii );

VARIANT varIn, varOut;
::VariantInit( &varIn ),
::VariantInit( &varOut );

varIn.vt = VT_INT_PTR;
varIn.byref = hMenu;

varOut.vt = VT_I4;
varOut.lVal = dwID;

// Add menu extensions from the registry

hr = pCmdTarget->Exec( &::CGID_ShellDocView, CmdID_AddMenuExtensions,
OLECMDEXECOPT_DODEFAULT, &varIn, &varOut );

if (SUCCEEDED(hr))
{
/********************************************************
Here's the place to add or remove any items from the menu
********************************************************/


// Display the popup menu

long lResult = ::TrackPopupMenuEx( hMenu, TPM_LEFTALIGN |
TPM_TOPALIGN | TPM_RETURNCMD | TPM_RIGHTBUTTON |
TPM_HORPOSANIMATION | TPM_VERPOSANIMATION,
pptPosition->x, pptPosition->y,
hwndWindow, NULL );

if (lResult) // if an item was selected

{
// send a command message
::SendMessage( hwndWindow, WM_COMMAND,
MAKEWPARAM(LOWORD(lResult), 0x0), 0 );
}
}
}

::DestroyMenu( hMenuParent );
}
}

pOleWnd->Release();
}

pCmdTarget->Release();
}

// cleanup
if (bFreeLibrary && hinstSHDOCLC)
::FreeLibrary( hinstSHDOCLC );

return S_OK;
}

If you want to add or remove any menu items to or from the context menu, you should do it at the place I have marked by "Here’s the place to add or remove any items from the menu" comment.  If you need to remove any items from the menu, you can use the preprocessor symbols defined inside mshtmcid.h.

Sample
The Scenario3.zip file that can be downloaded at the bottom of this article demonstrates an application that deletes the View Source item from the menu, changes the text of the Properties item, disables the Add to Favorites item, and adds an Exit Application item to the end of the context menu for the WebBrowser control.  The rest of the context menu is exactly like the standard context menu.  This application uses the technique I have shown above to load the menu resource from SHDOCLC.DLL.

This sample uses the CDocHostHtmlView class I presented in my Advanced CHtmlView Hosting article.  It overrides the CDocHostHtmlView::OnShowContextMenu( ) function to display its own context menu.

A point you must pay attention to is the ::SendMessage( ) calls after the ::TrackPopupMenuEx( ) call inside OnShowContextMenu( ).  If the selected item is the Exit Application item, I send the message to the main frame window, and otherwise I send it to the WebBrowser control’s window.  The reason is that the handler of the ID_APP_EXIT command message is not inside the WebBrowser control, it’s inside MFC.  If I send the message to the WebBrowser’s window, it doesn’t know how to handle it, and it simply ignores it.  Note that the MFC message routing doesn’t work here, so the Exit Application wouldn’t work in that case.

There’s one very important point in this sample.  If you try to disable the Add to Favorites item inside the OnShowContextMenu( ) function, you’ll notice that the item is not disabled.  Also the time when you add the Exit Application menu item inside OnShowContextMenu( ), if you run the application you’ll notice that that item will be disabled!  The reason for this is that the WebBrowser control uses a process like MFC’s Update UI Handlers.  It means that the WebBrowser control handles the WM_INITMENUPOPUP and for each and every item of the menu, it determines the state it should be in, and enables/disables the item accordingly.  Also, for menu items that it cannot find any command handler for, it simply disables them.  This is exactly what an MFC SDI or MDI application does, and allows us to use the Update UI Handler functions for updating menu items/toolbar buttons.  Obviously you don’t like the WebBrowser control to act this way, otherwise the only thing you can do for customizing menu items is to delete them.  To solve this problem, the simplest way I found is to subclass the WebBrowser control window temporarily.  In order to perform a subclass operation, you need the target window’s HWND; and you have it!  It’s the hwndWindow variable.  So, everything is solved now.  I derived a helper class from CWnd named CWebBrowserSubclassWnd, added an object of that class as a member of CCustomContextMenuView, and called CWnd::SubclassWindow( ) after I obtain the hwndWindow, and called CWnd::UnsubclassWindow( ) after I show the popup menu.  I do this because I only need to handle the WM_INITMENUPOPUP message inside my CWebBrowserSubclassWnd class, and I only need to do it when I want to show the context menu.  Inside the CWebBrowserSubclassWnd’s implementation, I added a message handler for WM_INITMENUPOPUP, and I first called the base class version (which calls the window procedure of the WebBrowser control, and let’s it to enable/disable menu items to its heart’s content), and then I added two simple lines that disables the Add to Favorites item, and enables the Exit Application item.  That is all we need for having a fully customized context menu!

Whenever you need a customized context menu for the WebBrowser control, you can determine which of the above three scenarios are applicable to your case, and handle it as I have shown in the sample applications.  The sample code for scenario 2 and 3 which I showed above is not dependent to MFC, so you can use it inside your ATL projects (or even your SDK projects!) as well!

 Download source code for Scenario 1
 Download source code for Scenario 2
 Download source code for Scenario 3

This article originally appeared on BeginThread.com. It’s been republished here, and may contain modifications from the original article.

Posted in Visual C++