IWebBrowser2 包装类






4.67/5 (6投票s)
一个包装 Microsoft Internet Explorer 的 IWebBrowser2 接口的类,使您能够以编程方式浏览、打印和保存网页内容。
引言
在这个互联网时代,许多应用程序都可以从浏览网站、打印 HTML 内容以及利用网页浏览器其他功能中获益。然而,在项目中原生构建这些功能需要付出很多努力。因此,没有必要重新发明轮子。微软通过 IWebBrowser2 接口,使我们能够利用 Internet Explorer 已有的功能。但是,正如任何处理过 COM 接口的程序员都知道,即使使用这个接口也是一项任务。这就是我开发 IEInstance 类的原因。它包装了 IWebBrowser2 类,让您可以通过简单的类函数调用实现 IE 接口的许多功能。所有这些都可以通过浏览器可见或不可见的方式进行。因此,例如,您可以添加浏览网页并从中收集数据、填写表单并提交它们,或执行其他与 Web 交互的功能。
代码工作原理
IEInstance 是主类,但它与其他几个类协同工作,全面包装 IWebBrowser2。它提供了浏览器的基本功能,如导航、后退、前进、刷新和停止。一旦拥有了 IWebBrowser2 对象,这些功能的实现就相对直接。该类添加的其他功能则不那么直接。
可以使用其 Initialize() 函数(以串行化的方式)在不同线程中访问和使用 IEInstance 对象。实现此功能有两个部分。首先,当调用 Open() 函数(使用对象的第一个步骤)时,该类会创建一个全局接口表并将 IWebBrowser2 接口添加到其中。
//Make this a global instance
if(CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable, (void **) &pGlobalTable)!=S_OK) throw 1010;
if(CLSIDFromProgID(OLESTR("InternetExplorer.Application"), &clsid)!=S_OK) throw 1020;
if(CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IUnknown, (LPVOID *) &pUnknown)!=S_OK) throw 1030;
if(pUnknown->QueryInterface(IID_IWebBrowser2, (LPVOID *) &pBrowser)!=S_OK) throw 1040;
pUnknown->Release();
if(!pBrowser) throw 1050;
//Add to global table
if(pGlobalTable->RegisterInterfaceInGlobal(pBrowser, IID_IWebBrowser2, &cookie)!=S_OK) throw 1060;
然后,当调用 Initialize() 函数时,它会将调用线程与当前“拥有”对象的线程进行比较。如果不同,该函数将从全局表中获取接口,并用适用于调用线程的指针替换 pBrowser 指针。原始指针存储在 pBrowserOrg 成员变量中,以便在调用 Uninitialize() 函数时可以恢复。
bool IEInstance::Initialize()
{
HRESULT hr;
CWinThread *pThread=NULL;
message.Empty();
pThread = AfxGetThread();
hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if(hr!=S_OK && hr!=S_FALSE) return false;
if(pCurrentThread && pThread==pCurrentThread) return true;
if(pHostThread!=pThread){
//Make sure this browser is not locked by another process then lock it
while(locked) Sleep(500);
locked = true;
//Preserve original browser pointer
pBrowserOrg = pBrowser;
//Obtain browser pointer from cookie
if(pGlobalTable->GetInterfaceFromGlobal(cookie, IID_IWebBrowser2,(void**) &pBrowser)!=S_OK) return false;
}
pCurrentThread = pThread;
return true;
}
IEInstance 旨在实现一些通常需要用户干预才能完成的功能。例如,保存当前文档。Save() 函数的作用与从标准 IE 窗口中选择保存文件时相同,会弹出一个“另存为”对话框。但是,SaveAs() 函数允许您以编程方式指定保存文件的位置。在 IE9 及更高版本中,这非常直接,因为您可以简单地查询 IWebBrowser2 以获取其 IHTMLDocument2 接口,然后查询 IHTMLDocument2 以获取其 IPersistStreamInit 接口,将其保存到 IStream,然后提取其数据并保存到文件。
// Get the right interface hr = pHTMLDocument2->QueryInterface(IID_IPersistStreamInit, (void**) &spPersist); if(spPersist==NULL) throw 1001; // Create the stream object hr = ::CreateStreamOnHGlobal(NULL, TRUE, &spStream); if(hr!=S_OK) throw 1002; // Save the HTML hr = spPersist->Save(spStream, FALSE); if(hr!=S_OK) throw 1003; // Get the size of the stream hr = spStream->Stat(&stgstats, STATFLAG_NONAME); if(hr!=S_OK) throw 1004; // Get the global memory hr = ::GetHGlobalFromStream(spStream, &hGlobal); if(hr!=S_OK) throw 1005; lpRawData = ::GlobalLock(hGlobal); if(lpRawData==NULL) throw 1006; dwSize = stgstats.cbSize.LowPart + 1; pFile = fopen(filename, "w"); if(pFile==NULL) throw 1007; fwrite(lpRawData, dwSize, sizeof(char), pFile); fclose(pFile); ::GlobalUnlock(hGlobal);
但是,旧版本的 IE 没有这个接口。当浏览器中的文档是 HTML 时,它们有一个 IPersistFile。但是,当它是 XML 时,这个接口也不存在。在这种情况下,您需要通过一系列接口查询才能最终获得浏览器的 IXMLDOMDocument 对象,然后就可以保存它了。由于这在当前已经有些过时,我将留给感兴趣的人在源代码中查找。
类似地,打印通常也需要用户干预才能完成。IEInstance 以允许您指定是打印到默认打印机、弹出打印对话框还是弹出打印预览窗口的方式实现打印。实际上,有几种方法可以以编程方式发出打印命令。IEInstance 通过查询 IWebBrowser2 以获取其 IHTMLDocument2 接口,然后查询 IHTMLDocument2 以获取其 IOleCommandTarget 对象,然后执行该接口的 Exec() 函数来实现这一点。使用此方法而不是简单地调用 IWebBrowser2::ExecWB() 是因为 Print() 函数还允许您发送自定义打印模板的路径,并且在我最初开发它时,此功能与 ExecWB() 的工作效果不佳。现在情况可能已经不同了。
打印最棘手的部分是,当 Exec() 函数执行时,它会向浏览器发出命令,但不会等待命令完成。这意味着您必须做一些事情来将打印命令与应用程序的其他操作串行化。最坏的情况下,您只想打印一些东西然后删除 IEInstance 对象,这意味着在对象被释放之前,函数可能永远不会完成。IEInstance 提供了一种方法来做到这一点。它通过使用其伴随类 IEEventSink 来实现。这个类包装了 Internet Explorer 的 IWebBrowserEvents2 接口,用于捕获事件。除了其他事件,它还会捕获 OnPrintTemplateTeardown 事件,当打印完成或打印预览窗口关闭时,IE 会触发该事件。您可以直接调用 CloseAfterPrint() 函数而不是删除 IEInstance 对象,它将在打印完成后自行删除。
处理文档
IEDocument 类用于存储活动页面的 MSHTML 对象指针。在告诉您的 IEInstance 对象导航到页面后,您需要调用 GetDocument() 函数,该函数会等待导航完成,然后填充 IEDocument 类中的指针,以便它们可以被查询和使用。这是通过查询 IWebBrowser2 以获取其 IHTMLDocument2 接口,然后调用 IHTMLDocument2::get_all() 函数,然后遍历其下的每个元素来完成的。每个元素都会与 IEDocument 类跟踪的所有 MSHTML 对象进行比较,以查看是否找到匹配项。如果找到,则存储一个指针,以便可以轻松访问该对象。在此过程中,表格被组织成 IETable 对象,这些对象存储每个单元格的元素,并按行组织,以便您可以轻松访问表格的特定行和列。
//Organize Tags into Types
varIndex.vt = VT_UINT;
for(i=0; i<pDocument->nElements; i++ ){
//Release previous dispatch (if applicable)
if(pDisp) pDisp->Release();
pDisp=NULL;
//Get dispatch to item i
varIndex.lVal = i;
VariantInit(&var);
if(pDocument->pAll->item(varIndex, var, &pDisp)!=S_OK) throw 1110;
//Get item i
if(pDisp->QueryInterface(IID_IHTMLElement, (void **)&pDocument->pElement[i])!=S_OK) throw 1120;
//Get tag name
pDocument->pElement[i]->get_tagName(&bstr);
tag = bstr;
SysFreeString(bstr);
bstr=NULL;
//HTML Tag
if(!pDocument->pHTML){
if(tag.CompareNoCase("HTML")==0) pDocument->pHTML = pDocument->pElement[i];
continue; //Skip all tags before the HTML tag is found
}
//Body
if(!pDocument->pBody){
if(pDisp->QueryInterface(IID_IHTMLBodyElement, (void **)&pDocument->pBody)==S_OK){
continue;
}
}
//Table
if(tag.CompareNoCase("TABLE")==0){
nTable = pDocument->nTables;
pDocument->nTables++;
pTable[nTable].pElement = pDocument->pElement[i];
nRows[nTable] = 0;
nCells[nTable] = 0;
pDocument->pElement[i]->get_innerHTML(&bstr);
tableHTML = bstr;
SysFreeString(bstr);
bstr=NULL;
nLevels=0;
find=0;
while(find>=0){
find = tableHTML.Find('<', find);
if(find<0) break;
if(find>=tableHTML.GetLength()-1) break;
find++;
if(tableHTML.Mid(find, 5).CompareNoCase("TABLE")==0){
nLevels++;
continue;
}
if(tableHTML.Mid(find, 6).CompareNoCase("/TABLE")==0){
nLevels--;
continue;
}
if(nLevels>0) continue;
if(tableHTML.GetAt(find+1)=='/') continue;
if(tableHTML.Mid(find, 2).CompareNoCase("TR")==0){
pTable[nTable].nRows++;
continue;
}
if(tableHTML.Mid(find, 2).CompareNoCase("TD")==0 || tableHTML.Mid(find, 2).CompareNoCase("TH")==0){
pTable[nTable].nCells++;
continue;
}
}
//Prepare row and cell arrays
if(pTable[nTable].nRows>0){
pTable[nTable].pRow = new IHTMLElement*[pTable[nTable].nRows];
if(!pTable[nTable].pRow) throw 1130;
}
if(pTable[nTable].nCells>0){
pTable[nTable].pCell = new IHTMLElement*[pTable[nTable].nCells];
pTable[nTable].cellRow = new int[pTable[nTable].nCells];
if(!pTable[nTable].pCell || !pTable[nTable].cellRow) throw 1140;
}
continue;
}
//Row
if(tag.CompareNoCase("TR")==0){
//Should only need this when no cells exist in the last row of a table
while(nRows[nTable]>=pTable[nTable].nRows) nTable--;
if(nTable<0) throw 1008;
pTable[nTable].pRow[nRows[nTable]] = pDocument->pElement[i];
nRows[nTable]++;
continue;
}
//Cell
if(tag.CompareNoCase("TD")==0 || tag.CompareNoCase("TH")==0){
//Last cell has been assigned go back to closest non-full table
while(nCells[nTable]>=pTable[nTable].nCells) nTable--;
if(nTable<0) throw 1150;
pTable[nTable].pCell[nCells[nTable]] = pDocument->pElement[i];
pTable[nTable].cellRow[nCells[nTable]] = nRows[nTable];
nCells[nTable]++;
continue;
}
//Hyperlinks
if(pDisp->QueryInterface(IID_IHTMLAnchorElement, (void **)&pAnchorElem)==S_OK){
pHyperlink[pDocument->nHyperlinks] = pDocument->pElement[i];
pDocument->nHyperlinks+=1;
continue;
}
//Forms
if(pDisp->QueryInterface(IID_IHTMLFormElement, (void **)&pForm[pDocument->nForms])==S_OK){
pDocument->nForms+=1;
continue;
}
//Buttons
if(pDisp->QueryInterface(IID_IHTMLButtonElement, (void **)&pButtonElem)==S_OK){
pButton[pDocument->nButtons] = pDocument->pElement[i];
pDocument->nButtons+=1;
continue;
}
if(pDisp->QueryInterface(IID_IHTMLInputButtonElement, (void **)&pInputButtonElem)==S_OK){
pButton[pDocument->nButtons] = pDocument->pElement[i];
pDocument->nButtons+=1;
continue;
}
//Select Drop Down Menus
if(pDisp->QueryInterface(IID_IHTMLSelectElement, (void **)&pDropDownMenu[pDocument->nDropDownMenus])==S_OK){
pDocument->nDropDownMenus+=1;
continue;
}
//Image Inputs
if(pDisp->QueryInterface(IID_IHTMLInputImage, (void **)&pInputImageElem)==S_OK){
pInputImage[pDocument->nInputImages] = pDocument->pElement[i];
pDocument->nInputImages+=1;
continue;
}
//Option Buttons
if(pDisp->QueryInterface(IID_IHTMLOptionButtonElement, (void **)&pOptionButton[pDocument->nOptionButtons])==S_OK){
pDocument->nOptionButtons+=1;
continue;
}
//Text Areas
if(pDisp->QueryInterface(IID_IHTMLTextAreaElement, (void **)&pTextArea[pDocument->nTextAreas])==S_OK){
pDocument->nTextAreas+=1;
continue;
}
//Text Boxes
if(pDisp->QueryInterface(IID_IHTMLInputTextElement, (void **)&pTextBox[pDocument->nTextBoxes])==S_OK){
pDocument->nTextBoxes+=1;
continue;
}
//File Boxes
if(pDisp->QueryInterface(IID_IHTMLInputFileElement, (void **)&pFileBox[pDocument->nFileBoxes])==S_OK){
pDocument->nFileBoxes+=1;
continue;
}
}
现在有了这些易于访问的对象指针,IEDocument 类包含许多可用于单击超链接或按钮、填写文本框或区域的函数。例如,使用 ButtonByName() 函数按名称查找特定按钮。它将返回您想要的按钮的索引,然后调用 ButtonClick() 函数并将此索引传递进去以单击该按钮。
int IEDocument::ButtonByName(const char *name)
{
int i;
for(i=0; i<nButtons; i++){
if(ButtonName(i)==name) return i;
}
return -1; //Not found
}
正如您所能想象的,您可以使用这些函数导航到 Web 表单,填写表单,然后提交它。
使用代码
这三个类以及其他支持类被编译成一个静态库,您可以通过简单地添加以下内容将其链接到您的项目中:
#include "IEInstance.h"
到您的源代码或头文件中。
然后,您可以创建一个 IEInstance 对象并调用 Open() 函数来启动 Internet Explorer。您可以选择将其唯一的参数 visible 设置为 true 或 false,以使窗口可见或隐藏。
然后,您可以继续处理浏览器,调用 Navigate() 函数导航到 URL,然后以编程方式单击链接、收集文本或其他所需功能。
调用导致浏览器导航的函数后,您需要调用 GetDocument() 函数。无论您是否希望 IEInstance 将文档对象收集到其 IEDocument 对象中,都需要这样做,因为此函数也是在继续之前等待导航完成的方法。还建议使用 IsErrorPage() 函数检查导航错误。
完成浏览器操作后,您可以调用 Release() 函数将浏览器窗口与对象断开连接,然后再删除它,或者调用 Close() 函数关闭浏览器窗口。
历史
版本 1.0:初始发布