提出問題:
VC知識庫《在線雜志》第六期有一篇文章“VC6中使用CHtmlView在對話框控制中顯示HTML文件”,很多讀者來信說很喜歡這種功能。但是美中不足的是在對話框的HTML頁面上單擊鼠標右鍵會彈出上下文菜單。從而可以象在IE中那樣看到頁面的源代碼。為了防止用戶查看HTML代碼,有人嘗試過在CHtmlCtrl派生的窗口中重載WM_CONTEXTMENU,或者在CHtmlView以及CHtmlCtrl類中禁用右鍵的上下文菜單和彈出式菜單,這兩個方法都沒有成功。那麼如何禁用HTML的這個上下文菜單呢? 本文就針對這個問題用不同的方法來完善上次的程序。
解答:
CHtmlCtrl類可以將CHtmlView轉換成在任何窗口中使用的控制。我用它寫了一個程序叫AboutHtml,此程序實現了一個HTML對話框。但疏忽了鼠標右鍵的上下文菜單,所以在HTML對話框中單擊鼠標右鍵,會彈出標准的浏覽器上下文菜單(如圖一),而這個菜單對於某些人來說可能是多余的。
圖一 不想要的上下文菜單
其實,要解決這個問題有一個非常簡單的辦法,真是易如反掌,甚至不用寫任何C++代碼!只要在HTML頁面中加一行指令即可:
//
//
這條指令告訴浏覽器不要顯示上下文菜單。也可以象下面這樣寫:
//
oncontextmenu="ShowMyMenu(); return false"
//
ShowMyMenu是一個顯示定制菜單的JavaScript過程。本文例子代碼之一AboutHtml1使用的就是oncontextmenu。源代碼可以從本文的開始處下載。
由於VC知識庫是一個關於C++以及Visual C++的網站,與JavaScript之類的腳本語言沒什麼關系。所以我們要用另一種稍微復雜一點的方法來實現相同的事情,那就是用C++來做。為此,正規的C++方法是實現IDocHostUIHandler接口,而且要做的事情很多。至於為什麼要實現它,請參見有關文檔。用WM_CONTEXTMENU 或者 WM_RBUTTONDOWN來處理這個問題的思路的確是通常Windows做事情的方式。但是問題是CHtmlCtrl窗口不是真正的輸入窗口。窗口有很多種,只要用Spy++工具看一下我們的例子程序就知道在你眼前會出現多少種窗口。如圖二所示,在實際的輸入窗口上,浏覽器窗口有三級父/子窗口。
Dialog
AfxFrameOrView42d // CHtmlCtrl
Shell Embedding
Shell DocObject View
Internet Explorer_Server
它是個接收輸入的Internet Explorer_Server服務器窗口,並且如果你想要截獲WM_CONTEXTMENU消息,必須子類化這個窗口。在MFC中,這意味著你必須獲取HWND並調用SubclassWindow。記住了,這是一種非常規方式,而且微軟的那幫家伙也明確禁止這樣做,不過我還是根據原來的程序寫了另一個版本AboutHtml2,我這麼做了。
圖二在Spy++中的父/子關系
獲得這個神秘的Internet Explorer_Server HWND的方法有很多種。但FindWindow不行,因為它只能得到頂層窗口。由於此服務器窗口是浏覽器的曾孫(great-grandchild),在所有層次上都沒有同胞兄弟,所以下列算法成立:
static HWND GetLastChild(HWND hwndParent)
{
HWND hwnd = hwndParent;
while (TRUE) {
HWND hwndChild = ::GetWindow(hwnd, GW_CHILD);
if (hwndChild==NULL)
return hwnd;
hwnd = hwndChild;
}
return NULL;
}
這個函數假設只有單子繼承鏈,如同浏覽器中的一個窗口——即每個父窗口肯定有一個子窗口——並且獲取最末尾(或最小)的子窗口就是Internet Explorer_Server窗口。一旦取得HWND,剩下的事情便是寫一個新的MFC類對它進行子類化。
class CMyIEWnd : public CWnd {
public:
afx_msg void OnContextMenu(CWnd* pWnd, CPoint pos) { }
DECLARE_MESSAGE_MAP();
};
這個類重載WM_CONTEXTMENU,其它什麼事情也不做:OnContextMenu是個空函數,返回的東西不顯示菜單,也不調用基類(CWnd)的方法。使用CMyIEWnd時,在CMyHtmlCtrl中添加一個實例:
//
class CMyHtmlCtrl : public CHtmlCtrl {
protected:
CMyIEWnd m_myIEWnd;
};
//
把這一切聯系在一起的最關鍵的一步是調用SubclassWindow。但在哪裡調用以及什麼時候調用呢?最好時機是在浏覽器加載頁面之後。
void CMyHtmlCtrl::OnNavigateComplete2(LPCTSTR strURL)
{
if (!m_myIEWnd.m_hWnd) {
HWND hwnd = GetLastChild(m_hWnd);
m_myIEWnd.SubclassWindow(hwnd);
}
}
具體處理過程是這樣的:當用戶打開“關於”對話框,對話框創建CHtmlCtrl窗口來打開文檔,當浏覽器將文檔打開以後,它發送一個通知,MFC將這個通知定向到OnNavigateComplete2。CMyHtmlCtrl::OnNavigateComplete2調用GetLastChild來獲得“真正的”輸入窗口並將它子類化。這時所有的消息將通過CMyIEWnd類去往Internet Explorer_Server,包括WM_CONTEXTMENU。這裡要注意,IE的HWND是可以修改的,所以如果除了“關於”對話框外,你還想做一些其它的事情的話,必須要對HWND進行反子類化(unsubclass)和重子類化(resubclass)處理。
使用這個技術有兩個重要事情需要注意。第一,它功能很強,因為你子類化了“真正的”IE窗口,你可以做幾乎任何事情。第二,如果你不小心而使用不當,那將會發生最糟糕最糟糕的事情。一旦你用這種方法控制了資源管理器窗口,等於是把所有賭注放進去了。記住不要用不正當的方式去玩弄浏覽器,而是要通過正式接口(IDocHostUIHandler)定制它!否則後果不堪設想。