1、概述
Internet Explorer提供了非常開發的介面,使開發人員不僅可以把其瀏覽器核心嵌入應用程式,還可以通過各種介面以實現更深層的控制。本文就將介紹對瀏覽器進行進階控制的話題之一——自訂操作功能表。
2、最簡單的情況
自訂的IE及WebBrowser的操作功能表,最簡單的方式就是在註冊表的HKEY_CURRENT_USER/Software/Microsoft/Internet Explorer/MenuExt下添加自訂的索引值,步驟如下:
1)添加一個新的鍵,其名稱即為將來顯示在操作功能表中的功能表項目名稱,如:
HKEY_CURRENT_USER/Software/Microsoft/Internet Explorer/MenuExt/&Google Search
2)將新增的鍵的預設值設定為一個包含指令碼的網頁的URL(或檔案路徑全名),該網頁中的指令碼將在使用者點擊操作功能表中的“Google Search”後被瀏覽器執行。
3)在新增的鍵下還可以建立一個二進位值Contexts,用以指定我們新增的功能表項目針對特定的網頁對象是否出現,其取值可以是如下值的組合(邏輯或)
Context Value
Default 0x1
Images 0x2
Controls 0x4
Tables 0x8
Text selection 0x10
Anchor 0x20
4)還可以建立一個DWORD類型的Flags項並將其值設定為0x01,這將使得前述指令碼在一個模態視窗中執行,就好像是通過window.showModalDialog調用的,但不同的是在指令碼中仍然可以訪問window對象。
5)執行個體指令碼如下:
通過修改註冊表自訂菜單的方法適用於Internet Explorer和WebBrowser,也具有良好的擴充性。但我們如果希望執行的是不僅僅是指令碼,二是自己的程式中代碼,這種方法就不適用了。
3、使用完全自訂的菜單
1)IDocHostUIhandler介面提供了一個ShowContextMenu方法,在需要顯示操作功能表之前,MSHTML引擎就會調用實現了IDocHostUIHandler介面的
宿主程式的ShowContextMenu方法。
HRESULTIDocHostUIHandler::ShowContextMenu(
DWORD dwID,
POINT *ppt,
IUnknown *pcmdtReserved,
IDispatch *pdispReserved
);
dwID參數的意義與Contexts的組合類別似;ppt為菜單的彈出點螢幕座標;pcmdtReserved介面指向IOleCommandTarget介面,可用於檢測網頁對象的狀態和執行命令等操作。pdispReserved在IE5以上版本中趕虻氖峭扯韻蟮腎Dispatch介面,用以區分不同對象,比如我們可以這樣來獲得網頁對象的指標:
IHTMLElement *pElem;
HRESULT hr;
hr = pdispReserved->QueryInterface(IID_IHTMLElement, (void**)pElem);
if(SUCCEEDED (hr)) {
BSTR bstr;
pElem->get_tagName(bstr);
USES_CONVERSION;
ATLTRACE("TagName:%s/n", OLE2T(bstr));
SysFreeString(bstr);
pElem->Release();
}
如果我們在該方法中返回S_OK,則告訴MSHTML我們將使用自己的菜單(介面),如果是S_FALSE,則彈出預設的菜單。
2)實現
原理清楚之後,實現起來非常簡單,和彈出一般的菜單沒什麼兩樣,舉例如下,顯示主架構的“檔案菜單”:
HRESULT CMyHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt, IUnknown * pcmdtReserved, IDispatch *)
{
// 載入主菜單
HMENU hMenuParent = ::LoadMenu( ::AfxGetInstanceHandle(), MAKEINTRESOURCE(IDR_MAINFRAME) );
if (hMenuParent)
{
HMENU hMenu = ::GetSubMenu( hMenuParent, 0 ); // 取得“檔案”子功能表
if (hMenu)
{
// 顯示菜單
TrackPopupMenuEx( hMenu,
TPM_LEFTALIGN | TPM_TOPALIGN,
ppt->x,
ppt->y,
::AfxGetMainWnd()->m_hWnd,
NULL );
}
::DestroyMenu( hMenuParent );
}
return S_OK;
}
4、自訂標準操作功能表
1)原理
更多的時候我們希望能在瀏覽器原來菜單的基礎上作一些修改,如刪掉“查看源檔案”,添加自己的功能表項目,等等,而不是完全不要原始的菜單,怎麼辦呢?藉助MSDN提供的例子,我們來看看:
HRESULT CBrowserHost::ShowContextMenu(DWORD dwID, POINT *ppt,IUnknown *pcmdTarget,IDispatch *pdispObject)
{
#define IDR_BROWSE_CONTEXT_MENU 24641
#define IDR_FORM_CONTEXT_MENU 24640
#define SHDVID_GETMIMECSETMENU 27
#define SHDVID_ADDMENUEXTENSIONS 53
HRESULT hr;
HINSTANCE hinstSHDOCLC;
HWND hwnd;
HMENU hMenu;
CComPtr spCT;
CComPtr spWnd;
MENUITEMINFO mii = {0};
CComVariant var, var1, var2;
hr = pcmdTarget->QueryInterface(IID_IOleCommandTarget, (void**)&spCT);
hr = pcmdTarget->QueryInterface(IID_IOleWindow, (void**)&spWnd);
hr = spWnd->GetWindow(&hwnd);//取得瀏覽器視窗控制代碼
hinstSHDOCLC = LoadLibrary(TEXT("SHDOCLC.DLL"));
if (hinstSHDOCLC == NULL)
{
// Error loading module -- fail as securely as possible
return;
}
hMenu = LoadMenu(hinstSHDOCLC, MAKEINTRESOURCE(IDR_BROWSE_CONTEXT_MENU));
hMenu = GetSubMenu(hMenu, dwID);
// Get the language submenu
hr = spCT->Exec(&CGID_ShellDocView, SHDVID_GETMIMECSETMENU, 0, NULL, &var);
mii.cbSize = sizeof(mii);
mii.fMask = MIIM_SUBMENU;
mii.hSubMenu = (HMENU) var.byref;
// Add language submenu to Encoding context item
SetMenuItemInfo(hMenu, IDM_LANGUAGE, FALSE, &mii);
// Insert Shortcut Menu Extensions from registry
V_VT(&var1) = VT_INT_PTR;
V_BYREF(&var1) = hMenu;
V_VT(&var2) = VT_I4;
V_I4(&var2) = dwID;
hr = spCT->Exec(&CGID_ShellDocView, SHDVID_ADDMENUEXTENSIONS, 0, &var1, &var2);
// Remove View Source
DeleteMenu(hMenu, IDM_VIEWSOURCE, MF_BYCOMMAND);//刪除“查看源檔案”功能表項目
// Show shortcut menu
int iSelection = ::TrackPopupMenu(hMenu,
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,//返回使用者選擇的功能表命令ID
ppt->x,
ppt->y,
0,
hwnd,
(RECT*)NULL);
// Send selected shortcut menu item command to shell
LRESULT lr = ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);//發送到Internet Explorer_Server進行內部處理。
FreeLibrary(hinstSHDOCLC);
return S_OK;
}
從上面的例子我們看出,基本的方法就是根據“shdoclc.dll”檔案中的菜單資源建立菜單,再通過來自pcmdTarget的IOlcCommandTarget介面獲得“編碼”菜單以及HKEY_CURRENT_USER/Software/Microsoft/Internet Explorer/MenuExt下的定義擴充菜單,然後以TPM_RETURNCMD標誌調用TrackPopupMenu或TrackPopupMenuEx快顯功能表,並將返回的功能表命令ID教給瀏覽器視窗進行處理。這種方法可以調用許多通過瀏覽器無法直接調用的命令和對話方塊(參閱:《Internet Explorer 編程簡述(五)調用IE隱藏的命令》)。
所以,我們只需要在快顯功能表之前做一些自訂動作即可達到修改預設菜單的目的,如上面代碼中就用刪除了“查看源檔案”功能表項目。
2)問題
如果我們不僅僅是刪除預設的功能表項目或是修改了預設的功能表項目,還添加了自己的功能表項目,會出現什麼情況呢?由於使用了類似於MFC中UpdateUI的機制,遇到不認識的CommandID,瀏覽器就會將其狀態設定為Disabled,所以我們自己添加的菜單是無法被選擇的。
可以想到,如果把菜單狀態設定為Enabled,並通過TPM_RETURNCMD標誌調用TrackPopupMenu或TrackPopupMenuEx,再把返回的命令ID教給合適的視窗(如主架構視窗)去處理不就行了。關鍵點就在於如何把菜單狀態設定為Enabled。
3)實現
解決的辦法是截獲WM_INITMENUPOPUP訊息,在菜單建立以後,尚未顯示之前修改功能表項目狀態,那瀏覽器就沒有辦法了。截獲WM_INITMENUPOPUP訊息則可使用子類化(subclass)的技術,前面通過IOleWindow介面我們得到了瀏覽器視窗的控制代碼hwnd,則可以這樣做:
HMENU g_hPubMenu = NULL;
WNDPROC g_lpPrevWndProc = NULL;
LRESULT CALLBACK CustomMenuWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_INITMENUPOPUP)
{
if (wParam == (WPARAM) g_hPubMenu)
{
::EnableMenuItem( 自訂的功能表命令ID, MF_ENABLED | MF_BYCOMMAND );
::CheckMenuItem( 自訂的功能表命令ID, MF_BYCOMMAND);
return 0;
}
}
return CallWindowProc(g_lpPrevWndProc, hwnd, uMsg, wParam, lParam);
}
HRESULT CMyHtmlView::OnShowContextMenu(DWORD dwID, LPPOINT ppt,
LPUNKNOWN pcmdtReserved, LPDISPATCH pdispReserved)
{
//瀏覽器菜單控制代碼儲存在g_hPubMenu中
......
// subclass瀏覽器視窗
g_lpPrevWndProc = (WNDPROC)::SetWindowLong(hwnd, GWL_WNDPROC, (LONG)CustomMenuWndProc);
//m_SubclassWnd.SubclassWindow( hwnd );//MFC中用此方法更簡便
// Show shortcut menu
int iSelection = ::TrackPopupMenu(hSubMenu,
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
ppt->x,
ppt->y,
0,
hwnd,
(RECT*)NULL);
// Unsubclass瀏覽器視窗
::SetWindowLong(hwnd, GWL_WNDPROC, (LONG)g_lpPrevWndProc);
g_lpPrevWndProc = NULL;
//m_SubclassWnd.UnsubclassWindow();
if (iSelection == 自訂的功能表命令ID )
{
::SendMessage( ::AfxGetMainWnd()->m_hWnd, WM_COMMAND, MAKEWPARAM(LOWORD(lResult), 0x0), 0 );
}
else
{
LRESULT lr = ::SendMessage(hwnd, WM_COMMAND, iSelection, NULL);
}
......
}
在MFC中則更為方便,從CWnd繼承一個視窗類別,假設為CWebBrowserSubclassWnd,為CMyHtmlView添加一個CWebBrowserSubclassWnd類型的成員變數m_SubclassWnd,在子類化和去除子類化時調用m_SubclassWnd.SubclassWindow( hwnd )和m_SubclassWnd.UnsubclassWindow()即可。相應的WM_INITMENUPOPUP訊息處理函數如下所示:
void CWebBrowserSubclassWnd::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
CWnd::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
pPopupMenu->EnableMenuItem( 自訂的功能表命令ID, MF_ENABLED | MF_BYCOMMAND );
pPopupMenu->CheckMenuItem( 自訂的功能表命令ID, MF_BYCOMMAND);
}
下面的圖片顯示了將“文字大小”功能表項目添加到“編碼”功能表項目的下面的效果。
5、新的問題
看完上面的代碼,我們又自然地想到瀏覽器編程中的另一個問題,那就是“編碼”菜單。我們指定,手動建立一個“編碼”菜單是比較麻煩的事,而且很難做到與瀏覽器操作功能表上的“編碼”菜單一樣的效果。何不使用上述的方法讓瀏覽器自己建立“編碼”菜單和處理相應的命令呢?
具體實現請看下一篇文章《Internet Explorer 編程簡述(七)完美的“編碼”菜單》
參考資料:
MSDN:Adding Entries to the Standard Context Menu
MSDN:How To Adding to the Standard Context Menus of the WebBrowser Control
MSDN:IDocHostUIHandler::ShowContextMenu Method
BeginThread.com:Custom WebBrowser Context Menus