IDispatch接口原理与应用
目录:
采用ATL智能指针类调用IDispatch接口的方法:...
7
使用类COleDispatchDriver调用IDispatch的方法:...
11
前言:
尽管com接口是跨语言的,但是很多语言在使用com时更多地通过Automation技术来和com对象通信。IDispatch接口是Automation的核心技术。
尽管c++程序员并不喜欢甚至讨厌使用IDispatch接口,因为调用它实在是非常的麻烦而且易出错。但是不可否认大量的现存组件是只基于IDispatch接口技术而开发的,有时候你没有选择,而且如果你想要写一些组件能够在web上运行,你也离不开IDisptch接口,因为VBScript这样的脚本语言不会聪明到能够理解你的基于虚函数表的普通com接口。
与其躲避它,不如征服它。本文中,我将结合自己的经验和读者一起探讨IDispatch接口的各种应用。并介绍几种能够加快我们使用IDispatch接口的c++类。
IDispatch接口的定义:
参照文件oaidl.h中的定义----
MIDL_INTERFACE("00020400-0000-0000-C000-000000000046")
IDispatch : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
/* [out] */ UINT *pctinfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ ITypeInfo **ppTInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
/* [in] */ REFIID riid,
/* [size_is][in] */ LPOLESTR *rgszNames,
/* [in] */ UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ DISPID *rgDispId) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
/* [in] */ DISPID dispIdMember,
/* [in] */ REFIID riid,
/* [in] */ LCID lcid,
/* [in] */ WORD wFlags,
/* [out][in] */ DISPPARAMS *pDispParams,
/* [out] */ VARIANT *pVarResult,
/* [out] */ EXCEPINFO *pExcepInfo,
/* [out] */ UINT *puArgErr) = 0;
};
我们通过IDispatch的GUID到注册表中搜索,可以搜索到如下结果:
注意在IDispatch接口GUID下面还有两个展开的GUID项,他们分别是ITypeInfo和ITypeLib接口。这两个接口在自动化应用中也是非常重要的。今后我们会经常看到他们。
IDispatch接口方法简介:
1)HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
/* [out] */ UINT *pctinfo) ;
判断实现了IDispatch接口的对象是否支持类型信息,如果返回1则支持,返回0则不支持。
2)
HRESULT STDMETHODCALLTYPE GetTypeInfo(
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ ITypeInfo **ppTInfo) = 0;
获取对象的类型信息接口指针,该方法调用之前总应该先调用方法GetTypeInfoCount(…)确认是否支持类型信息。
参数iTInfo必须为0,否则该方法将返回DISP_E_BADINDEX表示失败
参数lcid传递类型信息的地域标志。IDispatch接口的方法和属性在不同的语言环境(地域标志)可以使用不同的名称,因而lcid不同可能会导致返回的ITypeInfo接口指针不同。如果我们创建的组件根据不同的地域标志对属性和方法起不同的名字,我们就要使用这个参数,否则可以忽略。
3)HRESULT STDMETHODCALLTYPE GetIDsOfNames(
/* [in] */ REFIID riid,
/* [size_is][in] */ LPOLESTR *rgszNames,
/* [in] */ UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ DISPID *rgDispId)
IDispatch接口的属性实质上是方法,方法也就是成员函数,IDispatch接口把所有成员函数的入口地址放入到一个数组中,并且内部组织了一个Map,将数组索引和方法名称一一映射。我们常见的DISPID就是这些方法在数组中的索引。如果我们想调用某一个方法,我们就需要DISPID来让我们找到该方法的地址。
参数riid必须为NULL。
参数rgszNames为字符串数组,第一个字符串为方法或者属性的名称,后续的字符串为参数名称,IDispatch接口的参数也可以有名字。
参数cNames指定rgszNames数组中字符串的个数。
参数lcid传递地域标志,同GetTypeInfo方法中的参数。
参数rgDispId输出一个数组,每个数组成员对应rgszNames中的一个字符串名称。
关于DISPID的进一步说明:
typedef LONG DISPID;
typedef DISPID MEMBERID;
DISPID小于等于0的值都是有特殊意义的,如下面介绍的----
/* DISPID reserved to indicate an "unknown" name */
/* only reserved for data members (properties); reused as a method dispid below */
//如果GetIDsOfNames函数找不到与名称相对应的DISPID,返回该值
#define
DISPID_UNKNOWN ( -1 )
/* DISPID reserved for the "value" property */
//如果调用时不指定方法或者属性,则使用该缺省值
#define
DISPID_VALUE ( 0 )
/* The following DISPID is reserved to indicate the param
* that is the right-hand-side (or "put" value) of a PropertyPut
*/
//表明属性设置函数中某一个参数将接受新属性值
#define
DISPID_PROPERTYPUT ( -3 )
/* DISPID reserved for the standard "NewEnum" method */
//用于集合对象
#define
DISPID_NEWENUM ( -4 )
/* DISPID reserved for the standard "Evaluate" method,脚本语言中可以用[]调用该方法 */
#define
DISPID_EVALUATE ( -5 )
/* 表示某方法具有和构造函数相同的功能*/
#define
DISPID_CONSTRUCTOR ( -6 )
/* 表示某方法具有和析构函数相同的功能*/
#define
DISPID_DESTRUCTOR ( -7 )
/*
The
Collect property. You use this property if the method you are calling through Invoke is an accessor function.*/
#define
DISPID_COLLECT ( -8 )
/* The range -500 through -999 is reserved for Controls */
/* The range 0x80010000 through 0x8001FFFF is reserved for Controls */
/* The range -5000 through -5499 is reserved for ActiveX Accessability */
/* The range -2000 through -2499 is reserved for VB5 */
/* The range -3900 through -3999 is reserved for Forms */
/* The range -5500 through -5550 is reserved for Forms */
/* The remainder of the negative DISPIDs are reserved for future use */
4) HRESULT STDMETHODCALLTYPE Invoke(
/* [in] */ DISPID dispIdMember,
/* [in] */ REFIID riid,
/* [in] */ LCID lcid,
/* [in] */ WORD wFlags,
/* [out][in] */ DISPPARAMS *pDispParams,
/* [out] */ VARIANT *pVarResult,
/* [out] */ EXCEPINFO *pExcepInfo,
/* [out] */ UINT *puArgErr)
参数dispIdMember为方法或者属性的DISPID,就是我们通过GetIDsOfNames获得的。
参数riid必须为IID_NULL。
参数lcid为地域标志,同前面两个方法。
参数wFlags有下面若干值----
Value |
Description |
DISPATCH_METHOD |
表示将调用方法。如果属性名称和方法名称相同,则和DISPATCH_PROPERTYGET标志一起设置。 |
DISPATCH_PROPERTYGET |
获得属性 |
DISPATCH_PROPERTYPUT |
设置属性 |
DISPATCH_PROPERTYPUTREF |
通过引用设置属性 |
参数pDispParams为参数信息数组,元素类型为DISPPARAMS
typedef
struct tagDISPPARAMS
{
/* [size_is] */ VARIANTARG *rgvarg;//参数数组
/* [size_is] */ DISPID *rgdispidNamedArgs;//参数中的DISPID数组
UINT cArgs;//数组中的参数个数
UINT cNamedArgs;//命名参数的个数
}DISPPARAMS;
注意:
如果是属性设置,rgvarg数组中只有一个参数,如果是方法调用,可以包含0到多个参数;
rgvarg数组中的VARIANTARG参数的vt域为VT_BYREF时,该参数可写,否则为只读;
rgvarg数组中的VARIANTARG参数的vt域为VT_BYREF时,该参数可以作为输出参数;
rgvarg数组中的VARIANTARG参数的vt域不为VT_BYREF时,参数内的字符串或者指针变量的所有权在客户,客户必须自己释放资源,实现IDispatch接口的对象要想保留数据,则要拷贝数据或者调用指针变量的AddRef函数。
rgvarg数组中的VARIANTARG参数的vt域为VT_ERROR,并且scode域为DISP_E_PARAMNOTFOUND时,该参数可以作为可选参数,scode域作用是存放返回的HRESULT。
<<COM原理与应用>>中曾经提到rgvarg数组中的参数存放顺序和客户程序调用时传递的参数顺序刚好相反,我这里对此表示怀疑。
关于命名参数的详细讨论我在后面将谈到,现在只需要知道它可以不受参数次序的限制。
参数pVarResult保存函数调用后的返回信息,因为Invoke已经将返回值用于COM通用的HRESULT;
参数pExcepInfo返回异常信息;
参数puArgErr包含第一个产生错误的参数索引,当Invoke返回的HRESULT值为DISP_E_TYPEMISMATCH或DISP_E_PARAM_NOTFOUND值时。
创建支持IDispatch接口的COM对象:
本节我们利用ATL7.1创建一个COM对象Baby。CBaby类从IDispatch接口派生,并且同时支持vtable方式。我们设想他有一个属性Gender
(性别),我们可以设置和获取宝宝的性别属性
现在我们先创建一个ATL项目IDspCOM。然后添加类CBaby,选择接口类型为双重。我们先在看一下生成的idl文件中的接口定义:
[
object,
uuid(22C1BD80-2937-42FB-A7F8-5CEBD1257CB8),
dual,
nonextensible,
helpstring("IBaby 接口"),
pointer_default(unique)
]
interface IBaby : IDispatch
{
};
现在IBaby派生自IDispatch接口,除了继承了IDispatch的4个方法和IUnknown的3个方法外,它现在还没有任何自己的方法和属性。ATL将帮我们实现前面的7个方法,我们无需关心。我们现在来创建自己的属性Gender。
我们通过向导创建了该属性,idl文件如下:
interface IBaby : IDispatch{