System.Windows.Forms.WebBrowser あれこれ備忘録

ドメインの <iframe> の中身を参照する

<iframe>要素にアクセスするには、以下のようにします。複数あれば[1]や[2]になっていきます。(MSDN

HtmlWinfow iframeWindow = webBrowser1.Document.Window.Frames[0];

ただし、<iframe> の先が別のドメインだと、ここでUnauthorizedAccessExceptionが発生します。セキュリティ上の問題(クロスフレームスクリプティング)から禁じられているようです。

これをすり抜けるのが以下コードです。Micsorosft.mshtmlSHDocVwへの参照が必要です。前者は アセンブリ -> 拡張 にあります。後者はCOMからMicrosoft Internet Controlsを探して追加します。
参考:http://stackoverflow.com/questions/10645143/webbrowsercontrol-unauthorizedaccessexception-when-accessing-property-of-a-fram

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using SHDocVw;
using mshtml;

class Test
{
    public static void GetIFrame(WebBrowser browser)
    {
        HtmlWindow iframeWindow = browser.Document.Window.Frames;
        IHTMLWindow2 iframeWindowCom = (IHTMLWindow2)iframeWindow.DomWindow;

        // iframeの中身
        IHTMLDocument2 doc = CrossFrameIE.GetDocumentFromWindow(iframeWindowCom);
    }

    /// <summary>
    /// http://stackoverflow.com/questions/10645143/webbrowsercontrol-unauthorizedaccessexception-when-accessing-property-of-a-fram
    /// </summary>
    static class CrossFrameIE
    {
        // Returns null in case of failure.
        public static IHTMLDocument2 GetDocumentFromWindow(IHTMLWindow2 htmlWindow)
        {
            if (htmlWindow == null)
            {
                return null;
            }

            // First try the usual way to get the document.
            try
            {
                IHTMLDocument2 doc = htmlWindow.document;

                return doc;
            }
            catch (COMException comEx)
            {
                // I think COMException won't be ever fired but just to be sure ...
                if (comEx.ErrorCode != E_ACCESSDENIED)
                {
                    return null;
                }
            }
            catch (UnauthorizedAccessException)
            {
            }
            catch
            {
                // Any other error.
                return null;
            }

            // At this point the error was E_ACCESSDENIED because the frame contains a document from another domain.
            // IE tries to prevent a cross frame scripting security issue.
            try
            {
                // Convert IHTMLWindow2 to IWebBrowser2 using IServiceProvider.
                IServiceProvider sp = (IServiceProvider)htmlWindow;

                // Use IServiceProvider.QueryService to get IWebBrowser2 object.
                Object brws;
                sp.QueryService(ref IID_IWebBrowserApp, ref IID_IWebBrowser2, out brws);

                // Get the document from IWebBrowser2.
                IWebBrowser2 browser = (IWebBrowser2)(brws);

                return (IHTMLDocument2)browser.Document;
            }
            catch
            {
            }

            return null;
        }

        private const int E_ACCESSDENIED = unchecked((int)0x80070005L);
        private static Guid IID_IWebBrowserApp = new Guid("0002DF05-0000-0000-C000-000000000046");
        private static Guid IID_IWebBrowser2 = new Guid("D30C1661-CDAF-11D0-8A3E-00C04FC9E26E");
    }

    // This is the COM IServiceProvider interface, not System.IServiceProvider .Net interface!
    [ComImport, ComVisible(true), Guid("6D5140C1-7436-11CE-8034-00AA006009FA"),
    InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IServiceProvider
    {
        [return: MarshalAs(UnmanagedType.I4)]
        [PreserveSig]
        int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppvObject);
    }
}

IHTMLDocument2 から HtmlDocument に変換

上記コードで取れるのはIHTMLDocument2です。もちろんこのまま扱っても良いですが、ゆとりなのでラッパーに戻したいところです。

以下コードで System.Windows.Forms.HtmlDocument に戻します。戻すメソッドコンストラクタ)はpublicではないため、リフレクションで呼び出します。
参考:http://www.antoine.st/2009-05.html

static HtmlDocument ConvertCom2Wrapper(IHTMLDocument2 doc)
{
    Type thd = typeof(HtmlDocument);
    ConstructorInfo[] ci = thd.GetConstructors(
        BindingFlags.NonPublic | BindingFlags.Instance);
    HtmlDocument hdoc = (HtmlDocument)ci[0].Invoke(new object[] { null, doc });
    return hdoc;
}

WebBrowserのスクリーンショットを取る

参考: http://homepage2.nifty.com/nonnon/SoftSample/CS.NET/SampleWebBitmap.html

static Bitmap Screenshot()
{
    Rectangle scroolRectangle = webBrowser.Document.Body.ScrollRectangle;
    webBrowser.Width = scroolRectangle.Width;
    webBrowser.Height = scroolRectangle.Height;

    // WebBrowserのサイズ(WEBページのサイズ)に合わせてBitmap生成
    Bitmap bmp = new Bitmap(webBrowser.Width, webBrowser.Height);
    using (var g = Graphics.FromImage(bmp))
    {
        IntPtr hdc = g.GetHdc();

        // WebBrowser(WEBページ)のオブジェクト取得
        IntPtr web = Marshal.GetIUnknownForObject(webBrowser.ActiveXInstance);
        // WebBrowser(WEBページ)のイメージをBitmapにコピー
        Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
        Win32Api.OleDraw(web, Win32Api.DVASPECT_CONTENT, hdc, ref rect);

        Marshal.Release(web);
        g.ReleaseHdc(hdc);
    }
    return bmp;
}
static class Win32Api
{
    public const int DVASPECT_CONTENT = 1;

    [DllImport("ole32.dll")]
    public static extern int OleDraw(
        IntPtr pUnk,
        int dwAspect,
        IntPtr hdcDraw,
        ref Rectangle lprcBounds);
}

WebBrowserのスクロール位置を取得

static Point GetScrollPosition(WebBrowser browser)
{
    IHTMLDocument3 doc3 = (IHTMLDocument3)browser.Document.DomDocument;
    IHTMLElement2 elm = (IHTMLElement2)doc3.documentElement;
    return new Point(elm.scrollLeft, elm.scrollTop);
}

ある要素(HtmlElement)をクリックする

Focusをあらかじめ当てないとダメらしいです。だいぶはまりました。

static void Click(HtmlDocument doc)
{
    HtmlElement elem = doc.GetElementById("piyo");
    elem.Focus();
    elem.InvokeMember("click");
}

HtmlElementの絶対座標を取得する

親へ親へと辿りながら相対座標を足していきます。

static Point GetAbsolutePosition(HtmlElement elem)
{
    Point pos = Point.Empty;
    HtmlElement e = elem;
    while (e != null)
    {
        pos.Offset(e.OffsetRectangle.Location);
        e = e.OffsetParent;
    }
    return pos;
}