EternalWindows
MSHTML / ドキュメントとエレメント

IEが行っている動作の中で特に複雑であると予想されるのは、 サーバーから受信したhtmlファイルの中身を解析し、 それをウインドウ上に描画することでしょう。 これを行うには、htmlで使用される全てのタグや属性を理解しなければならず、 さらに個々のタグの階層を意識してレイアウトが崩れないように描画するという、 非常に高度な処理を要します。 ただし、IE含む多くのブラウザアプリケーションにおいて、 こうした解析と描画の処理はアプリケーション自体に内蔵されていません。 たとえば、IEではこうした処理をMSHTML(Microsoft HTML Object Library)というDLLに実装しており、 他のアプリケーションからも利用できるような汎用的な構造を作っています。 このような処理の分離により、他のブラウザアプリケーションは独自の描画処理を実装する必要がなくなり、 ブラウザを開発しないアプリケーションにおいても、 MSHTMLの機能を使用することにより、特定のタグのデータを取得するようなことが可能になります。

MSHTMLでは、htmlファイル内のテキストをドキュメントと呼び、ドキュメントの中に存在するタグをエレメントと呼びます。 ドキュメントはIHTMLDocumentで表され、このインターフェースのしかるべきメソッドを呼び出すことで、 特定のエレメントを表すIHTMLElementを取得できます。 このように、ドキュメントやエレメントをオブジェクトとして扱い、 アプリケーションから利用できるようにする仕組みはDOM(Document Object Model)と呼ばれます。 次に、ドキュメントの中に存在するエレメントの例を示します。

<a href = "http://eternalwindows.jp/">トップページへ戻ります<a>

aタグはアンカーエレメントと呼ばれ、特定のページにリンクする際に使用されます。 hrefというのはエレメントの属性であり、=の右側に存在する文字列は属性値と呼ばれます。 IHTMLElementを使用すれば、このような属性値を取得できます。

プログラミングの視点から考えた場合、IHTMLElementの取得には1つの疑問が生じると思われます。 それは、エレメントというものがドキュメントの中に複数存在するものであるため、 どうやって目的のエレメントだけを取得すればよいのかという点です。 このための対策として、エレメントにはidを指定することができるようになっています。

<a id = "sample" href = "http://eternalwindows.jp/">トップページへ戻ります<a>

idに指定する値はエレメントを一意に識別しているものとします。 このようなidがあれば、idで識別されるエレメントを取得できるようになります。

BOOL GetElementData(IHTMLDocument3 *pDocument3)
{
	BSTR         bstrId, bstrAttribute, bstrText;
	VARIANT      var;
	IHTMLElement *pElement;

	bstrId = SysAllocString(L"sample");
	pDocument3->getElementById(bstrId, &pElement);
	if (pElement == NULL) {
		SysFreeString(bstrId);
		return FALSE;
	}

	bstrAttribute = SysAllocString(L"href");
	VariantInit(&var);
	pElement->getAttribute(bstrAttribute, 0, &var);

	pElement->get_innerText(&bstrText);
	MessageBoxW(NULL, bstrText, var.bstrVal, MB_OK);

	VariantClear(&var);
	SysFreeString(bstrText);
	SysFreeString(bstrAttribute);
	SysFreeString(bstrId);
	pElement->Release();

	return TRUE;
}

IHTMLDocument3::getElementByIdを呼び出せば、第1引数のidで識別されるエレメントを取得できます。 このメソッドは、指定したidを持ったエレメントが存在しない場合でもS_OKを返すため、 戻り値ではなくインターフェースがNULLであるかで成否を確認します。 エレメントはIHTMLElementで表すことができ、 getAttributeは第1引数の属性の属性値をVARIANT構造体として第3引数に返します。 このVARIANT構造体はBSTR型として初期化されます。 get_innerTextを呼び出せばタグとタグの間にあるテキストを取得できますが、 タグの先頭から終端までを取得する場合はget_outerHTMLを呼び出します。

IHTMLElementは、エレメントを表す汎用的なインターフェースです。 このインターフェースを使用すれば、基本的にどのようなエレメントからもデータを取得することができますが、 エレメントによっては専用のインターフェースを使用した方が簡単な場合もあります。 次に、各エレメント専用のインターフェースを示します。

エレメント インターフェース
<a> IHTMLAnchorElement
<body> IHTMLBodyElement
<div> IHTMLDivElement
<img> IHTMLImgElement
<table> IHTMLTableElement

専用のインターフェースを取得するには、IHTMLElement::QueryInterfaceを呼び出すことになります。 たとえば、IHTMLElementがアンカーエレメントを表しているのであれば、IHTMLAnchorElementを取得できます。

今回のプログラムは、下記のアンカーエレメントのデータを書き換えます。 MSHTMLの機能を使用する関係上、mshtml.hのインクルードを行っています。

前のページに戻ります
#include <windows.h>
#include <exdisp.h>
#include <mshtml.h>
#include <oleacc.h>

#pragma comment (lib, "oleacc.lib")

BOOL PutElementData(IHTMLDocument3 *pDocument3);
BOOL GetDocumentFromIE(IHTMLDocument3 **pp);
BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IHTMLDocument3 *pDocument3;

	CoInitialize(NULL);

	if (!GetDocumentFromIE(&pDocument3)) {
		CoUninitialize();
		return 0;
	}
	
	PutElementData(pDocument3);

	CoUninitialize();
	
	return 0;
}

BOOL PutElementData(IHTMLDocument3 *pDocument3)
{
	BSTR               bstrId, bstrHref, bstrText;
	IHTMLElement       *pElement;
	IHTMLAnchorElement *pAnchorElement;

	bstrId = SysAllocString(L"sample");
	pDocument3->getElementById(bstrId, &pElement);
	if (pElement == NULL) {
		SysFreeString(bstrId);
		return FALSE;
	}
	
	bstrText = SysAllocString(L"トップページへ戻ります");
	pElement->put_innerText(bstrText);

	pElement->QueryInterface(IID_PPV_ARGS(&pAnchorElement));
	bstrHref = SysAllocString(L"http://eternalwindows.jp/");
	pAnchorElement->put_href(bstrHref);

	MessageBox(NULL, TEXT("エレメントのデータを変更しました。"), TEXT("OK"), MB_OK);

	SysFreeString(bstrText);
	SysFreeString(bstrHref);
	SysFreeString(bstrId);
	pAnchorElement->Release();
	pElement->Release();

	return TRUE;
}

BOOL GetDocumentFromIE(IHTMLDocument3 **pp)
{
	HWND    hwnd;
	UINT    uMsg;
	LRESULT lResult;
	HRESULT hr;
	
	EnumChildWindows(FindWindow(TEXT("IEFrame"), NULL), EnumChildProc, (LPARAM)&hwnd);
	if (hwnd == NULL)
		return FALSE;

	uMsg = RegisterWindowMessage(TEXT("WM_HTML_GETOBJECT"));
	if (!SendMessageTimeout(hwnd, uMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, (LPDWORD)&lResult))
		return FALSE;

	hr = ObjectFromLresult(lResult, IID_IHTMLDocument3, 0, (void **)pp);
	if (FAILED(hr))
		return FALSE;

	return TRUE;
}

BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam)
{
	TCHAR szClassName[256];

	GetClassName(hwnd, szClassName, sizeof(szClassName) / sizeof(TCHAR));
	if (lstrcmp(szClassName, TEXT("Internet Explorer_Server")) == 0) {
		*((HWND *)lParam) = hwnd;
		return FALSE;
	}
	else
		return TRUE;
}

エレメントを操作するには、何よりもまずIHTMLDocumentを取得する必要があります。 これにはいくつかの方法がありますが、今回は起動中のIEからIHTMLDocumentを取得する方法を使用しています。 操作対象のページは本ページを対象としているため、IEで本ページを開いている状態にしておきます。

BOOL GetDocumentFromIE(IHTMLDocument3 **pp)
{
	HWND    hwnd;
	UINT    uMsg;
	LRESULT lResult;
	HRESULT hr;
	
	EnumChildWindows(FindWindow(TEXT("IEFrame"), NULL), EnumChildProc, (LPARAM)&hwnd);
	if (hwnd == NULL)
		return FALSE;

	uMsg = RegisterWindowMessage(TEXT("WM_HTML_GETOBJECT"));
	if (!SendMessageTimeout(hwnd, uMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, (LPDWORD)&lResult))
		return FALSE;

	hr = ObjectFromLresult(lResult, IID_IHTMLDocument3, 0, (void **)pp);
	if (FAILED(hr))
		return FALSE;

	return TRUE;
}

この関数が行おうとしているのは、Internet Explorer_Serverというクラス名を持ったウインドウハンドルを取得し、 それに対してWM_HTML_GETOBJECTメッセージを送信することです Internet Explorer_ServerはIEのウインドウの子ウインドウに相当するため、 FindWindowで取得したIEのウインドウ(クラス名IEFrame)をEnumChildWindowsに指定します。 これにより、第2引数のコールバック関数に一連の子ウインドウが送られるため、 そこでInternet Explorer_Serverのウインドウを発見するようにします。 続いて、WM_HTML_GETOBJECTのメッセージ値をRegisterWindowMessageで取得し、 これをSendMessageTimeoutでInternet Explorer_Serverのウインドウに送信します。 このとき、アプリケーションがIEよりも整合性レベルが低い場合は、 UIPIの制限によって送信が失敗することに注意してください。 SendMessageTimeoutが成功したら、第7引数の戻り値をObjectFromLresultに指定することで、 IHTMLDocument3を取得できます。 IHTMLDocumentやIHTMLDocument2も取得することができますが、 これらのインターフェースには今回使用するgetElementByIdが含まれていません。

IHTMLDocument3を取得したら、自作関数のPutElementDataを呼び出します。 エレメントのデータを変更する部分は次のようになっています。

bstrText = SysAllocString(L"トップページへ戻ります");
pElement->put_innerText(bstrText);

pElement->QueryInterface(IID_PPV_ARGS(&pAnchorElement));
bstrHref = SysAllocString(L"fgh");
pAnchorElement->put_href(bstrHref);

上記ではタグに囲まれたテキストの変更にはIHTMLElementを使用していますが、 属性値の変更には専用のインターフェースであるIHTMLAnchorElementを使用しています。 put_hrefを呼び出せば、hrefの属性値を変更することができます。

パスベースの取得

今回は起動中のIEからIHTMLDocumentを取得しましたが、 IHTMLDocumentを取得するためにIEを起動しなければならないというのは、少しわずらわしいことであるといえます。 たとえば、ローカルに存在するhtmlファイルを解析したいのであれば、 そのファイルのファイルパスを基にIHTMLDocumentを取得したいはずです。 このような例を次に示します。

#include <windows.h>
#include <mshtml.h>

#pragma comment (lib, "urlmon.lib")

BOOL GetElementData(IHTMLDocument2 *pDocument2);
BOOL GetDocumentFromUrl(LPWSTR lpszUrl, BOOL bUrl, IHTMLDocument2 **pp);
BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IHTMLDocument2 *pDocument2;

	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	if (!GetDocumentFromUrl(L"http://eternalwindows.jp/browser/mshtml/mshtml00.html", TRUE, &pDocument2)) {
		CoUninitialize();
		return 0;
	}
	
	GetElementData(pDocument2);

	CoUninitialize();
	
	return 0;
}

BOOL GetElementData(IHTMLDocument2 *pDocument2)
{
	BSTR         bstr;
	IHTMLElement *pElement;
	
	pDocument2->get_body(&pElement);

	pElement->get_innerHTML(&bstr);
	MessageBoxW(NULL, bstr, L"OK", MB_OK);
	
	SysFreeString(bstr);

	return TRUE;
}

BOOL GetDocumentFromUrl(LPWSTR lpszUrl, BOOL bUrl, IHTMLDocument2 **pp)
{
	HRESULT        hr;
	BSTR           bstrState;
	IHTMLDocument2 *pDocument2;
	
	hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDocument2));
	if (FAILED(hr))
		return FALSE;

	if (bUrl) {
		IMoniker        *pMoniker;
		IBindCtx        *pBindCtx;
		IPersistMoniker *pPersistMoniker;
		
		CreateURLMoniker(NULL, lpszUrl, &pMoniker);
		CreateBindCtx(0, &pBindCtx);
		
		pDocument2->QueryInterface(IID_PPV_ARGS(&pPersistMoniker));
		hr = pPersistMoniker->Load(FALSE, pMoniker, pBindCtx, STGM_READWRITE);
		
		pPersistMoniker->Release();
		pBindCtx->Release();
		pMoniker->Release();
		
	}
	else {
		IPersistFile *pPersistFile;

		pDocument2->QueryInterface(IID_PPV_ARGS(&pPersistFile));
		hr = pPersistFile->Load(lpszUrl, STGM_READ);

		pPersistFile->Release();
	}

	for (;;) {
		Sleep(100);
		pDocument2->get_readyState(&bstrState);
		if (lstrcmpW(bstrState, L"complete") == 0) {
			SysFreeString(bstrState);
			break;
		}
		SysFreeString(bstrState);
	}
	
	*pp = pDocument2;

	return SUCCEEDED(hr);
}

GetDocumentFromUrlの第1引数には、htmlファイルを表すURLまたはファイルパスを指定します。 URLの場合は第2引数にTRUEを指定し、ファイルパスの場合はFALSEを指定します。 関数の処理内容としては、まずCoCreateInstanceにCLSID_HTMLDocumentを指定してドキュメントオブジェクトを作成します。 このオブジェクトはIHTMLDocument3などで識別することができますが、 作成した時点ではドキュメントの中身が空であるため、 目的のhtmlファイルを明示的にロードする必要があります。 パスがURLベースである場合はオブジェクトからIPersistMonikerを取得し、 IPersistMoniker::Loadによってhtmlファイルをロードします。 第2引数に指定するモニカは、CreateURLMonikerで作成しておくことになります。 パスがファイルパスである場合はオブジェクトからIPersistFileを取得し、 IPersistFile::Loadによってhtmlファイルをロードします。

MSHTMLはファイルを非同期にロードするため、Loadが制御を返した時点でロードが完了しているとは限りません。 このような状態で何らかのエレメントを取得することはできないため、 ロードが完了するまでの間は処理を続行しないようにしています。 具体的には、get_readyStateがロードの完了を意味するcompleteを返すまで、 Sleepで一定間隔待機しています。 スレッドがMTAに入っていなければ、get_readyStateは常にloadingを返すため、 CoInitializeExにCOINIT_MULTITHREADEDを指定しておく必要があります。

ロードが完了するまでスレッドを待機させるのではなく、完了の通知を受け取りたい場合があるかもしれません。 このような場合は、IDispatchを継承したクラスのアドレスを維持したVARIANT構造体を用意し、 これをIHTMLDocument2::put_onreadystatechangeに指定するようにします。 そうすると、ロードが完了した時点でIDispatch::Invokeが呼ばれるようになります。 もう1つの別の方法として、IConnectionPoint::AdviseにIPropertyNotifySinkを継承したクラスを指定する方法があります。 この場合は、ロードが完了した時点でIPropertyNotifySink::OnChangedが呼ばれるようになります。 IConnectionPointは、ドキュメントオブジェクトからIConnectionPointContainerを取得し、 それのFindConnectionPointにIID_IPropertyNotifySinkを指定することで取得できます。



戻る