综合使用COM的自动化接口、连接点最好的例子就是IE WebBrowser控件,这也是最常用的一个COM控件,借助它我们可以用网页编写界面、调用本地代码,也就是当前流行的混合式程序。
在介绍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 对象)。
首先,创建一个基于对话框的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自动化接口的方法。
同理,我们实现浏览器的跳转、向前、向后、停止和刷新功能如下:
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);
}
}
浏览器有很多事件,比如开始跳转,跳转完成,加载完成,怎么监控这些事件呢?答案是使用连接点。因为系统自带了IE COM组件的类型库,所以我们可以直接使用IDispEventImpl接口。
如下
class CMainDlg : public CAxDialogImpl,
public IDispEventImpl
这样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抓日志就可以看到不同加载事件的信息。
上文提到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;
}
接着看如下实现
template
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);
}
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(this)->IDD);
AdviseSinkMap(true);
bHandled = FALSE;
return 1;
}
LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled)
{
AdviseSinkMap(false);
bHandled = FALSE;
return 1;
}
看过上面的代码分析,动态的创建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 punkCtrl;
m_wndIE.CreateControlEx(L"Shell.Explorer", NULL, NULL, &punkCtrl);
//查询接口导航到指定url
if (punkCtrl)
{
CComQIPtr 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 punkCtrl;
m_wndIE.QueryControl(&punkCtrl);
this->DispEventUnadvise(punkCtrl, &__uuidof(DWebBrowserEvents2));
}
本文只是讲解了IE控件的基本使用,关于IE控件和本地代码的交互及其他高级功能,下回再分解。
完整演示代码下载链接
有个国外大牛封装的不用类型库完成常用事件的类可供参考,AtlBrowser.h 是封装类,BrowserDemo是演示代码,他的结构值得参考
参考文章
原创,转载请注明来自http://blog.csdn.net/wenzhou1219