在COM中使用数组参数-ICollection

在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的定义如下:

 

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 <class Base,

const IID* piid,

class T,

class Copy,

class CollType,

class ThreadModel = CComObjectThreadModel>

class ATL_NO_VTABLE CComEnumOnSTL :

   public IEnumOnSTLImpl<Base, piid, T, Copy, CollType>,

   public CComObjectRootEx< ThreadModel >

 

模板参数中,Base是枚举器所实现的接口,通常是IEnumVARIANT。piid是枚举器接口的IID,通常是IID_IEnumVARIANT。T是枚举器输出数值的类型,通常是VARIANT。Copy是复制类,用于将STL对象中的值转换成枚举器输出参数。CollType是用于存储数据的STL类型。ThreadModel是线程模式参数,可以是CComSingleThreadModel或CcomMultiThreadModel,缺省值是当前缺省的线程模式。

假设使用vector类保存数组元素。而vector参数是long型数据。可以通过以下方法实现枚举器。

1.  定义CollType

 

typedef std::vector<long> CollType;

 

2.  定义Copy类

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);

    }

};

 

3.  定义枚举器

通过以上定义的类就可以方便的定义枚举器类型了。

 

typedef CComEnumOnSTL<IEnumVARIANT,

&IID_IEnumVARIANT,

VARIANT,

CopyVariantLong,

CollType> EnumType;

 

1.4.2            ICollectionOnSTLImpl

ICollectionOnSTLImpl用于帮助实现ICollection接口。ICollectionOnSTLImpl定义如下:

 

template <class T,

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接口。

1.  定义ICollection类型

 

typedef ICollectionOnSTLImpl<INumberCollection,

CollType,

VARIANT,

CopyVariantLong,

EnumType> CollectionType;

 

2.  定义数组对象

定义数组对象和定义普通ATL的COM对象是类似的。只要把IDispatchImpl中的接口参数(第一个参数)变成刚刚完成的ICollectionOnSTLImpl参数就可以了。

 

class ATL_NO_VTABLE CNumberCollection :

    public CComObjectRootEx<CComSingleThreadModel>,

    public CComCoClass<CNumberCollection, &CLSID_NumberCollection>,

    public IDispatchImpl<CollectionType,

&IID_INumberCollection,

&LIBID_COLLECTIONOBJLib>

{

    ...

}

 

1.5     使用数组对象

对于通用的ICollection对象,只能够通过IDispatch访问。也就是说通过IDispatch::Invoke方法访问数组中的元素。

另一方面,ICollection对象通常指通过VARIANT类型传递数据。所以,我们也必须了解如何访问VARIANT类型的变量。

1.5.1            调用IDispatch

IDispatch是Automation中定义的接口。通过IDispatch,COM客户可以取得接口中每个方法和属性的类型、参数和返回值等信息。通过IDispatch的Invoke方法,COM客户还可以直接调用接口中的方法和属性。IDispatch的内容非常丰富,这里不可能做全面地介绍,所以指对如何通过Invoke方法调用IDispatch做一个简单的说明。

1.  Invoke方法的定义

 

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:可以是以下四个参数之一:
       DISPATCH_METHOD方法调用
       DISPATCH_PROPERTYGET()读属性
       DISPATCH_PROPERTYPUT()写属性
       DISPATCH_PROPERTYPUTREF()通过引用写属性

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))

  {

    ...

}

 

1.5.2            使用IEnumVARIANT枚举数据

要使用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;

    }

 

1.5.3            使用Item和Count

除了使用枚举器,还可以使用Item和Count属性读取元素。和使用枚举器相比,使用Item和Count可以随时取得任一个元素,但是速度会比使用枚举器慢。

可以参考通过Invoke读取Automation属性的方法取得数组元素。

1.5.4            VARIANT类型

在ICollection中,大量使用VARIANT数据。这里把VARIANT的使用方法总结一下:

 

1.  直接使用VARIANT变量

a.   定义VARIANT变量

可以直接定义VARIANT类型的变量。

 

VARIANT val;

 

b.   初始化VARIANT变量

在使用VARIANT变量之前,一定要初始化。

 

VariantInit(&val);

 

c.   设置变量值

设置变量值前如果VARIANT变量中已经有值,先要清除原有数据。

 

VariantClear(&val);

val.vt = VT_I4;   // 设置类型

val.lVal = 10;    // 设置变量值

 

d.   清除VARIANT变量

在使用完VARIANT变量后,要清除变量,否则会发生内存泄漏。

 

VariantClear(&val);

 

e.   动态分配VARIANT变量

如果要动态分配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);

 

2.  通过CComVariant使用VARIANT变量

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;

 

2      后记

由于时间关系,以及COM规范本身的复杂性。本文不可能面面俱到,只能起到抛砖引玉的作用。我这里有关于本文内容的实例代码,大家可以通过email索取。我的email地址是[email protected]

大家如果有什么不清楚的地方,也可以通过email探讨。如果大家想了解关于COM或dotNet的其它内容也可以告诉我。我以后会发表更多的文章,希望能对大家有所帮助。

你可能感兴趣的:(在COM中使用数组参数-ICollection)