在COM中使用数组参数-ICollection
关键字:DCOM、数组、自定义类型、Marshal、SafeArray、ICollection
1 使用ICollection
ICollection是从 IDispatch继承的接口。ICollection还需要一个IEnumVARIANT接口配合实现功能。IEnumVARIANT是从IUnknown继承的,而不是从IDispatch接口继承。
ICollection接口提供了最大的面向对象的设计灵活性和可重用性。在数组指针和SafeArray方法中,数组的每个元素必须事先计算出来,并且保存在特定的数据结构中。使用ICollection接口,可以设计出动态生成的数组,就是说数组的元素在需要的时候才进行计算,以便减少内存使用并加快处理速度。
1.1 ICollection和IEnumVARIANT
ICollection接口用于定义数组对象,而IEnumVARIANT接口用于定义枚举对象。枚举对象的作用是按顺序读取数组元素,有时,通过枚举对象可以获得更高的效率。
ICollection和IEnumVARIANT的定义如下:
XML:namespace prefix = o ns = "urn:schemas-microsoft-com:Office:office" />
interface ICollection : IDispatch
{
[propget, id(DISPID_LISTITEM)] HRESULT Item(
[in] const VARIANT varIndex,
[out, retval] VARIANT *pVal);
[propget, id(DISPID_LISTCOUNT)] HRESULT Count(
[out, retval] long *pVal);
[propget, id(DISPID_COLLCOUNT)] HRESULT length(
[out, retval] long *pVal);
[propget, id(DISPID_NEWENUM), restricted, hidden]
HRESULT _NewEnum([out, retval] IUnknown* *pVal);
... // 其它方法或属性
};
interface IEnumVARIANT : IUnknown
{
HRESULT Next(
unsigned long celt,
VARIANT * rgvar,
unsigned long * pceltFetched);
HRESULT Skip(unsigned long celt);
HRESULT Reset();
HRESULT Clone(IEnumVARIANT ** ppenum);
};
有的时候,COM对象不但要实现数组功能,而且还要实现其它功能。所以,大多数时候,COM对象实现的接口是从ICollection继承来的。
通过ICollection操纵数组大体上有两种方法。一种是通过Item属性用数组下标取得元素。这种方式,每次只能取得一个元素,而且要传递下标对象,所以效率比较低下。另一种方法是通过枚举器。数组对象的枚举器通过_NewEnum属性取得。通过枚举器只能按顺序获取元素,但每次可以取得任意多的元素,所以效率较高。ICollection对象可以只实现其中的一种访问方法,也可以两种都实现。ICollection中还有一个重要属性:Count。Count属性返回数组的长度,对于无法确定长度的数组,也可以不实现Count属性。
IEnumVARIANT接口用于定义枚举器。枚举器用于顺序读取数组元素。通过Next方法,可以一次读取任意多的元素。由于枚举器只可以按顺序访问数组元素,所以Next方法不需要传递下标。Skip方法用于跳过若干元素,而不读取。Reset把当前元素设置到数组头,这样就可以重新开始枚举。Clone用于获得一个新的枚举器。两个枚举器可以互不干扰的工作。
要注意的事,可能有某些数组对象的实现方法使用不同的属性名称。实际上ICollection中的属性名称是不重要的,重要的是Dispatch ID。只要通过Dispatch ID就可以取得正确的属性。
1.2 数组对象
数组对象是实现了ICollection接口的COM对象。数组对象的使用者通过ICollection接口取得数组中的数据,而完全不需要知道数组的具体实现方式。这种设计的好处是使用数组的代码可以完全不理会数组的实现方法,而当数组的实现发生变化时,使用数组的代码可以在二进制代码上保持兼容,也就是说目标代码不用编译就可以使用。
最简单制作数组对象的方法是使用ATL的模板。CComEnumOnSTL模板用于生成实现IEnumVARIANT接口的枚举对象。当然,如果要实现数组对象的所有优点,最好自己编写数组对象的代码。
1.3 ICollection参数的IDL声明
在IDL声明中。数组对象应该声明成IDispatch *。如果是输出或输入输出参数,则应该使用双重指针。
[id(0)] GetNumber([out] IDispatch ** ppObj);
[id(1)] SetNumber([in] IDispatch * pObj);
目前,我们看到的ICollection数组都是只读的。实际上ICollection完全可以设计成可读写的数组对象,只要把ICollection的Item属性设置成可读写的就可以了。关于可读写的ICollection对象请参考相关资料。
1.4 通过ATL实现数组对象
ATL通过两个模板实现对ICollection的支持。它们就是CComEnumOnSTL和ICollectionOnSTLImpl。CComEnumOnSTL用于实现基于STL对象的枚举器。ICollectionOnSTLImpl用于实现ICollection接口。下面详细描述这两个模板的功能和用法。
1.4.1 CComEnumOnSTL
CComEnumOnSTL的定义如下:
template const IID* piid, class T, class Copy, class CollType, class ThreadModel = CComobjectThreadModel> class ATL_NO_VTABLE CComEnumOnSTL : public IEnumOnSTLImpl public CComObjectrootEx< ThreadModel > 模板参数中,Base是枚举器所实现的接口,通常是IEnumVARIANT。piid是枚举器接口的IID,通常是IID_IEnumVARIANT。T是枚举器输出数值的类型,通常是VARIANT。Copy是复制类,用于将STL对象中的值转换成枚举器输出参数。CollType是用于存储数据的STL类型。ThreadModel是线程模式参数,可以是CComSingleThreadModel或CcomMultiThreadModel,缺省值是当前缺省的线程模式。 假设使用vector类保存数组元素。而vector参数是long型数据。可以通过以下方法实现枚举器。 typedef std::vector Copy类用于在STL类的元素类型和枚举器类型之间进行参数转换。每个Copy类必须有三个静态函数:init、copy、destroy。Init用于初始化枚举器类、copy用于把STL元素复制到枚举器参数、destroy用于销毁枚举器参数。 下面是用于在long和VARIANT之间转换的Copy类实例。 class CopyVariantLong { public: static void init(VARIANT * p) { VariantInit(p); } static HRESULT copy(VARIANT * pTo, const LONG * pFrom) { pTo->vt = VT_I4; pTo->lVal = *pFrom; return S_OK; } static void destroy(VARIANT * p) { VariantClear(p); } }; 通过以上定义的类就可以方便的定义枚举器类型了。 typedef CComEnumOnSTL &IID_IEnumVARIANT, VARIANT, CopyVariantLong, CollType> EnumType; ICollectionOnSTLImpl用于帮助实现ICollection接口。ICollectionOnSTLImpl定义如下: template class CollType, class ItemType, class CopyItem, class EnumType> class ICollectionOnSTLImpl : public T 在ICollectionOnSTLImpl模板中,T是要实现的接口,一般会使用从ICollection继承的接口。CollType参数是用于保存数据的STL类型,这个类型应该和枚举器中的相同。ItemType是ICollection中Item属性的类型,一般是VARIANT。CopyItem是Item属性的Copy类,和枚举器中的Copy类是相同的。EnumType是枚举器的类型。 可以通过以下步骤实现ICollection接口。 typedef ICollectionOnSTLImpl CollType, VARIANT, CopyVariantLong, EnumType> CollectionType; 定义数组对象和定义普通ATL的COM对象是类似的。只要把IDispatchImpl中的接口参数(第一个参数)变成刚刚完成的ICollectionOnSTLImpl参数就可以了。 class ATL_NO_VTABLE CNumberCollection : public CComObjectRootEx public CComCoClass public IDispatchImpl &IID_INumberCollection, &LIBID_COLLECTIONOBJLib> { ... } 对于通用的ICollection对象,只能够通过IDispatch访问。也就是说通过IDispatch::Invoke方法访问数组中的元素。 另一方面,ICollection对象通常指通过VARIANT类型传递数据。所以,我们也必须了解如何访问VARIANT类型的变量。 IDispatch是Automation中定义的接口。通过IDispatch,COM客户可以取得接口中每个方法和属性的类型、参数和返回值等信息。通过IDispatch的Invoke方法,COM客户还可以直接调用接口中的方法和属性。IDispatch的内容非常丰富,这里不可能做全面地介绍,所以指对如何通过Invoke方法调用IDispatch做一个简单的说明。 HRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, word wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr ); Invoke的参数如下: l dispIdMember:所调用的属性或方法的dispatch id l riid:保留,必须是IID_NULL l lcid:语言环境。一般使用LOCALE_THREAD_DEFAULT l wFlags:可以是以下四个参数之一: l pDispParams:参数数组 l pVarResult:返回值 l pExcepInfo:被调用方法或属性内部异常(如果发生异常) l puArgErr:当返回DISP_E_PARAMNOTFOUND或DISP_E_TYPEMISMATCH时,返回出错的参数序号。 以下是使用Invoke的例子。下例返回一个dispatch id是DISPID_LISTCOUNT的简单参数,实际上就是数组的长度。 VARIANT varResult; DISPPARAMS DispParams; EXCEPINFO excepInfo; UINT errArg; VariantInit(&varResult); DispParams.cArgs = 0; DispParams.cNamedArgs = 0; DispParams.rgdispidNamedArgs = NULL; DispParams.rgvarg = NULL; hr = pObj->Invoke( DISPID_LISTCOUNT, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, &DispParams, &varEnum, &excepInfo, &errArg); if (FAILED(hr)) { goto CleanUp; } 下例返回一个带参数的属性。 VARIANT varIndex; VARIANT varResult; DISPPARAMS DispParams; EXCEPINFO excepInfo; UINT errArg; VariantInit(&varIndex); VariantInit(&varResult); DispParams.cArgs = 1; DispParams.cNamedArgs = 0; DispParams.rgdispidNamedArgs = NULL; DispParams.rgvarg = &varIndex; VariantClear(&varIndex); VariantClear(&varResult); varIndex.vt = VT_I2; varIndex.iVal = (short) Index; hr = pObj->Invoke( DISPID_LISTITEM, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_PROPERTYGET, &DispParams, &varResult, &excepInfo, &errArg); if (FAILED(hr)) { ... } 要使用IEnumVARIANT枚举数据,首先必须取得IEnumVARIANT指针。取得IEnumVARIANT指针是通过ICollection的_NewEnum属性。具体操作可以参考上一节关于Invoke的说明。 在取得了IEnumVARIANT之后,就可以通过IEnumVARIANT顺序读取数组元素了。 请参考以下代码枚举数据:这段代码是将数组中的元素相加求总和。 ULONG Result = 0; ULONG res; while (1) { hr = pEnum->Next(1, &var, &res); if (FAILED(hr)) { goto CleanUp; } if (hr != S_OK || res != 1) { break; } hr = VariantChangeType(&var, &var, 0, VT_I4); if (FAILED(hr)) { goto CleanUp; } Result += var.lVal; } 除了使用枚举器,还可以使用Item和Count属性读取元素。和使用枚举器相比,使用Item和Count可以随时取得任一个元素,但是速度会比使用枚举器慢。 可以参考通过Invoke读取Automation属性的方法取得数组元素。 在ICollection中,大量使用VARIANT数据。这里把VARIANT的使用方法总结一下: 可以直接定义VARIANT类型的变量。 VARIANT val; 在使用VARIANT变量之前,一定要初始化。 VariantInit(&val); 设置变量值前如果VARIANT变量中已经有值,先要清除原有数据。 VariantClear(&val); val.vt = VT_I4; // 设置类型 val.lVal = 10; // 设置变量值 在使用完VARIANT变量后,要清除变量,否则会发生内存泄漏。 VariantClear(&val); 如果要动态分配VARIANT变量,应该使用标准的COM内存管理函数。 标准COM内存管理函数包括CoTaskMemAlloc、CoTaskMemFree和CoTaskMemRealloc。 VARIANT * pVal; pVal = (VARIANT *)CoTaskMemAlloc(size_of(VARIANT)); VariantInit(pVal); pVal->vt = VT_I4; pVal->lVal = 10; ... VariantClear(pVal); CoTaskMemFree(pVal); CComVariant是ATL对于VARIANT的简单包装。通过CComVariant可以更简单的使用VARIANT,而不必担心没有进行初始化或清除。如果没有特殊情况,应该尽量使用CComVariant而不要使用VARIANT。 以下是使用CComVariant的代码实例。 CComVariant Val; Val.vt = VT_I4; Val.lVal = 10; // Val 不必清除 以下是使用CComVariant数组的例子。 CComVariant * pVal; pVal = new CComVariant[10]; for (int i = 0; i < 10; ++i) { pVal[i].vt = VT_I4; pVal[I].lVal = i + 1; } ... delete[] pVal; 由于时间关系,以及COM规范本身的复杂性。本文不可能面面俱到,只能起到抛砖引玉的作用。我这里有关于本文内容的实例代码,大家可以通过eMail索取。我的email地址是.NET/editor/nelsonc@online.sh.cn">nelsonc@online.sh.cn。 大家如果有什么不清楚的地方,也可以通过email探讨。如果大家想了解关于COM或dotnet的其它内容也可以告诉我。我以后会发表更多的文章,希望能对大家有所帮助。 1. 定义CollType
2. 定义Copy类
3. 定义枚举器
1.4.2 ICollectionOnSTLImpl
1. 定义ICollection类型
2. 定义数组对象
1.5 使用数组对象
1.5.1 调用IDispatch
1. Invoke方法的定义
DISPATCH_METHOD方法调用
DISPATCH_PROPERTYGET()读属性
DISPATCH_PROPERTYPUT()写属性
DISPATCH_PROPERTYPUTREF()通过引用写属性1.5.2 使用IEnumVARIANT枚举数据
1.5.3 使用Item和Count
1.5.4 VARIANT类型
1. 直接使用VARIANT变量
a. 定义VARIANT变量
b. 初始化VARIANT变量
c. 设置变量值
d. 清除VARIANT变量
e. 动态分配VARIANT变量
2. 通过CComVariant使用VARIANT变量
2 后记