VC++の使い方

VC++の使い方 > VC++ Tips > IEコンポーネントの使い方
このページの内容
IEコンポーネントの使い方
IEコンポーネントの使い方2
イベントを取得する方法
ページを解析/変更する
ページを解析/変更する2
そのほかのTips
IEコンポーネント関係参考URL
ありがたいスポンサー様
All About ソフトウエアエンジニア
ネットで8%割引!自動車保険はアメリカンホーム・ダイレクト
人生の「チャンス」と「ピンチ」にモビット!
保険料が一生上がらない、保険料最大50%割引の一生涯の医療保険!

IEコンポーネントの使い方

IE4.0からIEコンポーネントを使えるようになりました。IEコンポーネントは、Donutなどの国産タブブラウザのほか、色々なアプリケーションで利用されています。

IEコンポーネントは、MFCのCHTMLViewを使えば簡単に作ることができますが、MFCを使いたくない場合もあります。この場合、ATLを選択するのが通常ですが、世の中のドキュメントの大半は ATL と AppWizard を組み合わせた使い方しか載っていません。ここでは、SDK的にIEコンポーネントを使う方法を取り上げます。


サンプルコードはこちら(35.9KB)(2002.12.15、プリコンパイル済みヘッダが見つからないエラーが出ないようにしました)。これをDLして解凍してください。詳細は、ソースファイルの中のコメント参照と言うことでお願いします。100行ほどのソースですので、ぜひ読んでみてください。

このサンプルでは、ダイアログにIEコンポーネントを貼り付けて使用しています。ダイアログにIEコンポーネントを張り付けるには、VC++のリソースエディタでダイアログを開いて、ダイアログ上で 『右クリック→ActiveXコントロールの挿入→Microsoft Web Browser』 を選択すればOKです。

スクリーンショット
スクリーンショット

VC++.NETでの注意点

リソースで作成したダイアログのIDをIDD_DIALOG_MAINとし、このダイアログに貼り付けるActiveXのIEコンポーネントのIDをIDC_EXPLORER1だとします。

xxx.rcファイルをテキストで開き、以下の箇所を修正

CONTROL "",IDC_EXPLORER1,"{8856F961-340A-11D0-A96B-00C04FD705A2}",

から

CONTROL "{8856F961-340A-11D0-A96B-00C04FD705A2}",IDC_EXPLORER1,"AtlAxWin71",

に修正します。(藤沢様、ご連絡ありがとうございました。)

IEコンポーネントの使い方2

ここでは、IEコンポーネントをCreateWindow関数から作成するサンプルを掲載します。


サンプルコードはこちら(38.4KB)。これをDLして解凍してください。詳細は、ソースファイルの中のコメント参照と言うことでお願いします。100行ほどのソースですので、ぜひ読んでみてください。

AtlAxWinInit()を呼び出せば、CreateWindow関数のクラス名に 『AtlAxWin』 、タイトルに ProgID や CLSID(IEコントロールの場合はURLでもOK)を入力すれば、ActiveXコントロールを作成できます。IEコントロールのProgIDは、Shell.Explorer.2なので、ここではこれを使いました。詳しくは、HOWTO: Dynamically Add ActiveX Controls to ATL Composite Control (Q218442)を参照してください。

HWND hwndIE = CreateWindow( "AtlAxWin", "Shell.Explorer.2", 
		WS_CHILD | WS_VISIBLE, 
		0, 0, 100, 100, 
		hWnd, (HMENU)0, hInstance, NULL);

// ActiveXコントロールのインターフェースを要求
CComPtr<IUnknown> punkIE;
if( AtlAxGetControl( hwndIE, &punkIE) == S_OK)
{
	// ポインタに格納
	pWB2 = punkIE;
}


ウインドウ上のアドレスバーにURLを入力してGOボタンを押せば、URLを開けます。ただし、開いたページでリンクをクリックしても、アドレスバーのURLは変化しません。手抜きばればれ。これを実装するには、IEコンポーネントにDWebBrowserEvents2でイベントが発生したら通知するようお願いする必要があります。この方法は次節で採り上げます。

スクリーンショット
スクリーンショット

VC++.NETでの注意点

CreateWindowの第一引数のクラス名を、AtlAxWinからAtlAxWin71に変更してください。(藤沢様、ご連絡ありがとうございました)

Visual C++ 2005 では AtlAxWin80 にする必要がありました。

イベントを取得する方法

IEコンポーネントで発生したイベントを取得する方法を紹介します。イベントには、ページを表示する前に呼び出されるBeforeNavigate2や、ステータスバーが変化する前に呼び出されるCommandStateChange、新しいウインドウが表示される前に呼び出されるNewWindow2などがあります。イベントの種類に関しては、DWebBrowserEvents2 Interface(MSDN)を参照して下さい。


サンプルコードはこちら(34.8KB)。これをDLして解凍してください。詳細は、ソースファイルの中のコメント参照と言うことでお願いします。

このサンプルでは、以下のような機能を追加しました。


以下は、得た知識を簡単に解説します。間違い多いと思うけど許してちょ。

IEコンポーネントで発生したイベントを受け取るには、IDispatchインターフェースをサポートしなければなりません。イベントを受け取るオブジェクトのことをCOMの世界の言葉でシンク(sink)と言います。

ATLを使えばCComObjectRootExとIDispEventImplからクラスを継承することで、簡単にシンクを実装できます。また、IDispatchをサポートしていることを宣言するためにBEGIN_COM_MAPマクロを、実際のイベントを処理するためにBEGIN_SINK_MAPマクロを使います。また、実際にIEコンポーネントに通知するときはDispEventAdvise関数を使います。

んー、こう書いても分かりにくいですねぇ。実際にソースを見て感触をつかんで下さい。


COMでのイベント授受についてはCOMからのイベントを捕まえる方法が参考になるでしょう。日本語ドキュメントなところが嬉しいですな。かなり参考にさせてもらいました。マジメに勉強するなら本を買った方がいいんでしょうけどね…。

IEコントロール内で右クリックしたときに発生するイベントを捕まえれば、コンテキストメニューを自前のものに置き換えることができる模様。ただし、これを実装するためにはHTMLドキュメントごとに、IDocHostUIHandlerを取得しる必要があるようです。んー、大変そう。詳しくは、HOWTO) Control the Context Menu in an ATL HTML Control (Q274202)を参照してください。

ページを解析/変更する

IHTMLDocument2のインターフェースを使えば、HTML文章のタグを列挙したり、タグを付け加えたりできます。IHTMLDocument2を取得するには、IWebBrowser2へのポインタがpWB2だとすると

CComPtr<IDispatch> pDisp ;
pWB2->get_Document( &pDisp) ;
CComQIPtr<IHTMLDocument2> pDoc = pDisp ;

として取得できます。ただし、エラー処理は省略してます。取得するタイミングは、DWebBrowserEvents2のDocumentCompleteイベントが発生したときが妥当でしょう(2002.10.1修正)。IHTMLDocument2は、新しいページを表示するごとに取得しなければいけません。IWebBrowser2インターフェースがIEコントロールごとに存在するように、IHTMLDocument2インターフェースはページごとに存在します。


さて、取得したIHTMLDocument2の調理法を紹介しましょう。Adding a custom search feature to CHtmlViewsをご覧ください。このように、HTML文章を検索したりハイライトしたりできていますね。ハイライトは、検索で引っかかった文字を<span>タグで挟むことで実現しています。

もうひとつは、Using CHtmlView as a controlです。ここでは、 『MSHTML 編集プラットフォーム』 を使用する方法について触れています。難しいことはいりません、pDoc->put_designMode(L"On");とするだけで、WYSIWYG に HTML を編集できるようになります。

編集プラットフォーム
編集プラットフォーム

上の画像は、リンク上で右クリックしてリンク先の変更ができているところです。その他にも、Word で文章を打つように文章を変更できます。これであなたも HTMLエディタを作れるかも。ただ、作りこもうとして、 IEコンポーネントの泥沼にはまるような気はしますが・・・。

最後の例はExample of Handling HTML Element Events in Microsoft Internet Explorerです。ここでは、特定のタグで発生したイベントを取得する方法が書いています。イベントには、onmouseoutやonkeypressなどがあります。これらのイベントに反応するコードを記述すれば、ユーザーのタグに対する操作に反応して、プログラムから HTML を書き換えることができるようになります。

ページを解析/変更する2

IE7はタブブラウザになるらしいですね。きっと IEコンポーネントにタブ機能がつくんでしょう。でも、インターフェースが肥大化すると、プログラマが調べものをする時間が多くなって悲しいんですよね…。


さてさて、今回は 「IEコンポーネントのインターフェース = JavaScriptのインターフェース」 ということを強調したいと思います。

例えば

document.all("temp").innerHTML = "hogehoge" ;

というJavaScriptがあったとします。このスクリプトは、ページをダイナミックに書き換るときによく見る例です。id が temp というタグの中身が書き変わります(例:<span id="temp">)。これを、COM でやろうと思ったら、次のようなソースになります。

// document の取得
CComPtr<IDispatch> pDisp ;
pWB2->get_Document( &pDisp) ;
CComQIPtr<IHTMLDocument2> pDoc = pDisp ;

// all の取得
CComPtr<IHTMLElementCollection> pCol ;
pDoc->get_all( &pCol) ;
long lSize ;
pCol->get_length( &lSize);

if( lSize != 0)
{
	// all の内容をリストアップ
	USES_CONVERSION ;
	for( long i = 0; i < lSize; i++)
	{
		CComQIPtr<IHTMLElement> pElement ;
		CComVariant  vintName( i) ;
		CComVariant  vintIndex( 0) ;
		CComPtr<IDispatch>  pdsp2 ;
		pCol->item( vintName, vintIndex, &pdsp2) ;
		pElement = pdsp2 ;

		// id が temp なら
		BSTR bstrId ;
		pElement->get_id( &bstrId) ;
		char* pszBuf = bstrId ? OLE2T( bstrId) : "" ;
		if( strcmp( pszBuf, "temp") == 0)
		{
			// innerHTML を設定
			pElement->put_innerHTML( L"hogehoge") ;
		}
	}
}

どうでしょうか。JavaScriptに比べてずいぶんと長いですが、やっていることは同じですね。JavaScript は、そもそも IEコンポーネント操るための手段なんですね。なので、IEコンポーネントをいじるためにいちいちコンパイルするのが面倒な場合は、JavaScript で実験することができます。それだけでなく、IEコンポーネントのインターフェースやTipsを調べるのは大変なので、JavaScript 辞典のようなページで調べものをした方が早いかもしれませんね。


最後に、すばらしいTipsのページを紹介して終わりにしましょう。IE Powertoys Plus! + miniToysです。このページでは、JavaScriptでいろいろやろうとしてます。なにより、JavaScriptなのでソースが見れるところが最高ですね。特に miniToys の方は、子ネタ集として最高です。コメントタグ解除・ズーム・グレイスケール化など、見ただけでよだれが出てきそうな素材満載です。

さらにJavaScript for CSSを追加。トップページは消えてますが、DOM を使って主にスタイルシートを変更する方法が書いてあります。

そのほかのTips

色々調べた結果をメモ代わりに書いておきます。IE コンポーネント関係で調べると、このページが比較的引っかかりやすいのでそれなりの情報を載せておかないと...

文字列を取得する方法

ここでは、IWebBrowser2 の get_LocationURL を使う方法を示してみます。ここに記述してある方法を用いれば、IHTMLElement2 の get_innerHTML のように BSTR* を引数にとる関数を呼び出すことが出来ます。

USES_CONVERSION;
BSTR bstrBuf;
pWB2->get_LocationURL(&bstrBuf);
char* pszUrl = (bstrBuf ? OLE2T(bstrBuf) : "");

USES_CONVERSION マクロを使うと OLE2T などのマクロをブロック内(関数など {} で囲まれた範囲)で使うことが出来ます。 OLE2T は BSTR などの UNICODE を SJIS に変換してくれます。変換後の文字列は free や LocalFree する必要がないことに注意してください。

ここでは、pszUrl に NULL が格納されるのを嫌い、(bstrBuf ? OLE2T(bstrBuf) : ""); としています。

ダウンロード途中の IHTMLDocument2 オブジェクトを取得する

通常、IHTMLDocument2 を取得するためには、DWebBrowserEvents2 の DocumentComplete イベントが発生してからが最良とされています。しかし、この DocumentComplete イベントはページ内の画像などもダウンロードし終わらないと発生しません。このようなこともあり、ダウンロード中に同時にドキュメントを解析したい場合もあるでしょう。

IWebBrowser2 オブジェクトさえ取得できていれば、ダウンロード中の HTML のドキュメントを順次取得できることを確認できました(IE6)。そのため、新規にページを表示するときに新しいスレッドを作り、そのスレッドの中で DocumentComplete が 呼ばれるまで 数秒おきに IHTMLDocument2 を取得し続けるのがよいでしょう。

about:blank のページに動的にページを入れ込みたい

メモリ上のページを表示するためのテクニックです。一部の 2ch ブラウザもこの方法を使っているようです(右クリックからプロパティーを見ると about:blank と表示される)。テンポラリファイルを作る必要がないでしょう。

ここらへんを参考にして作ってみた。いけたいけた!(コーディングスタイルがばらばらなのは、いろんなとこからのコピーしてきた内容の合成だからです)

	CComVariant vempty, vUrl(L"about:blank");
	pWB2->Navigate2 ( &vUrl, &vempty, &vempty, &vempty, &vempty );
	CComPtr<IDispatch> pdisp;
	CComQIPtr<IHTMLDocument2> pDoc;
	while(1)
	{
		HRESULT hr = pWB2->get_Document(&pdisp);
		if(SUCCEEDED(hr) && pdisp != NULL)
		{
			pDoc = pdisp;
			if(pDoc != NULL)
			{
				break;
			}
		}
		Sleep(100);
	}

	HRESULT hresult = S_OK;
	VARIANT *param;
	SAFEARRAY *sfArray;
	BSTR bstr = SysAllocString(OLESTR("<html><body><b>hoge</b>adfsf<hr></body></html>"));

	sfArray = SafeArrayCreateVector(VT_VARIANT, 0, 1);
	
	if (sfArray == NULL || pDoc == NULL) {
		goto cleanup;
	}

	hresult = SafeArrayAccessData(sfArray,(LPVOID*) & param);
	param->vt = VT_BSTR;
	param->bstrVal = bstr;
	hresult = SafeArrayUnaccessData(sfArray);
	hresult = pDoc->write(sfArray);

cleanup:
	SysFreeString(bstr);
	if (sfArray != NULL) {
		SafeArrayDestroy(sfArray);
	}

onclick や onmouseover などのイベントをハンドルしたい

(HOWTO) Sink HTML Document Events for WebBrowser Host をご覧ください。

他にも Handling HTML Element Events in Internet ExplorerPopup Blocker などのサンプルがありますが、どうしても自作アプリに組み込めません.... どなたか、ATL でうまく言った方がいれば、vc@nitoyon.com までご連絡いただけるとありがたいです。

ウインドウハンドルからIWebBrowser2を取得したい

Internet Explorer_Server クラスのウインドウハンドルを与えれば、そこからCComPtr<IWebBrowser2>を返してくれます。Keystroke Logger and More, Part 3 あたりを参考にしました。

CComPtr<IWebBrowser2> GetIEPtr( HWND hWnd)
{
	HWND hWndChild = hWnd ;
	if ( hWndChild )
	{
		CComPtr<IHTMLDocument2> spDoc;
		LRESULT lRes;

		UINT nMsg = ::RegisterWindowMessage( _T("WM_HTML_GETOBJECT") );
		::SendMessageTimeout( hWndChild, nMsg, 0L, 0L, SMTO_ABORTIFHUNG, 1000, (DWORD*)&lRes );

		HRESULT hr;
		hr = ObjectFromLresult( lRes, __uuidof(IHTMLDocument2),0,(void**)&spDoc) ;
		if ( SUCCEEDED(hr) )
		{
			CComQIPtr<IServiceProvider> psp1 = spDoc ;
			CComQIPtr<IServiceProvider> psp2 ;
			CComPtr<IWebBrowser2> pWB2 ;
			
			if( psp1)
			{
				psp1->QueryService( SID_STopLevelBrowser, IID_IServiceProvider, (void**)(&psp2));
				if( psp2)
				{
					psp2->QueryService(SID_SWebBrowserApp, IID_IWebBrowser2, (void**)(&pWB2));
					return pWB2 ;
				}
			}
		}
	}
	return NULL ;
}

英語を読みたくない

IE コンポーネントに限ったことではないですが、英語が読めないと厳しいです。とはいえ、私も日本語のドキュメントの方が読みやすいので 日本語ドキュメントがないときにだけ英語の文を読みます。IE コンポーネントでは日本語のドキュメントもさほど多くなく、英語ドキュメントを読む機会が多くなってしまうとは思います...

IEコンポーネント関係参考URL

COM や IEコンポーネント関係で参考になるURLをメモとして掲載しておく。