在COM中使用陣列引數-ICollection (轉)

worldblog發表於2008-01-31
在COM中使用陣列引數-ICollection (轉)[@more@]

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

 :namespace prefix = o ns = "urn:schemas--com::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 = CComThreadModel>

class ATL_NO_VTABLE CComEnumOnSTL :

  public IEnumOnSTLImpl,

  public CComObjectEx< ThreadModel >

 

模板引數中,Base是列舉器所實現的介面,通常是IEnumVARIANT。piid是列舉器介面的IID,通常是IID_IEnumVARIANT。T是列舉器輸出數值的型別,通常是VARIANT。Copy是複製類,用於將STL物件中的值轉換成列舉器輸出引數。CollType是用於資料的STL型別。ThreadModel是執行緒引數,可以是CComSingleThreadModel或CcomMultiThreadModel,預設值是當前預設的執行緒模式。

假設使用vector類儲存陣列元素。而vector引數是long型資料。可以透過以下方法實現列舉器。

1.  定義CollType

 

typedef std::vector 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

&IID_IEnumVARIANT,

VARIANT,

CopyVariantLong,

CollType> EnumType;

 

1.4.2  ICollectionOnSTLImpl

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介面。

1.  定義ICollection型別

 

typedef ICollectionOnSTLImpl

CollType,

VARIANT,

CopyVariantLong,

EnumType> CollectionType;

 

2.  定義陣列物件

定義陣列物件和定義普通ATL的COM物件是類似的。只要把IDispatchImpl中的介面引數(第一個引數)變成剛剛完成的ICollectionOnSTLImpl引數就可以了。

 

class ATL_NO_VTABLE CNumberCollection :

  public CComObjectRootEx,

  public CComCoClass,

  public IDispatchImpl

&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,

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規範本身的複雜性。本文不可能面面俱到,只能起到拋磚引玉的作用。我這裡有關於本文內容的例項程式碼,大家可以透過e索取。我的地址是/editor/nelsonc@online.sh.cn">nelsonc@online.sh.cn。

大家如果有什麼不清楚的地方,也可以透過email探討。如果大家想了解關於COM或的其它內容也可以告訴我。我以後會發表更多的文章,希望能對大家有所幫助。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-998949/,如需轉載,請註明出處,否則將追究法律責任。

相關文章