本文阅读基础:有一定的C++基础知识(了解继承、回调函数),对MFC的消息机制有一定了解,对COM的基础知识有一定了解,对ActiveX控件有一定了解。
一.前言ActiveX控件和它的容器程序如何通讯是一个值得研究的问题,因为这涉及到ActiveX控件和它的容器程序如何交互的问题。
VC知识库的杨老师写了一系列博客介绍了一些通讯方式。
链接如下:COM 组件设计与应用(十三)--事件和通知(VC6.0)COM 组件设计与应用(十四)--事件和通知()COM 组件设计与应用(十五)--事件和通知(VC6.0)COM 组件设计与应用(十六)--事件和通知()这些文章写得真的很好,语言幽默风趣,深入浅出。
我看后决心把它应用在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文件的开头下加入以下内容:view plaincopy to clipboardprint?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);};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;的数据成员,以及它的赋值接口函数:view plaincopy to clipboardprint?void CDrawView::SetCallBack(ICallBack* pCallBack){if (NULL!=pCallBack){m_pCallBack = 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的一些奥秘,里面有这样一段代码:view plaincopy to clipboardprint?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;};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++代码了。
然后我们在控件窗口类里添加一个画图视图的指针:view plaincopy to clipboardprint?CDrawView *m_pView;CDrawView *m_pView;实现刚才添加的接口函数,把回调接口传给视图类:view plaincopy to clipboardprint?void CDataXCtrl::SetCallBackPtr(ICallBack* pCallBack){AFX_MANAGE_STATE(AfxGetStaticModuleState());// TODO: Add your dispatch handler code hereif (NULL!=m_pView){m_pView->SetCallBack(pCallBack);}}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风格的。