EternalWindows
BHO / イベントの受信

BHO(Browser Helper Object)とは、Internet Explorer(以下IE)にプラグイン可能なDLLのことです。 BHOはIEの起動時にロードされ、当然ながらそのコードはIEのプロセスの中で動作するため、 任意の機能をIEに追加できます。 IEにロードできるDLLにはバンドオブジェクトと呼ばれる種類もありますが、 これは主にツールバーとしての実装を持つことを意図したDLLであり、予めその役割というものが決まっています。 しかし、BHOの動作内容は明確に規定されていないため、基本的にどのような処理を行っても問題ありません。 次に、今回作成するBHOの実行結果を示します。

HTMLは他のページへのリンクを持つことができますが、 このようなリンクの上にカーソルが乗った場合は、そのリンク先のHTMLを取得するようにしています。 つまり、実際にページへ切り替えることなくページの中身を確認するということです。 本来ならば、ページの内容はサムネイル形式で表示された方が好ましいといえますが、 今回はスタティックコントロールにHTMLのテキストを設定するだけにしています。

BHOとして機能するDLLは、COMオブジェクトを実装したインプロセスサーバーでなければなりません。 つまり、C++言語におけるクラスを定義し、それを型としたインスタンス(オブジェクト)をIEに返さなければなりません。 BHOにおけるオブジェクトの1つの条件は、必ずIObjectWithSiteというインターフェースを実装することです。 これを実装していなければ、IEからの情報を受け取ることができなくなってしまうからです。 オブジェクトがIObjectWithSiteを実装するためには、 当然ながらオブジェクトのクラスがIObjectWithSiteを継承している必要があります。

class CBho : public IObjectWithSite, public IDispatch
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP SetSite(IUnknown *pUnkSite);
	STDMETHODIMP GetSite(REFIID riid, void **ppvSite);

	STDMETHODIMP GetTypeInfoCount(UINT *pctinfo);
	STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);
	STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
	STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);
	
	CBho();
	~CBho();

private:
	// ここでは省略
};

SetSiteとGetSiteがIObjectWithSiteのメソッドになり、 GetTypeInfoCountからInvokeまでがIDispatchのメソッドになります。 IDispatchも継承しているのは、IEからのイベントを受信するためです。 こうしたイベントの受信は、もう1つ別のクラスを作成してそこで行うことが多いのですが、 今回のCBhoはあまり行う処理がないため、イベントの受信も兼ねるようにしています。

BHOをロードしたIEは、IObjectWithSite::SetSiteを呼び出すことによって、サイトと呼ばれるオブジェクトを渡そうとします。 このサイトからは、IEを識別するIWebBrowser2を取得できます。

STDMETHODIMP CBho::SetSite(IUnknown *pUnkSite)
{
	HRESULT hr = S_OK;
	
	if (m_pSite != NULL){
		m_pSite->Release();
		m_pSite = NULL;
	}

	if (pUnkSite != NULL) {
		IConnectionPointContainer *pConnectionPointContainer;
		
		m_pSite = pUnkSite;
		m_pSite->AddRef();
		
		IUnknown_QueryService(pUnkSite, SID_SWebBrowserApp, IID_PPV_ARGS(&m_pWebBrowser2));

		hr = m_pWebBrowser2->QueryInterface(IID_PPV_ARGS(&pConnectionPointContainer));
		if (FAILED(hr))
			return hr;
		
		pConnectionPointContainer->FindConnectionPoint(DIID_DWebBrowserEvents2, &m_pConnectionPoint);
		pConnectionPointContainer->Release();

		m_pConnectionPoint->Advise(static_cast<IDispatch *>(this), &m_dwCookie);

		m_hSession = OpenInternetSession();

		return S_OK;
	}
	else {
		if (m_hSession != NULL)
			InternetCloseHandle(m_hSession);

		if (m_pConnectionPoint != NULL) {
			m_pConnectionPoint->Unadvise(m_dwCookie);
			m_pConnectionPoint->Release();
		}
		
		if (m_pMouseMoveListener != NULL)
			m_pMouseMoveListener->Release();

		if (m_pWebBrowser2 != NULL)
			m_pWebBrowser2->Release();
	}

	return hr;
}

渡されたサイトはm_pSiteとして保存することになっており、既に初期化されている場合は開放するようにします。 サイトからIWebBrowser2を取得するには、QueryInterfaceにIID_IWebBrowser2を指定すればよいように思えますが、これは上手くいきません。 正しい手順は、サイトからIServiceProviderを取得し、IServiceProvider::QueryServiceにSID_SWebBrowserAppを指定することです。 このIServiceProviderの取得とQueryServiceの呼び出しは、IUnknown_QueryServiceでまとめて行うことができるためそのようにしています。 IConnectionPointContainerを取得しているのは、IEからのイベントを取得するためです。 FindConnectionPointにDIID_DWebBrowserEvents2を指定した場合は、イベントを表すIConnectionPointを取得できるため、 これのAdviseを呼び出します。 そうすると、イベントが発生した場合に第1引数のオブジェクトのIDispatch::Invokeが呼ばれます。 pUnkSiteがNULLである場合は、IEのタブが閉じられてDLLがアンロードされようとしていることを意味します。 この場合は、使用していたインターフェースを開放することになります。

OpenInternetSessionの内部は次のようになっています。

HINTERNET CBho::OpenInternetSession()
{
	CHAR      szUserAgentA[256];
	WCHAR     szUserAgentW[256];
	DWORD     dwLength;
	HINTERNET hSession;
	DWORD     dwTimeOut;
	
	UrlMkGetSessionOption(URLMON_OPTION_USERAGENT, szUserAgentA, sizeof(szUserAgentA), &dwLength, 0);
		
#ifdef UNICODE
	MultiByteToWideChar(CP_ACP, 0, szUserAgentA, -1, szUserAgentW, 256);
	hSession = InternetOpen(szUserAgentW, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
#else
	hSession = InternetOpen(szUserAgentA, INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0);
#endif

	dwTimeOut = 2 * 1000;
	InternetSetOption(hSession, INTERNET_OPTION_RECEIVE_TIMEOUT, &dwTimeOut, sizeof(DWORD));

	return hSession;
}

HTMLの受信にはHTTPリクエストを送信する必要があるため、それを行うことができるWinINet APIを使用します。 WinHTTPも候補ですが、IE自体が通信にWinINetを使用しているということもあり、WinINetを使用するようにしています。 WinINetを使用する場合は、最初にInternetOpenを呼び出してセッションハンドルを取得します。 第1引数はユーザーエージェント文字列であり、これはいわば通信を行うアプリケーションを識別しています。 BHOがIEの中で動作するという関係上、IEで使用されるユーザーエージェントを指定したいため、 UrlMkGetSessionOptionでこれを取得します。 文字列はANSIとして返ることになっているため、 UNICODEが定義されている場合はUNICODE文字列に変換します。 InternetSetOptionでタイムアウトを設定しているのは、HTMLを受信するまでの時間が長すぎるとブラウザを操作できなくなるからです。 タイムアウトはミリ秒単位で指定することができ、上記の場合であれば2秒の意味になります。

既に述べたように、IEからのイベントはIDispatch::Invokeによって通知されます。 通知されるイベントにはいくつかの種類がありますが、 今回はナビゲート(特定URLへのアクセス)が完了したことを示すDISPID_NAVIGATECOMPLETE2を検出します。

STDMETHODIMP CBho::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	if (dispIdMember == DISPID_NAVIGATECOMPLETE2) {
		IHTMLDocument2 *pDocument2;
		IDispatch      *pDispatch;
		VARIANT        varDispatch;

		m_pWebBrowser2->get_Document(&pDispatch);
		pDispatch->QueryInterface(IID_PPV_ARGS(&pDocument2));
		pDispatch->Release();
		
		if  (m_pMouseMoveListener != NULL)
			m_pMouseMoveListener->Release();
		m_pMouseMoveListener = new CMouseMoveListener(m_pWebBrowser2, m_hSession);
		
		varDispatch.vt = VT_DISPATCH;
		varDispatch.pdispVal = m_pMouseMoveListener;
		pDocument2->put_onmousemove(varDispatch);

		pDocument2->Release();
	}
	else
		return DISP_E_MEMBERNOTFOUND;

	return S_OK;
}

今回作成するBHOの目的は、リンク先にマウスが位置した場合にHTMLを表示するということになっていますが、 このためにはマウスの移動というものを検出しなければなりません。 この設定はドキュメントに対して行わなければならないため、 まずIWebBrowser2::get_Documentを呼び出します。 この処理は、IObjectWithSite::SetSiteの時点では失敗することに注意してください。 ドキュメントをIDispatchで識別してもたいしたことはできないため、 QueryInterfaceを呼び出してIHTMLDocument2で識別するようにします。 そして、IHTMLDocument2::put_onmousemoveを呼び出すことによって、 マウスの移動を検出するオブジェクトをドキュメントに設定します。 このオブジェクトはCMouseMoveListenerという型を持ち、クラスはIDispatchを継承しています。 マウスが移動された際にはIDispatch::Invokeが呼ばれることになりますが、詳しくは次節で説明します。


戻る