COM でコレクションを作成する
MINI SERIES
はじめに
前提条件
COM コンポーネントを Visual C++ 6.0 を使って作る手順を一応は知っている人を対象に書きました。
今回は、IData のコレクション、IDataCollection を作成することを想定しています。IData には、プロパティとして Name (BSTR) と、Value (VARIANT_BOOL) が存在しているものとします。そして、これを IDataCollection がコレクションとして所持します。
必要なプロパティ
COM インターフェイスがコレクションとして存在するためには、次の3つのプロパティを IDataCollection が持っている必要があります。
Count
- 種類: long
- 関数: 取得関数
コレクション内のオブジェクトの個数を取得するためのプロパティです。このプロパティが呼び出されたときに、IDataCollection が所有している IData の個数を返すようにコーディングします。
Item
- 種類: IData*
- 引数: [in] VARIANT Index
- 関数: 取得関数
- DispID: 0
コレクション内の目的のオブジェクトを取得するためのプロパティです。このプロパティはディフォルトのプロパティとして実装しなくてはいけないので、DISPID を 0 に設定します。引数は、どのオブジェクトを取得するかを指定するものですが、今回は配列番号と名前の両方からオブジェクトを取得できるようにするために VARIANT 型を使用することにします。
_NewEnum
- 種類: LPUNKNOWN
- 関数: 取得関数
- DispID: -4
- 属性: restricted
IDataCollection 内の IData 一覧を取得するためのプロパティです。DISPID を -4 に設定し、アトリビュートに restricted を追加する必要があります。内部で保持している IData を VARIANT 型の配列に格納して Variant 型の列挙子として返却するようにコーディングします。
プロジェクトの作成
Visual C++ 6.0 を起動して、「ATL Com AppWizard」 を新規に作成します。今回はプロジェクト名を CollectionSample という名前にします。そして、サーバタイプを 「ダイナミック リンク ライブラリ (DLL)」 として、作成しました。
IData の作成
インターフェイスの作成
「ATL オブジェクトの新規作成」 から、「シンプルオブジェクト」 を選択して、ショートネームに 「Data」 と打ち込みます。IData の "I" は 「インターフェイス」 のところで自動的に付加さるので、特に入力する必要はありません。
プロパティの実装
IData インターフェイスに次のプロパティを追加します。
Name
- 種類: BSTR
- 関数: 取得関数/設定 [PropPut]
Value
- 種類: VARIANT_BOOL
- 関数: 取得関数/設定 [PropPut]
属性の実装
CData クラス内にプロパティの値を保持するためのメンバ変数を定義します。
m_Name
- 種類: BSTR
- 制御: Private
m_Value
- 種類: bool
- 制御: Private
関数の実装
IData インターフェイスのプロパティを正常に機能させるために、関数にプログラムをコーディングします。
STDMETHODIMP CData::get_Name(BSTR *pVal)
プログラム
*pVal = ::SysAllocString(m_Name);
return S_OK;
解説
m_Name 属性に保持してあった BSTR 型の値を *pVal に代入しています。そのとき、BSTR は複製しなくてはいけないので、::SysAllocString() 関数を使用して、m_Name の複製を取得しています。
STDMETHODIMP CData::put_Name(BSTR newVal)
プログラム
SysFreeString(m_Name);
m_Name = ::SysAllocString(newVal);
return S_OK;
解説
m_Name 属性に、newVal に渡されてきた BSTR の値を保存しています。このとき、m_Name には BSTR の複製を保存しなくてはなりませんので、::SysAllocString() 関数を使用して newVal の複製を取得しています。また、使用しなくなった BSTR の値も開放しなくてはなりません。なので、あらかじめ新しい値を代入する前に ::SysFreeString() 関数を呼び出して m_Name の値を消去しています。
STDMETHODIMP CData::get_Value(VARIANT_BOOL *pVal)
プログラム
*pVal = (m_Value ? VARIANT_TRUE : VARIANT_FALSE);
return S_OK;
解説
m_Value 属性に保持されている真偽値を返すために *pVal に値を代入しています。VARIANT_BOOT というのは、Visual Basic において、Boolean として認識される型です。C++ の bool 型のままだと、Visual Basic は Boolean 型だと認識してくれませんので、このような変換をしています。m_Value の値が true の場合、VARIANT_TRUE が、そうでない場合は VARIANT_FALSE が *pVal に設定されます。
STDMETHODIMP CData::put_Value(VARIANT_BOOL newVal)
プログラム
m_Value = (newVal == VARIANT_TRUE);
return S_OK;
解説
m_Value 属性に、newVal で与えられた値を、適切な形に変換して格納しています。Visual Basic などから Boolean として渡されてくる値は Visual C++ の bool や BOOL の値とは違うので変換をしてあげる必要があります。ここでは、演算子を利用して、newVal の値が VARIANT_TRUE であるかを判断し、その結果をちょくせつ m_Value に代入しています。これによって、VARIANT_TRUE が渡された場合には m_Value には true が、VARIANT_FALSE が渡された場合には m_Value には false が設定されます。
初期化と後始末
このインターフェイスが(インスタンスが)生成されたときと削除されたときに適切な対処が行えるように、FinalConstruct() 関数と FinalRelease() 関数を追加します。これらは 「メンバ関数」 として実装します。
FinalConstruct
- 型: HRESULT
- 制御: Public
プログラム
m_Name = NULL;
m_Value = false;
return S_OK;
解説
単純にメンバ変数の値を初期化しています。m_Name に何も代入されていないことを明確にするために NULL を、m_Value にはとりあえず 「偽」 を設定しています。最後の戻り値は、エラーが発生したかどうかを伝えるために使用します。S_OK とは、エラーがなかったということを示しています。
FinalRelease
- 型: void
- 制御: Public
プログラム
::SysFreeString(m_Name);
解説
終了する前に m_Name に格納されている BSTR の値を開放する必要があります。::SysFreeString は、引数に与えられた m_Name の値が NULL の場合、何も処理をしませんので、m_Name が NULL であるかどうかにかかわらず使用することができます。
IDataCollection の作成
インターフェイスの作成
「ATL オブジェクトの新規作成」 から、「シンプルオブジェクト」 を選択して、ショートネームに 「DataCollection」 と打ち込みます。
プロパティの実装
IDataCollection インターフェイスに次のプロパティを追加します。
Count
- 種類: long
- 関数: 取得関数
Item
- 種類: IData*
- 引数: [in] VARIANT Index
- 関数: 取得関数
- DispID: 0
_NewEnum
- 種類: LPUNKNOWN
- 関数: 取得関数
- DispID: -4
- 属性: restricted
Item プロパティは id = 0 とし、_NewEnum プロパティは id = -4 とします。また、_NewEnum プロパティは、アトリビュートを操作して restricted を追加する必要があります。
属性の実装
CDataCollection クラス内に IData を保持するためのメンバ変数を定義します。
m_Count
- 種類: long
- 制御: Private
コレクション IData の個数を保持します。
m_Data
- 種類: IData**
- 制御: Private
IData の参照の参照です。IData 単体は IData* 型の変数へ格納する必要があり、今回はさらに IData を配列として保持したいので、IData** という形になりました。
下準備
C++ ソースファイルの先頭あたりで、次のような記述を行います。
namespace
{
// コレクションを返すときに使用する構造体
typedef CComObject<CComEnum<IEnumVARIANT, &IID_IEnumVARIANT, VARIANT, _Copy<VARIANT> > > EnumVar;
};
メソッドの実装
CDataCollection に IData を追加するためのメソッドを追加します。
Add
- 引数: [in] IData* data
関数の実装
IDataCollection インターフェイスのプロパティを正常に機能させるために、関数にプログラムをコーディングします。
STDMETHODIMP CDataCollection::get_Count(long *pVal)
プログラム
*pVal = m_Count;
return S_OK;
解説
m_Count 属性に保持してあった値を返却します。m_Count が適切な値を示すようにするのは Add メソッドの役割とします。
STDMETHODIMP CDataCollection::get_Item(VARIANT Index, IData **pVal)
プログラム
IData* result = NULL;
switch (Index.vt)
{
case VT_I4:
// 有効な範囲を Index が指しているかの判定
if (0 <= Index.lVal && Index.lVal < m_Count)
{
result = m_Data[Index.lVal];
}
break;
case VT_BSTR:
// Index に指定された名前を探し出します。
for (long i = 0; i < m_Count; i++)
{
BSTR temp;
m_Data[i]->get_Name(&temp);
if (! wcscmp(temp, Index.bstrVal))
{
result = m_Data[i];
break;
}
}
break;
}
// 最終調整
if (result != NULL)
{
// IData を複製して、pVal に返します。
return result->QueryInterface(pVal);
}
else
{
return E_INVALIDARG;
}
解説
まず、switch 文を使って、Index に long 型の値が渡された場合 (VT_I4) と、BSTR 型の値が渡された場合 (VT_BSTR) とに分けました。そして、long 型の場合には、それがすでにある IData* の配列の範囲内であるかを調べて、範囲内である場合にはその IData* を取得して、変数 result に確保します。また BSTR 型である場合は、IData* 配列の中から Name プロパティの値を取得して、Index の値と一致するのあるかを探します。そしてどちらの場合も、最後に QueryInterface() 関数を呼び出して、コピーを pVal に格納しています。
STDMETHODIMP CDataCollection::get__NewEnum(LPUNKNOWN *pVal)
プログラム
long i;
// まずは Variant 配列に、データを用意します。
VARIANT* temp;
temp = new VARIANT[m_Count];
for (i = 0; i < m_Count; i++)
{
VariantInit(&temp[i]);
temp[i].vt = VT_DISPATCH;
m_Data[i]->QueryInterface(IID_IDispatch, (void**)&(temp[i].pdispVal));
}
// 列挙子を書き出して返却します。
EnumVar* pVar = new EnumVar;
pVar->Init(&temp[0], &temp[m_Count], NULL, AtlFlagCopy);
pVar->QueryInterface(IID_IUnknown, (void**)pVal);
// 一時的に使用した Variant 配列を削除します。
for (i = 0; i < m_Count; i++)
{
VariantClear(&temp[i]);
}
delete [] temp;
return S_OK;
解説
はじめに m_Count 個の VARIANT 型配列を作成します。そしてそこへ、m_Data のデータをコピーします。コピー先が VARIANT 型なので、少々手間がかかっています。それが終わったら続いて列挙子の書き出し作業に入ります。最初の行の typedef は、プログラムを見やすくするためのものです (ATL COM プログラミング/SHOEISA より引用)。この型に列挙子を生成します。生成時に QueryInterface() 関数を呼び出して、*pVal に列挙子をセットします。最後に、役目を終えた temp 変数の値を消去しています。
STDMETHODIMP CDataCollection::Add(IData *data)
プログラム
long i;
IData** temp;
temp = new IData* [m_Count + 1];
// フィールドの複製と追加
for (i = 0; i < m_Count; i++)
{
temp[i] = m_Data[i];
}
temp[i] = data;
data->AddRef();
m_Count++;
// 最終調整
delete [] m_Data;
m_Data = temp;
return S_OK;
解説
IData* 型の配列を現在よりも1つ多めに(m_Count + 1)用意して、そこへ現在の m_Data の内容をそのままコピーします。そして多めに作った最後の要素(temp[i])に、引数で受け取った IData* の値を代入します。渡された IData の参照を保持することになるため、AddRef() 関数を呼び出して、参照カウンタをひとつ増加させます(COM のルール)。そして最後に、m_Count の値をひとつ増加させて、追加する前の配列を消去して(delete [] m_Data)、新たに作成した配列を m_Data に代入しています。