表示したWebページ全体をキャプチャしたい,の巻(ソース編)

C#2.0のWebBrowserクラスを使って、表示したWebページ全体をキャプチャしたい場合。
なお、こちらの記事は後編なので、前編となる能書きも併せてどうぞ。

目的達成へは、こんなコトをすれば良さげ。(経験則)
  • キャプチャするWebページ全体のサイズを、HtmlElement.ScrollRectangleプロパティで取得
  • そのサイズとデスクトップと同じ解像度を持つビットマップを生成
  • 生成したビットマップのデバイス・コンテキストを取得
  • HtmlDocument.DomDocumentプロパティからIOleObjectインタフェースを取得(キャスト)
  • WebBrowser.ActiveXInstanceプロパティからインタフェース(IUnknown)へのポインタを取得
    ActiveXInstanceプロパティは「独自に作成したコードから直接使用するためのものではありません」という扱いだけど、気にせず使用
  • IOleObject.GetExtent()メソッドで、ブラウザの現在のエクステント・サイズを保存
  • キャプチャするサイズ(単位はPixel)をHIMETRIC単位に変換
    HIMETRIC単位変換に必要となるGetDeviceCaps()メソッドは、gdi32.dllからインポート
  • 新しいエクステント・サイズをIOleObject.SetExtent()メソッドで設定
  • OleDraw()メソッドを使用してビットマップのデバイス・コンテキストに描画
    OleDraw()メソッドは、ole32.dllからインポート
  • ブラウザのエクステント・サイズを保存していたサイズに再設定
  • メモリ上のビットマップにはWebページ全体がキャプチャ済みなので、煮るなり焼くなり処理
  • 確保していたデバイス・コンテキストやポインタは忘れず解放

もっとスマートな方法がきっとあるだろうけど、今の自分はこのくらいが限界。
意見・改善案などあれば熱烈募集

なお、この方法での既知の問題点は下記のとおり。
  1. フレーム(FRAMESETタグ)を使っているWebページはキャプチャ不可
  2. スクロール・バーを表示(WebBrowser.ScrollBarsEnabledプロパティがtrue)していると、キャプチャ画像にスクロール・バーが必ず付加
    さらに、微妙にキャプチャできない領域が存在

存在するフレーム群は、HtmlWindow.FramesプロパティでHtmlWindowCollectionクラスとして再帰的に取得可能だけど。
フレームの配置(縦横)に応じたWebページ全体のサイズ計算が、いまひとつ良い方法が思い浮かばず。

スクロール・バーについては、非表示にしてしまえば無問題。
ただし動的に表示/非表示の切り替えを行うと、その都度ページが読み込まれてしまう(リロードされる)ので、場合によっては欲しい内容ではなくなってしまう可能性あり。

リロードされるなら、オフライン・モードにしてしまえばローカルのキャッシュが使われてページ内容は変わらずに済むのでは?
しかも通信は行われなくなるから、再表示時間も短くなるのでは?
こんな妄想を抱きつつ試してみたけどダメだった。

そもそもWebBrowser.IsOfflineプロパティは読み取り専用。
SHDocVw.dllからIWebBrowser2インタフェースを操作してオフライン設定にしてみたりしたが、うまくいかず。
これらに対する意見・解決案などあれば熱烈募集

WebCapture.lzh (右クリックして「対象をファイルに保存(A)...」)

そんなこんなで、サンプル・コード。
webBrowser1という名前のWebBrowserコントロールがフォームに含まれていること、が前提条件。
private void capture()
{
 HtmlDocument htmlDocument = this.webBrowser1.Document;
 HtmlElement htmlElement = htmlDocument.Body;
 Rectangle rectangle = new Rectangle(new Point(0, 0), htmlElement.ScrollRectangle.Size);

 using (Bitmap image = new Bitmap(rectangle.Size.Width, rectangle.Size.Height, Graphics.FromHwnd(this.Handle)))
 {
  using (Graphics graphics = Graphics.FromImage(image))
  {
   IOleObject oleObject = (IOleObject)htmlDocument.DomDocument;
   if (oleObject != null)
   {
    IntPtr imageDC = graphics.GetHdc();
    IntPtr pUnk = Marshal.GetIUnknownForObject(this.webBrowser1.ActiveXInstance);
    try
    {
     Size currentSize = new Size();
     oleObject.GetExtent(DVASPECT.DVASPECT_CONTENT, out currentSize);
     Size drawingSize = convertPixelToHIMETRIC(rectangle.Size, imageDC);
     oleObject.SetExtent(DVASPECT.DVASPECT_CONTENT, ref drawingSize);

     OleDraw(pUnk, DVASPECT.DVASPECT_CONTENT, imageDC, ref rectangle);
     oleObject.SetExtent(DVASPECT.DVASPECT_CONTENT, ref currentSize);
    }
    finally
    {
     Marshal.Release(pUnk);
     graphics.ReleaseHdc(imageDC);
    }
    image.Save(@"sample.png", ImageFormat.Png);
   }
   graphics.Dispose();
  }
  image.Dispose();
 }
}

あと、HIMETRIC単位への変換メソッドなどなど。
private Size convertPixelToHIMETRIC(Size size, IntPtr hdc)
{
 const int HIMETRIC_PER_INCH = 2540;

 Size newSize = new Size();

 newSize.Width = (int)((double)size.Width * HIMETRIC_PER_INCH / GetDeviceCaps(hdc, DEVICECAPS.LOGPIXELSX) + 0.5);
 newSize.Height = (int)((double)size.Height * HIMETRIC_PER_INCH / GetDeviceCaps(hdc, DEVICECAPS.LOGPIXELSY) + 0.5);

 return newSize;
}

[DllImport("ole32")]
private static extern int OleDraw(IntPtr pUnk, DVASPECT dwAspect, IntPtr hdcDraw, ref Rectangle lprcBounds);

[DllImport("gdi32")]
private static extern int GetDeviceCaps(IntPtr hdc, DEVICECAPS caps);

その他の諸々は、コチラのサンプル・プロジェクトをどうぞ。
ソリューション・ファイルをVisual Studio 2005で開けばビルド可能で、それなりに遊べます(?)

ブログ気持玉

クリックして気持ちを伝えよう!

ログインしてクリックすれば、自分のブログへのリンクが付きます。

→ログインへ

なるほど(納得、参考になった、ヘー)
驚いた
面白い
ナイス
ガッツ(がんばれ!)
かわいい

気持玉数 : 14

なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー) なるほど(納得、参考になった、ヘー)
面白い 面白い
ナイス ナイス ナイス
かわいい

この記事へのコメント

この記事へのトラックバック