MFC クライアント
COM オブジェクトにアクセスするには、何らかのクライアントが必要です。COM プログラミングを学習している開発者の役に立つように、クライアントには以下の特性を持たせます。
- 直観的にわかりやすく、インプリメントしやすいグラフィカル インターフェイス。
- 大規模な COM サーバーや別の COM サーバーの要求を処理できるように、簡単に拡張できるフレームワーク。
- インプリメンテーション コードの大半をウィザードとあらかじめ生成されたコードを使って生成する。
これらの点を考慮すると、AppWizard を使用するダイアログベースのアプリケーションとリソース エディタがMFC クライアントとして最適です。
MFC AppWizard を使用して、ダイアログ ベースのプロジェクトを選択します。MFC AppWizard のステップ 2 のページに表示されるダイアログ ボックス プロジェクトの個々の機能 ([バージョン情報] ボックス、3D コントロール、ActiveX コントロール) は、単純な MFC ダイアログ ベースのアプリケーションでは必要ないため、オフにします。
ユーザー インターフェイスの追加
一般的に、COM サーバー オブジェクトとやり取りするクライアント用のグラフィカル インターフェイスは、ボタン類とエディット ボックスやスライダ コントロールなどのコントロールから構成されます。これらのコントロールを使用して、COM オブジェクトのさまざまなプロパティやメソッドを操作します。したがって、COM サーバーによって公開されるプロパティおよびメソッドがレイアウトを大きく左右します。たとえば、サーバーが 1 つ以上のメソッドを公開する場合は、個々のメソッドに対応するボタン コントロールを追加することができます。こうすれば、ボタンを押すだけで関連するメソッドを簡単に実行することができます。
別の例としては、フィードバックを提供する何らかのコントロールによってプロパティを表す方法があります。これには、プロパティ文字列の現在の値を示す静的テキスト コントロールから、一定の範囲を持つプロパティの現在の値を示すスライダ コントロールに至るまでのさまざまなコントロールが含まれます。よく使われるコントロールを以下に示します。
- テキスト/数値文字列値を表すエディット コントロール。
- 読み取り専用文字列プロパティを表すスタティック テキスト コントロール。
- 色のプロパティおよび画像のプロパティを表す単純なビットマップ コントロール。
- 一定の範囲を持つプロパティを表すスライダー コントロール。
- ブール値を表すラジオ ボタンおよびチェック ボックス。
Visual C++ のダイアログ エディタによって追加されたユーザー インターフェイスは、ObjectString プロパティの現在の値を示す読み取り専用のスタティック テキスト コントロールと ObjectString プロパティを変更するためのボタンとエディット ボックスから構成されます。さらに、QueryString メソッドを呼び出す別のボタンも使用します。
単純なクライアントから COM オブジェクトへのアクセス
ユーザー インターフェイスを追加した後は、MyObj サーバー オブジェクトのインスタンスを作成し、操作できるように、クライアント アプリケーションの内部に変更を加える必要があります。これを行うには、クライアント アプリケーションにインターフェイスの宣言を追加するか、スマート ポインタ (と
#import ディレクティブ) を使用します。ここでは、スマート ポインタを使用する方法について説明します。
クライアント側から COM オブジェクトにアクセスする場合は、以下の 4 つの基本的なステップを経ます。
- COM ライブラリの初期化
- COM オブジェクトの CLSID の取得
- COM オブジェクトの作成と使用
- COM ライブラリの非初期化
COM ライブラリの初期化
COM オブジェクトは、複数の COM ライブラリによってサポートされています。これらのライブラリ (複数の DLL) は、オペレーティング システムの一部です。開発者は、COM ライブラリがオペレーティング システムに合わせて初期化されていることを前提とすることはできません。
したがって、まずやらなければならないことは、クライアント アプリケーションで COM ライブラリを初期化することです。クライアント アプリケーションは MFC を使用しているので、AfxOleInit 関数を使用します。この関数は、COM ライブラリが既に初期化されていなければ、初期化します。この初期化を行うのに適した場所は、アプリケーション クラスの InitInstance 関数の中です。クライアントが起動した後、COM オブジェクトのインスタンス化が試みられる前のかなり早い時期に InitInstance を呼び出します。以下に初期化のコードの例を示します。
//Init OLE libraries and support
if (!AfxOleInit())
{
AfxMessageBox("OLE initialization failed");
return FALSE;
}
COM オブジェクトの CLSID の取得
次のステップは、作成する COM オブジェクトの一意な識別子の取得です。一般に CLSID (128 ビットの整数) と呼ばれるこの識別子は、インスタンス化する COM オブジェクトを特定します。オペレーティング システムは、この番号とシステム レジストリを使ってコンポーネント ファイルを検索します。
この ID を取得する簡単な方法は、プログラム IDを CLSIDFromProgID!href(http://msdn.microsoft.com/library/sdkdoc/com/api1_51gk.htm) 関数に渡す方法です。この Win32 関数は、読み取り可能なプログラム IDを受け取り、レジストリを対象としてその識別子に関連付けられた CLSID を検索します。CLSID が見つかれば、その CLSID を返します。この番号は、後で、COM オブジェクトのインスタンスを作成するときに使用します。
注意 COM オブジェクトのプログラム IDを使用したからといって、正しい COM オブジェクトが見つかることが保証されるわけではありません。プログラム IDの場合は、CLSID と異なり、異なる COM オブジェクトが同じプログラム IDを持っていることがあります。ただし、ほとんどの場合は、プログラム IDで十分に間に合います。
クライアント アプリケーションがダイアログ ベースであるため、このステップを実行するのに適した場所は、OnInitDialog 関数の中です。この関数は、ダイアログが表示される前に呼び出されるため、アプリケーションは、すばやく実行を終了し、エラー (CLSID の取得の失敗) が発生したことをアプリケーションのユーザーに伝えることができます。CLSIDは、以下のコードを使って取得します。
CLSID clsID;
HRESULT hr;
hr= CLSIDFromProgID(OLESTR("MySvr.Object1"), &clsID);
if(FAILED(hr))
{
AfxMessageBox("Retrieval of ProgID failed");
return FALSE;
}
上のコードでは、プログラム ID (この例では MySvr.Object1) を CLSIDFromProgID 関数呼び出しに渡し、戻り値をチェックしてエラーの有無を確認します。CLSID が取得されれば、COM オブジェクトをインスタンス化できます。
COM オブジェクトの作成
#import ディレクティブを利用することにより、ATL COM オブジェクトの作成および使用の手順を簡易化することができます。このディレクティブは、スマート ポインタを使用することにより、あらゆる COM オブジェクトに対する比較的簡単で安全なアクセスを可能にします。スマート ポインタの宣言と用法は C++ ポインタに似ていますが、スマート ポインタには COM 固有の利点がいくつか追加されています。
以下の例では、サンプル COM サーバー アプリケーション MySvr のタイプ ライブラリをインポートし、生成されるヘッダ ファイルで名前空間が使用されることを防止しています (名前空間とスマート
ポインタの使用 を参照してください)。
#import"D:\VS\MyProjects\MySvr\MySvr.tlb" no_namespace
上に示したコードは、プロジェクトに自動的に含まれる 2 つの新しいファイル (.TLI タイプと .TLH タイプ) を作成します。これらのファイルの中で重要なのは、スマート ポインタ クラスの宣言です。
//
// Smart pointer typedef declarations
//
_COM_SMARTPTR_TYPEDEF(IMyObj, __uuidof(IMyObj));
このコード サンプルは、定義済みマクロ (_COM_SMARTPTR_TYPEDEF) を使用し、_com_ptr_t をベースにして、IMyObjPtr (名前の Ptr の部分はコンパイラによって自動的に追加される) と呼ばれる、_com_ptr_t!href(http://msdn.microsoft.com/library/devprods/vs6/vc++/vclang/_pluslang__com_ptr_t.htm) クラスを特殊化したスマート ポインタ クラスを作成します。_com_ptr_t クラスの中の有用な関数は、CreateInstance!href(http://msdn.microsoft.com/library/devprods/vs6/vc++/vclang/_pluslang__com_ptr_t.3a3a.createinstance) 関数です。このポインタは、クライアント アプリケーションのコードの中で頻繁に使われます。
スマート ポインタ クラスのほかに、プロパティにアクセスし、メソッドを呼び出すタイプ セーフなメンバ関数が含まれたインターフェイス オブジェクトが生成されます。このインターフェイス オブジェクトを以下に示します。
IMyObj : IUnknown
{
// Property data
__declspec(property(get=GetObjectString,put=PutObjectString))
_bstr_t ObjectString;
// Wrapper methods for error-handling
_bstr_t GetObjectString ( );
void PutObjectString ( _bstr_t pVal );
HRESULT QueryString ( );
// Raw methods provided by interface
virtual HRESULT __stdcall get_ObjectString ( BSTR * pVal ) = 0;
virtual HRESULT __stdcall put_ObjectString ( BSTR pVal ) = 0;
virtual HRESULT __stdcall raw_QueryString ( ) = 0;
};
上のコード中の IMyObj は、1 つのデータ メンバ (_bstr_t タイプの ObjectString) と、タイプ セーフな方法で ObjectString プロパティにアクセスする 2 つのメンバ関数 GetObjectString および SetObjectString を提供します。これらの関数については、COM オブジェクトの使用vcwhite_UsingtheCOMObject で説明しています。
通常、COM オブジェクトは、CoCreateInstance (別の Win32 関数) への呼び出しによって作成されます。これは、複数のパラメータを必要とする、かなり複雑な関数です。しかし、サンプル アプリケーションは COM サーバー オブジェクトのタイプ ライブラリをインポートしているため、これよりはるかに単純なメソッドを利用することができます。スマート ポインタ クラスは _com_ptr_t から派生しているため、CoCreateInstance 関数を改良した強力な関数を利用することができます。COM オブジェクトは、スマート ポインタ オブジェクトの CreateInstance 関数を呼び出し、新たに取得した目的の COM オブジェクトの CLSID を渡すことによって作成されます。OnInitDialog 関数から取り出した以下のコード サンプルでは、CLSIDFromProgID への呼び出しによって取得した CLSID を使用し、COM サーバー オブジェクト MySvr のインスタンスを作成しています。
CLSID clsID;
HRESULT hr;
hr= CLSIDFromProgID(OLESTR("MySvr.MyObj"), &clsID);
if(FAILED(hr))
{
AfxMessageBox("Retrieval of ProgID failed");
return FALSE;
}
m_pMyObj.CreateInstance(clsID); //does the work of CoCreateInstance
m_pMyObj (IMyObjPtr タイプ) がダイアログ クラスのデータ メンバである点に注目してください。これは、ダイアログ クラスのどの関数からもプロパティとメソッドにアクセスできるようにするために必要な措置です。
COMオブジェクトの使用
COM オブジェクトへのポインタが利用できるようになったので、ダイアログ ボックスのインターフェイスのインプリメンテーションを完成させることができます。このインターフェイスは、基本的に、目的のアクションに対応する COM ラッパー クラスの適切なメンバ関数の呼び出しから構成されます。
たとえば、クライアント アプリケーションには、Invoke QueryString メソッドというラベルが付いたボタンがあります。ユーザーがこのボタンを押すと、QueryString メソッドが呼び出されます。
以下のように IMyObj::QueryString 関数を呼び出すことによって、ハンドラを完成させます。
m_pMyObj->QueryString();
ダイアログ クラスの m_pMyObj ポインタを通じて QueryString 関数にアクセスしている点に注目してください。
ほかのカスタム ボタン ハンドラ (Update ObjectString 値) のインプリメンテーションを完成させるために、以下のコードを関数ハンドラの本体に追加しました。
_bstr_t newbstr;
CString tmpCStr;
GetDlgItemText(IDC_NEWPROPVAL, tmpCStr);
newbstr= tmpCStr;
m_pObject1->PutObjectString(newbstr);
//Update static text with new value
BSTR tmpBStr;
m_pMyObj->GetObjectString(&tmpBStr);
_bstr_t tmpbstr(tmpBStr, FALSE);
SetDlgItemText(IDC_CURPROPVAL, tmpbstr);
このコードは、ユーザーが Update ObjectString 値ボタンを押すと実行されます。このコードは、まず、新しい文字列値を IDC_NEWPROPVAL エディット ボックス コントロールから取り出します。次に、この値を書式化し、PutObjectString ラッパー メソッドに渡します。次に、GetObjectString ラッパー メソッドを呼び出すことによって、現在の ObjectString 値を表示する静的テキスト コントロールを更新します。戻り値は適切に書式化され、SetDlgItemText Win32 関数に渡されます。
注意 BSTR
と文字列の操作 で説明しているように、BSTR の操作に関連するメモリ リークを回避するには、_bstr_t コンストラクタへの呼び出しが必要です。
COM ライブラリの非初期化
クライアント アプリケーションが COM オブジェクトを使い終わったら、オブジェクトを破棄し、COM ライブラリを非初期化する必要があります。
ただし、MFC クライアント アプリケーションおよびスマート ポインタ インプリメンテーションの場合は、非初期化が自動的に行われます。フレームワークによって供給される標準の終了コード以外には、クライアント アプリケーションをクリーンに終了するために必要なものはありません。
名前空間とスマート ポインタの使用
#import ディレクティブと組み合わせて使用できる属性の 1 つは namespace (名前空間) です。名前空間は、C++ オブジェクトどうしの名前の衝突を防止するために使用します。MyClient サンプルは単純なので、名前空間属性を使用しません。しかし、複雑なアプリケーションの場合は、#import を使用する際に名前空間の衝突が発生することがよくあるため、以下では、名前空間の概要を説明します。
たとえば、ソース ファイルに CMyClass という単純な C++ クラスの宣言が含まれているとします。開発の過程で、別の単純な C++ クラス宣言が含まれた外部ファイルが後からプロジェクトに追加されます。インプリメンテーションが異なっているにもかかわらず、あいにく、両方のクラスが CMyClass という同じ名前を共有しています。コンパイル時には、この "名前空間の衝突" のためにコンパイラ エラーが発生します。
これを解決する 1 つの方法は、1 つまたは両方のクラスの名前空間を作成する方法です。これを行うには、CMyClass の宣言領域を名前空間で囲みます。たとえば、以下のコード サンプルは、CMyClass の名前空間 Original を作成します。
namespace Original {
class CMyClass {
Int m_iData;
};
} //namespace Original
CMyClass の名前空間が作成されたため、このクラスに対するすべての参照は、名前空間名による明示的な修飾子を使用する必要があります。たとえば、以下のコードは、スタック上に新しい CMyClass オブジェクトを作成します。
Original::CMyClass class1;
タイプ ライブラリをインポートするときは、no_namespace 属性を使うことによって名前空間を抑制することができます。ただし、名前空間を抑制すると、名前の衝突が起きることがあります。名前空間の名前を rename_namespace 属性によって変更することもできます。
BSTR と文字列の操作
COM プログラミングにおける重要な問題の 1 つは、BSTR 型変数の操作です。主に BSTR 型データを渡したり、コピーしたりするときに起きる現象ですが、BSTR 型オブジェクトが問題を引き起こすことがあります。単純なクライアント サンプルでは、以下の 2 つの問題が発生します。
- BSTR
型変数を取らない関数。
- BSTR
リソースをコピーすることによって発生するメモリ リーク。
_bstr_t オブジェクトを使ってこれらの問題を解決できることもあります。このオブジェクトは、BSTR データ型をカプセル化し、リソースの割り当てと割り当ての解除を自動的に管理し、広い範囲の自動データ変換 (その中の 1 つは単純なクライアント アプリケーションにきわめて有用) に対応します。
MyClient サンプル アプリケーション (特に OnUpdateProp 関数) では、ObjectString プロパティの現在の値が一般的な MFC 関数 CWnd::SetDlgItemText に渡されます。引数の 1 つは LPCTSTR です。あいにく、BSTR データ型は、必要とされる変換に対応していません。ただし、_bstr_t は LPCTSTR に変換できます。これにより、プロパティ値の明示的なキャストを行わなくても、以下のコードをコンパイルすることができます。
//Update static text with new value
BSTR tmpBStr;
m_pObject1->get_ObjectString(&tmpBStr);
_bstr_t tmpbstr(tmpBStr, FALSE); //necessary to avoid a memory leak
SetDlgItemText(IDC_CURPROPVAL, tmpbstr);
注意 変換の問題は、Unicode のビルドでは発生しません。ただし、Win32 のビルドでは変換が必要です。
キャストの問題のほかに、メモリの問題もあります。上記のコードの代わりに以下のコード サンプルを使用した場合は、メモリ リークが発生します。
//Update static text with new value
BSTR tmpBStr;
m_pObject1->get_ObjectString(&tmpBStr);
_bstr_t tmpbstr;
tmpbstr= tmpBStr; //Caution: Memory leak occurs
SetDlgItemText(IDC_CURPROPVAL, tmpbstr);
リークは、tmpbstr 変数が初期化されるときに発生します。tmpbstr 変数の作成時に SysAllocString への呼び出しが自動的に行われます。この新しい割り当ては後になっても解除されないため、メモリ リークが発生します。_bstr_t コンストラクタのこのバージョンを使用すると、SysAllocString への呼び出しなしに BSTR オブジェクトを tmpbstr にアタッチすることにより、問題を回避することができます。この問題の詳細については、http://msdn.microsoft.com/library/devprods/vs6/vc++/vclang/_pluslang__bstr_t.3a3a._bstr_t.htm を参照してください。
参考資料
このペーパーでは、ATL COM サーバーとそれに付随する MFC クライアントの開発を検討しました。この問題に関心がある場合は、以下のトピックをお読みになることをお勧めします。
以下の記事は、最新の MSDN ライブラリにあります。