2008年(909)
分类:
2008-05-06 21:58:19
原文出处:MSDN
Magazine C Q&A
下载源代码
译者注:
在以前的VC知识库 Online Journal 上有三篇文章:
“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 控制并非人们所想象的那样是子窗口。
CFindWnd ies(m_hWnd, "Internet Explorer_Server"); myHwndIE = ies.m_hWnd;这个类的构造函数调用函数:
FindChildClassHwnd(hwndParent, (LPARAM)this)函数,该函数又调用:
EnumChildWindows 和 FindWindowEx搜索所有后裔窗口直到找到类名匹配窗口为止。FindWindow 用来查找最顶层窗口,而搜索子窗口还得用 FindWindowEx,它是 Win32 API 函数。CFindWnd 返回第一个匹配的窗口,所以它只被用于查找你期望只有一个实例的窗口。通常在搜索特定窗口时,一般最保险的做法都是检查窗口类名。
BOOL CHtmlCtrl::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_CONTEXTMENU) return TRUE; // eat it return CHtmlView::PreTranslateMessage(pMsg); }这样做是可行的,因为MFC实现了非常有独创性的、强大的特性─在 CWinThread 的主消息泵中,MFC 调用 CWnd::WalkPreTranslateTree 函数。这个函数循环消息目的地窗口的所有父窗口,调用每一个父窗口的 PreTranslateMessage ,一旦截获消息发送到后裔窗口则停止循环。非常聪明!
头文件 HtmlCtrl.h //////////////////////////////////////////////////////////////// #pragma once //////////////////////////////////////////////////////////////// // 该结构在命令映射中定义一个入口,这个映射将文本串映射到命令IDs, // 如果命令映射中有一个映射到 ID_APP_ABOUT 的入口 “about”,并且 // HTML 有一个链接锚 ,那么单击该链接时将执行 // ID_APP_ABOUT 命令。为了设置这个映射,调用 CHtmlCtrl::SetCmdMap. // // struct HTMLCMDMAP { LPCTSTR name; // command name used in "app:name" HREF in // 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和以前相比,这个类功能更强,具备了 Get/SetHideContextMenu 属性处理机制,对 WM_CONTEXTMENU 消息的处理采取了发送到父窗口,而不是过滤掉它。这样就使得你能实现自己的上下文菜单。注意 WM_CONTEXTMENU 消息的发送是在鼠标右键向上释放的时候进行的,而不是按下时处理的。具体细节请参考源代码。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; }
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下面我们一步一步来分析实现过程,首先必须获取 IHTMLDocument2 接口: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; }
SPIHTMLDocument2 doc = GetHtmlDocument();SPIHTMLDocument2 与 CComQIPtr
// strHTML is LPCTSTR CComSafeArray如果不借助于 CComSafeArray 和 CComBSTR,而是用下列这些 API 函数来实现相同的处理,如 SafeArrayCreateVector,SafeArrayAccessData, 和 SafeArrayUnaccessData,那么至少还得写10-20行无聊的代码。一旦你上手了智能指针,你会觉得ATL的这些东西用起来真的很爽。sar; sar.Create(1,0); sar[0] = CComBSTR(strHTML);
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:hello, world"));针对简单的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("\n"), hwnd, cname, text); s = temp; } return TRUE; } //////////////////////////////////////////////////////////////////////// // 该函数创建一个文本串,这个串就是要显示在主窗口的 HTML 文档。 CString CMainFrame::FormatWindowListHTML() { // start w/top matter CString html = _T("\n\ %p %s %s
\ | AboutHtml3 例子程序 -- 顶层可见窗口清单 \n\ VC知识库 | |
窗口句柄(hwnd) | 窗口类名 | 窗口标题\ |
[关于] \ [退出]\n\ \n\ "); return html; } 最后,我想说明一下本文例子程序中其它的一些编程技巧和诀窍,主要是针对CHtmlCtrl类的功能扩展。早在“VC6中使用CHtmlView在对话框控制中显示HTML文件”(第六期)一文中,我曾经演示了如何实现“app:”伪协议来创建HTML链接(也就是锚点)与应用程序通信。例如:你可以象下面这样添加一个链接:
About然后,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文档中有一个锚点链接是 // ,则单击该链接将调用 ID_APP_ABOUT 命令。设置命令 // 映射的方法是调用 CHtmlCtrl::SetCmdMap 函数. // struct HTMLCMDMAP { LPCTSTR name; // 用于" 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最后一个关键的地方是 CHtmlCtrl::OnAppCmd 必须通过 PostMessage 发送命令,而不是用 SendMessage,因为如果不这样做,你会发现当执行 OnBeforeNavigate2 时,如果关闭程序会遇到麻烦(我费了好大的劲才发现这个问题)。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; }