EternalWindows
BHO / マウスの移動処理

前節では、IHTMLDocument2::put_onmousemoveを呼び出すことによって、 マウスの移動を検出するCMouseMoveListenerを設定しました。 今回はこのオブジェクトの実装について見ていきます。 まず、コンストラクタを示します。

CMouseMoveListener::CMouseMoveListener(IWebBrowser2 *pWebBrowser2, HINTERNET hSession)
{
	IDispatch *pDispatch;
	
	m_cRef = 1;
	m_hwndParent = NULL;
	m_hwndPopup = NULL;
	m_pWebBrowser2 = pWebBrowser2;
	m_hSession = hSession;

	m_pWebBrowser2->get_Document(&pDispatch);
	pDispatch->QueryInterface(IID_PPV_ARGS(&m_pDocument2));
	pDispatch->Release();

	CreatePopup();
}

Cbhoから渡されたIWebBrowser2は後で必要になるため、メンバとして保存するようにします。 また、IHTMLDocument2は多くのメソッドで必要になるため、この時点で取得しておきます。 CreatePopupは、リンク先のHTMLを表示するためのポップアップウインドウを作成します。

void CMouseMoveListener::CreatePopup()
{
	IShellBrowser *pShellBrowser;

	IUnknown_QueryService(m_pWebBrowser2, SID_STopLevelBrowser, IID_PPV_ARGS(&pShellBrowser));
	IUnknown_GetWindow(pShellBrowser, &m_hwndParent);

	m_hwndPopup = CreateWindowEx(0, TEXT("STATIC"), TEXT(""), WS_CHILD, 0, 0, 0, 0, m_hwndParent, (HMENU)1, 0, NULL);

	pShellBrowser->Release();
}

ウインドウを作成するためには、親ウインドウのハンドルが必要になります。 これにはまず、IWebBrowser2からIServiceProviderを取得し、IServiceProvider::QueryServiceでIShellBrowserを取得します。 そして、IShellBrowserからIOleWindowを取得し、IOleWindow::GetWindowでウインドウハンドルを取得できます。 IServiceProviderについてはIUnknown_QueryServiceでラッピング可能であり、 IOleWindowについてはIUnknown_GetWindowでラッピング可能です。 今回作成するポップアップウインドウの正体はスタティックコントロールであるため、 ウインドウクラスにはSTATICを指定しています。 ちなみに、MSHTMLにはIHTMLPopupというインターフェースが定義されていますが、 これを取得する際のIHTMLWindow4::createPopupでエラーが発生したため、 使用するに至っていません。

マウスが移動した場合は、IDispatch::Invokeが呼ばれることになります。 ここでは、リンク先のHTMLを含んだポップアップウインドウを表示します。

STDMETHODIMP CMouseMoveListener::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	POINT              pt;
	BSTR               bstrUrl, bstrHref;
	IHTMLElement       *pElement;
	IHTMLAnchorElement *pAnchorElement;
	CHAR               szText[4096];

	GetCursorPos(&pt);
	ScreenToClient(m_hwndParent, &pt);

	m_pDocument2->elementFromPoint(pt.x, pt.y, &pElement);
	if (pElement == NULL) {
		ShowWindow(m_hwndPopup, SW_HIDE);
		SysFreeString(m_bstrHref);
		m_bstrHref = NULL;
		return E_FAIL;
	}

	pElement->QueryInterface(IID_PPV_ARGS(&pAnchorElement));
	pElement->Release();
	if (pAnchorElement == NULL) {
		ShowWindow(m_hwndPopup, SW_HIDE);
		SysFreeString(m_bstrHref);
		m_bstrHref = NULL;
		return E_FAIL;
	}
	
	pAnchorElement->get_href(&bstrHref);
	if (lstrcmpW(bstrHref, m_bstrHref) == 0) {
		pAnchorElement->Release();
		return S_OK;
	}

	m_bstrHref = bstrHref;

	if (PathIsURL(bstrHref))
		bstrUrl = SysAllocString(bstrHref);
	else {
		m_pDocument2->get_URL(&bstrUrl);
		PathRemoveFileSpec(bstrUrl);
		lstrcat(bstrUrl, bstrHref);
	}

	if (SendRequest(bstrUrl, (LPBYTE)szText, sizeof(szText)))
		SetWindowTextA(m_hwndPopup, szText);
	else
		SetWindowTextA(m_hwndPopup, "error");

	MoveWindow(m_hwndPopup, pt.x, pt.y, 600, 500, TRUE);
	ShowWindow(m_hwndPopup, SW_SHOW);
	
	SysFreeString(bstrUrl);
	pAnchorElement->Release();

	return S_OK;
}

現在のカーソルの位置に存在するエレメント(タグ)を取得するために、IHTMLDocument2::elementFromPointを呼び出しています。 第1引数と第2引数はカーソルの位置であり、これはブラウザのクライアント座標を基準にしている必要があります。 エレメントを取得できた場合は、それがリンクを示すアンカーエレメント(Aタグ)であるかを確認しなければなりませんが、 これはQueryInterfaceでIHTMLAnchorElementを取得できるかどうかで分かります。 これが成功すれば、IHTMLAnchorElement::get_hrefを呼び出してリンク先を取得します。 これが前回参照したURLと一致する場合は、現在表示されているポップアップウインドウの中身を変更する必要はありませんから、処理を中断しています。 リンク先URLはhttp:から始まる絶対URLと、そうでない相対URLが考えられますが、 前者の場合はPathIsURLで判定可能です。 PathIsURLが失敗した場合は、IHTMLDocument2::get_URLで現在のURLを取得し、 PathRemoveFileSpecでファイル名の部分を除いてから相対URLと連結します。 リンク先のURLを取得したら、HTMLを取得するためにSendRequestという自作メソッドを呼び出します。 受信したHTMLのテキストは、本来ならばcharsetに基づいてエンコードする必要がありますが、 今回はそうした処理を行っていません。 よって、charsetがShift_JISの場合以外は文字化けを起こす可能性があります。 MoveWindowによってポップアップウインドウのサイズを調整し、 ShowWindowによってポップアップウインドウを表示します。

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

BOOL CMouseMoveListener::SendRequest(LPWSTR lpszUrl, LPBYTE lpBuffer, DWORD dwBufferSize)
{
	HINTERNET       hConnect, hRequest;
	URL_COMPONENTSW urlComponents;
	WCHAR           szHostName[INTERNET_MAX_HOST_NAME_LENGTH], szUrlPath[INTERNET_MAX_PATH_LENGTH];
	DWORD           dwSize;
	DWORD           dwStatusCode;

	ZeroMemory(&urlComponents, sizeof(URL_COMPONENTSW));
	urlComponents.dwStructSize     = sizeof(URL_COMPONENTSW);
	urlComponents.lpszHostName     = szHostName;
	urlComponents.dwHostNameLength = sizeof(szHostName) / sizeof(WCHAR);
	urlComponents.lpszUrlPath      = szUrlPath;
	urlComponents.dwUrlPathLength  = sizeof(szUrlPath) / sizeof(WCHAR);

	if (!InternetCrackUrlW(lpszUrl, lstrlen(lpszUrl), 0, &urlComponents))
		return FALSE;

	hConnect = InternetConnectW(m_hSession, szHostName, urlComponents.nPort, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
	if (hConnect == NULL)
		return FALSE;

	hRequest = HttpOpenRequestW(hConnect, L"GET", szUrlPath, NULL, NULL, NULL, 0,  0);
	if (hRequest == NULL) {
		InternetCloseHandle(hConnect);
		return FALSE;
	}

	if (!HttpSendRequest(hRequest, NULL, 0, NULL, 0)) {
		InternetCloseHandle(hRequest);
		InternetCloseHandle(hConnect);
		return FALSE;
	}

	dwSize = sizeof(DWORD);
	HttpQueryInfo(hRequest, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &dwStatusCode, &dwSize, NULL);
	if (dwStatusCode == HTTP_STATUS_OK){
		DWORD dwRead = 0;
		InternetReadFile(hRequest, lpBuffer, dwBufferSize, &dwRead);
	}

	InternetCloseHandle(hRequest);
	InternetCloseHandle(hConnect);

	return dwStatusCode == HTTP_STATUS_OK;
}

HTMLを取得するためには、サーバーにHTTPリクエストを発行しなければなりません。 このためにまず、InternetConnectでサーバーに接続する必要がありますが、 このメソッドはURLではなくサーバーのホスト名を要求します。 よって、InternetCrackUrlを呼び出してURLからホスト名を抜き出します。 InternetConnectでサーバーに接続したら、HttpOpenRequestでリクエストハンドルを作成します。 この際にはURLパス(URLのホスト名以下の部分)を指定しなければなりませんが、これはInternetCrackUrlでホスト名と共に取得済みです。 今回のWinINetは同期的に使用しているため、HttpSendRequestが制御を返した時点でリクエストはサーバーに発行され、レスポンスが返ってきています。 HttpQueryInfoを呼び出せば、レスポンスのヘッダからステータスコードを取得することができ、 これがHTTP_STATUS_OK(200)である場合はリクエストが成功したことを意味します。 この際には、InternetReadFileを呼び出してレスポンスのボディからデータを取得します。 なお、InternetOpenUrlを呼び出せば、InternetCrackUrlからHttpSendRequestまでの処理を一斉に行うことができますが、 一部のURLでは処理が上手くいかなかったため使用していません。

IHTMLXMLHttpRequestについて

IE7からは、HTTP通信を行うためのXMLHttpRequestオブジェクトが登場しました。 このオブジェクトは主に、javascript内でHTMLやXMLを非同期に取得する目的で使用されますが、 C/C++アプリケーションからでもIHTMLXMLHttpRequestを使用すれば扱うことができます。 次に、IHTMLXMLHttpRequestを取得する例を示します。

IHTMLXMLHttpRequest* CreateRequest(IHTMLDocument2 *pDocument2)
{
	IHTMLWindow2               *pWindow2;
	IHTMLWindow5               *pWindow5;
	IHTMLXMLHttpRequest        *pRequest;
	IHTMLXMLHttpRequestFactory *pFactory;
	VARIANT                    varFactory;
	HRESULT                    hr;

	pDocument2->get_parentWindow(&pWindow2);
	pWindow2->QueryInterface(IID_PPV_ARGS(&pWindow5));
	pWindow2->Release();
	
	CoInternetSetFeatureEnabled(FEATURE_XMLHTTP, SET_FEATURE_ON_PROCESS, TRUE);

	VariantInit(&varFactory);
	hr = pWindow5->get_XMLHttpRequest(&varFactory);
	if (FAILED(hr) || varFactory.vt == VT_EMPTY) {
		pWindow5->Release();
		return NULL;
	}

	varFactory.pdispVal->QueryInterface(IID_PPV_ARGS(&pFactory));
	pFactory->create(&pRequest);
	pWindow5->Release();
	varFactory.pdispVal->Release();

	return pRequest;
}

まず、IHTMLDocument2::get_parentWindowを呼び出してIHTMLWindow2を取得します。 このインターフェースにはXMLHttpRequestに関するメソッドはありませんが、 IHTMLWindow5にはそのようなメソッドが含まれているため、QueryInterfaceで取得するようにしています。 IHTMLWindow5::get_XMLHttpRequestで取得できるVARIANT構造体からは、 IHTMLXMLHttpRequestFactoryを取得することができ、これのcreateを呼び出せば、 IHTMLXMLHttpRequestを取得することができます。 XMLHttpRequestの機能を使用する場合は、CoInternetSetFeatureEnabledでFEATURE_XMLHTTPを有効にしておきます。

IHTMLXMLHttpRequestを取得したら、次のようなコードでレスポンスを取得できます。

void Send(IHTMLXMLHttpRequest *pRequest, BSTR bstrUrl)
{
	VARIANT varAsync, varUser, varPassword, varBody;
	BSTR    bstrUser, bstrPassword, bstrMethod, bstrBody;
	LONG    lStatus;

	bstrUser = SysAllocString(L"");
	bstrPassword = SysAllocString(L"");
	bstrMethod = SysAllocString(L"GET");
	bstrBody = SysAllocString(L"");

	varAsync.vt = VT_BOOL;
	varAsync.boolVal = VARIANT_FALSE;
	varUser.vt = VT_BSTR;
	varUser.bstrVal = bstrUser;
	varPassword.vt = VT_BSTR;
	varPassword.bstrVal = bstrPassword;
	pRequest->open(bstrMethod, bstrUrl, varAsync, varUser, varPassword);

	varBody.vt = VT_BSTR;
	varBody.bstrVal = bstrBody;
	pRequest->send(varBody);

	pRequest->get_status(&lStatus);
	if (lStatus == 200) {
		BSTR bstrText;
		pRequest->get_responseText(&bstrText);
		MessageBoxW(NULL, bstrText, TEXT("OK"), MB_OK);
		SysFreeString(bstrText);
	}

	SysFreeString(bstrBody);
	SysFreeString(bstrMethod);
	SysFreeString(bstrPassword);
	SysFreeString(bstrUser);
}

openメソッドには実行するメソッドやアクセスするURLを指定します。 第3引数は非同期に通信するかどうかであり、VARIANT_TRUEを指定した場合は非同期通信、VARIANT_FALSEを指定した場合は同期通信になります。 非同期通信の場合はIDispatchを実装したオブジェクトを作成して、それをIHTMLXMLHttpRequest::put_onreadystatechangeに指定することになるでしょう。 sendによってHTTPリクエスト発行が完了し、同期通信の場合はこの時点でサーバーからのレスポンスが返っています。 get_statusの結果が200である場合はリクエストが成功したことを意味するため、get_responseTextで返されたデータを取得します。 UTF-8形式でないファイルをXMLHttpRequestで取得した場合は、文字化けを起こす可能性がある点に注意してください。 なお、XMLHttpRequestは現在とは異なるドメインに対してリクエストを送信できませんが、 IE8から登場したIHTMLXDomainRequestならばこれは可能です。



戻る