65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WebBrowser 控件,简化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (91投票s)

2003 年 4 月 6 日

CPOL

13分钟阅读

viewsIcon

1645975

downloadIcon

28036

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_dataNULL,则表示没有 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

BeforeNavigate2,

DocumentComplete,

NavigateComplete2

导航/文档的 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

StatusTextChange,

TitleChange

将在状态区域或标题中显示文本。

请注意,尽管我使用了标准的 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 类:

历史

  • 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 发现。
© . All rights reserved.