EternalWindows
バンドオブジェクト / ページの保存

前節では、エディットコントロール上への文字列の入力を可能にしたため、 後はこの文字列をフォルダパスとしてWebページを保存するだけです。 ツールバーの「保存」ボタンが押されると、次の処理が実行されます。

else if (LOWORD(wParam) == ID_SAVE) {
	TCHAR szFolderPath[256];

	GetWindowText(m_hwndEdit, szFolderPath, sizeof(szFolderPath) / sizeof(TCHAR));
	if (CapturePage(szFolderPath))
		MessageBox(NULL, TEXT("ファイルを作成しました。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("ファイルの作成に失敗しました。"), NULL, MB_ICONWARNING);
}

wParamの下位ワードにはコントールのIDが格納されているため、 それが「保存」ボタンと一致しているかを調べます。 一致している場合はエディットコントロールから文字列を取得し、 これを自作関数のCapturePageに指定します。

CapturePageの内部は、次のようになっています。

BOOL CToolBand::CapturePage(LPTSTR lpszFolderPath)
{
	IDispatch      *pDispatch;
	IHTMLDocument2 *pDocument2;
	IHTMLElement   *pElement;
	IHTMLElement2  *pElement2;
	IOleObject     *pOleObject;
	LONG           lWidth, lHeight;
	SIZEL          sizelPrev, sizel;
	RECT           rc;
	BOOL           bResult;
	HDC            hdcMem;
	HBITMAP        hbmpMem;
	HBITMAP        hbmpMemPrev;
	BITMAP         bm;
	BOOL           bProtectedMode = FALSE;
	LPBYTE         lpData;
	DWORD          dwSize;

	m_pWebBrowser2->get_Document(&pDispatch);
	pDispatch->QueryInterface(IID_PPV_ARGS(&pDocument2));
	pDocument2->get_body(&pElement);
	pElement->QueryInterface(IID_PPV_ARGS(&pElement2));
	pElement2->get_scrollWidth(&lWidth);
	pElement2->get_scrollHeight(&lHeight);
	
	hdcMem = CreateCompatibleDC(NULL);
	hbmpMem = CreateBackbuffer(lWidth, lHeight);
	hbmpMemPrev = (HBITMAP)SelectObject(hdcMem, hbmpMem);
	
	pDispatch->QueryInterface(IID_PPV_ARGS(&pOleObject));
	pOleObject->GetExtent(DVASPECT_CONTENT, &sizelPrev);
	sizel.cx = lWidth;
	sizel.cy = lHeight;
	DPtoHIMETRIC(hdcMem, &sizel);
	pOleObject->SetExtent(DVASPECT_CONTENT, &sizel);

	SetRect(&rc, 0, 0, lWidth, lHeight);
	OleDraw(pOleObject, DVASPECT_CONTENT, hdcMem, &rc);
	pOleObject->SetExtent(DVASPECT_CONTENT, &sizelPrev);

	GetObject(hbmpMem, sizeof(BITMAP), &bm);
	lpData = CreateBitmapData(lWidth, lHeight, bm.bmBits, &dwSize);

	IEIsProtectedModeProcess(&bProtectedMode);
	if (bProtectedMode)
		bResult = SaveFileProtect(lpszFolderPath, lpData, dwSize);
	else
		bResult = SaveFile(lpszFolderPath, lpData, dwSize);

	CoTaskMemFree(lpData);
	SelectObject(hdcMem, hbmpMemPrev);
	DeleteObject(hbmpMem);
	DeleteDC(hdcMem);
	pElement2->Release();
	pElement->Release();
	pOleObject->Release();
	pDocument2->Release();
	pDispatch->Release();

	return bResult;
}

ページを保存する方法を考えていた当初は、IHTMLElementRenderを使用すれば簡単に実現できると思っていました。 このインターフェースにはDrawToDCというメソッドがあるため、 <body>タグからIHTMLElementRenderを取得して呼び出せば、 ページの内容がデバイスコンテキストに描画されると考えていたわけです。 しかし、実際に実行してみると現在見えている範囲しか描画されず、 スクロールを考慮した全体を描画することはできませんでした。 よって、次に示す各種処理を順に行うことになります。

m_pWebBrowser2->get_Document(&pDispatch);
pDispatch->QueryInterface(IID_PPV_ARGS(&pDocument2));
pDocument2->get_body(&pElement);
pElement->QueryInterface(IID_PPV_ARGS(&pElement2));
pElement2->get_scrollWidth(&lWidth);
pElement2->get_scrollHeight(&lHeight);

IWebBrowser2::get_Documentで取得したIDispatchは、現在表示されているドキュメント(ページ)を識別しています。 しかし、このインターフェースではページを操作することができないので、 QueryInterfaceでIHTMLDocument2を取得するようにしています。 IHTMLDocument2::get_bodyで<body>タグを表すIHTMLElementを取得し、 そこからさらにIHTMLElement2を取得しているのは、 IHTMLElement2::get_scrollWidth及びget_scrollHeightで、 スクロールを考慮したページの幅と高さを取得するためです。 get_clientWidthとget_clientHeightはクライアント領域のサイズしか取得しないため、 これらを使用することはできません。

hdcMem = CreateCompatibleDC(NULL);
hbmpMem = CreateBackbuffer(lWidth, lHeight);
hbmpMemPrev = (HBITMAP)SelectObject(hdcMem, hbmpMem);

メモリデバイスコンテキストを作成し、それに対してビットマップを割り当てる処理です。 CreateBackbufferという自作関数はビットマップをDIBセクションとして作成し、 このビットマップはSelectObjectを通じてメモリデバイスコンテキストに割り当てられます。 割り当てるビットマップはWebページの全体を格納できるサイズでなければならないため、 先に取得したlWidthとlHeightをサイズとして指定しています。

pDispatch->QueryInterface(IID_PPV_ARGS(&pOleObject));
pOleObject->GetExtent(DVASPECT_CONTENT, &sizelPrev);
sizel.cx = lWidth;
sizel.cy = lHeight;
DPtoHIMETRIC(hdcMem, &sizel);
pOleObject->SetExtent(DVASPECT_CONTENT, &sizel);

Webページをメモリデバイスコンテキストへ描画する際に、 ページの全域が対象になるようにするための処理です。 このためにはIOleObject::SetExtentを呼び出す必要があるため、 まずはドキュメントのQueryInterfaceからIOleObjectを取得しておきます。 GetExtentで現在のサイズを取得しているのは、SetExtentで変更したサイズを後で戻すためです。 SIZEL構造体にページのサイズを指定したらSetExtentを呼び出せるように思えますが、 このときに指定する幅や高さはHIMETRIC単位でなければなりません。 SIZEL構造体に指定したのはピクセル単位のサイズですから、 これをHIMETRIC単位へ変換するためにDPtoHIMETRICという自作関数を呼び出しています。

SetRect(&rc, 0, 0, lWidth, lHeight);
OleDraw(pOleObject, DVASPECT_CONTENT, hdcMem, &rc);
pOleObject->SetExtent(DVASPECT_CONTENT, &sizelPrev);

OleDrawを呼び出して、Webページをメモリデバイスコンテキストへ描画します。 第1引数はページを表すインターフェースであり、 上記ではpOleObjectを指定していますが、pDispatchやpDocument2でも問題はありません。 第2引数はDVASPECT_CONTENTを指定し、第3引数は描画先となるメモリデバイスコンテキストのハンドルを指定します。 第4引数は描画先の範囲であり、メモリデバイスコンテキストの全体となるように初期化しておきます。 描画が終了したら、先のGetExtentで保存しておいたサイズをSetExtentに指定します。

GetObject(hbmpMem, sizeof(BITMAP), &bm);
lpData = CreateBitmapData(lWidth, lHeight, bm.bmBits, &dwSize);

CreateBitmapDataという自作メソッドは、ビットマップファイルに書き込むべきデータを返します。 このデータには、BITMAPFILEHEADER構造体、BITMAPINFOHEADER構造体、ビットイメージが含まれ、 第4引数は返されたデータのサイズになります。

IEIsProtectedModeProcess(&bProtectedMode);
if (bProtectedMode)
	bResult = SaveFileProtect(lpszFolderPath, lpData, dwSize);
else
	bResult = SaveFile(lpszFolderPath, lpData, dwSize);

IEIsProtectedModeProcessは、現在IEが保護モードとして動作しているかを返します。 UACが有効な場合はIEが保護モードとして動作することになり、IEの中で動作するDLLはいくつかの操作が失敗することになります。 たとえば、スレッドのトークンに設定されている整合性レベルはLowになっているため、 整合性レベルがMediumのオブジェクト(デスクトップ上のファイルなど)にアクセスすることができなくなります。 こうしたことから、保護モードにおける保存処理は、通常の保存処理と異なることになります。

BOOL CToolBand::SaveFileProtect(LPWSTR lpszFolderPath, LPBYTE lpData, DWORD dwSize)
{
	LPCWSTR lpszExt = L"Bitmap Files|*.bmp|";
	LPCWSTR lpszDefExt = L"bmp";
	WCHAR   szTempPath[MAX_PATH];
	DWORD   dwResult;
	HRESULT hr;
	HANDLE  hState, hFile;
	LPWSTR  lpszTargetPath, lpszCachePath;

	hr = IEShowSaveFileDialog(NULL, L"", lpszFolderPath, lpszExt, lpszDefExt, 1, OFN_OVERWRITEPROMPT, &lpszTargetPath, &hState);
	if (hr != S_OK)
		return FALSE;
	
	hr = IEGetWriteableFolderPath(FOLDERID_InternetCache, &lpszCachePath);
	if (hr != S_OK) {
		CoTaskMemFree(lpszTargetPath);
		IECancelSaveFile(hState);
		return FALSE;
	}

	GetTempFileNameW(lpszCachePath, TEXT("tmp"), 0, szTempPath);

	hFile = CreateFileW(szTempPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpData, dwSize, &dwResult, NULL);
	CloseHandle(hFile);
	
	IESaveFile(hState, szTempPath);

	CoTaskMemFree(lpszTargetPath);
	CoTaskMemFree(lpszCachePath);

	return TRUE;
}

IEShowSaveFileDialogは、GetSaveFileNameのようにファイルを保存するためのダイアログを表示しますが、 この関数は成功時に特殊なハンドルを返します。 このハンドルをIESaveFileに指定した場合、第2引数のファイルパス(szTempPath)の中身が第1引数のハンドルに関連するパス(lpszTargetPath)にコピーされるため、 lpszTargetPathがデスクトップ上のファイルパスを格納しているならば、そこにファイルが作成されることになります。 これは結局どういうことかというと、DLLのスレッドは自分が書き込める場所に取り敢えずファイルを作成しておき、 目的のパスへの書き込みについてはIEにお願いするということです。 この書き込める場所についてはIEGetWriteableFolderPathで問い合わせ可能であり、 この関数がS_OKを返した場合は、第1引数の定数で識別されるパスが第2引数に返されます。 一方、E_ACCESSDENIEDが返った場合は、第1引数で識別されるパスにはアクセスできないことを意味します。 書き込めるパスが分かったらそこにファイルを作成することになりますが、 このファイルの中身は最終的にlpszTargetPathへコピーされるのであって、 その存在は一時的なものといえます。 よって、GetTempFileNameによって一時ファイルの名前を求めるようにし、 それに対してデータを書き込むようにしています。 IESaveFileは、LowのスレッドがMediumのフォルダにアクセスすることを間接的に可能にしているわけであり、 このようなことが暗黙的に可能では保護モードの意味がなくなりますから、 ハンドルを返す関数がIEShowSaveFileDialogであるという点は非常に重要です。 この関数はダイアログを表示しますから、ユーザーにファイルを作成してもよいかを確認できるわけです。 IEShowSaveFileDialogの引数については、第1引数がダイアログの親ウインドウのハンドルで、 第2引数が既定のファイル名、第3引数が既定のフォルダになります。 第4引数は選択可能な拡張子であり、第5引数は既定で付加する拡張子を指定します。 第6引数は第4引数の拡張子の中から既定で選択するためのインデックスを指定します。 これは1から始まります。 第7引数はOPENFILENAME構造体に使用されるフラグを指定でき、 OFN_OVERWRITEPROMPTは作成時にファイルが既に存在しているかを確認します。 第8引数は、選択したファイルパスを受け取る変数のアドレスを指定します。 第9引数は、IESaveFileで使用するハンドルを受け取る変数のアドレスを指定します。 何らかの事情でIESaveFileを呼び出す必要がなくなった場合は、IECancelSaveFileを呼び出します。

整合性レベルによって、スレッドは自分よりレベルの高いオブジェクトにアクセスできなくなるわけですが、 そうした原因によって関数が失敗してしまっては、スレッドの動作に支障を来たす可能性があります。 このため、スレッドがCreateFileやRegCreateKeyExを呼び出してアクセスが拒否されそうな場合は、 IE(正確にはacredir.dll)によってアクセス可能な場所へリダイレクトされ、表向きには関数が成功しているように見えることになります。 このリダイレクト先は、次のパス以下になります。

C:\Users\username\Appdata\Local\Microsoft\Windows\Temporary Internet Files\Virtualized

リダイレクトによって関数が成功するといっても、目的のパスにはファイルが作成されていないわけですから、 やはり保護モードで整合性レベルの高いオブジェクトにアクセスすることは避けた方がよいでしょう。 ちなみに、acredir.dllはAcRedirNotifyという関数をエクスポートしており、 これを呼び出せばリダイレクトの瞬間を検出できると思いましたが、 残念ながら上手くいきませんでした。


戻る