Dr. GUI と ATL

第 1 部 :1998 年 9 月 30 日
第 2 部 :1998 年 11 月 23 日

目次

第 1 部:非常に単純な ATL コンポーネント
第 2 部:複数のインターフェイスを持つコンポーネント

第1部:非常に単純な ATL コンポーネント

今までの復習、これからの予定

2 月に(1998 年のことですが、もっと昔に思えますね)、Dr. GUI は Active Template Library(ATL)についての連載コラムを開始しました。

しかしこの名医は、だれもが COM の使い方や C++ テンプレートの使い方を知っているわけではないことはよくわかっていました。そこでコラムを 1 つ、2 つ書いて、COM とテンプレートについて説明し、みんなが話題についてこられるようにすることを目指しました。

その顛末はご存知のとおりです。1 つ、2 つのコラムのつもりが、8 つ、9 つのコラムになってしまいました。具体的には、COM についての 8 つのコラム(「Dr. GUI、コンポーネント、COM、およびATLを使う」を参照)と、C++ テンプレートについての長いコラムが 1 つ(「Dr. GUI、テンプレートを語る」を参照)できてしまったのです。

しかし、これで Dr. GUI は ATL には触れずに必要な説明をすべて終わったので、いよいよ ATL に掘り下げていきます。では、さっそく始めましょう。

ATL コンポーネント

ATL とは何でしょう?

簡単な歴史

ATL は当初、小規模な COM コンポーネントをすばやく作成するための方法として設計されました。特に、複数階層アーキテクチャに、ビジネス ルールやデータベース アクセスなどのオートメーション コンポーネントを実装することを目的としていました。

ATL の最初のバージョンでは、ユーザー インターフェイスのための機能は何も備えていませんでした。たとえば、ATL 1.0 のコントロールはビジュアルな ActiveX コントロールにすることはできませんでした(つまり、ATL を使わずに C++ でこれを実装するために必要となる作業量と同程度の作業をせずにはできないということです)。バージョン 2.0 では、ビジュアルな ActiveX コントロールを構築するために必要なテンプレートが追加されました。ActiveX コントロールについては、後日説明します。

機能

ATL は、以下の機能を含めて、いくつかの重要な機能を提供します。

  • C++ の能力のすべて

  • 使いたい場合を除けば、ランタイム ライブラリはなし

  • オブジェクトとインターフェイスを比較的高いレベルで抽象化する方法

  • クラス ファクトリ、オブジェクト作成、参照カウント、および QueryInterface の自動処理

  • 標準インターフェイスの実装のストック

この最後のポイントがおそらくはもっとも重要なものです。何の助けもなく C++ で ActiveX コントロールを作成する場合の最大の問題は、ユーザーが実装するすべてのインターフェイスのすべてのメソッドの実装をユーザーが作成する必要があるということです。そして、ビジュアルな ActiveX コントロールの場合には、インターフェイスの数は 1 ダースを超えます。これらのメソッドはどれも、ユーザーが解決しようとしている問題には何の関係もないものですが、それでもユーザーはこれらの難解なメソッドを正しく作成する必要があるのです。

代替案

MFC、Visual Basic、Visual J++、Visual FoxPro、および Delphi が ActiveX コントロールの作成に愛用されているのはこのためです。これらのランタイム コードは、1 ダースものインターフェイスのすべてのメソッドをユーザーの代わりに実装するため、ユーザーは解決するべき問題に集中することができます。

しかし、ユーザーの ActiveX コントロールを実装するためにこれらの方法を使うのは、割りの合わない取り引きかもしれません。これらの製品のランタイム コードは、程度の差こそあれ、巨大で低速です。コントロールを小さく、高速なものにしたい場合には、C++ で直接に実装するか、ATL を使う必要があります。

容易で、高速なものにする必要性

ATL はどのようにして、最小限のコードにとどめることができるのでしょう。まず、ATL はランタイム ライブラリに依存しません。そのため、コントロールがランタイム ライブラリを使う必要があるのは、あなたのコードがそれを使う場合だけです。次に、テンプレートの魔法により(「Dr. GUI、テンプレートを語る」)、ATL コントロールには実際に必要とされるコードしか含まれません。このため、ATL コントロールのサイズと速度は、COM のエキスパートが C++ で手作業で作ったコントロールに比べて遜色のないものになります。しかし、はるかに簡単に作れるのです。

他の人のコード

Microsoft Foundation Classes の開発の中心人物の一人であるディーン マックローリーは、MFC を使う最大の利点の 1 つは、あなた自身が作成やデバッグをしたものではないコード、つまり他人のコードを再利用できることであるとしばしば言っています。MFC は、自分で書く必要のない何千行ものコードを無償で見られるようにしています。また、これにより OLE インプレース編集サーバーも簡単に作れます。何千行ものコードを書かなくてすむのです。

これらのコードは、テスト済み、デバッグ済みの場合には特に貴重です。たしかに、ライブラリが完璧とは限りません。しかし、成熟したライブラリのコードは、多くのプログラマによって使われてきており、デバッグされています。あなたがいかに優秀でも、限られた時間を考えれば、ライブラリのほうが高速で、堅牢なはずです。ライブラリは基本的に、すべての開発者の優れた知識をカプセル化しているため、あなたはこれを信頼し、再利用することができるのです。

テンプレート:再利用の別の形式

テンプレート ライブラリでは、関数ライブラリ、またはクラス ライブラリとは別の種類の再利用が行われます。まず、テンプレートの再利用では、ソース コードを直接再利用します。つまり、プログラムを対象にオプティマイザを最大限に適用して効率化できるということです。また、テンプレート ライブラリの中でバグを発見した場合には、テンプレートのソースを修正することでバグを修正できるということを意味しています。MFC や CRT を再構築し、新しいコピーを配布する必要はありません。新しいテンプレートを使ってプロジェクトの新しいバージョンをビルドするだけで、バグはなくなります。

あなたのソース コードを再配布してもかまわないのであれば、テンプレート ライブラリは 2 つの意味で最良のものになります。あなた自身ですべてを作成した場合と同じくらい優れたものであるほかの誰かのコードを、効率的に再利用することができるのです。

私たちの ATL コンポーネント

今回インストールするコンポーネントは非常に単純なものです。Beep メソッドが呼び出されると、ビープ音を発するコンポーネントです。しかし、もっとおもしろいものにするために、Beep が呼び出されたときにコンポーネントがビープ音を発する回数を示すプロパティも追加してあります。

このコンポーネントはスピーカからビープ音を出すため、本質的には単一スレッドです。したがって、そのようなものとして生成することにします。しかし、あらゆる COM コンポーネント クライアントから(スクリプト クライアントからでさえも)使えるように、デュアル インターフェイスを持たせることにします。

モジュールとそのコンポーネントを作成するための 2 つのステップ

モジュール(DLL または EXE)は、複数のコンポーネントを実装できるため(「Dr.GUI、コンポーネント、COM,およびATLを使う」 の中の図 7 を参照)、ATL ウィザードは統合開発環境(IDE)の中でコンポーネント作成工程を 2 つのステップに分けています。最初にモジュールを作成してから、モジュールの中にコンポーネントを追加します。

モジュールの作成

モジュールの作成は非常に簡単です。[ファイル]メニューから[新規作成…]を選択してから、[プロジェクト]タブを選択し、ディレクトリとプロジェクト名を記入します(ここではコントロールの名前を選択しないように気を付けてください。Dr. GUI は通常、わかりやすいように名前の最後に "Mod" を付けます)。プロジェクト ダイアログ ボックスは、次のように表示されます(ところで、Dr. GUI は「ATL COM AppWizard」がデフォルトとなっているのは偶然だとは思いません。それとも、アルファベット順で最初になっているだけでしょうか?)

OK]をクリックすると、今まで見た中でもっとも短いウィザードが表示されます。

(Dr. GUI は特に「ステップ 1/1 」の部分が気に入っています。)

ここで選択する主なものはモジュールのタイプです。DLL(インプロセス サーバー用)、EXE(プロセス外サーバー用)、または NT Service EXE が選択できます。ここではインプロセス(DLL)を選択します。

今のところは、MFC、MTS、またはプロキシ / スタブ コードは使わないため、最後の 3 つのチェックボックスは無視することにします。

終了]をクリックすると、ファイルのグループを含むプロジェクトができあがります。これは、sample ディレクトリの Step 0 サブディレクトリに入っています。

  • タイプ ライブラリの初期宣言だけを含む、プロジェクト用の .idl ファイル。

  • COM DLL がエクスポートする必要のある 4 つの機能のエクスポートを含むリンカの .def ファイル

  • プロジェクト名のバージョン リソースと文字列を含む .rc リソース ファイル。

  • リソース ID 定義を含むリソース ヘッダ ファイル。

  • 事前コンパイルされたヘッダを使用した高速構築のためのシステム インクルードを適切に行うための、stdafx.h および stdafx.cpp ファイル。

  • そして最後に、COM DLL のための必要なすべてのグローバル機能の実装を含むソース コード .cpp ファイル。このファイルについて詳しく説明します。

module.CPP ファイルの中身

このファイルの中には、インクルードと、グローバル オブジェクトの宣言と、1 つのマップが含まれています。

  #include "stdafx.h"
#include "resource.h"
#include <initguid.h>
#include "BeepCntMod.h"
#include "BeepCntMod_i.c"

CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()

stdafx.h と resource.h についてはすでに説明しました。ファイル initguid.h は、グローバル固有識別子(GUID)構造が定義されるようにするために、プロジェクトの中で 1 つのファイルだけでインクルードする、標準の OLE システム ファイルです。

BeepCntMod.h と BeepCntMod_i.c は、プロジェクトを構築するときに、Microsoft インターフェイス定義言語(MIDL)によって IDL ファイルから生成されます。MIDL は C++ コンパイラが実行される前に実行されるので、ファイルは、構築のための十分な時間を使用して作成されます。

BeepCntMod.h は、インターフェイスとコンポーネントの宣言を含みます。今のところ、これは非常につまらないファイルです。BeepCntMod_i.c も同じくらいつまらないものです。これは、私たちが使う GUID の定義を含んでいます。

次は、_Module と呼ばれる変数のグローバル宣言です。このオブジェクトは、クラス オブジェクト(クラス ファクトリ)と、クラスの登録など、その他のモジュール指向のコードのすべてを含みます。詳細については、MSDN Library オンラインの中で CComModule の資料を参照してください。

このセクションの最後に、空のオブジェクト マップがあります。オブジェクト ウィザードがこのマップに記入します。マップ配列の各要素は、CLSID と C++ クラス名を含みます。_Module オブジェクトは、CLSID に基づいてオブジェクトを作成できるように、このマップを読み取ります。

次は DllMain 関数です。これは、単に _Module オブジェクトの中の Init および Term 関数を呼び出すものです。

  extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason,
          LPVOID /*lpReserved*/)
{
  if (dwReason == DLL_PROCESS_ATTACH)
  {
    _Module.Init(ObjectMap, hInstance, &LIBID_BEEPCNTMODLib);
    DisableThreadLibraryCalls(hInstance);
  }
  else if (dwReason == DLL_PROCESS_DETACH)
    _Module.Term();
  return TRUE;  // ok
}

DllMainDisableThreadLibraryCalls も呼び出しているため、新しいスレッドが追加されるたびに DLL が呼び出しを受けることはありません。

次に、COM DLL に必要な 4 つの関数があります。

  STDAPI DllCanUnloadNow(void)
{
  return (_Module.GetLockCount()==0) ? S_OK :S_FALSE;
}

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
  return _Module.GetClassObject(rclsid, riid, ppv);
}

STDAPI DllRegisterServer(void)
{
  // オブジェクト、タイプライブラリおよびタイプライブラリ内の全てのインターフェイスを登録します
  return _Module.RegisterServer(TRUE);
}

STDAPI DllUnregisterServer(void)
{
  return _Module.UnregisterServer(TRUE);
}

これらの関数は、_Module オブジェクトの中の対応する関数を呼び出すことを除けば、何もしません。このため、登録と登録抹消の方法を知っていて、アンロードをしてもよいかどうかを指示し、クラス オブジェクトを返す(オブジェクトのインスタンス化処理一部)のは、_Module オブジェクトです。

これらの関数の機能について調べるには、資料で ATL ソースを見てください。Dr. GUI にとって、ソースを検索する場合の最良の友は、GREP に似た Visual C++ の「ファイル内を検索」機能です。

オブジェクトの作成

モジュール オブジェクトについて少し理解したところで、これに COM コンポーネントを追加することにします。

COM コンポーネントを追加するもっとも簡単な方法は、ATL オブジェクト ウィザードを使う方法です。[挿入]メニューから[ATL オブジェクトの新規作成…]を選択するだけで、次のようなダイアログ ボックスが表示されます。

ここで[シンプル オブジェクト]を選択します。[次へ]をクリックすると、オブジェクトの名前と、その属性を設定するためのプロパティ シートが表示されます。ここで、[ショート]編集コントロールの中に BeepCnt(「Beeper with Count(カウント付きのビー音発生機能)」の省略)と入力します。残りはウィザードが埋めてくれますが、必要であれば編集コントロールの内容を変更できます。

アトリビュート]タブをクリックすると、次のウィンドウが表示されます。すべてデフォルトでかまわないため、[OK]をクリックします。

「アパートメント」スレッド モデルでは、オブジェクトを複数のスレッドで使用できますが、COM により、オブジェクトを作成したスレッドだけが、そのオブジェクト上のメソッドを呼び出すことができるようになっています。オブジェクトの各インスタンスは本質的にシングル スレッドです。異なるスレッドで複数のオブジェクトを作成できることを思い出してください。このため、ユーザーのメソッドの中でグローバル データが使われている場合には、これに対するアクセスは、スレッドセーフにする必要がありました。いずれにしても通常はオブジェクトのインスタンス データしか使用していないため、これは大きな問題ではありません。グローバル データを使用する場合には、スレッドセーフにするか、このボックスでシングル スレッドを選択する必要があります。

アパートメント モデル オブジェクトでは、クラス ファクトリはマルチスレッド セーフにする必要がありますが、オブジェクト自体ではその必要はありません。ATL はユーザーの代わりにクラス ファクトリを実装しているため、ユーザーが心配しなくても、ATL がユーザーの代わりに必要なことをします。

このオブジェクトにはデュアル インターフェイスを使用します。あまり詳しくならないように説明すると、これは、オブジェクトをカスタム インターフェイスから呼び出すことも(COM シリーズで開発されたものと同じように)、自動化、つまり IDispatch インターフェイスから呼び出すこともできることを意味します。スクリプト言語は自動化インターフェイスを使用するため、[デュアル]を選択すると、このオブジェクトはスクリプト言語から使用できるようになります。カスタム インターフェイスを使用したほうがパフォーマンスははるかに高いため、クライアントでこれが使えるようなら(ほとんどの場合は可能です)、これを使いましょう。

アグリゲーション(集成)は、オブジェクトの内部に別のオブジェクトを含める(ネストする)ための特殊な方法です。アグリゲーションはかなり複雑な作業ですが、ATL はユーザーの代わりに細かいことをすべて処理します。このオブジェクトはアグリゲーションをしたほうが柔軟性が高くなるため、「はい」を選択したままにしておきます。

ISupportErrorInfo は、拡張エラー情報のためのもので、Visual Basic で主に使われます。これはスキップします。また、主にイベントに使われるコネクション ポイントもスキップします。フリー スレッド マーシャラもスキップします。これは、特定のタイプのマルチスレッド コントロールに使用されるものです。

おもしろいことをお教えましょう。私たちの COM オブジェクトは、開発が簡単なだけでなく、さらに強力なものになるのです。ほかの人が作成した ATL コードを使用することにより、アパートメント モデル、デュアル インターフェイス、およびアグリゲーションのサポートを自由に利用できます。

オブジェクトの中身

ATL オブジェクト ウィザードは、私たちのプロジェクトにいくつかのファイルを追加し、その他のいくつかの変更を加えています(ところで、Visual Studio に付いてくる WinDiff プログラムは、ウィザードがファイルをどのように変更するかを理解するための天の助けです。「変更前」のファイルを別のディレクトリに保存し、新しいウィザードを実行してから、WinDiff を使って何が変わったかを確認してみてください)。

サンプル ファイルをダウンロードしてある場合には、このセクションのファイルが、sample ディレクトリの Step 1 サブディレクトリに入っているはずです。

ATL オブジェクト ウィザードは、beepcnt.cpp および beepcnt.h オブジェクトの実装およびヘッダ ファイルを追加しています。beepcnt.cpp ファイルはほとんど空です(まだメソッドもプロパティも実装していないため)。ここには、あとで定義するメソッドとプロパティを実装するための関数が含まれることになります。beepcnt.h ファイルは、このコンポーネントの実装 クラスの宣言を含みます。

  class ATL_NO_VTABLE CBeepCnt :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CBeepCnt, &CLSID_BeepCnt>,
  public IDispatchImpl<IBeepCnt, &IID_IBeepCnt, &LIBID_BEEPCNTMODLib>
{
public:
  CBeepCnt()
  {
  }

DECLARE_REGISTRY_RESOURCEID(IDR_BEEPCNT)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CBeepCnt)
  COM_INTERFACE_ENTRY(IBeepCnt)
  COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IBeepCnt
public:
};

私たちはすでにテンプレートを使っています。このコンポーネント クラスは、1 つではなく、3 つのテンプレート化されたクラスから派生しています。CComObjectRootEx はオブジェクトの参照カウントを扱い、CComCoClass はクラス ファクトリ関係を処理します。もっとも興味深いテンプレート化されたクラスは、IDispatchImpl です。これは、インターフェイス ID として IID_IbeepCnt を使用する、IBeepCnt に基づくデュアル インターフェイスを提供します(カスタム インターフェイスを使用した場合には、IBeepCnt から直接派生します)。

このクラスは多数のマクロも含みます。ATL_NO_VTABLE は、このクラス用に vtable を構成しないようにコンパイラに指示します。これは、それ自体がインスタンス化されたものではないクラスでしか使用できません。しかし、CBeepCnt が私たちのオブジェクトの実装である場合には、直接にインスタンス化しないためにはどうしたらいいのでしょうか。

ATL が CBeepCnt からクラスを派生し、これをインスタンス化するというのが、その答えです。CBeepCnt タイプのオブジェクトは決して作成されることがないので、CBeepCnt は vtable を必要としません。このため、ATL_NO_VTABLE を使ってメモリを節約できます。

しかし、ほかのタイプのオブジェクトではどうなるのでしょう。この場合、作成されるオブジェクトのタイプは実際には CComObject<CBeepCnt> になります。言い換えるならば、私たちのオブジェクト タイプ をパラメータとして使用して、CComObject テンプレート クラスを使用するのです。派生の様子を示すと次のようになります。

どうしてこのようにするのでしょう? そうですね、CComObject の主な仕事は、IUnknown メソッドの実装を提供することです。IUnknown. から派生したすべてのインターフェイス間で実装を共有できるようにするためには、これらは、最後に派生されたクラスの中で提供する必要があります。CComObject のメソッドは、実装を CComObjectRootEx の中だけで呼び出していることに注目してください。

すでに推察しているかもしれませんが、ATL でよく発生するエラーには、実装したクラス(この例では CBeepCnt)を対象に new を呼び出してオブジェクトを作成しようとした場合があります。この場合は、奇妙なエラーが発生して失敗します。オブジェクトを作成するには、CoCreateInstance を使用してください。

次に注目すべきものは COM マップです。ここには、カスタム インターフェイスと IDispatch、つまりオートメーション インターフェイスという、これから実装する 2 つのインターフェイスのためのマクロが含まれています。IUnknown のためのエントリは、想定されているため、ここに含めることはしません。

またこのほかにも、リソース ID をレジストリ エントリに関連付けるためのマクロ(次に詳しく説明します)、偶然に削除されないようにオブジェクトの構成方法を変更するマクロなど、いくつかのマクロがあります。

新しいファイルのうち最後のもの、beepcnt.rgs は、ATL のレジストリ操作コードのスクリプトのソースを含みます。このほとんどは、COM ランタイム コードがコントロールを見つけることができるように設定しておく必要のある、レジストリ エントリと正確に対応しています。私たちの単純なコントロールの場合、コードは次のようになります。

  HKCR
{
  BeepCntMod.BeepCnt.1 = s 'BeepCnt Class'
  {
   CLSID = s '{AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF}'
  }
  BeepCntMod.BeepCnt = s 'BeepCnt Class'
  {
   CLSID = s '{AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF}'
   CurVer = s 'BeepCntMod.BeepCnt.1'
  }
  NoRemove CLSID
  {
   ForceRemove {AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF} =
                     s 'BeepCnt Class'
   {
     ProgID = s 'BeepCntMod.BeepCnt.1'
     VersionIndependentProgID = s 'BeepCntMod.BeepCnt'
     ForceRemove 'Programmable'
     InprocServer32 = s '%MODULE%'
     {
      val ThreadingModel = s 'Apartment'
     }
     'TypeLib' = s '{170BBD8D-4DE8-11D2-A2E0-00C04F8EE2AF}'
   }
  }
}

このスクリプトは、コンポーネントの登録と登録抹消の両方で使われます。登録時にはデフォルト設定で、すべてのキーが、レジストリの中にあるキーに追加されます。ForceRemove キーワードはこの動作を変更するものです。ForceRemove が適用されたキーは、追加して戻される前に、削除されます(サブキーも含めて)。

登録抹消の場合の標準では、スクリプトの中にリストされたすべてのキー(そのサブキーも)が削除されます。CLSID キーは上記のように NoRemove キーワードを使用して必ず上書きしてください。これをしないと、このコンポーネントの登録抹消によって CLSID ツリー全体が削除されるため、システム上のすべての COM オブジェクトが登録抹消されます。このため、レジストリ スクリプト ファイルを編集するときには特に注意してください。

しかし通常の場合は、このファイルを編集する必要はありません。オブジェクトとインターフェイスを追加すると、ウィザードがユーザーに代わってこれを編集します。

リソース コンパイラは、このスクリプトをコンポーネント リソース セクションの中に構築します。この中では ID(この場合は IDR_BEEPCNT)を使用して使用できます。

既存のファイルのいくつかにも多少の変更が加えられます。

  • BeepCntMod.cpp では BeepCnt.h を #include し、CBeepCnt:のオブジェクト マップ エントリが追加されています。

    BEGIN_OBJECT_MAP(ObjectMap)
    OBJECT_ENTRY(CLSID_BeepCnt, CBeepCnt)
    END_OBJECT_MAP()
    
    

  • .idl ファイルでは IDispatch から派生した IBeepCnt のインターフェイス エントリが追加され、BeepCnt クラスのタイプ ライブラリには coclass エントリが追加されています。

  • .rc ファイルではいくつかの言葉が変更され、DLL のリソース セクションにレジストリ スクリプト ファイルを含めるために 1 行が追加されています。

構築を行うと、MIDL は新しいオブジェクトを反映して新しい BeepCntMod.h を生成します。

メソッドとプロパティの追加

ATL コンポーネントの中身を理解したら、メソッドとプロパティは簡単に追加できます(たしかに、こんなに詳しく説明しなくてもできたはずですが、それではあまりおもしろくないでしょう)。

メソッドを追加するには、クラス表示に切り替えて、IBeepCnt インターフェイス コネクタを右クリックします。[メソッドの追加…]を選択し、ダイアログ ボックスに次のように入力します。

この方法でメソッドを追加すると、IDL ファイルには必要なコードがすべて追加され、BeepCnt.cpp と BeepCnt.h ファイルにも正しいコードが追加されます(この部分のコードは main ディレクトリに入っています)。

プロパティの追加も同じようにできます。インターフェイスを右クリックし、[プロパティの追加…]を選択します。そして、次のようにダイアログ ボックスに入力します。

この結果、ウィザードによってインターフェイス宣言にメソッドとプロパティが追加されて、.idl ファイルが更新されます。

  interface IBeepCnt :IDispatch
{
  [id(1), helpstring("method Beep")] HRESULT Beep();
  [propget, id(0), helpstring("property Count")]
   HRESULT Count([out, retval] long *pVal);
  [propput, id(0), helpstring("property Count")]
   HRESULT Count([in] long newVal);
};

ここでは、Visual Basic がプロパティをデフォルト プロパティと見なすようにするために、プロパティの ID を”0”に変更しています。これにより、Visual Basic コードの中で "Beeper.Count" ではなく、"Beeper" を使用してプロパティを参照できるようになります。

BeepCnt.h ファイルは、3 つの新しい関数の宣言を含み、BeepCnt.cpp ファイルは関数のスケルトン本体を含みます。

  STDMETHODIMP CBeepCnt::Beep()
{
  // TODO:ユーザーの実装 コードをここに追加
  return S_OK;
}

STDMETHODIMP CBeepCnt::get_Count(long *pVal)
{
  // TODO:ユーザーの実装 コードをここに追加
  return S_OK;
}

STDMETHODIMP CBeepCnt::put_Count(long newVal)
{
  // TODO:ユーザーの実装 コードをここに追加
  return S_OK;
}

私たちのコンポーネントがささやかでも実際になんらかの機能を実行するようにするためには、コードをいくらか作成する必要があります。最初に、ビープ音の回数のカウンタをクラスに追加し、これをコンストラクタの中で 1 に初期設定します。

    long cBeeps;
  CBeepCnt() :cBeeps(1) { }

次に、メソッドとプロパティを実装する関数を作成します。プロパティには、プロパティを設定する関数と、プロパティを取り出す関数を受け付けます。このコードはとても簡単です。

  STDMETHODIMP CBeepCnt::Beep()
{
  for (int i = 0; i < cBeeps; i++)
   MessageBeep((UINT) -1);
  return S_OK;
}

STDMETHODIMP CBeepCnt::get_Count(long *pVal)
{
  *pVal = cBeeps;
  return S_OK;
}

STDMETHODIMP CBeepCnt::put_Count(long newVal)
{
  cBeeps = newVal;
  return S_OK;
}

クライアントの作成

Visual C++ は Dr. GUI のお気に入りの言語ですが、それでも、オブジェクトのテストでは Visual Basic が王者であることは認めます。オブジェクトに参照を追加したら、小さいフォームを作成するのは簡単です。

… そして、この裏で動くコードを書きます。

  Dim BeeperCnt As BeepCnt

Private Sub Beep_Click()
  Text1 = BeeperCnt
  BeeperCnt.Beep
End Sub

Private Sub Set_Click()
  BeeperCnt = Val(Text1)
  Text1 = BeeperCnt
End Sub

Private Sub Form_Load()
  Set BeeperCnt = New BeepCnt
  Text1 = BeeperCnt
End Sub

やってみよう!

Dr. GUI は、説明を本当に理解できたかどうかはやってみるまではわからないと思っていますので、ここで実際にやってみることにしましょう(今回は前回に比べるととても簡単です)。

  • 少なくとも 1 つのプロパティと 1 つのメソッドを使用して、簡単なコンポーネントを 1 つ、2 つ作成してください。ユーザー インターフェイスを必要としない限り、何をしてもかまいません。ユーザーのメソッドにパラメータを追加してみたらどうですか。

  • 「おまけ」として、パラメータ化したプロパティを作成してください。

  • 必ず全部 Visual Basic クライアントを使用してテストしてください。

今までの復習、これからの予定

今回は、プロパティとメソッドを 1 つだけ含む単純な ATL COM オブジェクトを作成しました。次回は、ほかのインターフェイスから継承されるインターフェイスを含め、複数のインターフェイスを持つ、もっと複雑なオブジェクトについて研究します。

付録:コード例

ステップ 0

BeepCntMod.idl

  // BeepCntMod.idl :BeepCntMod.dll の IDL ソース
//

// このファイルは MIDL ツールによって処理され、
// タイプ ライブラリ(BeepCntMod.tlb)とマーシャリング コードが作成されます。

import "oaidl.idl";
import "ocidl.idl";

[
   uuid(170BBD8D-4DE8-11D2-A2E0-00C04F8EE2AF),
   version(1.0),
   helpstring("BeepCntMod 1.0 Type Library")
]
library BEEPCNTMODLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

};

BeepCntMod.def

  ;BeepCntMod.def :モジュール パラメータを宣言します。

LIBRARY   "BeepCntMod.DLL"

EXPORTS
   DllCanUnloadNow   @1 PRIVATE
   DllGetClassObject  @2 PRIVATE
   DllRegisterServer  @3 PRIVATE
   DllUnregisterServer   @4 PRIVATE

BeepCntMod.rc

  //Microsoft Visual C++ から生成されたリソース スクリプト
//
#include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE 2 リソースから生成されています
//
#include "winres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE 9, 1
#pragma code_page(1252)
#endif //_WIN32
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE
BEGIN
  "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE
BEGIN
  "#include ""winres.h""\r\n"
  "\0"
END

3 TEXTINCLUDE DISCARDABLE
BEGIN
  "1 TYPELIB ""BeepCntMod.tlb""\r\n"
  "\0"
END

#endif  // APSTUDIO_INVOKED

#ifndef _MAC
/////////////////////////////////////////////////////////////////////////////
//
// バージョン
//

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,1
 PRODUCTVERSION 1,0,0,1
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x4L
 FILETYPE 0x2L
 FILESUBTYPE 0x0L
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "040904B0"
    BEGIN
      VALUE "CompanyName", "\0"
      VALUE "FileDescription", "BeepCntMod Module\0"
      VALUE "FileVersion", "1, 0, 0, 1\0"
      VALUE "InternalName", "BeepCntMod\0"
      VALUE "LegalCopyright", "Copyright 1998\0"
      VALUE "OriginalFilename", "BeepCntMod.DLL\0"
      VALUE "ProductName", "BeepCntMod Module\0"
      VALUE "ProductVersion", "1, 0, 0, 1\0"
      VALUE "OLESelfRegister", "\0"
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x0409, 0x04B0
  END
END

#endif  // !_MAC

/////////////////////////////////////////////////////////////////////////////
//
// 文字列テーブル
//

STRINGTABLE DISCARDABLE
BEGIN
   IDS_PROJNAME               "BeepCntMod"
END

/////////////////////////////////////////////////////////////////////////////


#endif

#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE 3 リソースから生成されています。
//
1 TYPELIB "BeepCntMod.tlb"

/////////////////////////////////////////////////////////////////////////////
#endif  // APSTUDIO_INVOKED ではない

Resource.h

  //{{NO_DEPENDENCIES}}
// Microsoft Visual C++ が生成したインクルード ファイル
// BeepCntMod.rc によって使用される
//
#define IDS_PROJNAME 100



// 新しいオブジェクトの次のデフォルト値
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE    201
#define _APS_NEXT_COMMAND_VALUE     32768
#define _APS_NEXT_CONTROL_VALUE     201
#define _APS_NEXT_SYMED_VALUE      101
#endif
#endif

stdafx.h

  // stdafx.h :標準のシステム インクルード ファイルのためのインクルード ファイル、
//    または、頻繁に使用されるが、変更は頻繁ではない
//    プロジェクト固有のインクルード ファイル。

#if !defined(AFX_STDAFX_H__170BBD90_4DE8_11D2_A2E0_00C04F8EE2AF__INCLUDED_)
#define AFX_STDAFX_H__170BBD90_4DE8_11D2_A2E0_00C04F8EE2AF__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define STRICT
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#define _ATL_APARTMENT_THREADED

#include <atlbase.h>
// 何かを上書きしたい場合は、CcomModule からクラスを導出して
// 使用することができますが、_Module の名前は変更しないでください。
extern CComModule _Module;
#include <atlcom.h>

//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ は、前の行の直前に追加の宣言を挿入します。

#endif // !defined(AFX_STDAFX_H__170BBD90_4DE8_11D2_A2E0_00C04F8EE2AF__INCLUDED)

stdafx.cpp

  // stdafx.cpp :標準インクルードのみを含むソース ファイル。
// stdafx.pch は事前コンパイルされたヘッダになります。
// stdafx.obj は事前コンパイルされたタイプ情報を含みます。

#include "stdafx.h"

#ifdef _ATL_STATIC_REGISTRY
#include <statreg.h>
#include <statreg.cpp>
#endif

#include <atlimpl.cpp>

BeepCntMod.cpp

  // BeepCntMod.cpp :DLL エクスポートの実装


// 注 :Proxy/Stub 情報
//    別個の proxy/stub DLL を構築するには、
//    プロジェクト ディレクトリの中で nmake -f BeepCntModps.mk を実行します。

#include "stdafx.h"
#include "resource.h"
#include <initguid.h>
#include "BeepCntMod.h"

#include "BeepCntMod_i.c"


CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()

/////////////////////////////////////////////////////////////////////////////
// DLL エントリ ポイント

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
  if (dwReason == DLL_PROCESS_ATTACH)
  {
    _Module.Init(ObjectMap, hInstance, &LIBID_BEEPCNTMODLib);
    DisableThreadLibraryCalls(hInstance);
  }
  else if (dwReason == DLL_PROCESS_DETACH)
    _Module.Term();
  return TRUE;  // ok
}

/////////////////////////////////////////////////////////////////////////////
// DLL を OLE によってアンロードできるかどうかを判別するために使用されます。

STDAPI DllCanUnloadNow(void)
{
  return (_Module.GetLockCount()==0) ? S_OK :S_FALSE;
}

/////////////////////////////////////////////////////////////////////////////
// 要求されたタイプのオブジェクトを作成するためのクラス ファクトリを戻します。

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
  return _Module.GetClassObject(rclsid, riid, ppv);
}

/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - システム レジストリにエントリを追加します。

STDAPI DllRegisterServer(void)
{
  // オブジェクト、typelib と、typelib の中のすべてのインターフェイスを登録します
  return _Module.RegisterServer(TRUE);
}

/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer – システム レジストリからエントリを削除します

STDAPI DllUnregisterServer(void)
{
  return _Module.UnregisterServer(TRUE);
}

BeepCntMod.h

  /* この、常に生成されるファイルは、インターフェイスの定義を含みます。*/


/* MIDL コンパイラ、バージョン 5.01.0164 によって作成されるファイル */
/* Wed Sep 16 22:08:03 1998
 */
/* D:\ATL\BeepCntMod\BeepCntMod.idl のコンパイラ設定 :
  Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext
  error checks:allocation ref bounds_check enum stub_data
*/
//@@MIDL_FILE_HEADING( )


/* <rpcndr.h> のバージョンがこのファイルをコンパイルするために十分なものか確認 */
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 440
#endif

#include "rpc.h"
#include "rpcndr.h"

#ifndef __BeepCntMod_h__
#define __BeepCntMod_h__

#ifdef __cplusplus
extern "C"{
#endif

/* フォワード宣言 */

/* インポートされたファイルのヘッダ ファイル */
#include "oaidl.h"
#include "ocidl.h"

void __RPC_FAR * __RPC_USER MIDL_user_allocate(size_t);
void __RPC_USER MIDL_user_free( void __RPC_FAR * );


#ifndef __BEEPCNTMODLib_LIBRARY_DEFINED__
#define __BEEPCNTMODLib_LIBRARY_DEFINED__

/* ライブラリ BEEPCNTMODLib */
/* [helpstring][version][uuid] */


EXTERN_C const IID LIBID_BEEPCNTMODLib;
#endif /* __BEEPCNTMODLib_LIBRARY_DEFINED__ */

/* すべてのインターフェイスのための追加プロトタイプ */

/* 追加プロトタイプの終わり */

#ifdef __cplusplus
}
#endif

#endif

BeepCntMod_i.c

  /* このファイルは、*/
/* IID と CLSID の実際の定義を含みます */

/* このファイルを、サーバー、およびすべてのクライアントにリンクしてください */


/* MIDL コンパイラ、バージョン 5.01.0164 によって作成されたファイル */
/* Wed Sep 16 22:08:03 1998
 */
/* D:\ATL\BeepCntMod\BeepCntMod.idl のコンパイラ設定 :
  Oicf (OptLev=i2), W1, Zp8, env=Win32, ms_ext, c_ext
  error checks:allocation ref bounds_check enum stub_data
*/
//@@MIDL_FILE_HEADING( )
#ifdef __cplusplus
extern "C"{
#endif


#ifndef __IID_DEFINED__
#define __IID_DEFINED__

typedef struct _IID
{
  unsigned long x;
  unsigned short s1;
  unsigned short s2;
  unsigned char c[8];
} IID;

#endif // __IID_DEFINED__

#ifndef CLSID_DEFINED
#define CLSID_DEFINED
typedef IID CLSID;
#endif // CLSID_DEFINED

const IID LIBID_BEEPCNTMODLib = {0x170BBD8D,0x4DE8,0x11D2,{0xA2,0xE0,0x00,0xC0,0x4F,0x8E,0xE2,0xAF}};


#ifdef __cplusplus
}
#endif

ステップ 1

beepcnt.cpp

  // BeepCnt.cpp :CbeepCnt の実装
#include "stdafx.h"
#include "BeepCntMod.h"
#include "BeepCnt.h"

/////////////////////////////////////////////////////////////////////////////
// CBeepCnt

beepcnt.h

  // BeepCnt.h :CbeepCnt の宣言

#ifndef __BEEPCNT_H_
#define __BEEPCNT_H_

#include "resource.h"    // メイン シンボル

/////////////////////////////////////////////////////////////////////////////
// CBeepCnt
class ATL_NO_VTABLE CBeepCnt :
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CBeepCnt, &CLSID_BeepCnt>,
   public IDispatchImpl<IBeepCnt, &IID_IBeepCnt, &LIBID_BEEPCNTMODLib>
{
public:
   CBeepCnt()
   {
   }

DECLARE_REGISTRY_RESOURCEID(IDR_BEEPCNT)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CBeepCnt)
   COM_INTERFACE_ENTRY(IBeepCnt)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IBeepCnt
public:
};

#endif //__BEEPCNT_H_

BeepCnt.rc

  #include "resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE 2 リソースから生成されています。
//
#include "winres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// 英語リソース

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE
BEGIN
  "resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE
BEGIN
  "#include ""winres.h""\r\n"
  "\0"
END

3 TEXTINCLUDE DISCARDABLE
BEGIN
  "1 TYPELIB ""BeepCnt.tlb""\r\n"
  "\0"
END

#endif  // APSTUDIO_INVOKED


#ifndef _MAC
/////////////////////////////////////////////////////////////////////////////
//
// バージョン
//

VS_VERSION_INFO VERSIONINFO
 FILEVERSION 1,0,0,1
 PRODUCTVERSION 1,0,0,1
 FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
 FILEFLAGS 0x1L
#else
 FILEFLAGS 0x0L
#endif
 FILEOS 0x4L
 FILETYPE 0x2L
 FILESUBTYPE 0x0L
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "040904B0"
    BEGIN
      VALUE "CompanyName", "\0"
      VALUE "FileDescription", "BeepCnt Module\0"
      VALUE "FileVersion", "1, 0, 0, 1\0"
      VALUE "InternalName", "BeepCnt\0"
      VALUE "LegalCopyright", "Copyright 1998\0"
      VALUE "OriginalFilename", "BeepCnt.DLL\0"
      VALUE "ProductName", "BeepCnt Module\0"
      VALUE "ProductVersion", "1, 0, 0, 1\0"
      VALUE "OLESelfRegister", "\0"
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x409, 1200
  END
END

#endif  // !_MAC


/////////////////////////////////////////////////////////////////////////////
//
// レジストリ
//

IDR_BEEPCOUNT      REGISTRY DISCARDABLE  "BeepCount.rgs"

/////////////////////////////////////////////////////////////////////////////
//
// 文字列テーブル
//

STRINGTABLE DISCARDABLE
BEGIN
  IDS_PROJNAME      "BeepCnt"
END

#endif  // 英語リソース
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE 3 リソースから生成されています。
//
1 TYPELIB "BeepCnt.tlb"

/////////////////////////////////////////////////////////////////////////////
#endif  // APSTUDIO_INVOKED ではない

beepcnt.rgs

  HKCR
{
   BeepCntMod.BeepCnt.1 = s 'BeepCnt Class'
   {
      CLSID = s '{AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF}'
   }
   BeepCntMod.BeepCnt = s 'BeepCnt Class'
   {
      CLSID = s '{AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF}'
      CurVer = s 'BeepCntMod.BeepCnt.1'
   }
   NoRemove CLSID
   {
      ForceRemove {AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF} = s 'BeepCnt Class'
      {
         ProgID = s 'BeepCntMod.BeepCnt.1'
         VersionIndependentProgID = s 'BeepCntMod.BeepCnt'
         ForceRemove 'Programmable'
         InprocServer32 = s '%MODULE%'
         {
            val ThreadingModel = s 'Apartment'
         }
         'TypeLib' = s '{170BBD8D-4DE8-11D2-A2E0-00C04F8EE2AF}'
      }
   }
}

BeepCntMod.cpp

  // BeepCntMod.cpp :DLL エクスポートの実装


// 注 :Proxy/Stub 情報
//    別個の proxy/stub DLL を構築するには、
//    プロジェクト ディレクトリの中で nmake -f BeepCntModps.mk を実行してください。

#include "stdafx.h"
#include "resource.h"
#include <initguid.h>
#include "BeepCntMod.h"

#include "BeepCntMod_i.c"
#include "BeepCnt.h"


CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_BeepCnt, CBeepCnt)
END_OBJECT_MAP()

/////////////////////////////////////////////////////////////////////////////
// DLL エントリ ポイント

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
  if (dwReason == DLL_PROCESS_ATTACH)
  {
    _Module.Init(ObjectMap, hInstance, &LIBID_BEEPCNTMODLib);
    DisableThreadLibraryCalls(hInstance);
  }
  else if (dwReason == DLL_PROCESS_DETACH)
    _Module.Term();
  return TRUE;  // ok
}

/////////////////////////////////////////////////////////////////////////////
// DLL を OLE によってアンロードできるかどうかを判別するために使用される

STDAPI DllCanUnloadNow(void)
{
  return (_Module.GetLockCount()==0) ? S_OK :S_FALSE;
}

/////////////////////////////////////////////////////////////////////////////
// 要求されたタイプのオブジェクトを作成するためのクラス ファクトリを戻します

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
  return _Module.GetClassObject(rclsid, riid, ppv);
}

/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - システム レジストリにエントリを追加します

STDAPI DllRegisterServer(void)
{
  // オブジェクト、typelib と、typelib の中のすべてのインターフェイスを登録します
  return _Module.RegisterServer(TRUE);
}

/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - システム レジストリからエントリを削除します

STDAPI DllUnregisterServer(void)
{
  return _Module.UnregisterServer(TRUE);
}

BeepCntMod.idl

  // BeepCntMod.idl :BeepCntMod.dll の IDL ソース
//

// このファイルは、MIDL ツールによって処理され、
// タイプ ライブラリ (BeepCntMod.tlb) とマーシャリング コードが作成されます。

import "oaidl.idl";
import "ocidl.idl";
   [
      object,
      uuid(AE73F2F7-4E95-11D2-A2E1-00C04F8EE2AF),
      dual,
      helpstring("IBeepCnt Interface"),
      pointer_default(unique)
   ]
   interface IBeepCnt :IDispatch
   {
   };

[
   uuid(170BBD8D-4DE8-11D2-A2E0-00C04F8EE2AF),
   version(1.0),
   helpstring("BeepCntMod 1.0 Type Library")
]
library BEEPCNTMODLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   [
      uuid(AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF),
      helpstring("BeepCnt Class")
   ]
   coclass BeepCnt
   {
      [default] interface IBeepCnt;
   };
};

#

最後

BeepCntMod.idl

  // BeepCntMod.idl :BeepCntMod.dll の IDL ソース
//

// このファイルは、MIDL ツールによって処理され、
// タイプ ライブラリ (BeepCntMod.tlb) とマーシャリング コードが作成されます。

import "oaidl.idl";
import "ocidl.idl";
   [
      object,
      uuid(AE73F2F7-4E95-11D2-A2E1-00C04F8EE2AF),
      dual,
      helpstring("IBeepCnt Interface"),
      pointer_default(unique)
   ]
   interface IBeepCnt :IDispatch
   {
      [id(1), helpstring("method Beep")] HRESULT Beep();
      [propget, id(0), helpstring("property Count")] HRESULT Count([out, retval] long *pVal);
      [propput, id(0), helpstring("property Count")] HRESULT Count([in] long newVal);
   };

[
   uuid(170BBD8D-4DE8-11D2-A2E0-00C04F8EE2AF),
   version(1.0),
   helpstring("BeepCntMod 1.0 Type Library")
]
library BEEPCNTMODLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   [
      uuid(AE73F2F8-4E95-11D2-A2E1-00C04F8EE2AF),
      helpstring("BeepCnt Class")
   ]
   coclass BeepCnt
   {
      [default] interface IBeepCnt;
   };
};

BeepCnt.h

  // BeepCnt.h :CbeepCnt の宣言

#ifndef __BEEPCNT_H_
#define __BEEPCNT_H_

#include "resource.h"    // メイン シンボル

/////////////////////////////////////////////////////////////////////////////
// CBeepCnt
class ATL_NO_VTABLE CBeepCnt :
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CBeepCnt, &CLSID_BeepCnt>,
   public IDispatchImpl<IBeepCnt, &IID_IBeepCnt, &LIBID_BEEPCNTMODLib>
{
public:
   long cBeeps;

   CBeepCnt() :cBeeps(1) { }

DECLARE_REGISTRY_RESOURCEID(IDR_BEEPCNT)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CBeepCnt)
   COM_INTERFACE_ENTRY(IBeepCnt)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// IBeepCnt
public:
   STDMETHOD(get_Count)(/*[out, retval]*/ long *pVal);
   STDMETHOD(put_Count)(/*[in]*/ long newVal);
   STDMETHOD(Beep)();
};

#endif //__BEEPCNT_H_

BeepCnt.cpp

  // BeepCnt.cpp :CbeepCnt の実装
#include "stdafx.h"
#include "BeepCntMod.h"
#include "BeepCnt.h"

/////////////////////////////////////////////////////////////////////////////
// CBeepCnt


STDMETHODIMP CBeepCnt::Beep()
{
   for (int i = 0; i < cBeeps; i++)
      MessageBeep((UINT) -1);
   return S_OK;
}

STDMETHODIMP CBeepCnt::get_Count(long *pVal)
{
   *pVal = cBeeps;
   return S_OK;
}

STDMETHODIMP CBeepCnt::put_Count(long newVal)
{
   cBeeps = newVal;
   return S_OK;
}

第 2 部:複数のインターフェイスを持つコンポーネント

今までの復習、これからの予定

前回は、プロパティとメソッドをそれぞれ 1 つずつだけ持った単純な ATL COM オブジェクトを作りました。今回は、複数のインターフェイスを持つやや複雑なオブジェクトを取り上げます。インターフェイスのいくつかは、ほかのインターフェイスから継承をします。そして、このオブジェクトを複数の言語から使います。次回は、オートメーションの謎にせまる予定です。

今回実装するオブジェクトは、これまでの COM のシリーズの中で ATL を使わずに実装したオブジェクトです。ATL を使わない実装の詳細については、「Dr.GUI、COM、コンポーネント、ATLを語る、第 5 部」をご覧ください。

オブジェクトのインターフェイスは変わらないので、つぎのものが再利用できます。

実際のところ、すべての GUID を変えなれなければ、まったく何も手を加える必要はないでしょう。しかし、ここではオブジェクトの GUID を変えるので、オブジェクトを作成するクライアントのコードを変える必要があります。とはいっても、変更を加える必要があるのはそれくらいです。

インターフェイスを増やしましょう

この「COM、テンプレート、ATL」のシリーズをこれまで読んでいたなら、「第 5 部」で、3 つのインターフェイスを持つオブジェクトを作ったことを覚えていると思います。IFooIFoo2(この 2 つは IFoo から派生させました)、そしてほかの 2 つとは独立の IGoo の合計 3 つです。

このオブジェクトのダイヤグラムを以下に示します。

思い出しやすいように、以下にこの 3 つのインターフェイスの簡略版 IDL を示します。インターフェイス属性は省いています。

     interface IFoo :IDispatch
   {
      [id(1)] HRESULT Func1();
      [id(2)] HRESULT Func2([in] int inonly);
   };

   interface IFoo2 :IFoo
   {
      [id(3)] HRESULT Func3([out, retval] int *pout);
   };

   interface IGoo :IUnknown
   {
      HRESULT Gunc();
   };

 IFoo から IFoo2 インターフェイスを派生させたのは、IFoo に重要なメソッドを「作り忘れた」からです。オブジェクトの内部状態を設定するメソッドは用意したのですが、その状態を読み出すメソッドは忘れていたのです。このような形で派生をさせることによって、IFoo2 を知っている新しいクライアントは、改良された機能を利用することができますが、古いクライアントもそのまま問題なく動きます。古いクライアントは単に、Func3 を呼び出さないだけです。これが、既存の不変インターフェイスに機能を追加する方法なのです。そこから、新しいインターフェイスを派生させればいいのです。もちろん、COM から見れば、これらはまったく個別のインターフェイスです。

賢明な読者は、IFoo が、IUnknown ではなく、IDispatch から派生していることには気づいているでしょう。これは、IFoo をデュアル インターフェイスとして作ったからです。デュアル インターフェイスは、以前に解説した vtable メソッドを使って事前バインディングをするクライアントと、オートメーション呼び出しを使う実行時バインディングをするクライアント(スクリプティング言語など)のどちらからも呼び出せます。vtable(またはカスタム)インターフェイス、dispatch(またはオートメーション)インターフェイス、デュアル インターフェイスの違いについては、次回のコラムで取り上げます。メソッドの id 属性は、各メソッドのディスパッチ ID、すなわち dispid を示します。これらの ID は、インターフェイス内で重複してはなりません。そのために、Func3 の ID が 3 となっています(ID 番号の 1 と 2 は、基本インターフェイスで使われています)。IFoo がデュアル インターフェイスなら、IFoo2 など、IFoo から派生したインターフェイスもデュアル インターフェイス もデュアル インターフェイスのはずです。

IGoo は、普通のカスタム インターフェイスです。実行時バインディング クライアントが標準のインターフェイスから切り替わる手段はないので、IGoo をデュアル インターフェイスにしてオートメーションを有効にする必要はありません(標準のインターフェイスのいずれかのメソッドが IGoo インターフェイス ポインタを返した場合には、実行時バインディング クライアントにデュアル IGoo インターフェイスへのアクセスを与えることができます。しかし、IFooIFoo2 のメソッドはインターフェイス ポインタを返しません)。

モジュール、オブジェクト、最初のインターフェイスの作成

オブジェクトを作成するための手順は、この ATL シリーズの第 1部での手順にきわめてよく似ています。実際、モジュールの作成方法は前回とまったく同じなので、その手順をそのままなぞることができます。Dr. GUI はモジュールに「MultiInterfaceMod」という名前を与えていました。今度はオブジェクトに「MultiInterface」という名前を与えます。

つぎに、「MultiInterface」という名前の単純なオブジェクトを作ります。標準のインターフェイス名は、「IMultiInterface」です。しかし、それは使わずに、IFoo という名前に変えます。標準の設定ではデュアル インターフェイスを作るようになっています。スクリプティング言語など、実行時バインディング クライアントで利用できるようにしたいので、これはこのままで結構です。

この時点のコードを、メソッドとプロパティを追加する直前の前回のコードと比較してみてください。クラス名と GUID が違うくらいで、コードはほとんど同じはずです。

つぎに、IFoo インターフェイスのメソッドを追加します。前回同様、[Add Method]ダイアログ ボックスを使えばいいのですが、Beep の代わりに、前記の IDL(Func1Func2)で示したメソッドを追加します。ID について心配する必要はありません。ウィザードが適切に対処してくれます(少なくともこのインターフェイスについては)。ただし、パラメータのために、パラメータと属性を取得しなければなりません。

COMシリーズの第 5 部で紹介された 2 つの IFoo メソッドのコードを盗み(おっと、再利用)しましょう。オブジェクトの内部状態を保持するために、private メンバ変数を追加して初期化しなければなりません。オブジェクトのコーディングが終わったら、ビルドしてテストしてみましょう。

2 番目と 3 番目のインターフェイスの追加

残念ながら、既存のオブジェクトに追加のインターフェイスを追加するのは、思った以上に困難です。インターフェイスを追加するためのウィザードはないので、多少の手作業が必要になります(ただし、モジュールに別のオブジェクトを追加するのは、助けてくれるウィザードがあるので簡単です。また、対応するタイプ ライブラリがあるインターフェイスを実装するためのウィザードもあります。しかし、今回のものはカスタムの新しいインターフェイスなので、利用できるタイプ ライブラリもなく、手作業で作らなければなりません)。

明るい側面もあります。統合開発環境(IDE)は、IDL ファイルを含め、ソース ファイルに加えられた変更を検出できるので、変更を加えるとそれがただちにクラス ビュー ウィンドウに反映されます。

Dr. GUI としては、タイプ入力をするよりは、コピーして貼り付けるほうが好きなので、彼の最初のステップは、IDL ファイル中の既存のインターフェイス(IFoo)をコピーして、その複製を貼り付けることでした(これは、IFoo2 用です)。インターフェイス宣言の直前にある属性(大かっこで囲まれているもの)もコピーするのを忘れないでください。

ここ少し時間を取って、COM と IDL を理解しているかどうかを確認してみましょう。つぎの宣言に基づいて、IFoo2 インターフェイスのための宣言を作るには、何を変えればいいかわかりますか?

     [
      object,
      uuid(09809F2D-7E98-11D2-A320-00C04F8EE2AF),
      dual,
      helpstring("IFoo Interface"),
      pointer_default(unique)
   ]
   interface IFoo :IDispatch
   {
      [id(1), helpstring("method Func1")] HRESULT Func1();
      [id(2), helpstring("method Func2")] HRESULT Func2([in] int inonly);
   };

まず、uuid 属性の GUID を変えなければなりません。[ツール]メニューに GUIDGen がインストールされていなければ、よい機会なのでインストールしましょう([ツール]メニューから[カスタマイズ]を選び、プロパティシートの[ツール]タブをクリックして、guidgen.exe を実行する新しいメニュー項目を追加します)。

レジストリ形式の GUID を作成するために GUIDGen を実行します。新しい GUID をコピーしたら、GUID を囲んでいる中かっこを削除することを忘れないように。レジストリ形式は IDL 形式とほとんど同じですが、まったく同じというわけではありません(GUIDGen が生成するほかの形式はまったく似てもいません)。

つぎに、helpstring とインターフェイス名の IFooIFoo2 に変えます。また、インターフェイスの派生元を IDispatch から IFoo に変えることもたいへん重要です。そもそも、それが話の中心なのですから。最後に、2 つのメソッド宣言を削除します。IFoo から継承をするので、メソッドを宣言する必要はありません

これでおしまいです。できあがった IFoo2 の宣言は、つぎのようになっているはずです(Func3 のメソッド宣言はまた後で追加します)。変更を加えた箇所は太字で示します。あなたの GUID は、私の GUID とは違うものになることに注意してください。

     [
      object,
      uuid(1FF51B0A-79C3-11d2-A31B-00C04F8EE2AF),
      dual,
      helpstring("IFoo2 Interface"),
      pointer_default(unique)
   ]
   interface IFoo2 :IFoo
   {
      // method declarations deleted
   };

ここまでできたら、今度は IGoo インターフェイスを作ります。これはカスタムのインターフェイスにするので、これまでの変更のほかに、IUnknown から派生させて、dual 属性はすべて削除します。

     [
      object,
      uuid(58BD7C84-79C3-11d2-A31B-00C04F8EE2AF),
      // dual,
      helpstring("IGoo Interface"),
      pointer_default(unique)
   ]
   interface IGoo :IUnknown
   {
      // method declarations deleted
   };

オブジェクトが新しいインターフェイスを実装していることを示すために、coclass 宣言も編集しなければなりません。名医は、インターフェイスを追加したほか、デフォルトのインターフェイスを IFoo2 に変更して、実行時バインディング クライアントが追加の機能にもアクセスできるようにしています。以下は、coclass のセクションです。

     coclass MultiInterface
   {
      interface IFoo;
      [default] interface IFoo2;
      interface IGoo;
   };

Visual Studio が変更をクラス ビューにただちに反映しない場合、IDL ファイルを保存してみてください。3 つのインターフェイスが、オブジェクトの下ではなく、最上位レベルに表示されます。

新しいインターフェイスからの継承

新しいインターフェイスがまだオブジェクトの下に現れないのは、まだ新しいインターフェイスから継承をしていないからです。継承を正しく実施するとともに、COM マップに新しいインターフェイスに対応するエントリを追加しなければなりません。

IFoo2 は、IFoo から派生しているので、継承リストには、IFoo ではなく IFoo2 が入ります(IFoo は、IUnknown と同じように、リストには暗黙のうちに入っています)。そして、IFoo2 はデュアル インターフェイスなので、継承リストでは直接継承をする代わりに、IDispatchImpl テンプレートを使います。

IGoo からは直接継承をします。これはカスタム インターフェイスだからです。

つまり、つぎの形のクラス ヘッダを、

  class ATL_NO_VTABLE CMultiInterface :
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CMultiInterface, &CLSID_MultiInterface>,
   public IDispatchImpl<IFoo, &IID_IFoo, &LIBID_MULTIINTERFACEMODLib>
{

つぎのように変えます。

  class ATL_NO_VTABLE CMultiInterface :
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CMultiInterface, &CLSID_MultiInterface>,
   public IDispatchImpl<IFoo2, &IID_IFoo2, &LIBID_MULTIINTERFACEMODLib>,
   public IGoo
{

これに対し、ATL による QueryInterface の実装で使われる COM マップは、おのおののインターフェイスに対応するエントリがなければなりません(IUnknown を除く)。したがって、COM マップは、つぎのような形から、

  BEGIN_COM_MAP(CMultiInterface)
   COM_INTERFACE_ENTRY(IFoo)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

つぎのような形に変わります。

  BEGIN_COM_MAP(CMultiInterface)
   COM_INTERFACE_ENTRY(IFoo)
   COM_INTERFACE_ENTRY(IFoo2)
   COM_INTERFACE_ENTRY(IGoo)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

COM マップに対する変更が終わると、クラス ビューがただちに更新されます。

メソッドの追加

メソッドを追加する方法は、前回と同じですが、1 つだけ落とし穴があります。Visual Studio は、IFoo2IFoo から継承されているという事実は無視します。したがって、デフォルトではディスパッチ ID として 1 を生成します。これはメソッドを宣言するときにウィザードの[属性]ボタンをクリックして変更するか、後で IDL を手で書き換えることができます。ID を変えないと、なんとも役に立つつぎのような意味不明のエラー メッセージが表示されます。

  midl\oleaut32.dll :error MIDL2020 :error generating type library :
AddImplType failed :MultiInterface

なかなかのものでしょう?Func3 の ID を 1 または 2 以外の値に変えて ID の衝突を避ければ、エラーはなくなります。

この記事の最初のほうにある IDL コードを、メソッド追加ウィザードを使ってメソッドを追加するときの手引きとして使ってください。

最後に、2 つの新しいメソッドで、COMシリーズの第 5 部のコードを再利用しましょう。コンパイラ エラーがなくなったら、COM オブジェクトが作れるようになっているはずです。

クライアント コードの変更

COM の優れている点の 1 つは、自分の好きなテクノロジーを使ってオブジェクトを書くことができるという点です。これにより、古いクライアントでもオブジェクトを利用できます。そして、ProgID、CLSID、IID をすべて非 ATL バージョンのものと同じまましてあれば、(前回の記事の)古いクライアントも、再コンパイルさえせずに、今回作った新しいオブジェクトを使うことができます。そのまま何の問題もなく動きます。

しかし、実際にはそれらを変えているので、新しいオブジェクトを使えるようにするには、古いクライアントを修正しなければなりません。インターフェイスは変えていないので(変えたのは IID だけです)、メソッドを呼び出すコードを変える必要はありません。しかし、オブジェクトが正しく作られることを確認しなければなりません。

ちなみに、名医はこれらのクライアントに変更を加えるついでに、前のバージョンからバージョン 6.0 への移植もしました。Visual J++ を除き、移植は完全に自動化されていました。新しいバージョンに移植するにあたり、ソース コードのレベルで変更を加える必要はまったくありませんでした。

Visual Basic

Visual Basic で必要な変更点はつぎのものだけです。

  • Project.References ボックスで古いコントロールの選択を解除して、新しいコントロールを選択します。

  • オブジェクトの宣言部と new ステートメントでのオブジェクト名を新しい名前に変更します。

これらの変更を加えただけで、COMシリーズの第 6 部は問題なく動作しました。そして、GUID や ProgID を変えていなければ、何も変える必要がなかったことを覚えておいてください。

Visual J++

Visual J++ version 6.0 は、Visual J++ 1.1 に比べると大幅に変わっているため、名医は既存のプロジェクトを変換するのはあきらめて、新しいプロジェクトをゼロから開始しました(古いプロジェクトを変換するには、プロジェクト ファイルをダブルクリックせずに、Visual J++ 環境の中からファイルを開く必要があります。これは、新しい Visual J++ がもはや DevStudio IDE を使わないからです)。

新しい Windows EXE プロジェクト(Visual J++ 6.0 の新しいオプション)を開始して、[プロジェクト]メニューから[COM ラッパーの追加]を選択し、自分の COM オブジェクトを表すクラスを追加しました。その後で、1.1 のプロジェクトのソース コードを使って新しいクラスを作りました。ソース コードに加えた変更は、import ステートメントで正しいパッケージをインポートするようにしたことと、new ステートメントでクラスの名前を変えたことだけです。

これらの変更を加えただけで、COMシリーズの第 6 部は問題なく動作しました。

C/C++

C と C++ のドライバも更新するのは簡単でした。名医は、GUID の定義が利用できるように、各プロジェクトに正しいインターフェイス定義ファイル(MultiInterfaceMod_i.c)を追加し、正しいヘッダ ファイルをインクルードするだけですみました。おっと、そうだ。CoCreateInstance の CLSID も変える必要がありました。以上です(GUID は変わりましたが、IID のシンボル名は変わりませんでした)。

これらの変更を加えただけで、COMシリーズの第 7 部は問題なく動作しました。繰り返しになりますが、GUID や ProgID を変えていなければ、何も変える必要がなかったことを覚えておいてください。

Visual C++ とスマート COM ポインタ

スマート COM ポインタを使った Visual C++ プログラムも簡単に変換できました。そして、名前と GUID を変えていなければ、変換をする必要さえなかったでしょう。

最初の変更は、#import ステートメントを変更して新しいタイプ ライブラリをインポートするようにしたことです(タイプ ライブラリの名前が非常に長い場合は、テンプレートで長い内部識別子が使われないように、#import ステートメントによって作られる名前空間の名前を短い名前に変えます)。

これら以外の変更は簡単でした。using namespace ステートメントを変更して新しい名前空間を指すようにして、スマート ポインタのコンストラクタにあるつぎのオブジェクトの名前を、

  IFooPtr pIFoo(__uuidof(MyObject));

つぎのように変えます。

  IFooPtr pIFoo(__uuidof(MultiInterface));

たったこれだけです。これらの変更を加えただけで、COMシリーズの第 8 部は問題なく動作しました。

いざ、やってみよう!

Dr. GUI は、読者のみなさんが実際に試してみるまでは、ここで話題にしたことが本当にわかったかどかわからないということはよくわかっています。したがって、実際にやってみるしかありません!(それに、今回の話は、前回のに比べればはるかに簡単です...)。

  • オブジェクトごとに複数のインターフェイスを使う簡単なコンポーネントを 1 つか 2 つ作ります。いくつかをお互いに継承させ、いくつかを独立のままにしておきます。

  • 任意のクライアントを使って全体をテストします。

今までの復習、これからの予定

今回は、ほかのインターフェイスから継承をするインターフェイスを含め、複数のインターフェイスを持つ複雑なオブジェクトを取り上げました。そして、そのオブジェクトをさまざまな言語から使ってみました。次回は、オートメーションの秘密にせまります。