介绍
每台计算机都有这个控件,并且它有很多方法和事件,是个用来做演示的好例子。
从使用向导开始创建工程
向导生成的代码
在这一节我将介绍一些以前没有见过的新代码(由向导生成的),下一节介绍ActiveX包容类的细节。
首先要看的文件是stdafx.h,它包含了这些文件:
#include <atlbase.h> #include <atlapp.h> extern CAppModule _Module; #include <atlcom.h> |
接下来看看maindlg.h中声明的CMainDlg类:
class CMainDlg : public CAxDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>, public CMessageFilter, public CIdleHandler |
最后,看看WinMain()中新加的一行代码:
int WINAPI _tWinMain(...) { //... _Module.Init(NULL, hInstance); AtlAxWinInit(); int nRet = Run(lpstrCmdLine, nCmdShow); _Module.Term(); |
和MFC的程序一样,ATL也可以使用资源编辑器向对话框添加控件。首先,在对话框编辑器上点击鼠标右键,在弹出的菜单中选择“Insert ActiveX control”:
CAxDialogImpl
CONTROL "",IDC_IE,"{8856F961-340A-11D0-A96B-00C04FD705A2}", WS_TABSTOP,7,7,116,85 |
CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",IDC_IE,"AtlAxWin",
WS_TABSTOP,7,7,116,85
结果就是创建了一个相同ID的AtlAxWin窗口,窗口的标题是ActiveX控件的GUID。所以你调用GetDlgItem(IDC_IE)返回的值是AtlAxWin窗口的句柄而不是ActiveX控件本身。
SplitDialogTemplate()函数完成工作后,AtlAxCreateDialog()接着调用CreateDialogIndirectParam()函数使用修改后的模板创建对话框。
AtlAxWin and CAxWindow |
WM_CREATE的消息处理函数调用全局函数AtlAxCreateControl(),将AtlAxWin窗口的窗口标题作为参数传递给该函数,大家应该记得那实际就是浏览器控件的GUID。AtlAxCreateControl()有会调用一堆其他函数,不过最终会用到CreateNormalizedObject()函数,这个函数将窗口标题转换成GUID,并最终调用CoCreateInstance()创建ActiveX控件。
调用控件的方法
既然我们的对话框有一个浏览器控件,我们可以使用COM接口与之交互。我们做得第一件事情就是使用IWebBrowser2接口将其引导到一个新URL处。在OnInitDialog()函数中,我们将一个CAxWindow变量与包容控件的AtlAxWin联系起来。
CAxWindow wndIE = GetDlgItem(IDC_IE); |
CComPtr<IWebBrowser2> pWB2; HRESULT hr; hr = wndIE.QueryControl ( &pWB2 ); if ( pWB2 ) pWB2->Navigate ( CComBSTR("http://www.codeproject.com/"), &v, &v, &v, &v ); |
添加IDispEventSimpleImpl到CMainDlg的继承列表
填写事件映射链,它指示哪些事件需要处理
编写事件响应函数
CMainDlg的修改
将CMainDlg转变成COM对象的原因是事件相应是基于IDispatch的,为了让CMainDlg暴露这个接口,它必须是个COM对象。IDispEventSimpleImpl提供了IDispatch接口的实现和建立连接点所需的处理函数,当事件发生时IDispEventSimpleImpl还调用我们想要接收的事件的处理函数。
以下的类需要添加到CMainDlg的集成列表中,同时COM_MAP列出了CMainDlg暴露的接口:
#include <exdisp.h> // browser control definitions #include <exdispid.h> // browser event dispatch IDs class CMainDlg : public CAxDialogImpl<CMainDlg>, |
填写事件映射链
void DownloadBegin(); |
VT_EMPTY: void
VT_BSTR: BSTR 格式的字符串
VT_I4: 4字节有符号整数,用于long类型的参数
VT_DISPATCH: IDispatch*
VT_VARIANT>: VARIANT
VT_BOOL: VARIANT_BOOL (允许的取值是VARIANT_TRUE和VARIANT_FALSE)
#define _ATL_MAX_VARTYPES 8
struct _ATL_FUNC_INFO |
cc
我们的事件响应函数的调用方式约定,这个参数必须是CC_STDCALL,表示是__stdcall方式
vtReturn
事件响应函数的返回值类型
nParams
事件带的参数个数
pVarTypes
相应的参数类型,按从左到右的顺序
了解这些之后,我们就可以填写DownloadBegin事件处理的_ATL_FUNC_INFO结构:
_ATL_FUNC_INFO DownloadInfo = { CC_STDCALL, VT_EMPTY, 0 }; |
class CMainDlg : public ... { ... BEGIN_SINK_MAP(CMainDlg) SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,OnDownloadBegin, &DownloadInfo) END_SINK_MAP() }; |
好了,等了这么长时间(吹个口哨!),我们可以写事件处理函数了:
void __stdcall CMainDlg::OnDownloadBegin() { // show "Please wait" here... } |
void BeforeNavigate2 ( IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel ); |
描述BeforeNavigate2事件的_ATL_FUNC_INFO结构是这样的:
_ATL_FUNC_INFO BeforeNavigate2Info = { CC_STDCALL, VT_EMPTY, 7, { VT_DISPATCH, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_VARIANT|VT_BYREF, VT_BOOL|VT_BYREF } }; |
事件响应链的入口与前面的例子很相似:
BEGIN_SINK_MAP(CMainDlg) SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_DOWNLOADBEGIN,OnDownloadBegin, &DownloadInfo) SINK_ENTRY_INFO(37, DIID_DWebBrowserEvents2, DISPID_BEFORENAVIGATE2,OnBeforeNavigate2, &BeforeNavigate2Info) END_SINK_MAP() |
void __stdcall CMainDlg::OnBeforeNavigate2 ( IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel ) { CString sURL = URL->bstrVal; // ... log the URL, or whatever you''d like ... |
将CMainDlg转换成对象需要注意几件事情,首先必须修改全局函数Run(),现在CMainDlg是个COM对象,我们必须使用CComObject创建CMainDlg:
int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT) { CMessageLoop theLoop; _Module.AddMessageLoop(&theLoop); CComObject<CMainDlg> dlgMain; dlgMain.AddRef(); if ( dlgMain.Create(NULL) == NULL ) dlgMain.ShowWindow(nCmdShow); int nRet = theLoop.Run(); _Module.RemoveMessageLoop(); |
当然这并不是完美的解决方案,CComObjectStack用于短命的临时对象,不幸的是只要调用它的任何一个IUnknown方法都会引发断言错误。因为CMainDlg对象在开始监听事件时会调用AddRef,所以CComObjectStack不适用于这种情况。
BeforeNavigate2和NavigateComplete2:这些事件让程序可以控制URL的导航,如果你响应了BeforeNavigate2事件,你可以在事件的处理函数中取消导航。
CommandStateChange:这个事件告诉程序向前和向后导航命令何时可用,应用程序将相应的按钮变为可用或不可用。
程序有四个按钮控制浏览器工作:向后,向前,停止和刷新,它们分别调用IWebBrowser2相应的方法。
void __stdcall CMainDlg::OnBeforeNavigate2 ( IDispatch* pDisp, VARIANT* URL, VARIANT* Flags, VARIANT* TargetFrameName, VARIANT* PostData, VARIANT* Headers, VARIANT_BOOL* Cancel ) { USES_CONVERSION; CString sURL; sURL = URL->bstrVal; // You can set *Cancel to VARIANT_TRUE to stop the |
下面就是我们的程序工作起来的样子:
IEHoster还使用了前几章介绍过得类:CBitmapButton(用于浏览器控制按钮),CListViewCtrl(用于事件记录),DDX (跟踪checkbox的状态)和CDialogResize.
运行时创建ActiveX控件
在OnInitDialog()函数中我们使用 CAxWindow创建了一个新AtlAxWin,它定位于我们预先放置好的group box的位置上(这个group box随后被销毁):
LRESULT CAboutDlg::OnInitDialog(...) { CWindow wndPlaceholder = GetDlgItem ( IDC_IE_PLACEHOLDER ); CRect rc; CAxWindow wndIE; // Get the rect of the placeholder group box, then destroy // Create the AX host window. |
接下来我们用CAxWindow方法创建一个ActiveX控件,有两个方法可以选择:CreateControl()和CreateControlEx()。
CComPtr<IUnknown> punkCtrl; CComQIPtr<IWebBrowser2> pWB2; CComVariant v; // Create the browser control using its GUID. // Get an IWebBrowser2 interface on the control and navigate to a page. |
对于有ProgID的ActiveX控件可以传递ProgID给CreateControlEx(),代替GUID。例如,我们可以这样创建浏览器控件:
// 使用控件的ProgID: 创建Shell.Explorer: wndIE.CreateControlEx ( L"Shell.Explorer", NULL,NULL, &punkCtrl ); |
wndIE.CreateControl ( IDR_ABOUTPAGE ); |
这是显示结果:
键盘事件处理
最后一个但是非常重要的细节是键盘消息。ActiveX控件的键盘处理非常复杂,因为控件和它的宿主程序必须协同工作以确保控件能够看到它感兴趣的消息。例如,浏览器控件允许你使用TAB键在链接之间切换。MFC自己处理了所有工作,所以你永远不会意识到让键盘完美并正确的工作需要多么大的工作量。
不幸的是向导没有为基于对话框的程序生成键盘处理代码,当然,如果你使用Form View作为视图类的SDI程序,你会看到必要的代码已经被添加到PreTranslateMessage()中。当程序从消息队列中得到鼠标或键盘消息时,就使用ATL的WM_FORWARDMSG消息将此消息传递给当前拥有焦点的控件。它们通常不作什么事情,但是如果是ActiveX控件,WM_FORWARDMSG消息最终被送到包容这个控件的AtlAxWin,AtlAxWin识别WM_FORWARDMSG消息并采取必要的措施看看是否控件需要亲自处理这个消息。
如果拥有焦点的窗口没有识别WM_FORWARDMSG消息,PreTranslateMessage()就会接着调用IsDialogMessage()函数,使得像TAB这样的标准对话框的导航键能正常工作。