21. IE WebBrowser控件的基本使用

综合使用COM的自动化接口、连接点最好的例子就是IE WebBrowser控件,这也是最常用的一个COM控件,借助它我们可以用网页编写界面、调用本地代码,也就是当前流行的混合式程序。

1.ActiveX简介

在介绍IE WebBrowser使用之前,需要先了解下ActiveX的概念,关于ActiveX本文不打算详述,只介绍基本概念,建议一般掌握使用即可,真正需要编写的时候,在了解COM的基础上借助ATL也可以很快实现。

ActiveX是在COM基础上封装而来,分为ActiveX对象(Client)和ActiveX包容器(Server、Host)。ActiveX对象更像是一个带界面的COM,也可以说是一个COM控件,它实现了显示接口/属性方法设置接口/事件接口等。ActiveX包容器,是一个支持 COM控件的窗口,支持控件布局/事件处理等。ActiveX包容器通过控制站点(control site)将当前包容器的特性如包容器的缺省颜色、字体等设置暴露给ActiveX对象。

因此使用IE WebBrowser我们需要一个包容窗口(ActiveX 包容器)和创建一个IE WebBrowser对象(ActiveX 对象)


2.IE WebBrowser 静态创建

首先,创建一个基于对话框的WTL工程,注意勾选Enable ActiveX Control Hosting,如下


可以看到生成的代码,相比普通的对话框工程有两处不同:

1._tWinMain中调用AtlAxWinInit,这是为了注册"AtlAxWin"窗口,具体作用稍后剖析

2.对话框从CAxDialogImpl继承,这个窗口类也在稍后剖析


然后,切到对话框资源中,右键插入IE控件,窗体布局如下:



然后,在CMainDlg::OnInitDialog中如下调用:

	//查询得到IWebBrowser2接口
	CAxWindow wndIE = GetDlgItem(IDC_IE);
	if (wndIE.IsWindow())
	{
		wndIE.QueryControl(&m_spWebBrowser);
	}

	NavigateTo(L"http://www.baidu.com");
	return TRUE;
Navigate代码如下:

void CMainDlg::NavigateTo(LPCWSTR pszUrl)
{
	if (m_spWebBrowser)
	{
		CComVariant v;
		m_spWebBrowser->Navigate(CComBSTR(pszUrl), &v, &v, &v, &v);
	}
}
这里在OnInitDialog中 找到包容窗口wndIE,然后查询得到IE控件m_spWebBrowser,然后剩下的工作我们就可以直接对IE控件操作了,比如导航到指定网址调用Navigate方法即可。可以看到这里调用的Navigate方法是典型的 IDispatch自动化接口的方法。
至此,简单的IE控件使用就完成了,是不是很Easy。


同理,我们实现浏览器的跳转、向前、向后、停止和刷新功能如下:

void CMainDlg::OnGo(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	WCHAR szUrl[MAX_PATH] = {0};
	if (GetDlgItemText(IDE_URL, szUrl, MAX_PATH))
	{
		NavigateTo(szUrl);
	}
}

void CMainDlg::OnBack(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	if (m_spWebBrowser)
	{
		m_spWebBrowser->GoBack();
	}
}

void CMainDlg::OnForward(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	if (m_spWebBrowser)
	{
		m_spWebBrowser->GoForward();
	}
}

void CMainDlg::OnStop(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	if (m_spWebBrowser)
	{
		m_spWebBrowser->Stop();
	}
}

void CMainDlg::OnRefresh(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	CComVariant vLevel;

	if (m_spWebBrowser)
	{
		vLevel = REFRESH_COMPLETELY;
		m_spWebBrowser->Refresh2(&vLevel);
	}
}

3.事件响应

浏览器有很多事件,比如开始跳转,跳转完成,加载完成,怎么监控这些事件呢?答案是使用连接点。因为系统自带了IE COM组件的类型库,所以我们可以直接使用IDispEventImpl接口。

如下

class CMainDlg : public CAxDialogImpl<CMainDlg>,
				 public IDispEventImpl<IDC_IE,CMainDlg>
这样IDC_IE的接口事件会自动的调到CMainDlg中,我们只需要如下填写一个事件映射表即可。

	BEGIN_SINK_MAP(CMainDlg)
		SINK_ENTRY(IDC_IE, DISPID_DOWNLOADBEGIN, OnDownloadBegin)
		SINK_ENTRY(IDC_IE, DISPID_DOWNLOADCOMPLETE, OnDownloadComplete)
		SINK_ENTRY(IDC_IE, DISPID_BEFORENAVIGATE2, OnBeforeNavigate)
		SINK_ENTRY(IDC_IE, DISPID_NAVIGATECOMPLETE2, OnNavigateComplete)
		SINK_ENTRY(IDC_IE, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete)
	END_SINK_MAP()

对应的 连接点响应函数如下:

void __stdcall CMainDlg::OnDownloadBegin()
{
	OutputDebugString(L"OnDownloadBegin");
}

void __stdcall CMainDlg::OnDownloadComplete()
{
	OutputDebugString(L"OnDownloadComplete");
}

void __stdcall CMainDlg::OnBeforeNavigate(LPDISPATCH pDisp, VARIANT* URL, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, BOOL* Cancel)
{
	if (URL)
	{
		CString strUrl;
		strUrl.Format(L"OnBeforeNavigate %s", URL->bstrVal);
		OutputDebugString(strUrl);

		//可指定Cancel来阻止访问
		if (strUrl.CompareNoCase(L"OnBeforeNavigate http://www.sina.com/")==0)
		{
			*Cancel = TRUE;
			OutputDebugString(L"http://www.sina.com/ is Canceled!");

		}
	}
}

void __stdcall CMainDlg::OnNavigateComplete(LPDISPATCH pDisp, VARIANT* URL)
{
	if (URL)
	{
		CString strUrl;
		strUrl.Format(L"OnNavigateComplete %s", URL->bstrVal);
		OutputDebugString(strUrl);
	}
}

void __stdcall CMainDlg::OnDocumentComplete(LPDISPATCH pDisp, VARIANT* URL)
{
	if (URL)
	{
		CString strUrl;
		strUrl.Format(L"OnDocumentCompleteIe %s", URL->bstrVal);
		OutputDebugString(strUrl);
	}
}
debugview抓日志就可以看到不同加载事件的信息。


4.代码剖析

上文提到CAxDialogImpl,相对于CDialogImpl,它重写了Create和DoModal函数,也就是截取了创建对话框的过程,做了一些自己的初始化工作,查看源代码调用次序如下:

DoModal/DoModal -> AtlAxCreateDialog -> AtlAxDialogCreateT -> _DialogSplitHelper::SplitDialogTemplate,

_DialogSplitHelper::SplitDialogTemplate中如下处理:

		// Make first pass through the dialog template.  On this pass, we're
		// interested in determining:
		//    1. Does this template contain any ActiveX Controls?
		//    2. If so, how large a buffer is needed for a template containing
		//       only the non-OLE controls?

		DLGITEMTEMPLATE* pItem = pFirstItem;
		DLGITEMTEMPLATE* pNextItem = pItem;
		for (iItem = 0; iItem < nItems; iItem++)
		{
			pNextItem = FindNextDlgItem(pItem, bDialogEx);

			pszClassName = bDialogEx ?
				(LPWSTR)(((DLGITEMTEMPLATEEX*)pItem) + 1) :
				(LPWSTR)(pItem + 1);

			// Check if the class name begins with a '{'
			// If it does, that means it is an ActiveX Control in MSDEV (MFC) format
			if (pszClassName[0] == L'{')
			{
				// Item is an ActiveX control.
				bHasOleControls = TRUE;
			}
			else
			{
				// Item is not an ActiveX Control: make room for it in new template.
				cbNewTemplate += (BYTE*)pNextItem - (BYTE*)pItem;
			}

			pItem = pNextItem;
		}
对应的rc文件数据为:

IDD_MAINDLG DIALOGEX 0, 0, 618, 438
...
CAPTION "TestWebBrowser"
...
BEGIN
    CONTROL         "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}",WS_TABSTOP,7,41,604,390
    ...
END
结合上段代码,这里就是 查找窗体类名为{开头的就认为是ActiveX控件,然后如下生成新的模板,

		// Second pass through the dialog template.  On this pass, we want to:
		//    1. Copy all the non-OLE controls into the new template.
		for (iItem = 0; iItem < nItems; iItem++)
		{
			pNextItem = FindNextDlgItem(pItem, bDialogEx);

			pszClassName = bDialogEx ?
				(LPWSTR)(((DLGITEMTEMPLATEEX*)pItem) + 1) :
				(LPWSTR)(pItem + 1);

			if (pszClassName[0] != L'{')
			{
				// Item is not an OLE control: copy it to the new template.
				ULONG_PTR cbItem = (BYTE*)pNextItem - (BYTE*)pItem;
				ATLASSERT(cbItem >= (bDialogEx ?
					sizeof(DLGITEMTEMPLATEEX) :
					sizeof(DLGITEMTEMPLATE)));
				Checked::memcpy_s(pNew, cbNewTemplate, pItem, cbItem);
				pNew += cbItem;
				cbNewTemplateLast = cbNewTemplate;
				cbNewTemplate -= cbItem;
				ATLENSURE(cbNewTemplate <= cbNewTemplateLast);

				// Incrememt item count in new header.
				++DlgTemplateItemCount(pNewTemplate);
			}

			pItem = pNextItem;
		}

可知生成的新模板,都是原生的Win32控件,这里直接创建的窗体是不包含ActiveX控件的,那么 真正的ActiveX控件是在哪里创建的呢

接着看如下实现

template <class T, class TBase>
INT_PTR CALLBACK CAxDialogImpl< T, TBase >::DialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	CAxDialogImpl< T, TBase >* pThis = (CAxDialogImpl< T, TBase >*)hWnd;
	if (uMsg == WM_INITDIALOG)
	{
		HRESULT hr;
		if (FAILED(hr = pThis->CreateActiveXControls(pThis->GetIDD())))
		{
			pThis->DestroyWindow();
			SetLastError(hr & 0x0000FFFF);
			return FALSE;
		}
	}
	return CDialogImplBaseT< TBase >::DialogProc(hWnd, uMsg, wParam, lParam);
}

可知, ActiveX是在窗体过程WM_INITDIALOG时动态创建的,CreateActiveXControls部分代码如下:

CAxWindow2 wnd;
// Get control caption.
LPWSTR pszClassName = 
    bDialogEx ? 
        (LPWSTR)(((_DialogSplitHelper::DLGITEMTEMPLATEEX*)pItem) + 1) :
        (LPWSTR)(pItem + 1);
...

// Create AxWindow with a NULL caption.
wnd.Create(m_hWnd, 
    &rect, 
    NULL, 
    (bDialogEx ? 
        ((_DialogSplitHelper::DLGITEMTEMPLATEEX*)pItem)->style : 
        pItem->style) | WS_TABSTOP, 
    bDialogEx ? 
        ((_DialogSplitHelper::DLGITEMTEMPLATEEX*)pItem)->exStyle : 
        0,
    bDialogEx ? 
        ((_DialogSplitHelper::DLGITEMTEMPLATEEX*)pItem)->id : 
        pItem->id,
    NULL);

if (wnd != NULL)
{
    // Set the Help ID
    if (bDialogEx && ((_DialogSplitHelper::DLGITEMTEMPLATEEX*)pItem)->helpID != 0)
        wnd.SetWindowContextHelpId(((_DialogSplitHelper::DLGITEMTEMPLATEEX*)pItem)->helpID);
    // Try to create the ActiveX control.
    hr = wnd.CreateControlLic(pszClassName, spStream, NULL, bstrLicKey);
注释写的很清楚了,这段代码创建了一个 包容器窗口AxWindow2(类似AxWindow,不再深究),该窗体类名是前文注册的“AtlAxWin”,然后用 设计器中的类名“{8856F961-340A-11D0-A96B-00C04FD705A2}”当做CLSID创建IE控件


再看看连接点事件,可看CAxDialogImpl中如下实现

	LRESULT OnInitDialog(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
	{
		// initialize controls in dialog with DLGINIT resource section
		ExecuteDlgInit(static_cast<T*>(this)->IDD);
		AdviseSinkMap(true);
		bHandled = FALSE;
		return 1;
	}

	LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
	{
		AdviseSinkMap(false);
		bHandled = FALSE;
		return 1;
	}

在InitDialog和Destroy过程中动态的连接和释放连接。


5.IE WebBrowser动态创建

看过上面的代码分析,动态的创建ActiveX控件的过程其实就简单了:自己在InitDialog中创建AxWindow和IE控件;如果想响应事件的话,分别在InitDialog和Destroy过程中动态的连接和释放连接即可,如下:

BOOL CAboutDlg::OnInitDialog(CWindow wndFocus, LPARAM lInitParam)
{
	CRect rcClient;
	GetClientRect(&rcClient);
	rcClient.bottom -= 50;

	//创建包容窗口
	m_wndIE.Create(m_hWnd, rcClient, L"", 
				   WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN|WS_CLIPSIBLINGS);

	//创建ActiveX控件
	CComPtr<IUnknown> punkCtrl;
	m_wndIE.CreateControlEx(L"Shell.Explorer", NULL, NULL, &punkCtrl);

	//查询接口导航到指定url
	if (punkCtrl)
	{
		CComQIPtr<IWebBrowser2> pWB2 = punkCtrl;
		CComVariant v;

		if (pWB2)
		{
			this->DispEventAdvise(punkCtrl, &__uuidof(DWebBrowserEvents2));
			pWB2->Navigate(CComBSTR(L"http://jimwen.net"), &v, &v, &v, &v);
		}
	}

	return 0;
}

void CAboutDlg::OnDestroy()
{
	CComPtr<IUnknown> punkCtrl;
	m_wndIE.QueryControl(&punkCtrl);
	this->DispEventUnadvise(punkCtrl, &__uuidof(DWebBrowserEvents2));
}

这里连接点使用IDispEventSimpleImpl实现,具体参见演示代码。

本文只是讲解了IE控件的基本使用,关于IE控件和本地代码的交互及其他高级功能,下回再分解。


完整演示代码下载链接

有个国外大牛封装的不用类型库完成常用事件的类可供参考,AtlBrowser.h 是封装类,BrowserDemo是演示代码,他的结构值得参考

参考文章

原创,转载请注明来自http://blog.csdn.net/wenzhou1219

猜你喜欢

转载自blog.csdn.net/wenzhou1219/article/details/78308811
21.