Dr. GUI と COM イベント:第 2 部

1999 年 10 月 25 日

Dr. GUIとCOMイベント:第1部」も参照してください。

目次

Dr. GUIのビットとバイト
イベントを発行するATL COMオブジェクト
やってみよう
これまでの復習、これからの予定

少々冷たくなっていますが、名医は戻ってきました(暖房の利かない乗り物でキャンプへ向かう道中、ちょっと寒いなと思うことが何度もあり、そのおかげで名医はモーテルという名の建物を見つけざるを得ませんでした)。

Dr. GUI のビットとバイト

Web でのオドラマ

John Waters 監督の、他に類を見ない Divine(ご冥福をお祈りします)主演の映画 Polyester を憶えていますか?この映画を見に行くと、観客に番号の付いたスクラッチ&スニフ カードが渡されます。映画では、さまざまなシーンでスクリーンに番号がちらっと現れます。これを見た観客はカード上の一致する番号をひっかき、カードのにおいからスクリーン上で何が起きているのかを嗅ぎ取る、という仕組みです。何のにおいがしたか、Dr. GUI は言いません。悪趣味な奴と呼ばれたくないですから。

近い将来、DigiScents という名前の会社の目論見どおりにことが運べば、手持ちの PC に取り付けられる「Smell-O-Vision」というアタッチメントが入手できるようになります。これは嗅覚を使用して、Web をナビゲートできるようにする装置です。このアタッチメントは香油のセットを使用して、香りを合成するのです。

このアタッチメントについては、Web サイト(http://www.zdnet.com/zdnn/stories/news/0,4586,2354314,00.html)で調べることができます。最近の Wired 誌を見てもよいでしょう。スクラッチ&スニフ型の表紙になっています。

この人たち、次は何を考えるのでしょうね?

単位に注意せよ

ご承知のように、先日 Mars Climate Orbiter が消息を絶ちました。何が起きたのでしょう?

NASA の調査で明らかになったのは、オービタに取り組んでいたチームの 1 つが度量衡の単位として英語の単位(フィート、インチ、マイル、ポンド)を使用し、別のチームはメートル法を使用していたということです。単位が正確に変換されなかったために、宇宙船は間違った軌道に入ってしまい、失われる羽目になったのです。この事故については、http://mars.jpl.nasa.gov/msp98/news/mco990930.html で詳細が読めます。

教訓:パラメータの意味は、単位を含め、厳密に定義しておかなければなりません。言語のタイプをチェックする機能を使用して単位を守らせることができるなら、その機能は使うだけの値打ちがあるはずです。

Microsoft Exchange のための Developer Center を新設!

MSDN の Developer Center は、どんどん増えています。この中で一番新しいのが、Microsoft Exchange 開発者に向けた Developer Center(https://msdn.microsoft.com/exchange/default.asp)です。このサイトは近々公開される Exchange 2000 に関する情報を特集しており、ベータ版コピーの入手方法なども含まれています。現在 Exchange の開発に携わっていなくても、大事な情報を見逃さないように、時々はここをチェックしてください。

イベントを発行する ATL COM オブジェクト

COM イベント:簡単なおさらい

前回 の記事にも書いたように、イベントはちょっとだけ複雑です。

第1に、イベント インターフェイスのほとんどは、ディスパッチ インターフェイスです。というのも、IDispatch を実装してパラメータを解決するオブジェクトを作る方が、呼び出される方法を知るために任意のカスタム インターフェイスを使う方法よりも、クライアントにとってはるかに簡単だからです。イベント(ソース オブジェクト)を発行しているオブジェクトは、ディスパッチ インターフェイスをソース インターフェイスとして定義します。そしてそれを、タイプ ライブラリを通して、クライアント オブジェクトが使用できるようにします。イベントはインターフェイス経由で定義されるので、同じインターフェイスに複数のイベント メソッドがある場合もあります。

クライアントは IConnectionPointContainer の有無を問い合わせることで、オブジェクトがイベントを発行するかどうかを確認できます。オブジェクトが IConnectionPointContainer を実装していれば、1 つまたは複数のイベント インターフェイスを実装しているということになります。クライアントは、IConnectionPointContainer::EnumConnectionPoints を呼び出すことで、オブジェクトがどのイベント インターフェイスを実装しているのか知ることができます。また、IConnectionPointContainer::FindConnectionPoint を呼び出すことで、特定の接続ポイントを要求することもできます。

接続ポイントは、ソース オブジェクトによって実装されるミニチュアの COM オブジェクトであり、イベント インターフェイスに対するすべての接続を維持する責任があります。通常は、IUnknownIConnectionPoint だけを実装します。

クライアントは、いずれかの接続ポイントへのインターフェイス ポインタを取得すると、ミニチュアの COM オブジェクトを作成してイベントを受け取り、その後そのミニチュアの COM オブジェクトへのインターフェイス ポインタを、IConnectionPoint::Advise に渡すことで、イベント インターフェイスの接続を確立します。イベントを受け取るミニチュアのオブジェクトは、通常 IUnknownIDispatch だけを実装します。

接続されたら、ソース オブジェクトはクライアントによって渡されたインターフェイス ポインタのメソッドを呼び出すことで、イベントを発行できます。IConnectionPoint::Advise は、複数のクライアントによって呼び出される可能性があるので、ソース オブジェクトはループを実行して、クライアント全部にイベントを発行しなければなりません。このループは、各クライアントが Advise に渡したインターフェイス ポインタを使って、適切なメソッドを呼び出します。

この先イベントを受け取る必要がなくなった時点で、クライアントは IConnectionPoint::Unadvise を呼び出して、接続を切らなければなりません。これはクライアントが終了する前に済ませておかなければならない大事なことです。

ご覧のように、ここに含まれるコードには、少し複雑な(また、バグが発生しやすい)ものがいくつかあります。接続ポイント オブジェクトは接続のリストを維持しなければなりません。また、イベントを発生する過程で、この接続のリストを列挙することや、それぞれに対して適切なメソッドを呼び出す必要があります。おっと。危うく忘れるところでしたが、ディスパッチ インターフェイス メソッドも作成しなければなりませんね。そのためには、VARIANT パラメータの配列を作成し、これを多数のパラメータを受け取る IDispatch::Invoke メソッドに渡します。全体として、やるべき作業がたくさんあるわけですが、幸い ATL は、この種の作業が得意です。

これを ATL で実装するためにしなければならないこと

接続ポイントの説明でおわかりのように、イベントを発行するためには、次のように、やらなければならないことがたくさんあります。4 つの異なるオブジェクト(メイン ソース オブジェクト、接続ポイント列挙子、接続ポイント、接続列挙子)の 4 つのインターフェイス(IConnectionPointContainerIEnumConnectionPointsIConnectionPointIEnumConnections)を実装し、2 つのコレクション(接続ポイントと接続)を維持し、しかも、イベントを発行するときにすべての接続に対するインターフェイス ポインタの適切なメソッドを呼び出すコードを実装しなければなりません。

もっと簡単な方法はないのでしょうか。Microsoft Foundation Classes(MFC)を使えば、作業は簡単にできます。ただし、オブジェクトが大きくなってしまいます。あるいは、我らがヒーロー Active Template Libraries(ATL)を使用すれば、同じくらい簡単にできます。

ATL ならほぼ全てを実装

ATL は、IUnknownIDispatchIClassFactory などの重要なインターフェイスの、完全でデバッグされ、可能な限り効率化したテンプレート実装を提供しますが、それらと同様に、イベントを処理するための実装も提供します。

これらの実装は、ほとんどがテンプレート化されたクラスを通して提供されますが、イベント発行ループの実装は、特別な「代理(proxy)」クラスにあります。このクラスは[接続ポイントのインプリメント]ウィザードによって生成されます(このウィザード、以前は別の独立なプログラムで、「ATL プロキシ ジェネレータ」という、すごく目立った名前が付いていました。こんな名前ですから、イベントのことを考えると自分の髪の毛をむしりたくなるのも無理はありません)。

IDL がなければ駄目

まず、ソース インターフェイスの IDL(Interface Definition Language)コードが必要です。これについては前回の記事 で徹底的に説明しまたが、念のため、名医はここにも示すことにしました。ソース インターフェイス自体の IDL と・・・

  [
uuid(F2F660CF-3ED7-11D3-9C8C-000039714C10),
helpstring("_IAAAFireLimitEvents Interface")
]
dispinterface _IAAAFireLimitEvents
{
   properties:
   methods:
   [id(1), helpstring("method Changed")]
            HRESULT Changed(IDispatch *Obj,
                  CURRENCY OldValue);
   [id(2), helpstring("method SignChanged")]
            HRESULT SignChanged(IDispatch *Obj,
                  CURRENCY OldValue);
};

・・・オブジェクトの IDL(下記に太文字で示します)の coclass セクションで、ソース インターフェイスとしてインターフェイスを宣言することの両方が必要です。

  coclass AAAFireLimit
{
   [default] interface IAAAFireLimit;
   [default, source] dispinterface _IAAAFireLimitEvents;
};

ATL のウィザードを正しく使用すれば、上記のコードが自動的に生成されます。

クラスの派生

ATL は IConnectionPointContainerImpl および IConnectionPointImpl という名前で、IConnectionPointContainer および IConnectionPoint の標準的なテンプレートの実装を提供します。最終的には、両方を使用することになりますが、IConnectionPointImpl を間接的に使おうと思います。

メイン ソース オブジェクト(クラス名 CAAAFireLimit)に IConnectionPointContainer を実装するために、2 つのことをします。

まずは継承リストに以下を加えます。

  public IConnectionPointContainerImpl<CAAAFireLimit>,

名医はこのプロジェクトに「AAAFireLimit」という名前を付けました。ですからクラス名は、CAAAFireLimit です。先頭を「AAA」としたのは、このオブジェクトが Visual Basic のオブジェクト参照(Project.References)リストの先頭に確実に現れるようにすること以外の意味がありません。リストの先頭にいれば、オブジェクトを参照するのも、参照解除するのも、一段と容易です。

次に、オブジェクトの COM マップに、IConnectionPointContainer 用のエントリを追加します。

  COM_INTERFACE_ENTRY(IConnectionPointContainer)

接続ポイント マップ

接続ポイント コンテナは、1 接続ポイントごとに 1 エントリ(つまりソース インターフェイスごとに1エントリ)を持つ接続ポイント マップを利用します。このマップは、CAAAFireLimit クラス宣言に含まれています(ここでは示しません)。

  BEGIN_CONNECTION_POINT_MAP(CAAAFireLimit)
CONNECTION_POINT_ENTRY(DIID__IAAAFireLimitEvents)
END_CONNECTION_POINT_MAP()

この簡単なマップは IConnectionPointContainer の実装に、ID DIID__IAAAFireLimitEvents のインターフェイス(言い換えれば、インターフェイス IAAAFireLimitEvents)に対する接続ポイントは1つだけしかないことを知らせます。もっと多くのソース インターフェイスを実装する場合、CONNECTION_POINT_ENTRY マクロの数ももっと多くなります。

これで IConnectionPoint は解決されます。では、実際の接続ポイントと、イベントを発行するループを得るにはどうすればよいのでしょう。

ところで、ATL のオブジェクト ウィザードを正しく使用すれば、このコードは自動的に加えられます。ですが、ウィザードがどんなコードを加えたのかを知っておくことは、イベントをデバッグしなければならない場合や、自分自身でコードを既存のオブジェクトに加えなければならない場合の備えとして、とても大切なのです。

IconnectionPointImpl と proxy クラス

IConnectionPointImpl からの継承もするつもりでは、と考えていませんか?そうだとしたら、あなたの推理は正解です。ですが、継承は間接的に行います。このほうが一石二鳥だからです。直接 IConnectionPointImpl から継承する代わりに、テンプレート化された「proxy」クラスを Visual Studio に作成してもらいます。このクラスは IConnectionPointImpl から派生し、Fire_[event] という形式の名前を持つメンバ関数をイベント メソッドごとに実装します。対象としているインターフェイスには 2 つのメソッド、ChangedSignChanged がありますから、接続ポイントの proxy クラスは Fire_ChangedFire_SignChanged を実装します。

これらメソッドのそれぞれの実装には、IConnectionPoint::Advise が呼び出された各インターフェイス ポインタに対して呼び出しを行うのに必要なループが含まれています。

その後、テンプレート化された proxy クラスから継承を行います。この Fire_ メソッドを含むクラスは、「接続ポイントのインプリメント」ウィザード(以前は ATL プロキシ ジェネレータという名で知られていました)と呼ばれるプログラムによって生成されます。このウィザードがどのように動作するのかについては、後できちんとお話しします。さしあたりここではウィザードが、入力としてタイプ ライブラリを必要とするので、あらかじめ IDL ファイルのコンパイルしてからでないと、proxy を生成できないことを覚えておきましょう。

「接続ポイントのインプリメント」ウィザードは、次の行も CAAAFireLimit の継承リストに加えます。

  public CProxy_IAAAFireLimitEvents< CAAAFireLimit >,

CProxy_IAAAFireLimitEvents は、ウィザードによって生成されるクラスの名前です。最後に、ウィザードは接続ポイントのエントリを接続ポイント マップに加えます。

  BEGIN_CONNECTION_POINT_MAP(CAAAFireLimit)
CONNECTION_POINT_ENTRY(DIID__IAAAFireLimitEvents)
END_CONNECTION_POINT_MAP()

複数の接続ポイント(複数のソース インターフェイス)がある場合は、それぞれについて proxy クラスから派生させます。おかしな話だと思うかもしれません。なぜなら、同じ継承リストでは同じクラスから多重継承は行えはずだからです。しかし、それでよいのです。同じテンプレートが異なるパラメータを使用して異なるクラスを生成するので、厳密に言えば同じクラスから何度も継承はしていないことになるのです。それぞれが微妙に(しかし十分に)違うのです。

以下にこのクラスをリストしてみます。Fire_ メソッドの1つ(Fire_SignChanged)は省略してあります。proxy クラスは IConnectionPointImpl から派生することに注意してください。Fire_ メソッドのためのコードを見てください。これを自分で記述しなくてよいのですよ。うれしいでしょう?

  template <class T>
class CProxy_IAAAFireLimitEvents :
      public IConnectionPointImpl<T,
            &DIID__IAAAFireLimitEvents, CComDynamicUnkArray>
{
   //Warning this class may be recreated by the wizard.
public:
   HRESULT Fire_Changed(IDispatch * Obj, CY OldValue)
   {
      CComVariant varResult;
      T* pT = static_cast<T*>(this);
      int nConnectionIndex;
      CComVariant* pvars = new CComVariant[2];
      int nConnections = m_vec.GetSize();

      for (nConnectionIndex = 0;
                        nConnectionIndex < nConnections;
                        nConnectionIndex++)
      {
         pT->Lock();
         CComPtr<IUnknown> sp =
                        m_vec.GetAt(nConnectionIndex);
         pT->Unlock();
         IDispatch* pDispatch =
                        reinterpret_cast<IDispatch*>(sp.p);
         if (pDispatch != NULL)
         {
            VariantClear(&varResult);
            pvars[1] = Obj;
            pvars[0] = OldValue;
            DISPPARAMS disp = { pvars, NULL, 2, 0 };
            pDispatch->Invoke(0x1,
                              IID_NULL, LOCALE_USER_DEFAULT,
                              DISPATCH_METHOD, &disp,
                              &varResult, NULL, NULL);
         }
      }
      delete[] pvars;
      return varResult.scode;

   }
      //... Fire_SignChanged omitted, similar to Fire_Changed
};

IConnectionPointImpl の内部を見る

気づかれたかもしれませんが、私たちは IConnectionPoint のための COM_INTERFACE_ENTRY を含めませんでした。なぜでしょう。

私たちのメイン オブジェクトは IConnectionPoint を実装しません。それよりも、むしろ IConnectionPointContainer::FindConnectionPoint は、IUnknownIConnectionPoint を実装する別のオブジェクトへのポインタを返すのです。

このオブジェクトは new を使用することによって IConnectionPointImpl のコンストラクタの中で作成されるのだろうと名医は想像していました。ですが、ATL の設計者はもっと遠回り(そして効率的)でした。代わりに、多重継承の奇跡、テンプレート、およびちょっと厄介な(でも有効な)ポインタ操作の結果、接続ポイント オブジェクト(複数の場合もあります)が、それぞれが必要とするデータ(接続のコレクションに対して)とともにメイン オブジェクトに組み込まれます。これらには別々の COM ID が割り当てられます。このため、論理上は別々のオブジェクトであり、単にあなたのオブジェクトの一部として実装されただけということになります。これはまったく正当なことです。というのも、COM は実装方法を問わず、振る舞いだけを問題にするからです。COM のルールに従う限り、オブジェクトは好きな方法で実装できるのです。

ATL が使用する素晴らしいトリックについて書くには、このコラムでは深入りしすぎなので取り上げませんが、詳細を知りたい方は、「ATL Internals」(Rector、Sells 著、Addison-Wesley 出版)の 390 ページから 396 ページで勉強してください。また、自分で ATL のソース コードを解析してもよいでしょう。

大切な最後のステップ:IProvideClassInfo2

最後に、重要なことをもう 1 つ。クライアントにタイプ ライブラリを利用する方法やデフォルトのソース インターフェイスへのアクセス方法を知らせるのは礼儀正しいことです。これは IProvideClassInfo2 を実装することで、正しく行えます。実装は ATL が行いますから、あなたは、ただ、以下をメイン クラスの継承リストに加えるだけです。

     public IProvideClassInfo2Impl<&CLSID_AAAFireLimit,
            &DIID__IAAAFireLimitEvents,
            &LIBID_AAAFIRELIMITMODLib,
            LIBRARY_MAJOR, LIBRARY_MINOR>

2 つのマクロ、LIBRARY_MAJORLIBRARY_MINOR を定義して、タイプ ライブラリのバージョン番号と対応させなければなりません。これは、ヘッダ ファイルの先頭近くで行います。さしあたっては、次のように定義しました。新しいバージョンのオブジェクトをリリースするときには、これらを更新してください。

  // version number of type library
#define LIBRARY_MAJOR 1
#define LIBRARY_MINOR 0

エントリを COM マップに加えるのを忘れないでくださいね。オブジェクトは IProvideClassInfo2IProvideClassInfo を実装するので、エントリを 2 つ加える必要があります。

  COM_INTERFACE_ENTRY(IProvideClassInfo2)
COM_INTERFACE_ENTRY(IProvideClassInfo)

後は ATL がやってくれます。

スレッドの問題

以前、名医は、新しいスレッドを作成しても、それらからイベントを発行することはできないと説明しました。新しいスレッドから Fire_Changed を呼び出す誘惑に駆られますが、それは完全な間違いです。

問題は、イベント インターフェイス ポインタを格納するスレッド(IConnectionPoint::Advise を呼び出すときの)は、格納されたポインタを使用する(Fire_Changed を呼び出すときの)スレッドと同じではないという点です。Fire_Changed のコードは、格納されたインターフェイス ポインタを直接使用しています。コードは以下のようになります。

  CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);

これは、マーシャリングされていない(生の)インターフェイス ポインタを、スレッドを超えて渡すことはできないという COM のルールに違反します。インターフェイス ポインタは ATL と Fire_ メソッドの中にうまく隠されているので、つい無視してしまいがちです。そうだとしても、ことは正しく行わなくてはいけません。クライアントが、別のスレッドに対する呼び出しを扱えるかどうか、つまり、クライアントがマルチスレッド処理が可能かどうかを知る方法はないからです。ほとんどのクライアントは、マルチスレッド処理ができません。

これを正すためには、IConnectionPoint::Advise のインターフェイス ポインタを(オリジナルのスレッドで実行中に)マーシャリングしなければなりません。そして Fire_Changed で(新しいスレッドの実行中に)アンマーシャリングしなければいけません。新しいスレッドから CoInitializeEx を呼び出すことも忘れないでください。これは COM を使用するあらゆるスレッドで必要な処理です。

ここでこれをコードにして示すのは、少しばかり複雑すぎます。ですが、ソリューションの必要なユーザーは、ここにある情報(と、COM の追加機能)を使って、ソリューションを作成できるはずです。今後のコラムでこの問題について取り組むことを、名医は望んでいます。

ATL の実例

ATL の実装は、自動的にいろいろな作業をしてくれますが、先の説明は少々威圧的だったかもしれません。しかし恐れることはありません。ATL にはよくできたウィザードとチェックボックスがいくつもあり、上記のコードのほとんどを実装する助けになってくれます。おおよそしなければならないのは、ソース インターフェイスを定義し、正しい時点でイベントを発行することだけです。

以下で作成するオブジェクトは、とてもシンプルです。オブジェクトには値があり、あなたはこの値を受け取ったり、設定したり、値を加算したりできます。値を変更したり、値の符号が変わったりすると、イベントが発行されます。

ステップ 1:モジュールの作成

あらゆる COM オブジェクトと同じく、まずは作成するオブジェクトを格納するモジュールを作成します(モジュールがない場合)。このステップで何か特別なことをする必要はありません。プロジェクトとって適切であれば、ATL COM AppWizard の任意のオプションを選択します。Dr. GUI は名前を「AAA」(オブジェクトがアルファベット順で前の方に来るように)で始め、終わりに「Mod」(モジュールがオブジェクトとは別であるように)を付けました。

名前を入力したら、後はすべて既定のオプションを選ぶだけです。これについての指示は ATLオブジェクトについてのDr. GUIの最初の記事 を参照してください。

ステップ 2:オブジェクトの作成(接続ポイントの支援を受けること忘れないように)

これは通常どおり行います。[ATL オブジェクトの新規作成Insert.New ATL Object)]([挿入Insert)]メニューの)を使用して、[シンプル オブジェクトSimple Object)]を選びます。次に名前を入力します(前のステップと同じ名前で末尾に「Mod」が付かないこと)。ここで[OK]をクリックしてはいけません。このオブジェクトの属性を設定しなければなりませんから、[アトリビュートAttributes)]タブをクリックして[コネクション ポイントのサポートSupport Connection Points)]ボックスにチェックを入れます。ダイアログ ボックスがこのようになったら、[OK]をクリックします。

図 1:[コネクション ポイントのサポート(Support Connection Points)]にチェックを入れた ATL オブジェクト ウィザード

コネクション ポイントのサポートSupport Connection Points)]には必ずチェックを入れてください。

注意 どういうわけか、Dr. GUI のインストールした Visual Studio®には[ATL オブジェクトの新規作成(Insert.New ATL Object)]コマンドが含まれていませんでした。Service Pack 3 をインストールしたら、問題は解決しました。同じような問題に出くわしたら、Service Pack をインストールしてみてください。

ATL オブジェクト ウィザードが生成したコードに目を通すと、これまでに見かけなかったいくつかのコードが見られるでしょう。

  • ソース インターフェイスが IDL ファイルに加えられます(しかしメソッドの宣言はありません。これらは次のステップで加えます)。

  • ソース インターフェイスの宣言は IDL ファイルの coclass セクションに加えられます。

  • ATL クラスは現在、IConnectionPointImpl から派生しています。

  • 接続ポイント マップが加えられています(エントリはありませんが)。

ステップ 3:メソッドをソース(イベント)インターフェイスに加える

次のステップは、通常の Visual Studio の手順([Class View]の中でソース インターフェイス名を右クリックし、次に[メソッドの追加Add Method)]を選択)を使用して、2 つのメソッドを加えます。その 2 つのメソッドの IDL は上に示したとおりです。

Changed メソッドのプロトタイプは次のとおりです。

  HRESULT Changed(IDispatch *Obj, CURRENCY OldValue);

値が変わったときには、オブジェクトへのインターフェイス ポインタとともに、古い値も渡します。インターフェイス ポインタを渡せば、クライアントはすべてのメソッドとプロパティにアクセスできるようになります。これは強力な武器です

COM の CURRENCY 型を使用したことにも注意してください。このタイプについては、COMのオートメーション データ型に関するDr. GUIのコラム で説明しています。

SignChanged メソッドのプロトタイプもこれによく似ています。上記の IDL コードを参照してください。

ステップ 4:IDL のコンパイル

メソッドを宣言できたら、IDL ファイルをコンパイルしなければなりません。一番速いのは、[File View]に切り替えて IDL ファイルを見つけてそれを右クリックして[コンパイルCompile)]を選択する方法です(時間に余裕があるなら、プロジェクト全体をビルドしてもよいでしょう)。

ステップ 5:接続ポイントの実装

IDL をコンパイルできたら、[Class View]に戻ります。次に、オブジェクトのクラス名(ここでは CAAAFireLimit)を右クリックし、[接続ポイントの実装Implement Connection Point)]を選択します。タイプ ライブラリが正しくコンパイルされていれば、以下のようなダイアログが現れます。

図 2:イベント インターフェイスにチェックを入れた[コネクション ポイントのインプリメント(Implement Connection Point)]ボックス

接続ポイントを実装したいインターフェイスには必ずチェックを入れます(ところで、このウィザードこそ、以前の ATL プロキシ ジェネレータと呼ばれていたもののアップデート バージョンです)。

コードは、次のように変更されます。

  • 新しい「proxy」クラスをプロジェクト(この場合 CProxy_IAAAFireLimitEvents)に加えます。このクラスは、接続ポイントを実装であり、ソースは別のファイルにあります。これは IConnectionPointImpl から派生し、イベント インターフェイスのそれぞれのメソッドについて Fire_<method> メソッドを加えます。上に示したコードを見てもわかるように、ウィザードがこのメソッドを書いてくれるのは大助かりです。

  • この新しいクラスを派生リストに加えます。

  • このイベント インターフェイスについて、オブジェクトの接続ポイント マップにエントリを加えます。

私たちはこのクラスから継承をしているので、その Fire_ メソッドも簡単に呼び出せます。イベント インターフェイスに変更を加えた場合、それがどんな変更であっても、再度このステップを全部実行しなければならないので注意してください(つまり IDL を再コンパイルして、接続ポイントを実装し直さなければなりません)。これは proxy クラスを最初から再生成するので、そのファイルのコードを変更してはいけません。

ステップ 6:IClassInfo2 の実装を加える

IClassInfoIClassInfo2 の実装を提供するのは正しい礼儀です。そのために、先の関連セクションで説明したコードを加えます。

ステップ 7:オブジェクトのプロパティとメソッドを加えてイベントを発行する

最後に、イベントを発行する準備が整ったので、通常のプロパティとメソッドを加えてみましょう。

このオブジェクトの目的が単純であることを思い出してください。このオブジェクトは通貨型の値を保持し、値が変わったときや、値の符号が変わったときにイベントを発行します。したがって、値のためのプロパティを用意します(そして、ディスパッチ ID を 0 に設定することによって、このプロパティを規定値とします)。また Add メソッドも加えて、オブジェクトに数値を加算できるようにします。オブジェクトのインターフェイスの IDL は、次のようになります。

  interface IAAAFireLimit : IDispatch
{
   [propget, id(0), helpstring("property Value")]
            HRESULT Value([out, retval] CURRENCY *pVal);
   [propput, id(0), helpstring("property Value")]
            HRESULT Value([in] CURRENCY newVal);
   [id(1), helpstring("method Add")]
            HRESULT Add(CURRENCY cyAddend);
};

メソッドの実装は、ほぼ予想どおりになります。ただし、オブジェクトの値を変更する可能性のあるメソッドは、必要なイベントを発行するヘルパー メソッド CheckAndFire を呼び出します。

  STDMETHODIMP CAAAFireLimit::get_Value(CURRENCY *pVal)
{
   *pVal = m_cyValue; // can't change value
   return S_OK;
}

STDMETHODIMP CAAAFireLimit::put_Value(CURRENCY newVal)
{
   CURRENCY oldVal = m_cyValue;
   m_cyValue = newVal;
   CheckAndFire(oldVal);
   return S_OK;
}

STDMETHODIMP CAAAFireLimit::Add(CURRENCY cyAddend)
{
   CURRENCY cyOld = m_cyValue;
   VarCyAdd(m_cyValue, cyAddend, &m_cyValue);
   CheckAndFire(cyOld);
   return S_OK;
}

データ メンバ m_cyValue は、CURRENCY 型のクラス メンバです。オートメーション データ型について書いた前回のコラム で説明したように、COM の VarCyAdd 関数を使用して加算を実行できるようにしています。

イベントを発行する必要があるかどうかを調べられるように、加算前の古い値も保持しています(これは、イベントを発行せずに、値を既存の値に設定できることを意味します。つまり Changed は、本当に変更を意味するのです)。

CheckAndFire メソッドは、ちょっとだけ複雑です。Changed イベントの場合は、文句なしに簡単です。しかし SignChanged イベントの場合には、少しだけ厄介です。というのも、値が正から負になったり、またはその逆になったりしない限り、イベントを発行したくないからです。値がゼロになる場合は対象となりません。一方、値が正からゼロ、そして負になる場合や、その逆の場合、イベントを発行する必要があります。ここに示すロジックは、これらの条件を正しく扱います。といっても符号を追跡するために、別のインスタンス変数(hrOldSign。コンストラクタで VARCMP_EQ に初期化される)を使用しますが。

COM の VarCyCmp 関数を使用して比較をしている点に注目してください。また、グローバル変数(cyZero)も作成しました。この変数は適切なフォーマットでゼロに初期化されます。

  CURRENCY cyZero = { 0i64 };

以下が CheckAndFire メソッドです。

  void CAAAFireLimit::CheckAndFire(CURRENCY cyOld)
{
   // Fire event if value changed
   HRESULT hrCmpRes = VarCyCmp(m_cyValue, cyOld);
   if (hrCmpRes != VARCMP_EQ)
      Fire_Changed(this, cyOld);
   // Fire event if sign changed
   HRESULT hrCmpZero = VarCyCmp(m_cyValue, cyZero);
   if (hrCmpZero != VARCMP_EQ) {  // not equal to zero
      if (hrCmpZero != hrOldSign && hrOldSign != VARCMP_EQ) {
         Fire_SignChanged(this, cyOld);
      }
      hrOldSign = hrCmpZero;
   }
}

イベントを発行するときには、this ポインタ(これでクライアントはメソッドを呼び出すことができます)と前の値を渡します(これはクライアントに情報を提供するためです)。

最後に。コンストラクタで hrOldSign を初期化することに加えて、m_cyValuecyZero に初期化します。

こんなところです。あとは、オブジェクトをビルドして(そしてデバッグして)、イベントに応答できるクライアントの中で実行します。次回はイベントを受け取るクライアントについてお話ししようと思います。

既存のオブジェクトにイベントを加えるとしたら

既存のオブジェクトにイベントを加える場合には、[コネクション ポイントのサポートSupport Connection Points)]チェックボックスにチェックを入れて加えたコードを、必ず全部加えなければなりません。これはステップ 2(上記)で説明されているコードです。ウィザードは、今でも proxy クラスの生成に使用できます。名医としては、こちらをお薦めしますね。あなたに産みの苦しみを味わわせたくないですから。

やってみよう!

これまでお話ししてきたことを実際に試してみるまでは、説明をきちんと理解しているかどうかわからないことは Dr. GUI も十分わかっています。ならば、実際にやってみましょう!

メソッド呼び出しの結果としてイベントを発行する、あなた自身の COM オブジェクトを書いてみましょう(スレッドをまたがってインターフェイス ポインタをマーシャリングする方法を理解するまでは、スレッドを開始してスレッドからイベントを発行してはいけません)。ビジュアルな ATL コントロールを書く方法を知っているのなら、コントロールに何かが起きたとき(例えば、誰かがコントロールをクリックした場合など)、イベントを発行できます。

次に、Visual Basic クライアントを書くか、Web ページとスクリプトを使用して、これをテストします。サンプルコードに Visual Basic クライアントが用意してあります。

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

今回は、イベントを ATL COM オブジェクトに加える方法について解説しました。イベントを加えるために必要なステップはいくつかありますが、複雑なステップもウィザードと proxy ジェネレータを使えば簡単にできます。

次回は、Visual Basic アプリケーションと ATL COM オブジェクトでイベントを受け取る方法について解説する予定です。