本文阅读基础:有一定的C++基础知识(了解继承、回调函数),对MFC的消息机制有一定了解,对COM的基础知识有一定了解,对ActiveX控件有一定了解。
一. 前言
ActiveX控件和它的容器程序如何通讯是一个值得研究的问题,因为这涉及到ActiveX控件和它的容器程序如何交互的问题。VC知识库的杨老师写了一系列博客介绍了一些通讯方式。链接如下:
COM 组件设计与应用(十三)--事件和通知(VC6.0)
COM 组件设计与应用(十四)--事件和通知(vc.net)
COM 组件设计与应用(十五)--事件和通知(VC6.0)
COM 组件设计与应用(十六)--事件和通知(vc.net)
这些文章写得真的很好,语言幽默风趣,深入浅出。我看后决心把它应用在ActiveX控件的回调实现上,经过实践,觉得有些地方语焉不详,自己做些摸索,写就此文,算是对杨老师文章的一点补充。
二.通知的方法
ActiveX控件是一个窗口,它的容器程序自然也有一个父窗口;同时ActiveX控件是一个接口;ActiveX控件本质是一个COM组件,COM组件的客户端和服务器端本身有自己的通讯方式。从这两点我们可以想到二者之间的几种通讯方式:
我和我的同事曾争论ActiveX控件接口能否像一般C++的DLL那样在导出函数参数列表里设置一个回调函数指针那样实现回调,那时我认为是不行的。现在我看了ActiveX控件接口的参数类型,更加坚定了我的看法。其实从COM的初衷来看应该也是不行的,因为COM的初衷之一是提供一种跨语言的调用接口,而回调函数指针只对客户端是C++程序是有意义,对于VB、C#则无回调函数指针一说的。
三.实践检验
现在我们编一个这样的小程序:在ActiveX控件上画直线,在画直线的同时把坐标传给客户端的视图,在客户端的视图区上依据传进来的坐标信息,绘制出相应的直线。
在动手之前我简要介绍我的思路:所谓基于COM的回调虚接口实现ActiveX控件和客户端程序的通讯,大致是这样的,就是在ActiveX工程的内部的idl文件定义一个虚接口,在客户端程序定义一个虚接口的派生类来实现回调函数,在客户端程序传递派生类对象指针给ActiveX控件,在控件内部调用这个虚接口的函数来激发客户端程序的派生类的对应的回调函数。这里其实有一个关键问题,就是定义在idl文件中回调虚接口如何被ActiveX工程和客户端程序识别,而不至于成为未定义类型(说实话,这个问题折磨了我两个晚上,之所以这么麻烦,大概因为这个接口是定义在idl文件,而不是C++源文件中),下面我将介绍如何解决这个问题。
首先我们创建一个MFC ActiveX Control的工程:DataX,具体如下图:
接着在idl文件添加回调接口。这一步需要手动编辑idl文件。首先实现使用GUIDGEN.EXE(该工具在$/Microsoft Visual Studio 9.0/Common7/Tools路径下,在VC6.0,VC 8.0都有这个工具)产生一个IID,生成时注意选择是注册表形式,具体如下图:
然后在idl文件的开头下加入以下内容:
- import "oaidl.idl";
- import "ocidl.idl";
- [
- object,
- uuid(87C3DA69-C915-41f5-8142-D77816F22004), // 这个IID 可以用GUDIGEN.EXE 产生
- helpstring("ICallBack 接口"),
- pointer_default(unique)
- ]
- interface ICallBack : IUnknown {
- [id(1),helpstring("回调接口,响应鼠标按下")] HRESULT FireLButtonDown([in] LONG x,LONG y);
- [id(2),helpstring("回调接口,响应鼠标移动")] HRESULT FireMouseMove([in] LONG x,LONG y);
- [id(3),helpstring("回调接口,响应鼠标按下")] HRESULT FireLButtonUp([in] LONG x,LONG y);
- };
由于画图不是本文描述的重点,这里暂且略过。下面我们开始在ActiveX控件上实现绘图和添加传递回调接口的函数。这里所说的传递回调接口实现的功能是把客户端程序的回调接口派生类对象的指针传到ActiveX控件内部。具体如下:
这里介绍一下添加COM接口,首先在对应的接口选择:Add Method,如下图:
然后开始填充返回类型、参数等信息:
我发现必须选择下拉列表的参数类型,不选择的话就会出错:
这时我们需要要SetCallBackPtr函数中的IUnknown全部替换为ICallBack。为什么要进行替换?一方面ICallBack*类型才是我们真正要传递的指针类型;另一方面如果不进行替换,外部的客户端程序将不能识别ICallBack这个类型。我发现在idl文件定义的类型,似乎要在library ***
{
}
这一段出现过的类型外部才能识别,不过这个没有得到验证,有空得研究一个idl文件。这里大致要替换的地方包括三处,控件窗口类中的头文件的声明、cpp文件中的定义以及在idl文件中的声明。这一步算是解决了回调接口ICallBack的外部识别问题。
现在我们在CDrawView定义一个
ICallBack* m_pCallBack;的数据成员,以及它的赋值接口函数:
- void CDrawView::SetCallBack(ICallBack* pCallBack)
- {
- if (NULL!=pCallBack)
- {
- m_pCallBack = pCallBack;
- }
- }
这里经常会出现的一个错误是:use of undefined type 'ICallBack'。这里的根源在于C++源码文件并不能识别idl文件中的类型,就是一个内部识别问题。后来我发现其实编译所有COM工程都有一步就是将idl文件的内容翻译为C++能够识别的头文件。所以你会发现编译后工程文件夹会多出一些头文件和C文件,其中一个头文件的命令比较有意思,规律大致是工程名+idl.h。这个头文件其实就是编译idl文件生成的。因此要使用回调基类接口,就必须包含这个头文件。我这个工程的是:DataXidl.h。打开这个文件,可以从中看到COM的一些奥秘,里面有这样一段代码:
- ICallBack : public IUnknown
- {
- public:
- virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE FireLButtonDown(
- /* [in] */ LONG x,
- /* [in] */ LONG y) = 0;
- virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE FireMouseMove(
- /* [in] */ LONG x,
- /* [in] */ LONG y) = 0;
- virtual /* [helpstring] */ HRESULT STDMETHODCALLTYPE FireLButtonUp(
- /* [in] */ LONG x,
- /* [in] */ LONG y) = 0;
- };
看来在编译过程中已经将idl接口的代码转换为C++代码了。
然后我们在控件窗口类里添加一个画图视图的指针:
- CDrawView *m_pView;
实现刚才添加的接口函数,把回调接口传给视图类:
- void CDataXCtrl::SetCallBackPtr(ICallBack* pCallBack)
- {
- AFX_MANAGE_STATE(AfxGetStaticModuleState());
- // TODO: Add your dispatch handler code here
- if (NULL!=m_pView)
- {
- m_pView->SetCallBack(pCallBack);
- }
- }
4. 在派生类中实现派生类接口
现在我们开始新建一个单文档程序:CallbackTest,VS 2005风格的。来测试这个ActiveX控件。
我们首先将ActiveX接口导出,具体如下:
在工程上添加新类,选择"MFC Class From ActiveX Control":
在注册表上选择DataX控件,再接口_DDataX从生成类,如下图:
再增加一个接口器类CSink,用于接收ActiveX控件发送过来的通知:
这里会出现ICallBack未定义类型的问题。这时我只需要在stdafx.h把它的类型库或ocx导入进来即可,代码如下:
- #ifdef _DEBUG
- #import "../Intdir/Debug/DataX/DataX.tlb" no_namespace
- #else
- #import "../Intdir/Release/DataX/DataX.tlb" no_namespace
- #endif
你也可以这样写:
- #ifdef _DEBUG
- #import "../outdir/Debug/DataX.ocx" no_namespace
- #else
- #import "../outdir/Release/DataX.ocx" no_namespace
- #endif
效果是一样。我推荐你使用后一种用法,因为一般对ActiveX控件而言,别人可能会提供一个ocx文件给你,而不是一个tlb文件给你。
接着我们定义一个停靠栏类class CDrawBar : public CDockablePane专门负责放置ActiveX控件。
编译一下,你会发现一下子出现了7个错误,本质上只有一个错误:error C2259: 'CSink' : cannot instantiate abstract class。CSink不能实例化对象。我们明白了,CSink派生自一个虚接口,却没有实现它的纯虚函数。那么ICallBack类中有几个我们还没实现的纯虚函数呢?我数了一下,有六个:IUnknown::QueryInterface、IUnknown::AddRef、IUnknown::Release、ICallBack::raw_FireLButtonDown、ICallBack::raw_FireMouseMove、ICallBack::raw_FireLButtonUp'。前三个函数我们很容易明白,ICallBack派生自IUnknown类,IUnknown的三个纯虚函数自然也得实现(这三个函数的意义如果你没搞懂,我推荐你看潘爱民的《COM原理与应用》)。至于后面三个函数,一开始我百思不得其解,我记得我在ICallBack类定义函数名称是:FireLButtonDown、FireMouseMove和FireLButtonUp,怎么到了外部变成了:raw_FireLButtonDown、raw_FireMouseMove、raw_FireLButtonUp。后来我大致分析,可能是编译器将名称做了转换,VS上的说法是:Raw methods provided by interface。这里的回调流程是怎样的呢,我以下面的图来说明:
其中传递回调接口的派生类工作放在应用程序类的InitInstance函数。
首先在应用程序类定义一个保护数据成员:
- CSink m_Sink;
再在InitInstance函数的最后面添加如下代码:
- CMainFrame* pFrame = static_cast<CMainFrame*>(m_pMainWnd);
- if (NULL!=pFrame)
- {
- // 获取视图指针
- CTestCallbackView *pView = static_cast<CTestCallbackView*>(pFrame->GetActiveView());
- // 获取ActiveX控件指针
- CDDataX* pDataX = pFrame->GetActiveControl();
- // 传递回调接口指针
- pDataX->SetCallBackPtr(&m_Sink);
- // 设置视图窗口为发送消息的目标窗口
- m_Sink.SetMsgWnd(pView->GetSafeHwnd());
- }
下面以鼠标右键按下消息的回调代码。首先在ActiveX控件的视图类的鼠标按下消息响应函数:
- void CDrawView::OnLButtonDown(UINT nFlags, CPoint point)
- {
- // TODO: Add your message handler code here and/or call default
- m_BeginPoint = point;
- if (NULL!=m_pCallBack)
- {
- // 激发客户端的CSink::raw_FireLButtonDown函数
- m_pCallBack->FireLButtonDown(point.x,point.y);
- }
- CView::OnLButtonDown(nFlags, point);
- }
客户端的CSink对象收到通知后,给视图类发送鼠标按下消息:
- STDMETHODIMP CSink::raw_FireLButtonDown(long x,long y)
- {
- if (NULL!=m_hMsgWnd)
- {
- LPARAM lparam = MAKELPARAM(x,y);
- ::PostMessage(m_hMsgWnd,WM_LBUTTONDOWN,NULL,lparam);
- }
- return S_OK;
- }
效果图如下:
这种回调机制,在现在的开源或商业产品有没有应用实例呢?如果你研究过著名的开源的ActiveX控件:MapWinGIS。你会发现MapWinGIS实际上是采用这种回调机制的,比如MapWinGIS中的操作shp文件的类CShapefile的一个接口:
STDMETHOD(Open)(/*[in]*/ BSTR ShapefileName, /*[in, optional]*/ICallback * cBack, /*[out, retval]*/VARIANT_BOOL * retval);
这个函数的第二个参数的类型就是MapWinGIS内部定义的回调接口类型:ICallback。
四.小结
总结一下开发过程,再和杨老师提供的例程做一个简单比较。应该说我这个例程和杨老师的例程很类似。有点不同的是杨老师使用的是标准的ATL 组件,我这个使用的是MFC ActiveX控件。从本质而言,ActiveX控件类是一个com接口,只不过这个接口有些特殊,它既是一个窗口,因而也就具有了一般窗口的属性,能够接受和响应消息,同时它也是一个接口类。
本文出现的代码编译环境为:VS C++ 2008 + sp1, Win XP + sp3。
源码下载:点击下载 。