使用 WebBrowser 控件,简化






4.97/5 (91投票s)
SimpleBrowser 类使您的 MFC 应用程序中更容易使用 WebBrowser 控件。
引言
SimpleBrowser
旨在简化在 MFC 应用程序中使用 WebBrowser
控件。如果您以前尝试过,您会知道做一些简单的事情可能非常复杂。我想用该控件完成以下任务:
- 在我的代码中生成 HTML,并将其作为字符串传递给控件。
- 导航到程序中作为资源的 HTML 文档。
- 捕获控件的事件。
- 打印控件的内容。
WebBrowser
控件公开 IWebBrowser2
接口,该接口允许您控制浏览器。它使用 DWebBrowserEvents2
接口发送事件,该接口通知您浏览器正在做什么。不幸的是,这两者都没有为我列出的任务提供清晰而简单的方法。
SimpleBrowser 类
SimpleBrowser
用 MFC 友好的方法封装了 IWebBrowser2
接口。这些方法将参数转换为接口所需的格式。一些 IWebBrowser2
方法需要 BSTR
,其他需要 VARIANT
,甚至偶尔需要 SAFEARRAY
,这些对于偶尔使用的 MFC 用户来说都可能很麻烦(或者至少不直观)。
如何将 WebBrowser
导航到内存中的文档?一个显而易见的解决方案是将文档写入临时文件,然后通过 file://c:\dir\filename.ext 格式的 URL 将控件导航到该文件。这似乎很慢且繁琐。您还需要在完成后删除临时文件。IWebBrowser2
接口允许您获取当前 HTML 文档的接口指针 (IHTMLDocument2
),然后您可以直接操作文档,而无需临时文件。问题是,在浏览器成功导航到文档之前,该接口不可用。该接口确实包含一个 write()
方法,所以如果我们能获取文档,我们可以使用它将数据写入浏览器。
WebBrowser
支持 res: 协议,该协议允许您使用 URL 语法 res://EXE/type/resource 在 HTML 页面中使用您的程序资源。EXE
是指向您可执行文件的路径。Type
要么是资源类型的字符串名称,要么是 #nnn
格式的字符串,其中 nnn
是预定义资源类型(如 RT_BITMAP
等)的数值。Resource
是资源的字符串名称,或者是 #nnn
格式的字符串,其中 nnn
是资源 ID。只要您使用 res: 协议的字符串形式,就可以直接导航到 URL。这要求您的 .RC 文件中使用以下类型的语句:
"MyPage" "HTML" "MyPage.html"
然后,您可以使用 URL res://MyProgram.Exe/HTML/MyPage 导航到此页面。如果您想使用内置的 RT_HTML
资源类型和整数资源标识符,
IDR_MY_PAGE HTML "MyPage.html"
URL 会出现问题。WebBrowser
控件似乎不喜欢带有“#”字符的 URL,并且使用替换字符“%23”很麻烦(因为它后面跟着一个数值)。WebBrowser
在这两种情况下似乎都不能很好地处理 ANSI/Unicode 问题。
WebBrowser
允许您使用 IWebBrowser2
接口中的 ExecWB()
方法打印当前文档。不幸的是,ExecWB()
基本上是一个“万能”方法,需要几个晦涩的参数。事实证明,有一种方法可以为打印指定页眉和页脚格式,但机制很麻烦。SimpleBrowser
将此封装在一个方法中。
SimpleBrowser
通过可覆盖的虚拟函数公开 DWebBrowserEvents2
事件。事件数据在传递给虚拟函数之前会被转换为 MFC 友好的格式。这些函数的基类版本通过标准的 WM_NOTIFY
机制向父窗口发送通知。
Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd,UINT nID)
Create
是标准的窗口创建函数。Create(...)
函数的值得注意之处在于它在创建浏览器 *之后* 会做什么。SimpleBrowser
导航到预定义的文档 about:blank。当该导航完成时,通过 SimpleBrowser
使用 Write()
函数(见下文)传递的任何文本将被写入浏览器。
CreateFromControl(CWnd *pParentWnd,UINT nID)
CreateFromControl()
在对话框中“创建”SimpleBrowser
,方法是替换另一个控件。您可以布局对话框,将一个静态控件放在您想要浏览器窗口的位置。在这种情况下,pParentWnd
是一个对话框(例如 this
),而 nID
标识静态控件。CreateFromControl()
获取静态控件的位置,销毁静态控件(因为它将不再使用),然后使用 Create(...)
在其位置创建浏览器。浏览器使用最初分配给静态控件的 ID (nID
)。您不必使用静态控件;任何控件类型都可以。我使用了一个带有“static edge”样式的静态控件(这使得控件的范围更容易看到)。
IHTMLDocument2 *GetDocument()
GetDocument()
返回浏览器控件中加载的当前 HTML 文档的 IHTMLDocument2
接口指针。该接口指针可用于直接操作文档,以防您有 SimpleBrowser
提供的直接方法不支持的特殊想法。如果您将控件导航到 HTML 文档以外的内容(例如,WebBrowser
控件可以愉快地让您导航到 Microsoft Word 文档,该文档将由控件托管),GetDocument()
将返回 NULL
。
Write(LPCTSTR string)
Write(...)
允许您在字符串中创建 HTML 文档,并在应用程序的浏览器窗口中显示它。这对于创建不适合固定 Windows 控件集显示的显示或报表,或需要特殊格式的信息很有用。HTML 易于生成,并且您可以“免费”获得打印(参见下面的 Print()
和 PrintPreview()
方法)。
SimpleBrowser
使用类似以下的代码将字符串写入 WebBrowser
控件:
// get document interface
IHTMLDocument2 *document = GetDocument();
if (document != NULL) {
// construct text to be written to browser as SAFEARRAY
SAFEARRAY *safe_array = SafeArrayCreateVector(VT_VARIANT,0,1);
VARIANT *variant;
SafeArrayAccessData(safe_array,(LPVOID *)&variant);
variant->vt = VT_BSTR;
variant->bstrVal = CString(string).AllocSysString();
SafeArrayUnaccessData(safe_array);
// write SAFEARRAY to browser document
document->write(safe_array);
document->Release();
document = NULL;
}
Write(...)
方法将字符串附加到当前文档。一个方便之处在于 WebBrowser
控件容忍“格式不正确”的 HTML 文档。
<html><body>.... | 没有尾部的 </body> 或 </html> 标签。 |
<html><body>...</body></html> <html><body>...</body></html> |
多个完整的文档。 |
带有 <b>标签</b> 的纯文本... | 根本没有 <html>...</html> 或 <body>...</body> 标签。 |
这允许您使用多个 Write(...)
调用来构建您的文档。您还可以根据需要更新浏览器内容,而无需每次都重新构建整个文档。
Clear()
Clear()
删除 WebBrowser
控件中的任何现有内容。如果您在控件中有 HTML 文档,Clear()
会通过关闭并重新打开当前文档来清空显示,然后刷新显示。这似乎比导航到 about:blank(当控件中没有 HTML 文档时使用)更快。
NavigateResource(int resource_ID)
res: 协议允许您在 HTML 页面中使用可执行文件中的资源。如前所述,res: 协议对于传递给 WebBrowser
的 URL 来说并不太方便,尤其是在使用数字资源 ID 时。我的解决方案是将 HTML 资源加载到字符串中,然后使用 Write(...)
方法。NavigateResource()
期望资源在 .RC 文件中定义如下:
IDR_MY_PAGE HTML "MyPage.html"
HTML 是您使用 IDE 插入 HTML 资源时使用的资源类型。
NavigateResource()
在加载为 MBCS(ANSI)编译的应用程序中的 UNICODE HTML 资源,或反之亦然时会变得很有趣。我的方法是将文档转换为匹配应用程序。直到 UNICODE 资源包含 ANSI 字符集(例如日文汉字)中不存在的字符,而应用程序是 MBCS 时,此转换才有效。在这种情况下,转换无效。这应该不会造成太大损害,因为您可能不会尝试在 ANSI 应用程序中显示远东 HTML 文档。
Print(LPCTSTR header,LPCTSTR footer)
IWebBrowser2
接口允许您使用 ExecWB
方法打印 WebBrowser
控件的当前内容。我提供的 Print(...)
函数消除了 ExecWB()
的晦涩参数,并提供了一种简单的方法来指定打印页面的页眉和页脚。
// construct two element SAFEARRAY;
// first element is header string,
// second element is footer string
HRESULT hr;
VARIANT header_variant;
VariantInit(&header_variant);
V_VT(&header_variant) = VT_BSTR;
V_BSTR(&header_variant) =
CString(header).AllocSysString();
VARIANT footer_variant;
VariantInit(&footer_variant);
V_VT(&footer_variant) = VT_BSTR;
V_BSTR(&footer_variant) =
CString(footer).AllocSysString();
long index;
SAFEARRAYBOUND parameter_array_bound[1];
SAFEARRAY *parameter_array = NULL;
parameter_array_bound[0].cElements = 2;
parameter_array_bound[0].lLbound = 0;
parameter_array = SafeArrayCreate(VT_VARIANT,1,
parameter_array_bound);
index = 0;
hr = SafeArrayPutElement(parameter_array,
&index,
&header_variant);
index = 1;
hr = SafeArrayPutElement(parameter_array,
&index,
&footer_variant);
VARIANT parameter;
VariantInit(¶meter);
V_VT(¶meter) = VT_ARRAY | VT_BYREF;
V_ARRAY(¶meter) = parameter_array;
// start printing browser contents
hr = _Browser->ExecWB(OLECMDID_PRINT,
OLECMDEXECOPT_DODEFAULT,
¶meter,
NULL);
// release SAFEARRAY
if (!SUCCEEDED(hr)) {
VariantClear(&header_variant);
VariantClear(&footer_variant);
if (parameter_array != NULL) {
SafeArrayDestroy(parameter_array);
}
}
使用 Print(...)
函数有一个需要注意的地方,这是由于 WebBrowser
控件处理打印的方式。ExecWB()
方法将文档的副本传递给一个单独的线程,然后该线程执行实际的打印操作。ExecWB()
方法立即返回,而不等待线程完成。因此,没有简单的方法来确定打印已完成。事实上,如果在打印进行中销毁浏览器,只有部分内容会被打印。WebBrowser
控件会发出打印模板拆解事件(见下文),该事件在打印完成后发出。
PrintPreview()
显示控件中加载的当前内容的打印预览。
处理事件
SimpleBrowser
实际上是一个基本的 CWnd
,它是实际 WebBrowser
控件的容器。使用 MFC,WebBrowser
控件通过“事件接收器映射”将事件信号发送到其容器。可以“接收”的事件由 DWebBrowserEvents2
接口描述。SimpleBrowser
通过将事件信息转换为 MFC 友好的格式(CString
等)并调用虚拟函数来将这些事件转发给外部世界。这些函数的基类实现使用 WM_NOTIFY
机制将事件发送到父窗口。
SimpleBrowser
支持这些事件:
事件(虚拟函数) | 通知类型 |
描述 |
OnBeforeNavigate2(CString URL, CString frame, void *post_data, int post_data_size, CString headers) |
BeforeNavigate2 |
导航开始前调用;URL 是目标,frame 是框架名称(如果没有则为 "" )。如果 post_data 为 NULL ,则表示没有 POST 数据。headers 值包含将发送到服务器的标头。返回 true 取消导航,返回 false 继续。 |
OnDocumentComplete(CString URL) |
DocumentComplete |
导航到文档已完成;URL 是位置。 |
OnDownloadBegin() |
DownloadBegin |
信号导航操作的开始。 |
OnProgressChange(int progress, int progress_max) |
ProgressChange |
导航进度更新。我看到 WebBrowser 发送了 ProgressChange 事件,其中 progress > progress_max ,请注意这一点。 |
OnDownloadComplete() |
DownloadComplete |
导航操作已完成。 |
OnNavigateComplete2(CString URL) |
NavigateComplete2 |
导航到超链接已完成。URL 是位置(如果使用了 Write() 或 NavigateResource() ,则 URL = "about:blank ")。 |
OnStatusTextChange(CString text) |
StatusTextChange |
状态文本已更改。 |
OnTitleChange(CString text) |
TitleChange |
标题文本已更改。 |
OnPrintTemplateInstantiation() |
PrintTemplateInstantiation |
打印已开始。 |
OnPrintTemplateTeardown() |
PrintTemplateTeardown |
打印已完成。 |
如果您从 SimpleBrowser
派生自己的类,您的事件处理程序可以通过调用(或不调用)基类函数来决定是否通知父窗口有关事件的信息。
父窗口中的通知函数需要消息映射中的一个条目,如下所示:
ON_NOTIFY(SimpleBrowser::NotificationType,control ID,OnNotificationType)
函数本身看起来像这样:
afx_msg void OnNotificationType(NMHDR *pNMHDR,LRESULT *pResult);
...
void MyDialog::OnNotificationType(NMHDR *pNMHDR,LRESULT *pResult)
{
SimpleBrowser::Notification
*notification = (SimpleBrowser::Notification *)pNMHDR;
...
*pResult = 0;
}
Notification
结构用于传递描述事件的信息。
元素 | 适用于 NotificationType : |
描述 |
NMHDR hdr |
全部 | 标准通知头。hdr.hwndFrom = SimpleBrowser 的窗口句柄,hdr.idFrom = SimpleBrowser 的控件 ID,hdr.code = 通知 NavigationType 。 |
CString URL |
|
导航/文档的 URL。 |
CString frame |
BeforeNavigate2 |
目标框架。 |
void *post_data |
BeforeNavigate2 |
如果导航包含 POST 数据,post_data 将指向包含数据的缓冲区。请注意,数据仅在调用期间有效。如果事件处理程序需要保存数据,它应进行复制。如果没有 POST 数据,post_data 将为 NULL 。 |
int post_data_size |
BeforeNavigate2 |
POST 数据的大小;如果没有则为 0。 |
CString headers |
BeforeNavigate2 |
要发送到服务器的标头。 |
int progress |
ProgressChange |
当前进度值。 |
int progress_max |
ProgressChange |
当前进度值的上限。 |
CString text |
|
将在状态区域或标题中显示文本。 |
请注意,尽管我使用了标准的 WM_NOTIFY
机制和 NMHDR
通知头,但 SimpleBrowser
不支持常见的通知,如 NM_CLICK
,因为这些事件由 WebBrowser
控件内部处理。
关于代码
SimpleBrowser
与 Internet Explorer 5.0 及更高版本提供的 WebBrowser
控件兼容。
演示程序说明了如何在对话框应用程序中使用 SimpleBrowser
。在顶部的编辑控件中输入文本,然后单击“Write”按钮以调用 Write(...)
并使用编辑控件的内容。资源(ANSI)和资源(UNICODE)按钮使用 NavigateResource(...)
显示程序资源中的 HTML 文档,分别编码为 ANSI 和 UNICODE。
演示程序使用从 SimpleBrowser
派生的类 (SimpleBrowser_Example
) 来展示如何处理事件。SimpleBrowser_Example
构建一个描述事件的字符串,并将该字符串传递给对话框,然后对话框将该字符串显示在对话框左下角的编辑控件中。对话框左下角的编辑控件显示的信息与通过对话框本身中的通知机制处理的信息相同。
我包含了 Visual C++ 6.0 工作区/项目、Visual Studio .NET、Visual Studio 2005 和 Visual Studio 2008 解决方案/项目文件。该项目通过 stdafx.h 中的 #define
编译为 MBCS(例如 ANSI)或 UNICODE。
鸣谢和参考文献
我想感谢以下来源提供的信息,我用这些信息开发了 SimpleBrowser
类:
- zli>Joan Murt 的文章 HTMLCtrl with dynamic HTML content 说明了如何将控件“导航”到内存中的 HTML 文档。(谢谢,Joan。)
- 这篇文章(很抱歉,不在 CodeProject 上)提供了第二个关于如何导航到内存中文档的示例。
- Paul DiLascia 在 2000 年 1 月的《Microsoft System Journal》杂志上的 文章提供了一种在对话框中重用
CHTMLView
类的方法。这并不是我使用的方法,但信息仍然很有价值。请注意,链接不再有效,Lascia 先生已于 2008 年去世。 - Microsoft 知识库文章 241750:BUG: CHtmlView Leaks Memory by Not Releasing BSTRs in Several Methods。更多关于使用
CHTMLView
的信息。 - Microsoft 知识库文章 267240:HOWTO: Print Custom Headers and Footers for a WebBrowser Control。
- Microsoft 知识库文章 256195:HOWTO: Handle Data From a Post Form When Hosting WebBrowser Control。
历史
- 2003 年 4 月 6 日
- 初始版本。
- 2003 年 4 月 11 日
- 用
Write(...)
和Clear()
替换了原始的NavigateString()
方法。 - 添加了
GetDocument()
方法。 - 添加了通知。
- 修改了
Create()
以等待文档可用。 - 扩展了
BeforeNavigate2
事件处理,以包含 POST 数据和标头。 - 根据需要修改了文章文本。
- 2011 年 12 月 31 日
- 更新了文章中的链接。
- 原始代码在处理在将自己的文本写入
WebBrowser
控件之前必须完全加载文档的要求时,采用了不佳的方法。在早期版本中,它使用了“私有”消息循环,而在最终版本中,它在Create()
函数中使用了一个带有Sleep(0)
调用的忙等待循环来等待文档可用。新代码删除了忙等待循环。相反,如果文档未准备好,代码会简单地保存要写入的文本。当文档准备好后,任何已延迟的文本都会被写入浏览器。 - 错误修复:修正了
SimpleBrowser::Notification
类中的内存泄漏。该类没有提供析构函数,这意味着通知中传递的任何 POST 数据都会泄漏。 - 添加了一个新的
ParsePostData()
函数,以帮助处理 POST 数据。 - 错误修复:
OnBeforeNavigate2()
处理程序未包含标头。感谢 Vic Mackey 捕获了错误并提供了修复。 - 错误修复:根据 cwswpl (Stephen) 的建议,添加了键盘翻译(tab、delete 等)。Stephen 还注意到了处理初始“文档就绪”问题的糟糕方法,并提出了一个与我使用的非常相似的解决方案。我没有使用他禁用
WebBrowser
上下文菜单的建议,因为该控件包含一个用于自定义上下文菜单或完全消除它的接口。 - 增强功能:将
IWebBrowser2 *_Browser
成员设为protected
而不是private
,这是根据 Davide Calabro 的建议。还根据 Davide 的另一项建议修改了CreateFromControl()
函数,以包含样式参数。 - 增强功能:根据 Toni Bauer 提供的代码,添加了对“打印模板实例化”和“打印模板拆解”事件的处理。拆解事件尤其有用,因为它在打印完成后触发。
- 2011 年 2 月 12 日
- 错误修复:修正了通知结构中 POST 数据的双重删除,该问题由 qmcock 发现。