“VC6中使用CHtmlView在對話框控制中顯示HTML文件”
“如何禁用HTML頁面的上下文菜單”
“Convert CHtmlView to CHtmlCtrl...”
這三篇文章的原文實際上都出自 MSDN Magazine 及其前身 MSJ 的“C++ Q&A”專欄作家 Paul DiLascia 之手。此君從1995年開始就成為 MS 在 C++/MFC 方面的高級寫手,Paul 在 Windows 應用開發領域的造詣頗深。直到現在仍然在為該專欄撰寫技術文章,只不過其文章已不僅僅涉及 C++/MFC,偶爾也寫一些 C#。為了微軟的 .NET 戰略,Paul 可謂忠實、勤奮和敬業......
本文是以上文章所涉及內容的延伸。如果你已經對前述文章討論的東西了然於心,那麼可以直接切入本文的正題。如果你沒有看過上面提到的文章,建議最好先看一下,以便了解本文內容的背景,這樣對於理解本文所討論的東西會更有幫助。
背景簡介
話說在第六期的“VC6中使用CHtmlView在對話框控制中顯示HTML文件”一文中,主要討論並示范了如何改進 MFC 的 CHtmlView 類,使它能處理基於對話框的應用和各種其它類型的窗口應用,其思路是通過創建 CHtmlView 的派生類 CHtmlCtrl,使得 CHtmlView 擺脫了對文檔/視圖的依賴。
在第十一期的“如何禁用HTML頁面的上下文菜單”一文中,主要討論了如何通過子類化 IE 服務器窗口(Internet Explorer_Server)來禁用 CHtmlCtrl 的上下文菜單。實際上,真正顯示HTML的窗口並不是浏覽器(CHtmlView/CHtmlCtrl)窗口,而是一個名為“Internet Explorer_Server”的最底層的子孫窗口。這一點可以通過 Spy++ 來證實,為了獲得該窗口的句柄(HWND),在實現過程中使用了一個函數 GetLastChild(HWND hwndParent),其定義如下:
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”窗口。 在通常情況下,這個假設都成立。不幸的是,如果 HTML 文檔中包含象 ComBoxes(組合框) 這樣的控制時,這個假設就不靈了。用 Spy++ 不難發現情況並不象你期望的那樣─Internet Explorer_Server是最後的子窗口。實際上,在IE中,Edit 和 Button 控制並非人們所想象的那樣是子窗口。
獲得 Win32 窗口句柄的更好的方法
為了解決這個問題,本文設計了一個更加完善的類:CFindWnd,用更好的算法專門來獲取 IE 窗口。CFindWnd 查找某個窗口(給定窗口名字)的第一個子窗口。 例如,它的使用方法如下:
CFindWnd ies(m_hWnd, "Internet Explorer_Server");
myHwndIE = ies.m_hWnd;
這個類的構造函數調用函數:
FindChildClassHwnd(hwndParent, (LPARAM)this)
函數,該函數又調用:
EnumChildWindows 和 FindWindowEx
搜索所有後裔窗口直到找到類名匹配窗口為止。FindWindow 用來查找最頂層窗口,而搜索子窗口還得用 FindWindowEx,它是 Win32 API 函數。CFindWnd 返回第一個匹配的窗口,所以它只被用於查找你期望只有一個實例的窗口。通常在搜索特定窗口時,一般最保險的做法都是檢查窗口類名。
百家爭鳴
有一個讀者來信指出:根本沒有必要使用子類IE窗口的方法來禁用上下文菜單。完全可以在 CHtmlCtrl 內部實現,象下面這樣:
BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
{
if (pMsg->message == WM_CONTEXTMENU)
return TRUE; // eat it
return CHtmlView::PreTranslateMessage(pMsg);
}
這樣做是可行的,因為MFC實現了非常有獨創性的、強大的特性─在 CWinThread 的主消息泵中,MFC 調用 CWnd::WalkPreTranslateTree 函數。這個函數循環消息目的地窗口的所有父窗口,調用每一個父窗口的 PreTranslateMessage ,一旦截獲消息發送到後裔窗口則停止循環。非常聰明!
經驗證明:要使前面的代碼段按照期望的結果運行,你還必須截獲 WM_RBUTTONDOWN 和 WM_RBUTTONDBLCLK 消息,同時還要做必要的檢查以保證目標窗口的類名是 “Internet Explorer_Server”,這樣就不會意外地捕獲其它子窗口的上下文菜單(除非你確實要這麼做)。下面是 CHtmlCtrl::PreTranslateMessage 的最終代碼:
頭文件 HtmlCtrl.h
////////////////////////////////////////////////////////////////
#pragma once
////////////////////////////////////////////////////////////////
// 該結構在命令映射中定義一個入口,這個映射將文本串映射到命令IDs,
// 如果命令映射中有一個映射到 ID_APP_ABOUT 的入口 “about”,並且
// HTML 有一個鏈接錨 <A HREF="app:about">,那麼單擊該鏈接時將執行
// ID_APP_ABOUT 命令。為了設置這個映射,調用 CHtmlCtrl::SetCmdMap.
//
//
struct HTMLCMDMAP {
LPCTSTR name; // command name used in "app:name" HREF in
// <A UINT nID;
};
//////////////////
// 這個類將 CHtmlView 轉換為普通的能在對話框和框架中使用的控制
//
class CHtmlCtrl : public CHtmlView {
protected:
HTMLCMDMAP* m_cmdmap; // command map
BOOL m_bHideMenu; // hide context menu
public:
CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
~CHtmlCtrl() { }
// get/set HideContextMenu property
BOOL GetHideContextMenu() { return m_bHideMenu; }
void SetHideContextMenu(BOOL val) { m_bHideMenu=val; }
// Set doc contents from string
HRESULT SetHTML(LPCTSTR strHTML);
// set command map
void SetCmdMap(HTMLCMDMAP* val) { m_cmdmap = val; }
// create control in same place as static control
BOOL CreateFromStatic(UINT nID, CWnd* pParent);
// create control from scratch
BOOL Create(const RECT& rc, CWnd* pParent, UINT nID,
DWORD dwStyle = WS_CHILD|WS_VISIBLE,
CCreateContext* pContext = NULL)
{
return CHtmlView::Create(NULL, NULL, dwStyle, rc, pParent,
nID, pContext);
}
// 重寫該函數可以截獲子窗口消息,從而禁用上下文菜單。
virtual BOOL PreTranslateMessage(MSG* pMsg);
// 通常,CHtmlView 自己是在 PostNcDestroy 銷毀的,但對於一個界面控制來說
// 我們不想那樣做,因為控制一般都是作為另一個窗口對象的成員實現的。
//
virtual void PostNcDestroy() { }
// 重寫以便旁路掉對 MFC doc/view 框架的依賴,CHtmView 僅僅在這裡依附於框架。
afx_msg void OnDestroy();
afx_msg int OnMouseActivate(CWnd* pDesktopWnd, UINT nHitTest,
UINT msg);
// 重寫以便截獲 "app:" 偽協議
virtual void OnBeforeNavigate2( LPCTSTR lpszURL,
DWORD nFlags,
LPCTSTR lpszTargetFrameName,
CByteArray& baPostedData,
LPCTSTR lpszHeaders,
BOOL* pbCancel );
// 你可以重寫處理 "app:" 命令的代碼。注意只是在不使用命令映射機制時才需要重寫
virtual void OnAppCmd(LPCTSTR lpszCmd);
DECLARE_MESSAGE_MAP();
DECLARE_DYNAMIC(CHtmlCtrl)
};
實現文件 HtmlCtrl.cpp
////////////////////////////////////////////////////////////////
// 可用於對話框和其它窗口,不需要框架
//
// 特性:
// - SetCmdMap 可以設置 "app:command" 鏈接的命令映射.
// - SetHTML 可以將串內容設置成 HTML.
#include "StdAfx.h"
#include "HtmlCtrl.h"
// 聲明 typedef ATL 智能指針;如:SPIHTMLDocument2
#define DECLARE_SMARTPTR(ifacename) typedef CComQIPtr<ifacename> SP##ifacename;
// IHTMLDocument2 的智能指針
DECLARE_SMARTPTR(IHTMLDocument2)
// 一個很有用的宏,用於檢查 HRESULT
#define HRCHECK(x) hr = x; if (!SUCCEEDED(hr)) { \
TRACE(_T("hr=%p\n"),hr);\
return hr;\
}
... // same as earlier version
// Return TRUE if hwnd is Internet Explorer window.
inline BOOL IsIEWindow(HWND hwnd)
{
static LPCSTR IEWNDCLASSNAME = "Internet Explorer_Server";
char classname[32]; // always char, never TCHAR
GetClassName(hwnd, classname, sizeof(classname));
return strcmp(classname, IEWNDCLASSNAME)==0;
}
//////////////////
// 重寫後捕獲 "Internet Explorer_Server" 窗口上下文菜單消息.
//
BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
{
if (m_bHideMenu) {
switch (pMsg->message) {
case WM_CONTEXTMENU:
case WM_RBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONDBLCLK:
if (IsIEWindow(pMsg->hwnd)) {
if (pMsg->message==WM_RBUTTONUP)
// let parent handle context menu
GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam,
pMsg->lParam);
return TRUE; // eat it
}
}
}
return CHtmlView::PreTranslateMessage(pMsg);
}
//////////////////
// 重寫後將 "app:" 鏈接傳遞到虛函數,而不是浏覽器.
//
void CHtmlCtrl::OnBeforeNavigate2( LPCTSTR lpszURL,
DWORD nFlags, LPCTSTR lpszTargetFrameName, CByteArray& baPostedData,
LPCTSTR lpszHeaders, BOOL* pbCancel )
{
const char APP_PROTOCOL[] = "app:";
int len = _tcslen(APP_PROTOCOL);
if (_tcsnicmp(lpszURL, APP_PROTOCOL, len)==0) {
OnAppCmd(lpszURL + len); // call virtual handler fn
*pbCancel = TRUE; // cancel navigation
}
}
//////////////////
// 當浏覽器試圖導航到 "app:foo" 時調用該函數.
// 默認的處理例程查找"foo"命令的命令映射,並向找到的父窗口發送
// WM_COMMAND 消息。調用 SetCmdMap 設置命令映射。如果要實現更
// 復雜的處理,只要重寫這個函數即可.
//
void CHtmlCtrl::OnAppCmd(LPCTSTR lpszCmd)
{
if (m_cmdmap) {
for (int i=0; m_cmdmap[i].name; i++) {
if (_tcsicmp(lpszCmd, m_cmdmap[i].name) == 0)
// Use PostMessage to avoid problems with exit command. (Let
// browser finish navigation before issuing command.)
GetParent()->PostMessage(WM_COMMAND, m_cmdmap[i].nID);
}
}
}
//////////////////
// 將串轉換為 HTML 文檔
//
HRESULT CHtmlCtrl::SetHTML(LPCTSTR strHTML)
{
HRESULT hr;
// Get document object
SPIHTMLDocument2 doc = GetHtmlDocument();
// Create string as one-element BSTR safe array for
// IHTMLDocument2::write.
CComSafeArray<VARIANT> sar;
sar.Create(1,0);
sar[0] = CComBSTR(strHTML);
// open doc and write
LPDISPATCH lpdRet;
HRCHECK(doc->open(CComBSTR("text/html"),
CComVariant(CComBSTR("_self")),
CComVariant(CComBSTR("")),
CComVariant((bool)1),
&lpdRet));
HRCHECK(doc->write(sar)); // write contents to doc
HRCHECK(doc->close()); // close
lpdRet->Release(); // release IDispatch returned
return S_OK;
}
和以前相比,這個類功能更強,具備了 Get/SetHideContextMenu 屬性處理機制,對 WM_CONTEXTMENU 消息的處理采取了發送到父窗口,而不是過濾掉它。這樣就使得你能實現自己的上下文菜單。注意 WM_CONTEXTMENU 消息的發送是在鼠標右鍵向上釋放的時候進行的,而不是按下時處理的。具體細節請參考源代碼。
動態生成並顯示 HTML 文檔
前面的例子程序在處理HTML文檔時,都是把它作為應用程序的資源進行處理的,如果碰到需要動態產生HTML文檔信息的情況,這種處理方法便無法滿足需要,那麼如何動態 顯示生成的HTML文檔呢?下面我們就來解決這個問題。
大家知道,將純文本格式化成HTML是再簡單不過的事情了,雖然用C++來實現這個過程有點單調乏味,但仍然是可以做到的。如果你曾經寫過JavaScript腳本,那麼肯定知道加載頁面時 ,可以調用 document.write 直接將HTML寫到文檔中。其實在C++中做法也一樣,只不過編碼看起來會有些繁瑣和凌亂,因為要用到 COM 接口 IHTMLDocument2 以及 BSTRs、SAFEARRAYs 等數據類型來處理字符串。所幸的是 ATL 具備了大量的類可以助我們一臂之力。
下圖是本文例子程序運行時的畫面,使用 HTML 文檔格式動態顯示頂層窗口的信息:
圖一 例子程序運行畫面
這個程序的代碼原型來自第十一期《在線雜志》中“如何禁用HTML頁面的上下文菜單” 一文的例子,它實現了一個 HTML “關於”對話框,其顯示的HTML頁面是從資源中加載的:
m_page.LoadFromResource(_T("about.htm"));
CHtmlView::LoadFromResource 打開 res://AboutHtml.exe/about.htm,這裡“AboutHtml.exe” 是可執行程序的實際名字,“res://”是一個偽協議。為了顯示頂層窗口的信息,最好的辦法是動態產生 HTML 頁面,而不是從資源中加載,為此我在 CHtmlCtrl類中添加了一個新函數:CHtmlCtrl::SetHTML,
////////////////////////////////////
// 通過串設置 HTML 文檔內容
//
HRESULT CHtmlCtrl::SetHTML(LPCTSTR strHTML)
{
HRESULT hr;
// 獲得文檔對象
SPIHTMLDocument2 doc = GetHtmlDocument();
// 創建只有一個元素(串)的 BSTR 數組元素
// IHTMLDocument2::write.
CComSafeArray<VARIANT> sar;
sar.Create(1,0);
sar[0] = CComBSTR(strHTML);
// 打開文檔並寫入
LPDISPATCH lpdRet;
HRCHECK(doc->open(CComBSTR("text/html"),
CComVariant(CComBSTR("_self")),
CComVariant(CComBSTR("")),
CComVariant((bool)1),
&lpdRet));
HRCHECK(doc->write(sar)); // write contents to doc
HRCHECK(doc->close()); // close
lpdRet->Release(); // release IDispatch returned
return S_OK;
}
下面我們一步一步來分析實現過程,首先必須獲取 IHTMLDocument2 接口:
SPIHTMLDocument2 doc = GetHtmlDocument();
SPIHTMLDocument2 與 CComQIPtr<IHTMLDocument2> 一樣是一個指向 IHTMLDocument2 的ATL智能指針,(當今 Windows 編程已進入 COM 時代,作為一名編寫 Windows 應用程序的開發人員,如果你使用 COM 技術,但沒有用過智能指針,那麼這段代碼會對你有所裨益),接著,必須創建一個SAFEARRAY,以便存放作為 BSTR 數組唯一元素的 HTML 串,SAFEARRAY是一個 COM 數據結構,其作用是在不同平台之間安全地傳遞數組數據,ATL提供了 CComBSTR 和 CComSafeArray 兩個類,為開發人員在處理 BSTRs 和安全數組時減輕了許多痛苦:
// strHTML is LPCTSTR
CComSafeArray<VARIANT> sar;
sar.Create(1,0);
sar[0] = CComBSTR(strHTML);
如果不借助於 CComSafeArray 和 CComBSTR,而是用下列這些 API 函數來實現相同的處理,如 SafeArrayCreateVector,SafeArrayAccessData, 和 SafeArrayUnaccessData,那麼至少還得寫10-20行無聊的代碼。一旦你上手了智能指針,你會覺得ATL的這些東西用起來真的很爽。
現在有了文檔對象以及在安全數組中的內容,接下來便可以打開文檔,進行寫入操作,關閉文檔等等。IHTMLDocument2::write需要 VARIANTS 和 BSTRs 類型的數據,這裡ATL又一次顯示了它的優勢:
LPDISPATCH lpdRet;
doc->open(CComBSTR("text/html"), // MIME type
CComVariant(CComBSTR("_self")), // open in same window
CComVariant(CComBSTR("")), // no features
CComVariant((bool)1), // replace history entry
&lpdRet)); // IDispatch returned
doc->write(sar); // write it
doc->close(); // close
lpdRet->Release();
CHtmlCtrl::SetHTML 非常好用。使用它時有一個技巧:當第一次創建 CHtmlCtrl 時,它沒有文檔(GetHtmlDocument返回NULL)。所以在調用 CHtmlCtrl::SetHTML 之前,你必須創建一個文檔,最簡單的方法就是打開一個空文檔,就象下面這樣:
m_wndView.Navigate(_T("about:blank"));
此外,如果HTML很簡單,你可以用 about: 代替 CHtmlCtrl::SetHTML 來得到HTML,如下面的代碼:
m_wndView.Navigate(_T("about:<HTML><B>hello, world</B></HTML>"));
針對簡單的HTML可以這麼做,如果比較復雜的文檔則要調用 SetHTML。本文附帶的例子程序動態構造了一個包含圖像、表格、鏈接等元素的HTML文檔, 該文檔列出所有頂層窗口的信息,然後將它們顯示出來,如圖一所示。
例子程序的參考代碼如下:
//////////////////////////////////////
// HtmlApp.cpp
class CMyApp : public CWinApp {
public:
virtual BOOL InitInstance();
protected:
afx_msg void OnAppAbout();
DECLARE_MESSAGE_MAP()
} theApp;
BEGIN_MESSAGE_MAP(CMyApp, CWinApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
END_MESSAGE_MAP()
BOOL CMyApp::InitInstance()
{
// Create main frame window (don''t use doc/view stuff)
CMainFrame* pMainFrame = new CMainFrame;
if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
return FALSE;
pMainFrame->ShowWindow(m_nCmdShow);
pMainFrame->UpdateWindow();
m_pMainWnd = pMainFrame;
return TRUE;
}
//////////////////////////////////////////////////////
// “關於”對話框使用 HTML 控制顯示內容.
//
class CAboutDialog : public CDialog {
protected:
CHtmlCtrl m_page; // HTML control
virtual BOOL OnInitDialog();
public:
CAboutDialog() : CDialog(IDD_ABOUTBOX, NULL) { }
DECLARE_DYNAMIC(CAboutDialog)
};
IMPLEMENT_DYNAMIC(CAboutDialog, CDialog)
///////////////////////
// 初始化“關於”對話框
//
BOOL CAboutDialog::OnInitDialog()
{
// cmd map for CHtmlCtrl handles "app:ok"
static HTMLCMDMAP AboutCmds[] = {
{ _T("ok"), IDOK },
{ NULL, 0 },
};
VERIFY(CDialog::OnInitDialog());
VERIFY(m_page.CreateFromStatic(IDC_HTMLVIEW, this)); // create HTML
// ctrl
m_page.SetHideContextMenu(TRUE); // hide context
// menu
m_page.SetCmdMap(AboutCmds); // set command
// table
m_page.LoadFromResource(_T("about.htm")); // load HTML from
// resource
return TRUE;
}
/////////////////////
// 運行“關於”對話框
void CMyApp::OnAppAbout()
{
static CAboutDialog dlg; // static to remember state of hyperlinks
dlg.DoModal(); // run it
}
////////////////////////////////////////////////////////////////
// MainFrm.h
// 典型的主框架處理例程......
class CMainFrame : public CFrameWnd {
public:
CMainFrame(){ }
virtual ~CMainFrame() { }
protected:
CHtmlCtrl m_wndView; // CHtmlCtrl 作為主窗口視圖
CStatusBar m_wndStatusBar; // status line
CToolBar m_wndToolBar; // toolbar
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnContextMenu(CWnd* pWnd, CPoint pos);
// helper to format main window HTML
CString FormatWindowListHTML();
DECLARE_DYNCREATE(CMainFrame)
DECLARE_MESSAGE_MAP()
};
////////////////////////////////////////////////////////////////
// MainFrm.cpp
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_WM_CREATE()
ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()
// Commmand map for app: commands in main window HTML.
HTMLCMDMAP MyHtmlCmds[] = {
{ _T("about"), ID_APP_ABOUT },
{ _T("exit"), ID_APP_EXIT },
{ NULL, 0 },
};
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
VERIFY(CFrameWnd::OnCreate(lpCreateStruct)==0);
...
// create/init html control as view
VERIFY(m_wndView.Create(CRect(), this, AFX_IDW_PANE_FIRST));
m_wndView.SetHideContextMenu(TRUE); // 隱藏上下文菜單
m_wndView.SetCmdMap(MyHtmlCmds); // 設置命令
m_wndView.Navigate(_T("about:blank")); // 創建文檔
m_wndView.SetHTML(FormatWindowListHTML()); // 設置獲取的HTML內容串
SetActiveView(&m_wndView); // 設置MFC活動視圖
return 0;
}
///////////////////////////////////////////////////////////////////
// 處理上下文菜單的命令函數,當前該函數只是顯示TRACE信息,以示被調用過。
//
void CMainFrame::OnContextMenu(CWnd* pWnd, CPoint pos)
{
TRACE(_T("CMainFrame::OnContextMenu\n"));
}
//////////////////////////////////////////////////////////////////////
// 這個函數創建在主窗口視圖中顯示的 HTML。
// EnumWindows 回調該函數:如果窗口可見,則將窗口信息添加到 HTML table中。
//
static BOOL CALLBACK MyEnumWindowsProc(HWND hwnd, LPARAM lp)
{
DWORD style = GetWindowLong(hwnd, GWL_STYLE);
if (style & WS_VISIBLE) {
CString& s = *(CString*)lp;
char cname[256];
GetClassName(hwnd, cname, sizeof(cname));
TCHAR text[1024];
GetWindowText(hwnd, text, sizeof(text)/sizeof(text[0]));
CString temp;
temp.Format(_T("<TR><TD>%p </TD><TD>%s</TD><TD>%s</TD></TR>\n"),
hwnd, cname, text);
s += temp;
}
return TRUE;
}
////////////////////////////////////////////////////////////////////////
// 該函數創建一個文本串,這個串就是要顯示在主窗口的 HTML 文檔。
CString CMainFrame::FormatWindowListHTML()
{
// start w/top matter
CString html = _T("<HTML><BODY STYLE=\"font-family:Verdana;\" \
link=\"#02B7B7\" vlink=\"#02B7B7\">\n\
<TABLE WIDTH=\"100%\">\n\
<TR><TD VALIGN=top><A target=\"new\" HREF=\"http://www.vckbase.com\"><IMG \
BORDER=0 ALT=\"VCKBASE Online Journal\" SRC=\"res://AboutHtml3.exe/mlogo.gif\"></A></TD>\
<TD COLSPAN=2><B>AboutHtml3 例子程序 -- 頂層可見窗口清單</B><BR>\n\
<SMALL><A target=\"new\" \
HREF=\"http://www.vckbase.com\"> VC知識庫 </A></SMALL></TD></TR>\n\
<TR><TD><B>窗口句柄(hwnd)</B></TD><TD><B>窗口類名</B></TD><TD WIDTH=75%><B>窗口標題</B>\
</TD><TR>\n");
// enumerate top-level windows to append their info
EnumWindows(MyEnumWindowsProc, (LPARAM)&html);
// append bottom matter. note commands app:about and app:exit
html += _T("</TABLE>\n\
<P><B>[<A HREF=\"app:about\">關於</A>] \
[<A HREF=\"app:exit\">退出</A>]</B>\n\
</BODY>\n\
</HTML>");
return html;
}
最後,我想說明一下本文例子程序中其它的一些編程技巧和訣竅,主要是針對CHtmlCtrl類的功能擴展。早在“VC6中使用CHtmlView在對話框控制中顯示HTML文件”(第六期)一文中,我曾經演示了如何實現“app:”偽協議來創建HTML鏈接(也就是錨點)與應用程序通信。例如:你可以象下面這樣添加一個鏈接:
<A HREF="app:about">About</A>
然後,CHtmlCtrl::OnBeforeNavigate2 會識別出“app:”偽協議並以“about”作為參數調用專門的虛函數 CHtmlCtrl::OnAppCmd 。你可以創建自己的命令並在派生類中改寫 OnAppCmd 來處理自己建立的命令。使用了 CHtmlCtrl 一段時間後。我發現經常需要派生 CHtmlCtrl 類,每次都得改寫這個函數,自己感覺很麻煩!為了簡化這個過程,我發明了一個簡單的命令映射機制,利用這種機制可以輕松將“app:command”之類的轉換為通常熟知的 WM_COMMAND 命令 ID:
HTMLCMDMAP MyHtmlCmds[] = {
{ _T("about"), ID_APP_ABOUT },
{ _T("exit"), ID_APP_EXIT },
{ NULL, 0 },
};
這個映射機制的使用方法是象下面這樣調用 CHtmlCtrl::SetCmdMap 函數:
m_wndHtmlCtrl.SetCmdMap(MyHtmlCmds);
這樣一來,當用戶單擊“app:about”鏈接時,CHtmlCtrl::OnAppCmd 便會搜索命令映射,找到“about”入口,然後將與ID_APP_ABOUT 對應的 WM_COMMAND 消息發送到其父窗口,這個技巧主要是仰仗MFC神奇的命令路由通道實現的,借助此通道,任何窗口都可以處理此命令。真是爽啊!本文例子程序正是用這種特性將“關於”和“退出”命令作為HTML鏈接直接添加到主窗口中。CHtmlCtrl類實現的細節代碼如下:
////////////////////////////////////////////////////////////////
// HtmlCtrl.h
#pragma once
/////////////////////////////////////////////////////////////////////////
// 此結構定義一個命令映射入口,映射將文本串映射到命令IDs。如果你的命令映射
// 入口包含 "about" 映射到ID_APP_ABOUT,並且HTML文檔中有一個錨點鏈接是
// <A HREF="app:about">,則單擊該鏈接將調用 ID_APP_ABOUT 命令。設置命令
// 映射的方法是調用 CHtmlCtrl::SetCmdMap 函數.
//
struct HTMLCMDMAP {
LPCTSTR name; // 用於" <A HREF..." 中的 "app:name" 的命令名.
UINT nID;
};
////////////////////////////////////////////////////////////////////////
// 將 CHtmlView 轉換為框架或對話框中常規控制的類.類似於CListView/CListCtrl
// 和 CTreeView/CTreeCtrl
//
class CHtmlCtrl : public CHtmlView {
protected:
HTMLCMDMAP* m_cmdmap; // 命令映射
BOOL m_bHideMenu; // 隱藏上下文菜單
public:
CHtmlCtrl() : m_bHideMenu(FALSE), m_cmdmap(NULL) { }
~CHtmlCtrl() { }
// 獲取/設置 HideContextMenu 屬性
BOOL GetHideContextMenu() { return m_bHideMenu; }
void SetHideContextMenu(BOOL val) { m_bHideMenu=val; }
// 根據串創建 HTML 文檔
HRESULT SetHTML(LPCTSTR strHTML);
// 設置命令映射
void SetCmdMap(HTMLCMDMAP* val) { m_cmdmap = val; }
// 先創建一個靜態控制,然後用相同的再創建一個控制
BOOL CreateFromStatic(UINT nID, CWnd* pParent);
// 創建控制
BOOL Create(const RECT& rc, CWnd* pParent, UINT nID,
DWORD dwStyle = WS_CHILD|WS_VISIBLE,
CCreateContext* pContext = NULL)
{
return CHtmlView::Create(NULL, NULL, dwStyle, rc, pParent,
nID, pContext);
}
// 重寫用以解釋子窗口消息來禁用上下文菜單
virtual BOOL PreTranslateMessage(MSG* pMsg);
// 通常,CHtmlView 是在 PostNcDestroy 中將自己摧毀,
// 但用於窗口控制,我們不想那麼做,因為控制通常是作為
// 另一個窗口對象的成員來實現的.
//
virtual void PostNcDestroy() { }
// 重寫該函數以便旁路掉對 MFC 文檔/視圖框架的依賴. 此處是 CHtmView 依賴框架才能生存的唯一一個地方.
afx_msg void OnDestroy();
afx_msg int OnMouseActivate(CWnd* pDesktopWnd, UINT nHitTest,
UINT msg);
// 改寫該函數用以捕獲 "app:" 偽協議
virtual void OnBeforeNavigate2( LPCTSTR lpszURL,
DWORD nFlags,
LPCTSTR lpszTargetFrameName,
CByteArray& baPostedData,
LPCTSTR lpszHeaders,
BOOL* pbCancel );
// 你可以重寫這個虛函數用以處理 "app:" 命令.
// 如果不涉及命令映射,則不用該寫.
virtual void OnAppCmd(LPCTSTR lpszCmd);
DECLARE_MESSAGE_MAP();
DECLARE_DYNAMIC(CHtmlCtrl)
};
HtmlCtrl.cpp
///////////////////////////////////////////////////////////////////////
// 實現 CHtmlCtrl 類 — 窗口控制中的 Web 浏覽器。重寫 CHtmlView 以便擺脫
// 框架約束,可以用於對話框或任何其它窗口
//
// 特性:
// - SetCmdMap 使你能為"app:command"鏈接設置命令映射.
// - SetHTML 使你能將一個串設置成HTML文檔內容.
#include "StdAfx.h"
#include "HtmlCtrl.h"
// 這個宏聲明的 typedef 用於 ATL 智能指針,如:SPIHTMLDocument2
#define DECLARE_SMARTPTR(ifacename) typedef CComQIPtr<ifacename> SP##ifacename;
// IHTMLDocument2 接口智能指針
DECLARE_SMARTPTR(IHTMLDocument2)
// 這是個很有用的宏,用來檢查 HRESULTs
#define HRCHECK(x) hr = x; if (!SUCCEEDED(hr)) { \
TRACE(_T("hr=%p\n"),hr);\
return hr;\
}
... // same as earlier version
// 如果 hwnd 是 IE 窗口,則返回 TRUE。
inline BOOL IsIEWindow(HWND hwnd)
{
static LPCSTR IEWNDCLASSNAME = "Internet Explorer_Server";
char classname[32]; // 必須是 char 類型, 不能是 TCHAR
GetClassName(hwnd, classname, sizeof(classname));
return strcmp(classname, IEWNDCLASSNAME)==0;
}
///////////////////////////////////////////////////////////////////
// 重寫函數捕獲 "Internet Explorer_Server" 窗口上下文菜單消息。
//
BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg)
{
if (m_bHideMenu) {
switch (pMsg->message) {
case WM_CONTEXTMENU:
case WM_RBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONDBLCLK:
if (IsIEWindow(pMsg->hwnd)) {
if (pMsg->message==WM_RBUTTONUP)
// 讓父窗口處理上下文菜單
GetParent()->SendMessage(WM_CONTEXTMENU, pMsg->wParam,
pMsg->lParam);
return TRUE; // eat it
}
}
}
return CHtmlView::PreTranslateMessage(pMsg);
}
////////////////////////////////////////////////////////////////////
// 重寫函數傳遞 "app:" 鏈接到虛函數,而不是浏覽器。
//
void CHtmlCtrl::OnBeforeNavigate2( LPCTSTR lpszURL,
DWORD nFlags, LPCTSTR lpszTargetFrameName, CByteArray& baPostedData,
LPCTSTR lpszHeaders, BOOL* pbCancel )
{
const char APP_PROTOCOL[] = "app:";
int len = _tcslen(APP_PROTOCOL);
if (_tcsnicmp(lpszURL, APP_PROTOCOL, len)==0) {
OnAppCmd(lpszURL + len); // 調用虛擬函數例程
*pbCancel = TRUE; // 取消導航
}
}
////////////////////////////////////////////////////////////////////////
// 當浏覽器試圖導航到 "app:foo"時調用此函數. 缺省的命令處理映射為"foo",如果
// 找到命令ID,則向父窗口發送一個 WM_COMMAND 消息,調用 SetCmdMap 設置命令
// 映射。如果你想要作稍微復雜一些的處理,必須重寫 OnAppCmd。
//
void CHtmlCtrl::OnAppCmd(LPCTSTR lpszCmd)
{
if (m_cmdmap) {
for (int i=0; m_cmdmap[i].name; i++) {
if (_tcsicmp(lpszCmd, m_cmdmap[i].name) == 0)
// 使用 PostMessage 發送消息,避免退出命令出現的問題 (在發出命令前浏覽器結束導航。)
GetParent()->PostMessage(WM_COMMAND, m_cmdmap[i].nID);
}
}
}
///////////////////
// 將串轉為HTML文檔
//
HRESULT CHtmlCtrl::SetHTML(LPCTSTR strHTML)
{
HRESULT hr;
// 獲取文檔對象
SPIHTMLDocument2 doc = GetHtmlDocument();
// 創建串,將它作為BSTR數組的唯一個元素,因為 IHTMLDocument2::write 使用BSTR類型
CComSafeArray<VARIANT> sar;
sar.Create(1,0);
sar[0] = CComBSTR(strHTML);
// 打開文檔進行寫操作
LPDISPATCH lpdRet;
HRCHECK(doc->open(CComBSTR("text/html"),
CComVariant(CComBSTR("_self")),
CComVariant(CComBSTR("")),
CComVariant((bool)1),
&lpdRet));
HRCHECK(doc->write(sar)); // 將內容寫入文檔
HRCHECK(doc->close()); // 關閉文檔
lpdRet->Release(); // 釋放 IDispatch 然後返回
return S_OK;
}
最後一個關鍵的地方是 CHtmlCtrl::OnAppCmd 必須通過 PostMessage 發送命令,而不是用 SendMessage,因為如果不這樣做,你會發現當執行 OnBeforeNavigate2 時,如果關閉程序會遇到麻煩(我費了好大的勁才發現這個問題)。
最後,祝大家編程愉快!
本文配套源碼