EternalWindows
MSHTML / テキストの検索

今回は、特定のエレメントに表示されているテキストを操作する方法について説明します。 ここで言う操作とは、特定の文字列を検索したり選択したりすることであり、 IHTMLTxtRangeを通じて行うことができます。 次に、bodyタグのIHTMLTxtRangeを取得する例を示します。

pDocument2->get_body(&pElement);
pElement->QueryInterface(IID_PPV_ARGS(&pBodyElement));
pBodyElement->createTextRange(&pTxtRange);

IHTMLDocument2::get_bodyを呼び出せば、bodyタグを表すIHTMLElementを取得できます。 このタグはIHTMLBodyElementという専用のインターフェースでも識別できるため、 QueryInterfaceによってIHTMLBodyElementを取得しています。 IHTMLBodyElement::createTextRangeを呼び出せば、IHTMLTxtRangeを取得できます。

IHTMLTxtRangeを使用するうえで重要となるのは、位置という概念です。 たとえば、文字列を選択する場合は開始位置と終了位置が必要になりますから、 こうした位置情報を適切に指定しておくことになります。 開始位置はmoveStartで指定し、終了位置はmoveEndで指定します。

BSTR bstrUnitText = SysAllocString(L"textedit");
pTxtRange->moveStart(bstrUnitText, -1, &lActualCount); // 開始位置をタグの先頭に設定する
pTxtRange->moveEnd(bstrUnitText, 1, &lActualCount); // 終了位置をタグの終端に設定する
pTxtRange->select(); // 範囲(開始位置から終了位置)を選択する

moveStartやmoveEndの第1引数にtexteditという文字列を指定した場合、 タグの範囲(たとえば<body>から</body>)の先頭または終端に移動することを意味します。 具体的には、moveStartの第2引数に-1を指定した場合に開始位置がタグの先頭の文字になり、 moveEndの第2引数に1を指定した場合に終了位置がタグの終端の文字になります。 texteditを指定する場合は、第2引数の値はこれら以外ではあってはならないことに注意してください。 第3引数に返る値は、texteditの場合は意味を持ちません。 selectは、現在の範囲内の文字列を選択するメソッドです。 上記の場合、タグ内の全ての文字列が選択状態になります。

開始位置や終了位置を任意の位置に指定したい場合は、moveStartまたはmoveEndの第1引数にcharacterを指定します。

BSTR bstrUnitText = SysAllocString(L"textedit");
BSTR bstrUnitChar = SysAllocString(L"character");

pTxtRange->moveStart(bstrUnitText, -1, &lActualCount); // 開始位置をタグの先頭に設定する
pTxtRange->moveStart(bstrUnitChar, 3, &lActualCount); // 現在の開始位置から3つ目の文字を新しい開始位置にする

moveStartにcharacterを指定した場合は、現在の位置から第3引数の値だけ開始位置が移動します。 現在の位置というのは、先のmoveStartによってタグの先頭に設定されていますから、 上記の場合であればタグの先頭から3つ目の文字が開始位置ということになります。 characterを指定した場合は、実際に移動した値が第3引数に返ります。

今回のプログラムは、bodyタグから特定の文字列を検索します。 メッセージボックスに応答することにより、検索が順次進んでいきます。

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

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

BOOL SearchText(IHTMLDocument2 *pDocument2, BSTR bstrFind);
BOOL GetDocumentFromIE(IHTMLDocument2 **pp);
BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam);

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

	CoInitialize(NULL);

	if (!GetDocumentFromIE(&pDocument2)) {
		CoUninitialize();
		return 0;
	}
	
	bstrFind = SysAllocString(L"取得");
	SearchText(pDocument2, bstrFind);
	SysFreeString(bstrFind);

	CoUninitialize();
	
	return 0;
}

BOOL SearchText(IHTMLDocument2 *pDocument2, BSTR bstrFind)
{
	IHTMLElement     *pElement;
	IHTMLBodyElement *pBodyElement;
	IHTMLTxtRange    *pTxtRange;
	BSTR             bstrUnitText, bstrUnitChar, bstrCmdBackColor;
	VARIANT          var;
	VARIANT_BOOL     varBool;
	LONG             lActualCount;
	int              nFindCount = 0;
	
	pDocument2->get_body(&pElement);
	pElement->QueryInterface(IID_PPV_ARGS(&pBodyElement));
	pBodyElement->createTextRange(&pTxtRange);

	bstrUnitText = SysAllocString(L"textedit");
	bstrUnitChar = SysAllocString(L"character");
	bstrCmdBackColor = SysAllocString(L"BackColor");

	for (;;) {
		pTxtRange->findText(bstrFind, 0, 0, (VARIANT_BOOL *)&varBool);
		if (varBool == VARIANT_FALSE)
			break;
		
		nFindCount++;

		var.vt = VT_I4;
		var.lVal = 0x0000ff;
		pTxtRange->execCommand(bstrCmdBackColor, VARIANT_FALSE, var, &varBool);

		pTxtRange->scrollIntoView(VARIANT_TRUE);

		pTxtRange->moveStart(bstrUnitChar, 1, &lActualCount);
		pTxtRange->moveEnd(bstrUnitText, 1, &lActualCount);
	}

	SysFreeString(bstrCmdBackColor);
	SysFreeString(bstrUnitChar);
	SysFreeString(bstrUnitText);

	pTxtRange->Release();
	pBodyElement->Release();
	pElement->Release();

	return nFindCount > 0;
}

BOOL GetDocumentFromIE(IHTMLDocument2 **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_IHTMLDocument2, 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;
}

検索を行うにはIHTMLTxtRange::findTextを呼び出すことになります。 第1引数は検索したい文字列を指定し、第4引数には検索の結果が返ります。 第2引数と第3引数は0で問題ないと思われます。 検索の結果がVARIANT_TRUEである場合は検索が成功したことを意味し、 VARIANT_FALSEである場合は検索が失敗したことを意味します。 findTextをループ文内に記述しているのは、検索する文字列が複数存在する可能性があるからであり、 検索に一致した回数はnFindCountに記録されていきます。 これが0より大きい場合は、1回は検索に成功したということで関数を成功とみなします。

findTextは、現在の開始位置と終了位置の間を検索の範囲としています。 IHTMLTxtRangeの既定の位置はタグの先頭と終端になっているため、 1回目の呼び出しはbodyタグ全体が検索の対象になります。 検索が成功した場合は、その見つかった位置が現在の位置に変更される点に注意してください。 これを証明するために、次のような処理が行われています。

var.vt = VT_I4;
var.lVal = 0x0000ff;
pTxtRange->execCommand(bstrCmdBackColor, VARIANT_FALSE, var, &varBool);

pTxtRange->scrollIntoView(VARIANT_TRUE);

execCommandは現在の位置の文字列に対して、第1引数の文字列で識別されるコマンドを実行します。 今回の場合はBackColorという文字列ですから、現在の位置の文字列の背景色が変更されることになります。 現在の位置というのは先のfindTextによって、見つかった文字列の位置に調整されていますから、 見つかった文字列のみが強調されて表示されることになります。 コマンドの引数はVARIANT構造として第3引数に指定し、BackColorの場合は背景色の色を格納しておきます。 vtメンバにVT_I4を指定した場合は、lValにRGB値を指定することができます。 scrollIntoViewは、現在の位置に対してスクロールを実行します。 これにより、見つかった文字列は必ず確認できるようになります。

findTextが成功したら、該当文字列が他にも存在しないかを確認するために再びfindTextを呼び出すことになります。 ただし、その前に次の処理が行われます。

pTxtRange->moveStart(bstrUnitChar, 1, &lActualCount);
pTxtRange->moveEnd(bstrUnitText, 1, &lActualCount);

既に呼び出したfindTextによって開始位置と終了位置が変更されたことを思い出してください。 この状態でfindTextを呼び出すと、検索の範囲が既に検索された文字列の先頭と終端に限定されるため、 常に同じ部分が検索にヒットすることになってしまいます。 よって、検索されていない残りの範囲を対象とするために、 開始位置と終了位置をmoveStartとmoveEndで指定します。 moveStartにはcharacterと1を指定しているため、現在の開始位置から1つ進んだ文字が新しい開始位置となります。 一方、moveEndにはtexteditを指定しているため、タグの終端が終了位置になります。

コピーの実行

IHTMLTxtRange::execCommandに指定できるコマンドは、BackColor以外にも数多くの種類が存在します。 例として、現在選択されているテキストをコピーする方法を取り上げます。

void Copy(IHTMLDocument2 *pDocument2)
{
	IHTMLSelectionObject *pSelection;
	IDispatch            *pDispatch;
	IHTMLTxtRange        *pTxtRange;
	BSTR                 bstrCmdCopy;
	VARIANT              var;
	VARIANT_BOOL         varBool;
	HRESULT              hr;

	hr = pDocument2->get_selection(&pSelection);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("IHTMLSelectionObjectの取得に失敗しました。"), TEXT("OK"), MB_OK);
		return;
	}

	pSelection->createRange(&pDispatch);
	pDispatch->QueryInterface(IID_PPV_ARGS(&pTxtRange));

	bstrCmdCopy = SysAllocString(L"Copy");
	
	var.vt = VT_EMPTY;
	pTxtRange->execCommand(bstrCmdCopy, VARIANT_FALSE, var, &varBool);
	if (varBool == VARIANT_TRUE)
		MessageBox(NULL, TEXT("コピーに成功しました。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("コピーに失敗しました。"), NULL, MB_ICONWARNING);
	
	SysFreeString(bstrCmdCopy);

	pTxtRange->Release();
	pDispatch->Release();
	pSelection->Release();
}

IHTMLDocument2::get_selectionを呼び出せば、現在選択されているオブジェクトを表すIHTMLSelectionObjectを取得できます。 ここから、createRangeとQueryInterfaceを呼び出せばIHTMLTxtRangeを取得できるため、 execCommandを呼び出してコマンドを実行できます。 Copyというコマンドは、文字通りテキストをクリップボードにコピーすることができますが、 テキストが選択されていない場合でもコピーは成功と判断されるようです。 これと同じように、get_selectionもテキストが選択されていない状態で成功します。



戻る