EternalWindows
WebBrowser コントロール / イベントシンクの実装

WebBrowser コントロールをホストするにあたって、 コントロールからイベントを取得する方法を理解しておくと便利です。 たとえば、何らかのページにアクセスした場合は、タブのタイトルをそのページのものに変更するべきですが、 これはイベントとして通知されることになっています。 こうした通知を受け取るためにはホストがイベントシンクというオブジェクトを作成し、 これをコントロールに渡すようにします。 そうすると、コントロールはイベントシンクが実装しているIDispatchを通じて、イベントを通知することができます。 CWebBrowserHostは、コントロールにイベントシンクを渡すために、CEventSink::Createを呼び出します。

BOOL CEventSink::Create(CWebBrowserHost *pWebBrowserHost, IUnknown *pUnknown)
{
	HRESULT                   hr;
	IConnectionPointContainer *pConnectionPointContainer;
	
	m_pWebBrowserHost = pWebBrowserHost;
	
	hr = pUnknown->QueryInterface(IID_PPV_ARGS(&pConnectionPointContainer));
	if (FAILED(hr))
		return FALSE;
	
	pConnectionPointContainer->FindConnectionPoint(DIID_DWebBrowserEvents2, &m_pConnectionPoint);
	pConnectionPointContainer->Release();

	m_pConnectionPoint->Advise(this, &m_dwCookie);
	
	return TRUE;
}

第2引数に指定されるIUnknownは、WebBrowser コントロールを識別しています。 コントロールはIConnectionPointContainerを実装しており、 FindConnectionPointを呼び出すことでIConnectionPointを取得できます。 このIConnectionPoint::Adviseでイベントシンクを渡すようにすれば、 イベントシンクに対してイベントが通知されるようになります。

WebBrowser コントロールでイベントが発生した場合は、IDispatch::Invokeが呼ばれます。 第1引数はイベントの種類を表す定数が指定され、 たとえばタイトル変更の場合はDISPID_TITLECHANGEになります。 イベント毎に渡される引数はそれぞれ異なるため、 引数の意味をリファレンスで調べておく必要があります。

http://msdn.microsoft.com/en-us/library/aa768283(VS.85).aspx

上記URLにアクセスすれば、各イベントと引数の意味を調べることができます。 イベントの引数は、IDispatch::InvokeのpDispParams->rgvargから取得できますが、 これらはリファレンスとは逆順に指定されることに注意してください。 たとえば、CommandStateChange(long Command, VARIANT_BOOL Enable)というプロトタイプなら、 pDispParams->rgvarg[1].lValにCommandの値が格納され、 pDispParams->rgvarg[0].boolValにEnableの値が格納されます。 次に、Invokeの実装を示します。

STDMETHODIMP CEventSink::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	if (dispIdMember == DISPID_BEFORENAVIGATE2)
		OnBeforeNavigate2(pDispParams->rgvarg[6].pdispVal, pDispParams->rgvarg[5].pvarVal, pDispParams->rgvarg[4].pvarVal, pDispParams->rgvarg[3].pvarVal, pDispParams->rgvarg[2].pvarVal, pDispParams->rgvarg[1].pvarVal, pDispParams->rgvarg[0].pboolVal);
	else if (dispIdMember == DISPID_NAVIGATECOMPLETE2)
		OnNavigateComplete2(pDispParams->rgvarg[1].pdispVal, pDispParams->rgvarg[0].pvarVal);
	else if (dispIdMember == DISPID_NEWWINDOW3)
		OnNewWindow3(pDispParams->rgvarg[4].ppdispVal, pDispParams->rgvarg[3].pboolVal, pDispParams->rgvarg[2].lVal, pDispParams->rgvarg[1].bstrVal, pDispParams->rgvarg[0].bstrVal);
	else if (dispIdMember == DISPID_COMMANDSTATECHANGE)
		OnCommandStateChange(pDispParams->rgvarg[1].lVal, pDispParams->rgvarg[0].boolVal);
	else if (dispIdMember == DISPID_TITLECHANGE)
		OnTitleChange(pDispParams->rgvarg[0].bstrVal);
	else if (dispIdMember == DISPID_STATUSTEXTCHANGE)
		OnStatusTextChange(pDispParams->rgvarg[0].bstrVal);
	else if (dispIdMember == DISPID_NAVIGATEERROR)
		m_nNavigateCount = 0;
	else
		return DISP_E_MEMBERNOTFOUND;

	return S_OK;
}

各イベントの処理は、専用の関数で行うようになっています。 各関数の処理を順に見ていきます。

void CEventSink::OnBeforeNavigate2(IDispatch *pDisp, VARIANT *url, VARIANT *Flags, VARIANT *TargetFrameName, VARIANT *PostData, VARIANT *Headers, VARIANT_BOOL *Cancel)
{
}

OnBeforeNavigate2は、何らかのURLにアクセスしようとする場合に送られます。 今回は何も処理を行っていませんが、CancelにVARIANT_TRUEを指定すれば第2引数のurlへのアクセスを防ぐことができます。

void CEventSink::OnNavigateComplete2(IDispatch *pDispatch, VARIANT *URL)
{
	CWebBrowserHost *pWebBrowserHost;
	IWebBrowser2    *pWebBrowser2;
	BSTR            bstr;

	pWebBrowserHost = g_pWebBrowserContainer->GetActiveBrowser();
	pWebBrowserHost->QueryBrowserInterface(IID_PPV_ARGS(&pWebBrowser2));
	pWebBrowser2->get_LocationURL(&bstr);

	g_pWebBrowserContainer->OnNavigateComplete2(bstr);

	SysFreeString(bstr);
	pWebBrowser2->Release();
	pWebBrowserHost->Release();
}

OnNavigateComplete2は、第2引数のURLへのアクセスした際に呼ばれます。 この際には、CWebBrowserContainer::OnNavigateComplete2を呼び出して、 URLをレバーコントロールのアドレスバーに設定したいところですが、そう簡単にはいきません。 たとえば、広告などを含んでいるHTMLでは、その広告を表示する際にOnNavigateComplete2が呼ばれるため、 これではアドレスバーに広告のURLが設定されてしまいます。 IWebBrowser2::get_LocationURLを呼び出せば正しいURLを取得できるため、 現在アクティブなCWebBrowserHostからIWebBrowser2を取得しています。

void CEventSink::OnNewWindow3(IDispatch **ppDisp, VARIANT_BOOL *Cancel, DWORD dwFlags, BSTR bstrUrlContext, BSTR bstrUrl)
{
	*Cancel = VARIANT_TRUE;
	
	g_pWebBrowserContainer->OnNewWindow3(bstrUrl);
}

OnNewWindow3は、新しくウインドウが作成される際に呼ばれます。 たとえば、メニューから「新しいウインドウで開く」を選択した場合や、 shiftキーを押しながらリンクをクリックした場合に呼ばれます。 既定の処理ではIEのウインドウが表示されることになるため、 CancelにVARIANT_TRUEを指定することでこれを防ぎます。 そして、CWebBrowserContainerに処理を渡すことにより、 新しいウインドウではなく新しいタブを作成するようにします。

void CEventSink::OnCommandStateChange(long Command, VARIANT_BOOL Enable)
{
	BOOL bEnableForward, bEnableBack;

	m_pWebBrowserHost->GetTravelState(&bEnableForward, &bEnableBack);

	if (Command == CSC_NAVIGATEFORWARD) {
		if (Enable == VARIANT_TRUE)
			bEnableForward = TRUE;
		else
			bEnableForward = FALSE;
	}
	else if (Command == CSC_NAVIGATEBACK) {
		if (Enable == VARIANT_TRUE)
			bEnableBack = TRUE;
		else
			bEnableBack = FALSE;
	}
	else 
		return;
	
	m_pWebBrowserHost->SetTravelState(bEnableForward, bEnableBack);

	g_pWebBrowserContainer->OnCommandStateChange(Command, Enable);
}

OnCommandStateChangeは、WebBrowser コントロールの履歴の状態が変化した際に呼ばれます。 CSC_NAVIGATEFORWARDは「進む」に関する履歴を識別し、 EnableがVARIANT_TRUEである場合はそれを実行することが可能になったことを意味します。 こうした情報は後でも必要になるため、SetTravelStateでCWebBrowserHostに保存しておきます。 「進む」や「戻る」が実行可能になる場合は、レバーコントロールの履歴ボタンが変更されなければならないため、 CWebBrowserContainerにそれを伝えます。

void CEventSink::OnStatusTextChange(BSTR Text)
{
	g_pWebBrowserContainer->OnStatusTextChange(Text);
}

OnStatusTextChangeは、ステータスバーに設定すべき文字列が存在する場合に呼ばれます。 たとえば、カーソルがリンクの上を通ると、そのリンクのURLが渡されます。 CWebBrowserContainerでは、実際にステータスバーに文字列を設定するための処理を行っています。

void CEventSink::OnTitleChange(BSTR Text)
{
	g_pWebBrowserContainer->OnTitleChange(Text);
}

OnTitleChangeは、ページのタイトルが変更された場合に呼ばれます。 この場合はタブのタイトルを変更したいため、CWebBrowserContainerにそれを任しています。

ページのフィルタリングについて

WebBrowser コントロールで特定のURLへのアクセスを防ぎたい場合、簡単な方法が2つあります。 まず1つは、今回取り上げたOnBeforeNavigate2を処理する方法です。

void CEventSink::OnBeforeNavigate2(IDispatch *pDisp, VARIANT *url, VARIANT *Flags, VARIANT *TargetFrameName, VARIANT *PostData, VARIANT *Headers, VARIANT_BOOL *Cancel)
{
	if (lstrcmpW(url->bstrVal, g_szFilterUrl) == 0)
		*Cancel = VARIANT_TRUE;
}

url->bstrValには現在アクセスしようとしているURLが格納されているため、 これがフィルタリングしたいURLと一致するかどうかを調べます。 一致した場合はCancelにVARIANT_TRUEが格納され、現在のページが新しいページに切り替わることはありません。 つまり、リンクをクリックしても何も反応しないことになります。

単純にページのアクセスをキャンセルするのではなく、 アクセスがフィルタされた旨をHTMLで表示したいことがあるかもしれません。 このような場合は、IDocHostUIHandler::TranslateUrlを処理します。

STDMETHODIMP CWebBrowserHost::TranslateUrl(DWORD dwTranslate, OLECHAR *pchURLIn, OLECHAR **ppchURLOut)
{
	if (lstrcmpW(pchURLIn, g_szFilterUrl) == 0) {
		LPWSTR lp;
		DWORD  dwSize = (lstrlenW(g_szRedirectUrl) + 1) * sizeof(WCHAR);

		lp = (LPWSTR)CoTaskMemAlloc(dwSize);
		lstrcpyW(lp, g_szRedirectUrl);
		*ppchURLOut = lp;
		return S_OK;
	}
	
	return S_FALSE;
}

pchURLInにアクセス先のURLが格納されているため、これとフィルタリングしたいURLが一致するかどうかを調べます。 一致する場合はCoTaskMemAllocで確保したメモリに代わりとしてアクセスするURLを指定し、 それをppchURLOutに格納してS_OKを返します。 そうすると、表示されるHTMLはg_szRedirectUrlが示すHTMLになります。 カレントディレクトリにアクセス禁止の旨を示したHTMLを用意しておき、 それをg_szRedirectUrlに格納していれば、 アプリケーションが用意したHTMLを表示するようなことが可能です。 TranslateUrlは、OnBeforeNavigate2でキャンセルを行った場合は呼ばれません。

WebBrowser コントロールは、既定でいくつかのフィルタリングをサポートしています。 たとえば、Windows VistaではWindows Parental Controls(WPC)という仕組みによって、特定のユーザーが特定のURLにアクセスできないよう設定できますが、 WebBrowser コントロールはこの設定を考慮しています。 つまり、ページにアクセスすれば自動でWPCにおける警告ページが表示されるため、 アプリケーションがWPCで禁止されたURLを取得し、それと現在のURLを比較するような必要はありません。 もし、取得したい場合はIWPCBlockedUrlsを使用することになります。

void CWebBrowserHost::EnumWPCBlockedUrls()
{
	IDispatch        *pDispatch;
	IServiceProvider *pServiceProvider;
	IWPCBlockedUrls  *pBlockedUrls;
	HRESULT          hr;
	DWORD            i, dwCount;
	BSTR             bstr;

	hr = m_pWebBrowser2->get_Document(&pDispatch);
	if (FAILED(hr)) {
		TCHAR szBuf[256];
		wsprintf(szBuf, TEXT("%x"), hr);
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
		return;
	}

	pDispatch->QueryInterface(IID_PPV_ARGS(&pServiceProvider));
	hr = pServiceProvider->QueryService(IID_IWPCBlockedUrls, IID_PPV_ARGS(&pBlockedUrls));
	if (FAILED(hr)) {
		TCHAR szBuf[256];
		wsprintf(szBuf, TEXT("%x"), hr);
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
		return;
	}	

	pBlockedUrls->GetCount(&dwCount);

	for (i = 0; i < dwCount; i++) {
		pBlockedUrls->GetUrl(i, &bstr);
		MessageBox(NULL, bstr, TEXT("OK"), MB_OK);
		SysFreeString(bstr);
	}

	if (dwCount == 0)
		MessageBox(NULL, TEXT("ブロックされているURLは存在しません。"), TEXT("OK"), MB_OK);
	
	pBlockedUrls->Release();
	pServiceProvider->Release();
	pDispatch->Release();
}

MSDNには、IWPCBlockedUrlsの使い方は記述されていますが、IWPCBlockedUrlsをどう取得すればよいかは記述されていません。 get_DocumentからIServiceProvider経由で取得できることは確認していますが、 ブロックしているURLが存在するにも関わらずIWPCBlockedUrls::GetCountで0が返ります。 IWPCBlockedUrlsを使用する場合は、mshtml.hのインクルードが必要です。



戻る