COMサーバーへのイベントハンドラ登録

追記:2021年02月08日
たまにグーグル検索等からアクセスがあるのですが、このページのサンプルコードはInternet Explorerを前提としています。既にInternet Explorer 11はマイクロソフトでのサポートは終了している関係で、あまり参考にもなりません。あらかじめご了承ください。
サンプルソースへのリンクはリンク切れになっています。 githubに移しました。(2022年05月08日)


タイトルは少し用語の使い方が変かもしれない(^^;

RADツールを使わずに・・・C++ と Win32 SDKのみでゴリゴリ書く・・・と言った場合は、非常に手間がかかる。特に難解という程でもないけど、最低限知っておかなければならない必要な知識多すぎるのと、決まり切ったルーチン(退屈な)処理を書かなければならない。職業Windowsプログラマー御用達(笑)のVisual Studioを使えばこの辺の処理を代行してくれるので非常に開発効率があがりますが、趣味でやっているサンデープログラマーにはVisual Studioはあまりに高価すぎるし・・・無償のExpress Editionで十分って感じです。当然だけど・・・Express Editionにはそういった、ラクできる機能の一切は省略されている。

そんなことはさておき・・・、
このエントリは、WebBrowserコントロールで発生する各種イベントに対応するイベントハンドラをC++とWindowsSDK(以前はPlatform SDKと呼ばれていたのですが呼称が変わったみたいです)のみで作成・登録する一連の処理の備忘録です。COM/OLEの詳細な知識は必要ないですが、COM初級レベルの知識は必須です。

まず、なんらかの方法でWebBrowserのインスタンスを作らなければなりませんが、Windowsには都合良く?InternetExplorerが利用できます。これは、クラスIDにCLSID_InternetExplorer、インターフェイスIID_IWebBrowser2をCoCreateInstanceに渡してサクッと作ります。

 //shlobj.h と comutil.h のインクルードが必要です。

IWebBrowser2* pIE;
_variant_t empty;
VariantInit(&empty);

CoCreateInstance(CLSID_InternetExplorer,
                 NULL,
                 CLSCTX_LOCAL_SERVER,
                 IID_IWebBrowser2,
                 reinterpret_cast<LPVOID*>(&pIE));

pIE->Navigate(_bstr_t("http://www.yahoo.co.jp"),&empty,&empty,&empty,&empty);
pIE->put_Visible(TRUE);//デフォルトでIEは非表示状態ですので、表示させます。

終了処理は、pIE->Quit()で閉じて、pIE->Release()でインターフェイスポインタを解放します。

 

さて本題。
上で起動したIEで、普通は表示されているページのリンクをクリックしたりすると他のページに移動します。実際、プログラムでは、この「他のページに移動した!」っていうイベントを何とかして捕まえて、各々のイベントに対応した処理を行うのが通常です。
WebBrowserコントロールで発生した各種イベントは、WebBrowserコントロールのインスタンスに登録されている(ちょっと意味が違うけど)イベントシンクというオブジェクトのインターフェイスポインタのメソッドを次々に実行(Invoke)していきます。

で、このイベントシンクって一体何なのか?ということですが・・・WebBrowserコントロールに関して言えば、まんま、IDispatchインターフェイスをインプリメントしたもの・・・です。で、IDispachすべてを実装しなくてはいけないか? ということでもなくて、実際にコールされるのはIDispach::Invoke のみなので、最低限IDispach::Invokeさえ実装すればいいんです。なーーーんだ、意外と簡単です(^^;。ATLが使えればもっとかなりラクできて柔軟に作れるのですが・・・。

まずは、WebBrowserインスタンスへのイベントシンクの登録です。最終的にはIConnectionPoint::Advise()でイベントシンクのインターフェイスポインタとWebBrowserのインスタンスを接続させます。IConnectionPointが何なのかは、その詳細を知る必要はとりあえずはないです。こういう手順を踏むという方法さえ分かればオケです。
まずIWebBrowser2のインタフェイスポインタ(pIE)からIConnectionPointContainerインターフェイスポインタを取得します。続いてIConnectionPointContainer::FindConnectionPoint()でIConnectionPointのインターフェイスポインタを取得できます。FindConnectionPointの第一引数には、DWebBrowserEvents2のインターフェイスIDを指定します。

IConnectionPointContainer *pCPC = NULL;
IConnectionPoint *pCP = NULL;
pIE->QueryInterface(IID_IConnectionPointContainer,reinterpret_cast<LPVOID*>(&pCPC));

//DWebBrowserEvents2
pCPC->FindConnectionPoint(DIID_DWebBrowserEvents2,&pCP);

あとは、IConnectionPoint::Advise でイベントシンクを接続します。

 

肝心のイベントシンクですが、最初に、まんまIDispatchを実装すればいい、と書きました。WebBrowserコントロールはイベント発火時に上で少し触れたDWebBrowserEvent2のメンバメソッドをイベントの種類に応じて実行するわけですが、そのとき登録された(IConnectionPoint::Adviseで接続された)インターフェイスポインタのInvokeコマンドをコールするようです。ですので、実装するのは、IDispatch::Invokeのみで、あとは適当に return E_NOTIMPL; などでお茶を濁してもいいようです(^^;;;

//こんな感じでしょうか。

class CEventSink : public IDispatch
{
  ULONG RefCounter;

public:
  //constructor & destructor
  CEventSink()
   {
     RefCounter = 0;
   }
  virtual ~CEventSink(){}

  //IUnknow Interface
  virtual STDMETHODIMP_(ULONG) AddRef(void)
    {
      return ++RefCounter;
    }
  virtual STDMETHODIMP_(ULONG) Release(void);
    {
      if(--RefCounter == 0)
        delete this;

      return RefCounter;
    }

  virtual STDMETHODIMP QueryInterface(REFIID riid, void **ppvOut);
    {
      HRESULT retVal = S_OK;

      if(IsEqualIID(riid,IID_IUnknown) || IsEqualIID(riid,IID_IDispatch))
        {
          *ppvOut = reinterpret_cast<IDispatch*>(this);
          this->AddRef();
        }
      else
        {
          *ppvOut = NULL;
          retVal = E_NOINTERFACE;
        }

      return retVal;
    }

  //IDsipatch Interface
  virtual STDMETHODIMP GetTypeInfoCount(UINT* pctinfo)
    {
      return E_NOTIMPL;
    }

  virtual STDMETHODIMP GetTypeInfo(UINT itinfo,
                                   LCID lcid,
                                   ITypeInfo** pptinfo)
    {
      return E_NOTIMPL;
    }

  virtual STDMETHODIMP GetIDsOfNames(REFIID riid,
                                     LPOLESTR* rgszNames,
                                     UINT cNames,
                                     LCID lcid,
                                     DISPID* rgdispid)
    {
      return E_NOTIMPL;
    }

  virtual STDMETHODIMP Invoke(DISPID dispidMember,
                              REFIID riid,
                              LCID lcid,
                              WORD wFlags,
                              DISPPARAMS* pdispparams,
                              VARIANT* pvarResult,
                              EXCEPINFO* pexcepinfo,
                              UINT* puArgErr)
    {
        //ここにイベントの種類に応じて処理をする。

        //どのイベントが発生したのかは、dispidMember を調べれば分かる。
        //SDKドキュメントのDWebBrowserEvent2の各々のメンバーメソッドを
        //参照すれば、どのイベントにどういうDISPIDが飛んでくるのかが記述している。
        //また、各イベントのパラメータはpdispparamsに格納されているようです。
    }
};

長くなりましたが、ここまで来たらあとはこのCEventSinkのインスタンスを作成して、上述のIConnectionPoint::Adviseに渡せばいいんです。

//dwCookieは、あとでWebBrowserインスタンスとイベントシンクとの接続を解除するのに必要です。

DWORD dwCookie = 0L;
IUnknown *pUnk = NULL;
CEventSink *pES = new CEventSink();

pES->QueryInterface(IID_IUnknown,reinterpret_cast<LPVOID*>(&pUnk));
pCP->Advise(pUnk,&dwCookie);

これで一応基本的なイベントは拾えるはずです。
WebBrowserコントロールのインスタンスからIHTMLDocument2を得て、たとえば様々なタグのonclickやonmouseoverなどの各種イベントを処理したいときも、基本的にはこれらの流れと同じです。違うのは、IConnectionPointのインターフェイスポインタを得るときに、DIID_HTMLElementEvents2を指定することです。WebBrowserコントロールがどのようなConnectionPointを持っているのかは、IConnectionPointContainer::EnumConnectionPointsメソッドで列挙子を得ることで取得できます。

 

WebBrowserコントロールだけでなく、COMサーバーからイベントを受け取る手順は全て同じ。イベントシンクを作って、ConnectionPointを探して、IConnectionPoint::Adviseで接続するだけ。意外に簡単ですが、手間だけがかかります。

僕が作ったテストサンプルソースはこちら。
githubに移しました。

修正:
E_NOINTERFACE ではなく、E_NOTIMPL の間違いを修正しました。完全に勘違いです。

サンプルも間違っていますが、面倒なので修正してません。ソースは汚いのであまり参考になりません。
あしからず。