最近想自己写个按键精灵的插件,于是接触到这个问题: 怎么在一个组件里实现两个自动化接口。
主要针对的ATL,MFC貌似没这个问题,具体MFC是怎么实现的自己没有深究。
按键精灵的插件会在一个组件里实现两个dispinterface,具体请看oleview工具截图:
刚开始对这个问题不理解,以为不是问题,自己用ATL尝试了几次,才发现不是那么回事,于是google之。
MSDN上是这么说的,看这里
ATL不提供任何为将多个双重接口支持。IDispatch的单个实现。 但是,有几个已知的方法来手动合并接口,如创建包含创建一个新的对象,执行 QueryInterface 函数或使用嵌套的对象一个基于typeinfo的实现的单独 IDispatch 接口来创建 IDispatch 接口的模板选件类。
这些方法都有潜在的命名空间冲突问题,以及代码复杂性和可维护性。 建议不要创建多个双绑定接口。
虽然ATL不支持,但是上面也说了,还是有方法的,于是再google之,终于找到一篇相关问题的文章,里面说的很细,还提供了几种不同的方案:
网址:https://www.sellsbrothers.com/posts/details/12657
自己比较喜欢第2和第3种方案,对比来说,第3种方案比较容易理解和实现。
当然我是用的第3种方案的简单实现,没有从typeinfo接口再继承,还是自己实现了一个类,代理其实接口的IDispatch调用,废话不说了,上代码:
#ifndef _XMULTIDISPIMPL_H_ #define _XMULTIDISPIMPL_H_ #include <atlcom.h> #define INTERFACE_MASK 0xFFFF0000UL #define DISPID_MASK 0x0000FFFFUL template<class tihclass = CComTypeInfoHolder> struct _TIH_ENTRY { tihclass *ptih; // 类型库指针,实现IDispatch调用 DWORD dispEncode; // 函数调用id编码,在GetIdsOfNames函数中对返回的dispid进行编码,尝试解决dispid重复的问题 DWORD offset; // 接口虚函数表偏移,IDispatchImpl<...> }; template <class T, class tihclass = CComTypeInfoHolder> class ATL_NO_VTABLE XMultiDispImpl: public IDispatch { public: typedef _TIH_ENTRY<tihclass> TIH_ENTRY; public: STDMETHOD(GetTypeInfoCount)(UINT* pctinfo) { //TODO: 考虑是否按多个类型库处理 *pctinfo = 1; return S_OK; } STDMETHOD(GetTypeInfo)(UINT itinfo, LCID lcid, ITypeInfo** pptinfo) { //TODO: 考虑是否按多个类型库处理 T* pT = static_cast<T*> (this); TIH_ENTRY* pEntry = pT->GetTypeInfoHolder(); if (pEntry->ptih) { // 默认返回第一个接口的类型库 return pEntry->ptih->GetTypeInfo(itinfo, lcid, pptinfo); } return E_FAIL; } STDMETHOD(GetIDsOfNames)(REFIID riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid) { // NOTE: 函数名字不能冲突, // 名字相同时按顺序查找接口映射表中的接口, // 返回第一个匹配的接口函数对应的dispid T* pT = static_cast<T*> (this); TIH_ENTRY* pEntry = pT->GetTypeInfoHolder(); HRESULT hr = DISP_E_UNKNOWNNAME; while (pEntry->ptih != NULL) { hr = pEntry->ptih->GetIDsOfNames(riid, rgszNames, cNames, lcid, rgdispid); if (SUCCEEDED(hr)) { for (UINT i = 0; i < cNames; i++) { rgdispid[i] |= pEntry->dispEncode; } return hr; } else if (hr != DISP_E_UNKNOWNNAME) { return hr; } pEntry++; } return DISP_E_UNKNOWNNAME; } STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr) { T* pT = static_cast<T*> (this); TIH_ENTRY* pEntry = pT->GetTypeInfoHolder(); HRESULT hr = DISP_E_MEMBERNOTFOUND; if (dispidMember & INTERFACE_MASK) { // 函数id是编码过的,查找对应的接口进行调用,一般是脚本一类的动态调用 while (pEntry->ptih != NULL) { if (pEntry->dispEncode == (dispidMember & INTERFACE_MASK)) { // 找到接口,调用并退出 hr = pEntry->ptih->Invoke((IDispatch*)(((DWORD)pT)+pEntry->offset), (dispidMember & DISPID_MASK), riid, lcid, wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr); return hr; } pEntry++; } } else { // 函数id未编码,逐个接口进行尝试,一般是VC生成的接口类进行的静态调用 // NOTE: 不同的接口,如果存在dispid相同的函数, // 请保证其函数参数个数或者参数类型或者返回值类型不要相同, // 否则可能会调用到错误的接口函数 while (pEntry->ptih != NULL) { hr = pEntry->ptih->Invoke((IDispatch*)(((DWORD)pT)+pEntry->offset), dispidMember, riid, lcid, wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr); if (SUCCEEDED(hr)) { // 调用成功退出 return hr; } pEntry++; } } return DISP_E_MEMBERNOTFOUND; } }; // 映射表宏定义,需要在组件的头文件中引用 #define BEGIN_MULTI_DISPATCH_MAP(CLS) typedef CLS theDerived; static theDerived::TIH_ENTRY* GetTypeInfoHolder() { const DWORD _dwCnt = __COUNTER__; static theDerived::TIH_ENTRY pDispEntries[] = { // 函数id编码,占用id的高16位bit #define MULTI_DISPATCH_ENCODE() (((DWORD)(__COUNTER__) - _dwCnt) << 16) #define MULTI_DISPATCH_ENTRY(theBase) { &theBase::_tih, MULTI_DISPATCH_ENCODE(), offsetofclass(theBase, theDerived) }, #define END_MULTI_DISPATCH_MAP() { NULL, 0UL, 0UL } }; return(pDispEntries); } #endif // sentry
使用方法,在组件类的头文件中,让我们的组件继承我们的类:
class ATL_NO_VTABLE CQMPlugin : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CQMPlugin, &CLSID_QMPlugin>, public ISupportErrorInfo, <span style="color:#3366ff;">public XMultiDispImpl<CQMPlugin>,</span> public IDispatchImpl<IQMPlugin, &IID_IQMPlugin, &LIBID_zdLib, /*wMajor =*/ 1, /*wMinor =*/ 0>, public IDispatchImpl<IQMPluginStandard, &IID_IQMPluginStandard, &LIBID_zdLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
蓝色是要手动添加的代码
在BEGIN_COM_MAP和END_COM_MAP中添加如下代码:
BEGIN_COM_MAP(CQMPlugin) <span style="color:#3366ff;">COM_INTERFACE_ENTRY2(IDispatch, XMultiDispImpl<CQMPlugin>)</span> COM_INTERFACE_ENTRY(IQMPlugin) COM_INTERFACE_ENTRY(IQMPluginStandard) COM_INTERFACE_ENTRY(ISupportErrorInfo) END_COM_MAP()
意思是说当外部程序查询IDispatch接口,返回我们实现的类的虚函数表
下面就是要添加接口映射表了,目前感觉这里还是看着不是很爽,暂时没有解决办法:
<span style="color:#3366ff;">typedef IDispatchImpl<IQMPlugin, &IID_IQMPlugin, &LIBID_zdLib, /*wMajor =*/ 1, /*wMinor =*/ 0> TQMPlugin; typedef IDispatchImpl<IQMPluginStandard, &IID_IQMPluginStandard, &LIBID_zdLib, /*wMajor =*/ 1, /*wMinor =*/ 0> TQMPluginStandard; BEGIN_MULTI_DISPATCH_MAP(CQMPlugin) MULTI_DISPATCH_ENTRY(TQMPlugin) MULTI_DISPATCH_ENTRY(TQMPluginStandard) END_MULTI_DISPATCH_MAP()</span>
记住要先typedef 再用MULTI_DISPATCH_ENTRY,不然会编译失败,这也是让人不爽的地方。
其他的可以按正常的ATLCOM接口开发步骤进行开发了。
下面就是注意事项了:
1. 如果要在一个组件里实现多个disp接口,对于每个接口的方法或者属性,不要出现重名的情况,代码中有说明;
2. 函数的dispid可以相同,但是如果dispid相同,请一定让两个函数的参数个数,参数类型或者返加值类型不要全部相同,不然可能调用到错误的接口函数;
3. 理论上这个类实现的多接口是支持静态调用和动态调用的
4. 对于dispid相同的情况,代码是通过在dispid的高16bit设置标志还区别的,对于VBS一类的动态脚本调用是没有问题的,在脚本里可以把组件当成只实现了一个接口
5. 因为使用的dispid的高16bit,所以这个类最多支持65536个接口,同时每个接口的方法和属性不能超过65536个,有需要的可以自行在代码里调整。
最后希望代码能帮助到大家,没有什么比自己的代码被别人认可更让人。。。,找不到形容词了,欢迎大定留言哈。