S-JIS[2007-09-29/2007-11-07] 変更履歴

WebBrowser(VC++2005)

Visual C++2005でATLを使わずにCOM(ActiveX)のWebBrowserコントロールを使う方法。
はい、ただ単に.NETフレームワークとかATLとかの使い方が分からないだけです(爆)

VB.NET2003のWebBrowserコントロール

参考:


メイン

COMを使う場合、最初にCoInitialize()を呼び出し、最後にCoUninitialize()を呼び出す必要があるんだそうだ。
これはスレッド毎に行う必要があるらしいけど…そこまで(と言うか全然)使い込んでない…。CoInitializeEx()の方が新しくて推奨らしい。

COM関連の関数の戻り値の型は、ほとんどHRESULT。[2007-10-01]
HRESULTでは、S_OKS_FALSEなどS_で始まるものは基本的に正常、E_で始まるものがエラーらしい。
なので 成功したかどうかはSUCCEEDEDマクロ(失敗したかどうかはFAILEDマクロ)を使って判断する。

#include <windows.h>
#include <tchar.h>

#import <shdocvw.dll>
#import <mshtml.tlb>
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
	HRESULT hr = ::CoInitialize(NULL);	//COMを使うときのお約束
	if (FAILED(hr)) {
		return -1;
	}

	NewIE();		//新しいIEを開く
	WriteIE();	//HTML文字列を書き込む
	ListIE(); 	//WebBrowserを列挙する

	HWND hwnd = ::FindWindow(NULL, _T("Google - Microsoft Internet Explorer"));
	if (hwnd != NULL) {
		SHDocVw::IWebBrowser2Ptr pIE = FindIE(hwnd);	//WebBrowserを取得する
		GoogleSearch(pIE);				//Googleに入力してみる
	} else {
		::OutputDebugString(_T("not found Google window\n"));
	}

	EventTest();	//WebBrowserのイベントを捕捉してみる

	::CoUninitialize();	//COM使用終了

	return 0;
}

shdocvw.dllを#importしている。
これはCOMの仕様に沿ったDLL(?)で、#importすることによって その中で定義されているCOMインターフェース(具体的には「SHDocVw::」で始まるクラス。つまりIWebBrowser2とか)が使えるようになる。
#importは、VB等で言うところの「参照設定」だと思われる。
この#importにより、プロジェクトのワークディレクトリの中にshdocvw.tlhとshdocvw.tliというファイルが作られる。tlhはタイプライブラリーヘッダー、tliは実装(implements)かなぁ?中を見てみるとそれぞれC++のヘッダーファイル・ソースファイルになっている。#importによってそのヘッダーファイルが読み込まれる(し、ソースファイルもコンパイルされる)。

mshtml.tlbも同様で、「MSHTML::」で始まるクラスを使う為に宣言している。

なお、ビルドするとこれらに関して大量の警告が出るが、とりあえず無視しても大丈夫そう。(→#imoprtに属性を付与することによって警告を消せる

実行時に生成した全てのCOMのインスタンスが解放されていない場合、CoUninitialize()で実行時エラーになるので注意。


アパートメント

COMにはアパートメント(アパート)と呼ばれる実行形態の種類(?)があるんだそうだ。[2007-11-07]
とりあえずよく目にするのがSTA(シングルスレッドアパートメント)とMTA(マルチスレッドアパートメント)。

どのアパートメントで実行されるかは、最初のCOMの初期化方法(CoInitialize())によって決まる。というか、CoInitialize()は必ずSTAになるらしい。
アパートメントを指定するにはCoInitializeEx()を使う。
CoInitialize()は自動的にCOINIT_APARTMENTTHREADEDを指定したことになっているんだそうだ。
MTAにするにはCOINIT_MULTITHREADEDを指定する。

ただし、CoInitializeEx()やCOINIT_MULTITHREADEDを使うためには コンパイル時にDCOMを指定する必要があり、また、実行環境がDCOMに対応している必要がある。
DCOMが何だかは知らないが(爆)、WindowsNT4.0以降は対応しているらしいのでWindowsXPも対応しているだろう。

#define _WIN32_DCOM
#include <windows.h>
//	HRESULT hr = ::CoInitialize(NULL);
//	HRESULT hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);	//STA
	HRESULT hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);	//MTA


STAはCOMの有効範囲(?)がそのスレッド内。つまりスレッドをまたいでCOMオブジェクトを使えない? 別スレッドでCOMを使いたい場合はスレッド毎にCoInitialize()あるいはCoInitializeEx(STA)を呼んで初期化する必要があるらしい。
MTAにすると全スレッド共通で使えるそうだ。

アパートメントを変えると、COMオブジェクトの関数によっては使えるようになったり使えなくなったりする
とりあえずIWebBrowserやMSHTML系統はSTAで動かした方が安全だと思う(MTAにしたら、IHTMLDocument2::get_parentWindow()がE_NOINTERFACEになった)。根本的なところが分かってないんだけど、MTAでも上手く排他すればちゃんと動くのかな??
けど、一部のイベントについてはMTAでないと捕捉できないっぽい。たぶん、イベントの発生は非同期だから(別スレッドで発生するので)マルチスレッドでないと対応していないのだろう。

参考: ゆうすさんのアパートのわかりにくい動作


新しいIEを開く

新しいInternetExplorerのウィンドウを開く例。

void NewIE()
{
	SHDocVw::IWebBrowser2Ptr pIE;

	//IEの起動
	HRESULT hr = pIE.CreateInstance(__uuidof(SHDocVw::InternetExplorer));
//	HRESULT hr = pIE.CreateInstance(_T("InternetExplorer.Application"));
	if(FAILED(hr)){
		TCHAR str[256];
		wsprintf(str, _T("NewIE#CreateInstance error:%x\n"), hr);
		::OutputDebugString(str);
		return;
	}

	//IEの表示
	pIE->PutVisible(VARIANT_TRUE);

	pIE->Navigate(_T("http://www.ne.jp/asahi/hishidama/home/tech/vcpp/index.html"));
}

pIEという変数(の定義)がミソ。
SHDocVw::IWebBrowser2Ptrという名前なのでポインターのように見えるが、SHDocVw::IWebBrowser2を継承した具象クラスなので(ローカル変数としての)インスタンスが生成されている。(ちなみにSHDocVw::IWebBrowser2は抽象クラスなのでそのままではインスタンス化できない)
なので「pIE.CreateInstance();」という呼び出し方が出来る。
なおかつ、演算子「->」「=」「==」「!=」等が定義(オーバーロード)されているらしく、ポインターのようにも扱える。だから逆に慣れないとややこしい^^;

イメージ的には、「.」や「=」はCOMとして操作する場合に使用し、「==」「!=」「->」は取得したオブジェクト固有の操作に使用する感じ。

なお、HRESULTS_OKより小さい値はエラーを表している。そのエラーコードは十六進数表記でwinerror.hに定義されているので、エラーになったら検索してみるべし。

参考: ウジャシリさんのVisual C++ Internet Explorer操作編


IEにHTMLを書き込む

HTMLをプログラムから書き込む方法。[2007-10-09]

void WriteIE()
{
	// 新規IEの作成
	SHDocVw::IWebBrowser2Ptr pIE;
	HRESULT hr = pIE.CreateInstance(__uuidof(SHDocVw::InternetExplorer));
	if (FAILED(hr)) return;

	pIE->PutVisible(VARIANT_TRUE);

	// 空のHtmlDocumentの作成
	pIE->Navigate(_T("about:blank"));

	writeHtml(pIE,
		_T("<html>\r\n")
		_T("<head>\r\n")
		_T("<title>sample</title>\r\n")
		_T("</head>\r\n")
		_T("<body>\r\n")
		_T("HTMLドキュメントに書き込むサンプル\r\n")
		_T("</body>\r\n")
		_T("</html>")
	);
}
bool writeHtml(SHDocVw::IWebBrowser2Ptr &pIE, LPCTSTR html)
{
	bool ret = false;

	MSHTML::IHTMLDocument2Ptr pDoc(pIE->Document);
	if (pDoc == NULL) return ret;

	SAFEARRAY* sfArray = ::SafeArrayCreateVector(VT_VARIANT, 0, 1);
	if (sfArray == NULL) return ret;

	_bstr_t bstr;

	VARIANT* var;
	HRESULT hr = ::SafeArrayAccessData(sfArray, reinterpret_cast<void**>(&var));
	if (FAILED(hr)) goto end;

	bstr = html;
	V_VT  (var) = VT_BSTR;
	V_BSTR(var) = bstr;

	hr = ::SafeArrayUnaccessData(sfArray);
	if (FAILED(hr)) goto end;

//	hr = pDoc->write(sfArray); //HTMLドキュメントへのHTMLの書き込み
	hr = pDoc->writeln(sfArray);
	if (FAILED(hr)) goto end;

	hr = pDoc->close();
	if (FAILED(hr)) goto end;

	ret = true;
end:
	::SafeArrayDestroy(sfArray);
	return ret;
}

SafeArrayCreateVector()で空のSAFEARRAYを作り出し、VARIANTを介してそこにHTMLの文字列をセットするという手順を踏まないといけないようだ。SafeArrayUnaccessData()は そのVARIANTをSAFEARRAYに固定するものだと思う。
で、そのSAFEARRAYをIHtmlDcoument2#write()に渡してやるとHTMLが書き込まれる。

close()するまでIEはビジー状態になる模様。
逆にclose()しなければ、write()・writeln()を何度も呼ぶことでHTMLをどんどん追加できる。
(ちなみに、書き込まれているHTMLを削除するにはclear()を呼ぶ)

IEの“ソースの表示(V)”で見ると、書き込んだHTMLそのものになっていることが分かる。
HTMLの文字列内に改行コードが入っていればソースも改行された状態になっている(writeln()なら、最後にも改行が入る)。

参考: mako10さんのVC++ 覚書き(IEコンポーネントに直接書込み)


IEを列挙する

現在実行中のInternetExplorerを 全て列挙する例。
実際にはExplorerやその他のアプリケーション(WebBrowserを生成したプログラムによるもの?)も取得されちゃうけど。

void ListIE()
{
	SHDocVw::IShellWindowsPtr pSW;
	HRESULT hr = pSW.CreateInstance(__uuidof(SHDocVw::ShellWindows));
	if(FAILED(hr)){
		TCHAR str[256];
		wsprintf(str, _T("ListIE#CreateInstance error:%x"), hr);
		::OutputDebugString(str);
		return;
	}

	for (long i = 0; i < pSW->Count; i++) {
		IDispatchPtr p = pSW->Item(i);
		SHDocVw::IWebBrowser2Ptr pWB = p;
//		SHDocVw::IWebBrowser2Ptr pWB(p);
		if (pWB != NULL){
			BSTR name;
			pWB->get_FullName(&name); //実行モジュールのフルパス

			HWND hwnd;
			pWB->get_HWND((long*)&hwnd); //ウィンドウハンドル

			MSHTML::IHTMLDocumentPtr pDoc = pWB->GetDocument();
			if (pDoc != NULL) {
				//IE
				TCHAR str[256];
				wsprintf(str, _T("ListIE[%2d][%06x]:%p, %s (%p)\n"), i,hwnd,pWB,name,pDoc);
				::OutputDebugString(str);
			} else {
				//Explorer(IE以外)
				TCHAR str[256];
				wsprintf(str, _T("ListIE[%2d][%06x]:%p, %s\n"), i,hwnd,pWB,name);
				::OutputDebugString(str);
			}

			SysFreeString(name);
		}
	}
}

ShellWindowsのCOMオブジェクトを生成すると、それがWebBrowserの一覧を持っている模様。
その一覧の1つ1つに対してpWBを取得して処理を行う。
pWB = p;」による代入または「pWB(p);」によるコンストラクターによってpWBの初期化を行っている。
これらは一見普通の代入のように見えるが、「代入元が自分自身(IWebBrowser2またはその派生)でないものは受け付けない」という処理が入っている。
受け付けられなかったときは「pWB == NULL」になる。(“==演算子”がオーバーロードされているので、(pWBがNULLなのではなく、)pWBが内部で持っているポインターがNULLということ)

pWBから実行モジュールのパスやらウィンドウハンドルやら、様々な情報が取り出せる。
BSTRで文字列を取得するタイプの関数では、その文字列を使い終わった後にSysFreeString()を呼び出して解放する必要がある。

IEであるかどうかは、GetDocument()によってHTMLドキュメントが取得できるかどうかで判断するのが常套手段らしい。

参考: Visual C++ Q&A掲示板Blueさんのコード


HWNDからHTMLドキュメントを探す

既に開いているInternetExplorerのHWNDからHTMLドキュメントを取得する例。[2007-10-27]

まず前提として、Win32APIのFindWindow()なりEnumWindows()なりを使って、IEのウィンドウハンドルは見つけておく。
そのウィンドウハンドルのウィンドウクラス名(GetClassName()で取得可)は「IEFrame」のはず。

IE(IEFrame)は子ウィンドウを色々持っている。それらはツールバーであったりメニューバーであったりする。googleツールバーなんかもそう。
そのうちの一つ、実際にHTMLの画面本体(クライアント領域?)を表示している部分が、ウィンドウクラス名「Shell DocObject View」のウィンドウ。
さらにその子ウィンドウである「Internet Explorer_Server」がHTMLドキュメントを持っている。

おまけ1:この構成はIE6の話であり、IE7の場合はタブブラウザーなので違うんだそうだ。
 IE6:「IEFrame」→                             「Shell DocObject View」→「Internet Explorer_Server」
 IE7:「IEFrame」→「TabWindowClass」→「Shell DocObject View」→「Internet Explorer_Server」
おまけ2:クライアント領域っぽい場所の座標を指定してChildWindowFromPoint(IEFrame,x,y)で取ってこられる子ウィンドウは「Shell DocObject View」。
おまけ3:「Shell DocObject View」と「Internet Explorer_Server」のスクリーン座標・サイズは同一(なので、その情報からは区別できない)。
おまけ4:「Internet Explorer_Server」のHWNDにWM_CHARやWM_IME_CHARを送ると、アクティブな入力エリアに文字を入力することが出来る(つまりtype=fileのテキストエリアに入力可能!)。WM_KEYDOWNやWM_KEYUPは不要、というか無視されるっぽい。

HTMLドキュメントを取得するには、「Internet Explorer_Server」(以下IESと略す)のウィンドウに対して取得用メッセージを送ればよい。
したがって、IEFrameのHWNDから子孫ウィンドウを探索してIESのウィンドウを探し出す必要がある。
ウィンドウクラス名を探索条件とするならFindWindowEx()が使えるかな〜と思ったけど、これは直接の子ウィンドウだけが探索対象のようで、孫(ひ孫)ウィンドウであるIESはFindWindowEx()のみでは探せない。
結局、EnumChildWindows()を使って1つずつ判定していくしか無いようだ。(EnumChildWindows()は子孫ウィンドウを全部探索対象にするっぽい。無駄が多いとも言えるわけだが…)

その判定方法は。 IESのウィンドウクラス名が「Internet Explorer_Server」だと分かっているので“ウィンドウクラス名が「Internet Explorer_Server」であること”でもいいのだが、今回の目的はHTMLドキュメントを取得することなので、“HTMLドキュメントが取得できること”にして判定条件をちょっと省略する。
厳密に言えば、IESであることを確認してからドキュメント取得、という手順なんだろうけど、どうせIESでなければドキュメント取得はできないから。
「IESならばdocが取得できる」の逆「docが取得できるならばIES」が成り立つのかどうかは知らないけど…

HTMLドキュメントを取得する為のメッセージはWM_HTML_GETOBJECT
これはIES独自(?)のメッセージだと思われる。したがって通常のメッセージが定義されているヘッダーファイル(winuser.h)上には定義が無い。 でもウィンドウズには登録されてるっぽいので、RegisterWindowMessage()を使ってメッセージIDを取得できる。
これをIESに対して送信し、成功すればLRESULTにHTMLドキュメントのポインターが返ってくるので、それを取り出す

#include <oleacc.h>	//ObjectFromLresult
#pragma comment(lib, "oleacc.lib")

static const UINT WM_HTML_GETOBJECT = ::RegisterWindowMessage(_T("WM_HTML_GETOBJECT"));
// 子ウィンドウ列挙・IESの判定処理
static BOOL CALLBACK FindHtmlDocumentProc(
  HWND hwnd,    // ウィンドウハンドル
  LPARAM lParam // アプリケーション定義の値:今回はpDocへのポインター
)
{
	LRESULT res = 0;
	::SendMessageTimeout(hwnd, WM_HTML_GETOBJECT, 0, 0, SMTO_ABORTIFHUNG, 1000, reinterpret_cast<PDWORD_PTR>(&res));
	if (res == 0) {
		// オブジェクトが取得できなかったときは、IESではなかったのだろう
		return TRUE; //探索続行
	}

	MSHTML::IHTMLDocumentPtr &pDoc = *reinterpret_cast<MSHTML::IHTMLDocumentPtr*>(lParam);
	HRESULT hr = ::ObjectFromLresult(res, __uuidof(MSHTML::IHTMLDocument), 0, reinterpret_cast<void**>(&pDoc));
	if (SUCCEEDED(hr)) {
		return FALSE; //探索終了
	}

	return TRUE; //探索続行
}
// IEのウィンドウハンドルからIHtmlDocumentを取得する(見つからなかった場合はNULL)
MSHTML::IHTMLDocumentPtr FindHtmlDocument(HWND hwnd)
{
	MSHTML::IHTMLDocumentPtr pDoc;
	::EnumChildWindows(hwnd, FindHtmlDocumentProc, reinterpret_cast<LPARAM>(&pDoc));
	return pDoc;
}

なお、IEのウィンドウを自分で作った場合、最初は「Shell DocObject View」(およびIES)のウィンドウは存在していない。
Navigate()なりを実行して初めて作られる。

参考:


HTMLドキュメントからWebBrowserを探す

HTMLドキュメントからWebBrowserコントロールを取得する例。[2007-10-27]

#include <shlguid.h>	//SID_STopLevelBrowser,SID_SWebBrowserApp
SHDocVw::IWebBrowser2Ptr FindWebBrowser(MSHTML::IHTMLDocumentPtr& pDoc)
{
	IServiceProviderPtr sp1 = pDoc;
	if (sp1 != NULL) {
		IServiceProviderPtr sp2;
		sp1->QueryService(SID_STopLevelBrowser, IID_IServiceProvider, reinterpret_cast<void**>(&sp2));
		if (sp2 != NULL) {
			SHDocVw::IWebBrowser2Ptr pIE;
			sp2->QueryService(SID_SWebBrowserApp, __uuidof(SHDocVw::IWebBrowser2), reinterpret_cast<void**>(&pIE));
			return pIE;
		}
	}
	return NULL;
}

HTMLドキュメントからサービスプロバイダー(COMの各インターフェースの階層を管理している?)を取得し、最終的にIWebBrowser2を取得できれば、それが目的のもの。
サービスプロバイダーを「取得する」というか、意味合い的には「キャストしている」だけという気がする。

参考: VC++ラウンジIE上のコントロール


なお、ほとんど書くまでもないけれど、逆にWebBrowserからHTMLドキュメントを取得するには、取得する方法が用意されているのでそれを使う。

	MSHTML::IHTMLDocument2Ptr pDoc(pIE->Document);
	MSHTML::IHTMLDocument2Ptr pDoc(pIE->GetDocument());
	IDispatch *disp;
	HRESULT hr = pIE->get_Document(&disp);
	if (FAILED(hr)) return; //取得失敗
	MSHTML::IHTMLDocument2Ptr pDoc(disp);

IEを探す

既に開いているInternetExplorerのHWNDからWebBrowserコントロールを取得する例。[/2007-10-27]
前で作成したHWNDからHTMLドキュメントを探す関数HTMLドキュメントからWebBrowserを探す関数を呼び出している。

SHDocVw::IWebBrowser2Ptr FindIE(HWND hwnd)
{
	MSHTML::IHTMLDocumentPtr pDoc = FindHtmlDocument(hwnd);
	if (pDoc != NULL) {
		return FindWebBrowser(pDoc);
	}
	return NULL;
}

逆にWebBrowserコントロールからHWNDを取得するのは簡単。

	HWND hwnd = (HWND)LongToHandle(pIE->HWND);
	HWND hwnd = (HWND)LongToHandle(pIE->GetHWND());
	long l;
	HRESULT hr = pIE->get_HWND(&l);
	if (FAILED(hr)) return; //取得失敗
	HWND hwnd = (HWND)LongToHandle(l);

ここで取得できるHWNDは、「IEFrame」のもの。


WebBrowserのイベント

IWebBrowser2のイベントをハンドリングする例。[2007-09-30]

  1. IDispatchを継承したクラス(イベント受信用)を作る。COMの仕様に則れば色々実装する必要があるのだろうが、とりあえず最小限だけあれば事足りるようだ。
  2. そして、Advise()という関数を使ってIWebBrowserにそのイベントクラス(のインスタンス)を登録する。
  3. すると、イベントが発生したときに登録したクラスのInvoke()という関数が呼ばれる
    ので、そこで何らかの処理をすればよい。

今回の例では、そのイベントクラスのインスタンスはグローバルに1つだけ用意して使う。
というわけでそのクラスを作ったんだけど、共通部分をSingletonEventというファイルに分けてみた。やりたい事の本体はSingletonEventを継承したWebBrowserMyEventにある。

ただし、STAだとDocumentCompleteが捕捉できない 。[2007-11-07]

SingletonEvent.h

#include <comdef.h>

class CSingletonEvent : public IDispatch
{
private:
	ULONG m_cRef;
	DWORD m_dwCookie;

public:
	// IUnknown
		virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
			/* [in] */ REFIID riid,
			/* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject);

		virtual ULONG STDMETHODCALLTYPE AddRef(void);

		virtual ULONG STDMETHODCALLTYPE Release(void);

	// IDispatch
		virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( 
			/* [out] */ UINT *pctinfo);

		virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( 
			/* [in] */ UINT iTInfo,
			/* [in] */ LCID lcid,
			/* [out] */ ITypeInfo **ppTInfo);

		virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( 
			/* [in] */ REFIID riid,
			/* [size_is][in] */ LPOLESTR *rgszNames,
			/* [in] */ UINT cNames,
			/* [in] */ LCID lcid,
			/* [size_is][out] */ DISPID *rgDispId);

		virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
			/* [in] */ DISPID dispIdMember,
			/* [in] */ REFIID riid,
			/* [in] */ LCID lcid,
			/* [in] */ WORD wFlags,
			/* [out][in] */ DISPPARAMS *pDispParams,
			/* [out] */ VARIANT *pVarResult,
			/* [out] */ EXCEPINFO *pExcepInfo,
			/* [out] */ UINT *puArgErr) = 0; //派生したクラスで実装する

public:
	//CSingletonEventではインスタンスを
	//グローバルに1つ持つだけなので、m_cRec=1で初期化する。
	//こうすると、Release内で自分を解放する処理には行かないはず。
	CSingletonEvent() : m_cRef(1)
	{
	}
	virtual ~CSingletonEvent() {}

protected:
	HRESULT Advise(IUnknown* pUnkCP, const IID& iid);
	HRESULT Unadvise(IUnknown* pUnkCP, const IID& iid);
};

SingletonEvent.cpp:

#include "SingletonEvent.h"

ULONG CSingletonEvent::AddRef()
{
	return ++m_cRef;
}
ULONG CSingletonEvent::Release()
{
	if(--m_cRef != 0){
		return m_cRef;
	}

	//コンストラクターでm_cRec=1という初期化をしている為、
	//AddRef()Release()がちゃんと対応していれば ここに来ることは無いはず。
	//newでインスタンスを作る方法の場合は、ここでdelete this;する。
	return 0;
}
HRESULT CSingletonEvent::QueryInterface(REFIID riid, void** ppv)
{
	if(riid == IID_IUnknown) {
		*ppv = (IUnknown*)this;
	} else if(riid == IID_IDispatch) {
		*ppv = (IDispatch*)this;
	} else {
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	AddRef();
	return S_OK;
}

HRESULT CSingletonEvent::GetTypeInfoCount(UINT* pCountTypeInfo)
{
	return S_OK;
}

HRESULT CSingletonEvent::GetTypeInfo(UINT iTypeInfo, LCID lcid, ITypeInfo** ppITypeInfo)
{
	return S_OK;
}

HRESULT CSingletonEvent::GetIDsOfNames(REFIID riid, LPOLESTR* rgszNames, 	UINT cNames, LCID lcid, DISPID* rgDispId)
{
	return S_OK;
}

HRESULT CSingletonEvent::Advise(IUnknown* pUnkCP, const IID& iid)
{
	if(pUnkCP == NULL) {
		return E_INVALIDARG;
	}

	IConnectionPointContainerPtr pCPC;
	HRESULT hRes = pUnkCP->QueryInterface(IID_IConnectionPointContainer, reinterpret_cast<void**>(&pCPC));
	if (SUCCEEDED(hRes)) {
		IConnectionPointPtr pCP;
		hRes = pCPC->FindConnectionPoint(iid, &pCP);
		if (SUCCEEDED(hRes)) {
			hRes = pCP->Advise(this, &m_dwCookie);
		}
	}
	return hRes;
}

HRESULT CSingletonEvent::Unadvise(IUnknown* pUnkCP, const IID& iid)
{
	if(pUnkCP == NULL) {
		return E_INVALIDARG;
	}

	IConnectionPointContainerPtr pCPC;
	HRESULT hRes = pUnkCP->QueryInterface(IID_IConnectionPointContainer, reinterpret_cast<void**>(&pCPC));
	if (SUCCEEDED(hRes)) {
		IConnectionPointPtr pCP;
		hRes = pCPC->FindConnectionPoint(iid, &pCP);
		if (SUCCEEDED(hRes)) {
			hRes = pCP->Unadvise(m_dwCookie);
		}
	}
	return hRes;
}

WebBrowserMyEvent.h

#include "SingletonEvent.h"

#import <shdocvw.dll>

class CWebBrowserMyEvent : public CSingletonEvent
{
public:
		virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
			/* [in] */ DISPID dispIdMember,
			/* [in] */ REFIID riid,
			/* [in] */ LCID lcid,
			/* [in] */ WORD wFlags,
			/* [out][in] */ DISPPARAMS *pDispParams,
			/* [out] */ VARIANT *pVarResult,
			/* [out] */ EXCEPINFO *pExcepInfo,
			/* [out] */ UINT *puArgErr);

public:
	CWebBrowserMyEvent()
	{
	}
	virtual ~CWebBrowserMyEvent() {}

	HRESULT AdviseIE(SHDocVw::IWebBrowser2Ptr pIE);
	HRESULT UnadviseIE(SHDocVw::IWebBrowser2Ptr pIE);
};

extern CWebBrowserMyEvent g_WebBrowserMyEvent;

WebBrowserMyEvent.cpp:

#include <windows.h>
#include <tchar.h>
#include <exdispid.h> //DISPID_*
#include "WebBrowserMyEvent.h"

CWebBrowserMyEvent g_WebBrowserMyEvent; //唯一のインスタンス

//WebBrowser2のイベントを登録する
HRESULT CWebBrowserMyEvent::AdviseIE(SHDocVw::IWebBrowser2Ptr pIE)
{
	return Advise(pIE, __uuidof(SHDocVw::DWebBrowserEvents2));
}

HRESULT CWebBrowserMyEvent::UnadviseIE(SHDocVw::IWebBrowser2Ptr pIE)
{
	return Unadvise(pIE, __uuidof(SHDocVw::DWebBrowserEvents2));
}

//イベントが発生すると、この関数が呼ばれる
HRESULT CWebBrowserMyEvent::Invoke(DISPID dispidMember,
		REFIID riid, LCID lcid, WORD wFlags,
		DISPPARAMS* pDispParams,
		VARIANT* pvarResult,
		EXCEPINFO* pExcepInfo,
		UINT* puArgErr)
{
	if (!pDispParams) {
		return E_INVALIDARG;
	}

	switch (dispidMember) {
	case DISPID_BEFORENAVIGATE2:
		::OutputDebugString(_T("DISPID_BEFORENAVIGATE2\n"));
		break;
	case DISPID_DOCUMENTCOMPLETE:		 //MTAでないとこのイベントは捕捉できない
		::OutputDebugString(_T("DISPID_DOCUMENTCOMPLETE\n")); 
		break;
	default: {
		TCHAR str[256];
		wsprintf(str, _T("other event: %d\n"), dispidMember);
		::OutputDebugString(str);
		// return E_NOTIMPL;
		}
		break;
	}

	return S_OK;
}

使う.cpp

#include <ocidl.h>		//tagREADYSTATE
void EventTest()
{
	HWND hwnd = ::FindWindow(NULL, _T("about:blank - Microsoft Internet Explorer"));
	if (hwnd == NULL) {
		::OutputDebugString(_T("not found blankIE window\n"));
		return;
	}

	SHDocVw::IWebBrowser2Ptr pIE = FindIE(hwnd);
	g_WebBrowserMyEvent.AdviseIE(pIE); //イベントの登録

	pIE->Navigate(_T("http://www.ne.jp/asahi/hishidama/home/tech/vcpp/index.html"));
	while(pIE->GetBusy() != FALSE || pIE->GetReadyState() != READYSTATE_COMPLETE) {
		//この間にInvoke()でイベントが捕捉できる
		::Sleep(200);
	}

	g_WebBrowserMyEvent.UnadviseIE(pIE); //イベント登録の解消
}

g_WebBrowserMyEventはインスタンスが1つしかないので、Advise()で使うdwCookieも1つしかない。[2007-10-01]
したがって、解放せずにAdviseIE()を何度も呼ぶとたぶん駄目だろう。(しかも解放の順番を入れ替えたりすると…)

参考: CodeGuruのKeystroke Logger and More, Part 3


WebBrowserの履歴

IEでは、Navigate()による直接のURL移動の他に、GoBack()・GoForward()によって履歴を使った移動が出来る。[2007-10-27]
が、履歴が無いのに移動しようとするとエラー(HRESULTが0x80004005:E_FAIL)になる。
.NETフレームワークのクラスではCanGoBackやCanGoForwardというプロパティーで履歴の有無を確認できるようなのでWebBrowserにも無いかなーと思ったら、TravelLog(旅行か!)というデータで履歴を取得・操作できるらしい。

#include <tlogstg.h> //ITravelLogStg

_COM_SMARTPTR_TYPEDEF(ITravelLogStg, __uuidof(ITravelLogStg)); //ITravelLogStgPtrを自分で定義しておく
void History(SHDocVw::IWebBrowser2Ptr pIE)
{
	IServiceProviderPtr sp = pIE;
	if (sp == NULL) {
		::OutputDebugString(_T("not found IServiceProvider\n"));
		return;
	}

	ITravelLogStgPtr pTLS;
	HRESULT hr = sp->QueryService(SID_STravelLogCursor, IID_ITravelLogStg, reinterpret_cast<void**>(&pTLS));
	if (pTLS == NULL) {
		TCHAR buf[64];
		wsprintf(buf, _T("not found ITravelLogStg:%x\n"), hr);
		::OutputDebugString(buf);
		return;
	}

	〜
}

ところがどっこい。コンパイルは通るんだけど、、、
うちの環境(WindowsXP SP2・IE6 SP2)では実行時にITravelLogStgの取得で失敗する。HRESULT=0x80040155(REGDB_E_IIDNOTREG)「インターフェイスが登録されていません」

そんなバカなー!と思ってレジストリを検索してみたら、確かにITravelLogStgもそのIIDである「7EBFDD80-AD18-11d3-A4C5-00C04F72D6B8」も見つからない。
ITravelLog関連はshdocvw.dllにあるそうなので、tlogstg.hを#includeしないといけない時点で確かに存在してないんだろうけど…。
でもなんでじゃー?! IEで普通にヒストリーバックを使えるんですけど〜!?

ネットで探してみても、こういう現象に遭った人はいないみたい…。挫折。。。

参考:


HTMLの操作

SHDocVw::IWebBrowser2Ptrが取得できれば、あとはそこからHTMLドキュメント(MSHTML::IHTMLDocumentPtr)を取得し、必要なHTMLの要素を取得するだけ。

#include <comutil.h> //_bstr_t
void GoogleSearch(SHDocVw::IWebBrowser2Ptr pIE)
{
	//HTMLドキュメント取得
	MSHTML::IHTMLDocument3Ptr pDoc = pIE->GetDocument();
	if (pDoc == NULL) return;

	//<xxx name="q">のタグを取得(Googleの検索ボックスはそういう名前になっている)
	MSHTML::IHTMLElementCollectionPtr pCollection = pDoc->getElementsByName(_bstr_t("q"));
	if (pCollection == NULL) return;

	//1個目のアイテムが<input type="text">のはず
	MSHTML::IHTMLInputTextElementPtr pText = pCollection->item(0);
	if (pText != NULL) {
		pText->Putvalue(_bstr_t("VC++ WebBrowser")); //検索文字列の入力
		if (pText->form != NULL) {
			pText->form->submit(); //サブミット
		}
	}
}

IHTMLDocument4とか5とかもあって、これがバージョンを表しているなら大きい方がいいのかと思ったら、定義されている関数が違う。
(この数値は、導入された順序(バージョン)という意味合いなようだ。)
必要な関数が定義されている番号のクラスを指定する必要がある。

参考: Visual C++ Q&A掲示板IE上のメソッドをコントロール


VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま