EternalWindows
WebBrowser コントロール / コンテナの実装

今回から、実際にブラウザを開発する作業に入ります。 ブラウザが持っている各機能はクラスによって分別されて実装されているため、 まずはどの部分をどのクラスが実装しているのかを確認します。

続いて、どのクラスがどのクラスを使用するかを確認します。 これは、次のように階層で示すと分かりやすいと思われます。

上に存在するクラスが、直下のクラスを使用すると考えてください。 たとえば、CWebBrowserContainerの下には、CMenuMgr、CRebarMgr、CSidebar、CStatusbarがありますから、 CWebBrowserContainerがこれらのクラスへのポインタを持っていることを意味します。 場合によっては、下のクラスが上のクラスの機能を使用したくなることもありますが、 このような場合にはその処理をCWebBrowserContainerに依頼します。 CWebBrowserContainerはグローバルに定義されているため、 どのクラスからでも機能を使用できます。 全てのクラスはIUnknownを継承するようになっていますが、 これはReleaseという共通のメソッドでオブジェクトを破棄するためです。

それでは、実際にクラスの実装を見ていくことにします。 今回、取り上げるクラスはCWebBrowserContainerであり、 これは先の図に示したように他のクラスのポインタを多く持っています。 このクラスの目的はいわば、何らかの処理を他のクラスに伝えることであり、 自らが具体的な処理を行うことはほとんどありません。 よって、クラスの実装はそれほど複雑なものではありません。 CWebBrowserContainerはWinMainで作成されます。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	int nResult = 0;

	OleInitialize(NULL);
	
	g_pWebBrowserContainer = new CWebBrowserContainer;
	if (g_pWebBrowserContainer != NULL) {
		nResult = g_pWebBrowserContainer->Run(hinst, nCmdShow);
		g_pWebBrowserContainer->Release();
	}

	OleUninitialize();

	return nResult;
}

ブラウザの開発ではCOMやOLEの機能を使用することになるため、 最初にOleInitializeを呼び出しておくようにします。 CWebBrowserContainerはグローバルに参照されることになっているため、 g_pWebBrowserContainerはグローバル変数として定義されています。 Runというメソッドでは通常のWindowsアプリケーションと同じように、 ウインドウクラスの登録やウインドウの作成を行います。

int CWebBrowserContainer::Run(HINSTANCE hinst, int nCmdShow)
{
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = ::WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = g_szContainerClassName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpfnWndProc   = HostProc;
	wc.lpszClassName = g_szHostClassName;
	if (RegisterClassEx(&wc) == 0)
		return 0;

	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpfnWndProc   = SidebarProc;
	wc.lpszClassName = g_szSidebarClassName;
	if (RegisterClassEx(&wc) == 0)
		return 0;
	
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpfnWndProc   = SuggestProc;
	wc.lpszClassName = g_szSuggestClassName;
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, g_szContainerClassName, g_szWinodowName, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		if (!TranslateAccelerator(&msg)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int)msg.wParam;
}

コンテナのウインドウが使用するウインドウクラスの名前はg_szContainerClassNameに定義されており、 これをRegisterClassExで登録します。 自作のウインドウクラスが必要になるのは、コンテナの他にホストやサイドバーも同じであるため、 それらのためのウインドウクラスもここで登録しておきます。 メッセージループでTranslateAcceleratorという自作メソッドを呼び出しているのは、 WebBrowser コントロールがメッセージをアクセラレータキーとして処理したかを調べるためです。 処理していていない場合は、コンテナがメッセージを処理します。

WindowProcのWM_CREATEでは、Createという自作メソッドを呼び出します。

BOOL CWebBrowserContainer::Create(HWND hwnd)
{
	INITCOMMONCONTROLSEX ic;
		
	ic.dwSize = sizeof(INITCOMMONCONTROLSEX);
	ic.dwICC  = ICC_BAR_CLASSES | ICC_COOL_CLASSES | ICC_TAB_CLASSES | ICC_TREEVIEW_CLASSES;
	InitCommonControlsEx(&ic);
	
	m_hwnd = hwnd;
	
	m_pMenuMgr = new CMenuMgr;
	m_pMenuMgr->Create(hwnd);
	
	m_pRebarMgr = new CRebarMgr;
	m_pRebarMgr->Create(hwnd);

	m_pSidebar = new CSidebar();
	m_pSidebar->Create(hwnd);
	m_pSidebar->Show(TRUE);
	
	m_pStatusbar = new CStatusbar();
	m_pStatusbar->Create(hwnd);

	CoInternetSetFeatureEnabled(FEATURE_TABBED_BROWSING, SET_FEATURE_ON_PROCESS, TRUE);
	CoInternetSetFeatureEnabled(FEATURE_FEEDS, SET_FEATURE_ON_PROCESS, TRUE);
	CoInternetSetFeatureEnabled(FEATURE_SECURITYBAND, SET_FEATURE_ON_PROCESS, TRUE);
	CoInternetSetFeatureEnabled(FEATURE_WEBOC_POPUPMANAGEMENT, SET_FEATURE_ON_PROCESS, TRUE);
	CoInternetSetFeatureEnabled(FEATURE_RESTRICT_FILEDOWNLOAD, SET_FEATURE_ON_PROCESS, TRUE);

	SetUserAgent();

	return TRUE;
}

CWebBrowserContainerが管理するクラスを作成するにあたって、 それぞれで必要になるウインドウクラスをInitCommonControlsExで初期化します。 それぞれのクラスの初期化はCreateメソッドで行うことができ、 CSidebarの場合はサイドバーを既定で表示状態にするためにShowを呼び出します。 CoInternetSetFeatureEnabledの第2引数にSET_FEATURE_ON_PROCESSを指定し、 第3引数にTRUEを指定した場合は、第1引数のフィーチャをプロセス単位で有効することができます。 フィーチャが何であるかは上手く説明することができませんが、 それを設定することでWebBrowser コントロールの動作が変化する点は確かです。 たとえば、FEATURE_TABBED_BROWSINGを指定すればポップアップメニューから「新しいタブで開く」を選択できますし、 FEATURE_FEEDSを指定すればrssファイルの中身がxml型式で表示されないようになります。 また、FEATURE_SECURITYBANDを指定すれば制限時に情報バーを表示できます。 たとえば、FEATURE_WEBOC_POPUPMANAGEMENTはポップアップウインドウの表示を制限する意味を持ちますが、 これが設定されている状態でFEATURE_SECURITYBANDも設定されている場合は、 ポップアップウインドウが表示される前に次のようなバーが表示されます。

上図のように、ポップアップウインドウの制限が通知されますが、 バーの上で右クリックを押せば、メニューから制限を解除することもできます。 FEATURE_RESTRICT_FILEDOWNLOADは、ページのアクセス時にファイルが自動でダウンロードすることを制限します。 これが設定されている状態でFEATURE_SECURITYBANDも設定されている場合は、 自動ダウンロードが行われる前に次のようなバーが表示されます。

このバーについても、右クリックメニューでファイルのダウンロードを開始することができます。

Createで呼び出していたSetUserAgentは、ユーザーエージェント文字列を設定します。 この文字列はHTTPリクエストに含まれ、アクセスしてきたブラウザの判定にサーバーが使用します。

void CWebBrowserContainer::SetUserAgent()
{
	CHAR  szUserAgent[512];
	DWORD dwLength;
	
	UrlMkGetSessionOption(URLMON_OPTION_USERAGENT, szUserAgent, sizeof(szUserAgent), &dwLength, 0);

	szUserAgent[dwLength - 2] = '\0';
	lstrcatA(szUserAgent, "; Sample WebBrowser/1.0)");
	UrlMkSetSessionOption(URLMON_OPTION_USERAGENT, (char *)szUserAgent, lstrlenA(szUserAgent), 0);

	MultiByteToWideChar(CP_ACP, 0, szUserAgent, -1, g_szUserAgentW, 512);
}

既定のユーザーエージェント文字列の最後に、独自ブラウザの名前を連結するようにしたいため、 まずはUrlMkGetSessionOptionで既定のユーザーエージェント文字列を取得します。 このとき、第1引数はURLMON_OPTION_USERAGENTを指定します。 ObtainUserAgentStringという名前通りの関数もありますが、 GET/SETの組み合わせの方が分かりやすいと考えたため、 UrlMkGetSessionOptionを使用しています。 ユーザーエージェント文字列の最後は、"CLR 3.0.30729)"のようになっていると思われるため、 この")"の部分を"; Sample WebBrowser/1.0)"に置き換えます。 そして、新しいユーザーエージェント文字列をUrlMkSetSessionOptionに指定します。 g_szUserAgentWは、CSuggestWindow::Createで使用することになっています。

CWebBrowserContainerは様々なクラスを管理しており、ウインドウのサイズが変更された場合はそれらのサイズも変更しなければなりません。 サイズ調整はWM_SIZEで行われています。

case WM_SIZE:
	if (m_pMenuMgr == NULL || m_pRebarMgr == NULL || m_pStatusbar == NULL || m_pSidebar == NULL)
		return 0;
	m_pStatusbar->SetWindowSize(wParam, lParam);
	m_pRebarMgr->SetWindowSize(wParam, lParam);
	ResizeWindow();
	return 0;

各クラスの中で初期化されていないものがある場合はまだCWebBrowserContainer::Createが制御を返しておらず、 現段階でサイズ調整を行う必要はないと判断します。 ResizeWindowは、ホストとサイドバーのサイズを調整する自作メソッドであり、 これらはタブ切り替えの際にも呼ばれるため関数化しています。 一方、スタータスバーとレバーコントロールのサイズ調整はこの場だけでよいため、 ResizeWindowには含めないようにしています。 ResizeWindowの実装は次のようになっています。

void CWebBrowserContainer::ResizeWindow()
{
	int  nWidthClient, nHeightClient;
	int  nHeightStatus, nHeightRebar, nWidthSidebar;
	RECT rc;

	if (m_pMenuMgr == NULL || m_pRebarMgr == NULL || m_pStatusbar == NULL || m_pSidebar == NULL)
		return;

	GetClientRect(m_hwnd, &rc);
	nWidthClient = rc.right - rc.left;
	nHeightClient = rc.bottom - rc.top;

	GetWindowRect(m_pStatusbar->GetWindow(), &rc);
	nHeightStatus = rc.bottom - rc.top;

	GetWindowRect(m_pRebarMgr->GetWindow(), &rc);
	nHeightRebar = rc.bottom - rc.top;

	if (m_pSidebar->IsShow()) {
		nWidthSidebar = 250;
		m_pSidebar->SetWindowSize(0, nHeightRebar, nWidthSidebar, nHeightClient - nHeightRebar - nHeightStatus);
	}
	else
		nWidthSidebar = 0;

	if (m_pActiveWebBrowserHost != NULL)
		m_pActiveWebBrowserHost->SetWindowSize(nWidthSidebar, nHeightRebar, nWidthClient - nWidthSidebar, nHeightClient - nHeightRebar - nHeightStatus);
}

ホストとサイドバーを表示するにあたって、それがメインウインドウのサイズを超えないようにしなければなりません。 よって、メインウインドウのクライアント領域の幅と高さを取得しています。。 また、ホストやサイドバーはレバーコントロールやステータスバーを覆い尽くしてはいけませんから、 それらの高さ(nHeightStatusとnHeightRebar)も取得しています。 サイドバーが現在表示状態とされているならば、SetWindowSizeでサイドバーのサイズを設定します。 高さについては、メインウインドウのクライアント領域の高さからレバーコントロールとステータスバーを除くようにします。 m_pActiveWebBrowserHostは、現在選択されているタブに関連するCWebBrowserHostを識別します。 これがNULLでないということは、現在何らかのタブが存在していることを意味しているため、 そのタブのためにCWebBrowserHostを表示します。

CWebBrowserContainerが作成したウインドウは、 レバーコントロールやステータスバーの親ウインドウでもあります。 よって、これらのコントロールの通知はCWebBrowserContainerが受け取ります。 また、メニューが割り当てられているのもこのウインドウですから、 メニュー項目に関する通知も受けとります。 こうした通知を専用のクラスを伝えるために、以下のメッセージを処理します。

case WM_COMMAND:
	m_pRebarMgr->OnCommand(wParam, lParam);
	m_pMenuMgr->OnMenuCommand(LOWORD(wParam));
	return 0;

case WM_NOTIFY:
	m_pStatusbar->OnNotify(wParam, lParam);
	m_pRebarMgr->OnNotify(wParam, lParam);
	return 0;

case WM_INITMENUPOPUP:
	m_pMenuMgr->SetMenuState((HMENU)wParam, LOWORD(lParam));
	return 0;

レバーコントロールの履歴ボタンが押された場合はWM_COMMANDが生成されるため、m_pRebarMgrに通知します。 また、メニュー項目を選択した場合も通知されるため、m_pMenuMgrに通知します。 WM_NOTIFYは、レバーコントロールやステータスバーで操作が行われた場合に送られため、 m_pStatusbarとm_pRebarMgrに通知します。 WM_INITMENUPOPUPは、トップメニューのポップアップメニューが表示される段階で送られます。 この際には項目にチェックを付ける処理などが必要になるため、m_pMenuMgrに通知します。

DEPについて

Windows XP SP2から導入されたDEP(Data Execution Prevention)は、 実行可能とされていないページからコードが実行されようとした場合に例外を発生させます。 アプリケーション内でこの機能が有効になっている場合は、 外部から書き込まれた予期しないコードの実行を防ぐことができるため、 ブラウザのように外部入力を受け取るアプリケーションは、この機能を有効にしておくべきといえます。 Visual Studio 2010では、プロジェクト/プロパティ/構成プロパティ/リンカー/詳細設定/にて、 既定でDEPが有効にされているため、この環境で開発している場合は特別な処理は不要です。

開発環境でDEPを有効にできない場合は、アプリケーション内で明示的にDEPを有効にします。 これには、SetProcessDEPPolicyを呼び出します。

SetProcessDEPPolicy(PROCESS_DEP_ENABLE)

PROCESS_DEP_ENABLEを指定することでDEPを有効にすることができます。 関数が定義されていない場合は、次のように明示的リンクします。

void CWebBrowserContainer::EnableDEP()
{
	typedef BOOL (WINAPI *LPFNSETPROCESSDEPPOLICY)(DWORD);

	WCHAR                   szBuf[256];
	DWORD                   dwLength;
	HMODULE                 hmod;
	LPFNSETPROCESSDEPPOLICY lpfnSetProcessDEPPolicy;

	hmod = LoadLibrary(TEXT("kernel32.dll"));
	if (hmod == NULL)
		return;

	lpfnSetProcessDEPPolicy = (LPFNSETPROCESSDEPPOLICY)GetProcAddress(hmod, "SetProcessDEPPolicy");
	if (lpfnSetProcessDEPPolicy == NULL) {
		FreeLibrary(hmod);
		return;
	}
	
	lpfnSetProcessDEPPolicy(1);

	FreeLibrary(hmod);
}

SetProcessDEPPolicyはkernel32.dllにて実装されているため、このDLLからアドレスを取得します。 PROCESS_DEP_ENABLEは1として定義されているため、この値をlpfnSetProcessDEPPolicyに指定します。



戻る