Accessing IE’s DOM structure from C#

IE のレンダリング エンジンと言えば Trident と呼ばれる mshtml.dll です。なんと単一の DLL でサイズが 20MB もあります。この Trident、COM 経由でデータにアクセスするための API を実装しているらしい、ということで試してみました。検索すると、わりといろいろな人が試していてメジャーな方法らしい。ウィキにも書いてありました。

Trident (layout engine) – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Trident_(layout_engine)

基本となる元ネタはこの KB。今回は Win32 ではなく C# で書き直すことにします。

How to get IHTMLDocument2 from a HWND
http://support.microsoft.com/kb/249232/en

上記 KB のプログラムをざっと見ると、ウィンドウ ハンドルに対して WM_HTML_GETOBJECT というウィンドウ メッセージを送ると、COM インターフェースである IHTMLDocument2 が返ってくるらしい。ウィンドウ ハンドルは普通の方法で列挙して、クラス名が "Internet Explorer_Server" であるものを探せばいいようだ。簡単でいい感じ。

IHTMLDocument2 インターフェースのリファレンスは MSDN も載っています。最新版だと IHTMLDocument8 まであるらしい。

Scripting Object Interfaces (MSHTML) (Windows)
http://msdn.microsoft.com/en-us/library/hh801967(v=vs.85).aspx

今回の開発環境はこれで。プログラムは Windows 8 や 8.1 でも動くはずです。さすがに試していませんが、.NET Framework 4.0 を入れれば XP や Vista でも行けるはず・・。

  • OS: Windows 7 SP1 x64
  • IE: Internet Explorer 11 + KB3003057 (Nov. 2014 Update)
  • IDE: Visual Studio 2012
  • CLI: .NET Framework 4.0

適当な C# プロジェクトを作って、リファレンスを追加します。が、ここで一つ罠が。Visual Studio の Reference Manager を見ると、Microsoft HTML Object Library という Type Library が既定で存在しています。これは既に mshtml のライブラリが GAC に登録されているからであり、おそらく .NET か Windows のインストール時に追加されたものと考えられます。

image

手元の環境だと、"C:\Windows\assembly\GAC\Microsoft.mshtml\7.0.3300.0__b03f5f7f11d50a3a" に存在する Microsoft.mshtml.dll がそのライブラリでした。

image

このライブラリを使っても問題はないのですが、IE のアップデート時や累積パッチの適用時に GAC は更新されないらしく、古いのです。開発環境の Windows 7 において Object Explorer から確認すると分かりますが、例えば IHTMLDocument5 インターフェースまでしか存在せず、 IHTMLDocument6 以降がありません。

image

便利なことに、IE 更新時には適用される mshtml.dll  に対応する Type Library ファイル mshtml.tlb も一緒に提供されるので、このファイルからアセンブリ DLL を作れば最新のライブラリを使うことができます。TLB から DLL を作る方法は、以前に紹介した tlbimp というツールが使えます。"Developer Command Prompt for VS2012" を開いてコマンドを実行するだけです。

出力例はこんな感じ。ファイル名は mshtml.ie11.dll にしていますが、何でもよいです。system32 にある mshtml.tlb から DLL を作りましたが、生成された DLL  は 32bit からでも 64bit からでも使うことができます。

E:\VSDev\Projects\iehack> tlbimp /reference:C:\windows\system32\mshtml.tlb /out:mshtml.ie11.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.17929
Copyright (C) Microsoft Corporation.  All rights reserved.

TlbImp : error TI2005 : No input file has been specified.

E:\VSDev\Projects\iehack>tlbimp C:\windows\system32\mshtml.tlb /out:mshtml.ie11.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.17929
Copyright (C) Microsoft Corporation.  All rights reserved.

TlbImp : warning TI3001 : Primary interop assembly ‘Microsoft.mshtml, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ is already registered for type library ‘C:\windows\system32\mshtml.tlb’.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.IActiveIMMApp.GetDefaultIMEWnd’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : warning TI3016 : The type library importer could not convert the signature for the member ‘mshtml.ie11._userBITMAP.pBuffer’.
TlbImp : warning TI3016 : The type library importer could not convert the signature for the member ‘mshtml.ie11._FLAGGED_BYTE_BLOB.abData’.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.IEventTarget2.GetRegisteredEventTypes’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.IEventTarget2.GetListenersForType’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : warning TI3016 : The type library importer could not convert the signature for the member ‘mshtml.ie11.tagSAFEARRAY.rgsabound’.
TlbImp : warning TI3015 : At least one of the arguments for ‘mshtml.ie11.ICanvasPixelArrayData.GetBufferPointer’ cannot be marshaled by the runtime marshaler.  Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate.
TlbImp : Type library imported to E:\VSDev\Projects\iehack\mshtml.ie11.dll

 

警告がたくさん出ましたが、よく分からないので (汗) 無視します。

生成された mshtml.ie11.dll をプロジェクトに追加して Object Explorer で見てみると、今度は IHTMLDocument8 がありました。それにしてもクラスやらインターフェースが多いです。アセンブリだけで 15MB 近いサイズになります。

image

あとは素直に C# を書くだけです。

//
// Program.cs
//
// http://support.microsoft.com/kb/249232/en
//

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace iehack {
    class Program {
        public delegate int WNDENUMPROC(IntPtr hwnd, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

        [DllImport("user32.dll", SetLastError = true)]
        static extern int EnumChildWindows(IntPtr hwndParent, WNDENUMPROC lpEnumFunc, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true)]
        static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        static extern uint RegisterWindowMessage(string lpString);

        [Flags]
        public enum SendMessageTimeoutFlags : uint {
            SMTO_NORMAL = 0x0,
            SMTO_BLOCK = 0x1,
            SMTO_ABORTIFHUNG = 0x2,
            SMTO_NOTIMEOUTIFNOTHUNG = 0x8,
            SMTO_ERRORONEXIT = 0x20
        }

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageTimeout(
            IntPtr hWnd,
            uint Msg,
            IntPtr wParam,
            IntPtr lParam,
            SendMessageTimeoutFlags fuFlags,
            uint uTimeout,
            out IntPtr lpdwResult);

        [DllImport("oleacc.dll", PreserveSig = false)]
        [return: MarshalAs(UnmanagedType.Interface)]
        static extern object ObjectFromLresult(IntPtr lResult,
             [MarshalAs(UnmanagedType.LPStruct)] Guid refiid, IntPtr wParam);

        private static int EnumWindowsProc(IntPtr hwnd, IntPtr lParam) {
            // http://msdn.microsoft.com/en-us/library/windows/desktop/ms633576(v=vs.85).aspx
            // The maximum length for lpszClassName is 256.

            StringBuilder ClassName = new StringBuilder(256);
            int Ret = GetClassName(hwnd, ClassName, ClassName.Capacity);
            if (Ret != 0) {
                if (string.Compare(ClassName.ToString(), "Internet Explorer_Server", true, CultureInfo.InvariantCulture) == 0) {
                    var TargetList = GCHandle.FromIntPtr(lParam).Target as List<IntPtr>;
                    if (TargetList != null) {
                        TargetList.Add(hwnd);
                    }
                }
            }

            return 1;
        }

        private static int EnumTopWindowsProc(IntPtr hwnd, IntPtr lParam) {
            EnumChildWindows(hwnd, EnumWindowsProc, lParam);
            return 1;
        }

        static uint WM_HTML_GETOBJECT = 0;

        public static object GetDom(IntPtr Window, Guid InterfaceType) {
            const int Timeout = 1000;

            if (WM_HTML_GETOBJECT == 0) {
                WM_HTML_GETOBJECT = RegisterWindowMessage("WM_HTML_GETOBJECT");
            }

            IntPtr Result = IntPtr.Zero;
            SendMessageTimeout(Window, WM_HTML_GETOBJECT,
                IntPtr.Zero, IntPtr.Zero,
                SendMessageTimeoutFlags.SMTO_ABORTIFHUNG,
                Timeout,
                out Result);

            return ObjectFromLresult(Result, InterfaceType, IntPtr.Zero);
        }

        List<IntPtr> WindowHandles;

        public void Run() {
            WindowHandles = new List<IntPtr>();
            var ListHandle = GCHandle.Alloc(WindowHandles);
            EnumChildWindows(IntPtr.Zero, EnumTopWindowsProc, GCHandle.ToIntPtr(ListHandle));

            int i = 0;
            foreach (var ie in WindowHandles) {
                uint pid, tid;
                tid = GetWindowThreadProcessId(ie, out pid);

                Console.WriteLine("[{0}] hWnd = 0x{1:x}, pid = 0x{2:x4}, tid = 0x{3:x4}", i++, ie.ToInt64(), pid, tid);

                var dom2 = GetDom(ie, typeof(mshtml.ie11.IHTMLDocument2).GUID) as mshtml.ie11.IHTMLDocument2;
                if (dom2 != null) {
                    Console.WriteLine("url = " + dom2.url);

                    var dom6 = GetDom(ie, typeof(mshtml.ie11.IHTMLDocument6).GUID) as mshtml.ie11.IHTMLDocument6;
                    if (dom6 != null) {
                        Console.WriteLine("docmode = " + dom6.documentMode);
                    }
                }
            }
        }

        static void Main(string[] args) {
            var p = new Program();
            p.Run();
        }
    }
}

やろうと思えばもっと工夫できそうな気もしますが、データが取得できることが確認できればいいので最低限で。このサンプルを実行すると、ウィンドウ クラス名が "Internet Explorer_Server" であるウィンドウ全てに対して、そのコントロールが開いている URL と DocMode を表示します。

DocMode とは、IE8 から導入された機能です。主に HTML レイアウトの後方互換性を維持するために、古いバージョンの IE のレンダリング モードをある程度まで再現することができるようになっています。例えば IE8 向けに作られたサイトで、IE11 で開くとレイアウトが崩れてしまうような場合には、HTML 内の meta 要素で DocMode を明示的に 8 に指定して回避できることがあります。

Specifying legacy document modes (Internet Explorer)
http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx

IE8/IE9の「ブラウザーモード」と「ドキュメントモード」のまとめ: 小粋空間
http://www.koikikukan.com/archives/2011/02/07-005555.php

手元の環境で実行した結果がこんな感じです。IE の各タブだけでなく、以下の例では Windows Live Writer で使われている Web コントロールの情報も表示されています。

E:\VSDev\Projects\iehack\bin\x64\Release>iehack.exe
[0] hWnd = 0x40914, pid = 0x1d94, tid = 0x0d6c
url =
https://www.google.com/?gws_rd=ssl
docmode = 11
[1] hWnd = 0x1505aa, pid = 0x1c58, tid = 0x2010
url =
https://www.facebook.com/
docmode = 10
[2] hWnd = 0x804e6, pid = 0x1c58, tid = 0x1094
url = about:blank
docmode = 11
[3] hWnd = 0x204d2, pid = 0x118c, tid = 0x0948
url = file://C:\Users\John\AppData\Local\Temp\WindowsLiveWriter1286139640\B8DD92B2061F\index.htm
docmode = 7

このサンプルでは試していませんが、KB249232 の例で背景色を変更しているように、値を取得するだけではなく、変更することもできます。しかし DocumentMode プロパティを始めとして、読み取り専用のプロパティもあります。

[SAP] [C#] SAP GUI Scripting from .NET

SAP GUI Scripting という機能があります。eCATT (まともに使ったことはない) のように、SAP GUI を外部から操作できる機能です。テスト シナリオの実行やテスト データ入力などを大量処理する場合に使えそうです。HP Quality Center やら HP Load Runner はこの機能を利用しているんでしょうかね。

これはあくまでも SAP GUI を操作する機能ですので、アプリケーション サーバーを直接操作する BAPI や Enterprise Services とは全く別の機能です。

この SAP GUI Scripting、実際どのぐらい使われているかどうかは不明です。古くからある機能なので、公開されている情報も古いものが多い気がします。VBScript やネイティブ アプリ、.NET からも操作できるような記述がありますが、SAP Note に添付されているサンプルが VB6 で書かれたとおぼしきものだったり。いいサンプルなんですけど。

そんなわけで、.NET で SAP GUI Scripting を操作してみました。

まず、SAP GUI Scripting の実体は単なる ActiveX コントロールです。VBA から SAP にアクセスするときに、SAP GUI の ActiveX コントロールを使っている人はそこそこいるような、いないような。ファイルは sapfewse.ocx です。SAP GUI をインストールすると以下のパスに作成されます。

C:\Program Files (x86)\SAP\FrontEnd\SAPgui\sapfewse.ocx

image

それさえ分かれば、あとは普通に使えます。手順は以下の通り。

  1. TypeLib を使って OCX ファイルから IDL ファイルを作成
  2. IDL をコンパイルして TLB ファイルを作成
  3. TLBIMP を使って TLB ファイルから .NET アセンブリを作成
  4. .NET アプリを書く

環境はこんな感じ。SDK は新しいの入れておかないと駄目だなー。

OS: Windows 7 SP1
IDE: Visual Studio 2010 SP1
SDK: Windows SDK  v7.0A (まずい 7.1 入れてない・・・)
SAP GUI: 720 PL3

1. TypeLib を使って OCX ファイルから IDL ファイルを作成

Windows SDK に付属している OLE-COM Object Viewer を管理者特権で開いて下さい。Visual Studio をインストールすると勝手にインストールされると思います。

image

メニューから File > View TypeLib を選んで下さい。

image

[ファイルを開く] ダイアログが表示されるので、前述の sapfewse.ocx を開いて下さい。

image

メニューから File > Save As… を選択し、適当なところに IDL ファイルを保存して下さい。ファイルの末尾が切れる場合は、右ペインのテキストをコピペしてテキスト ファイルとして保存して下さい。

image

ファイルはこんな感じです。以前の記事で RPC サーバー / クライアントを作ったときにも IDL ファイルは出てきました。あのときは手で書きましたが、このように既存のオブジェクトから自動生成することもできます。

image

 

2. IDL をコンパイルして TLB ファイルを作成

ネイティブだったら IDL ファイルを使ってそのまま RPC クライアントを作ればいいのですが、.NET の場合はもう数ステップ必要です。

次に IDL ファイルをコンパイルして TLB ファイルを作る必要があります。Windows SDK に含まれている midl.exe を使います。これも RPC のときに使いました。Visual Studio Command Prompt を管理者特権で開き、以下のコマンドを実行します。

> midl sapfewse.IDL

image

が、エラーとなります。おーん。

.\sapfewse.IDL(601) : error MIDL2025 : syntax error : expecting a type specification near "GuiComponent"

.\sapfewse.IDL(601) : error MIDL2026 : cannot recover from earlier syntax errors; aborting compilation

syntax error とか・・・自動生成したものをそのまま使ってるんですがね。
エラーとなっている 601 行付近はこんな感じです。

    [
      uuid(93A37525-9118-4731-92A7-B93DF1E34455)
    ]
    dispinterface _Dsapfewse {
        properties:
            [id(0x00007d01), readonly           
]
            BSTR Name;
            [id(0x00007d0f), readonly           
]
            BSTR Type;
            [id(0x00007d20), readonly           
]
            long TypeAsNumber;
            [id(0x00007d21), readonly           
]
            VARIANT_BOOL ContainerType;
            [id(0x00007d19), readonly           
]
            BSTR Id;
            [id(0x00007d26), readonly           
]
            GuiComponent* Parent;
            [id(0x00007d13), readonly      ← 601 行目     
]

インターフェース _Dsapfewse のメンバーを定義しているところです。GuiComponent というキーワードが認識されていないようなので、定義を探してみます。すると、701 行目に定義が見つかります。原因はこれですね。物事には順番ってものがあります。

[
  uuid(ABCC907C-3AB1-45D9-BF20-D3F647377B06),
  noncreatable
]
coclass GuiComponent {
    [default] dispinterface ISapComponentTarget;
};

GuiComponent の定義を _Dsapfewse の定義の上に移動させます。こんな感じ。

[
  uuid(ABCC907C-3AB1-45D9-BF20-D3F647377B06),
  noncreatable
]
coclass GuiComponent {
    [default] dispinterface ISapComponentTarget;
};

[
  uuid(93A37525-9118-4731-92A7-B93DF1E34455)
]
dispinterface _Dsapfewse {
    properties:
        [id(0x00007d01), readonly           

さて、再チャレンジ。またエラーでございます。

image

.\sapfewse.IDL(612) : error MIDL2025 : syntax error : expecting a type specification near "GuiComponentCollection"
.\sapfewse.IDL(612) : error MIDL2026 : cannot recover from earlier syntax errors; aborting compilation

今度は GuiComponentCollection か。というわけで、今度は GuiComponentCollection の定義を _Dsapfewse の上に移動します。で、また違う箇所でエラ・・・。

試行錯誤の上、以下のような修正をしてコンパイル エラーがクリアされました。やれやれ手間のかかる・・・。でもまあ、定義の順番を入れ替えるだけでよかったから幸運なのかも。

  • GuiComponent の定義を _Dsapfewse の前に移動
  • GuiComponentCollection の定義を _Dsapfewse の前に移動
  • GuiUtils の定義を _Dsapfewse の前に移動
  • GuiCollection の定義を _Dsapfewse の前に移動
  • GuiConnection の定義を _Dsapfewse の前に移動
  • GuiSession の定義を _DsapfewseEvents の前に移動
  • GuiFrameWindow の定義を ISapSessionTarget の前に移動
  • GuiSessionInfo の定義を ISapSessionTarget の前に移動
  • GuiVComponent の定義を ISapWindowTarget の前に移動
  • GuiScrollbar の定義を ISapScreenTarget の前に移動
  • GuiContextMenu の定義を ISapScreenTarget の前に移動
  • GuiComboBoxEntry の定義を ISapComboBoxTarget の前に移動
  • GuiTab の定義を ISapTabbedPane の前に移動

image

コンパイルが通ると、sapfewse.tlb というファイルが生成されます。これはバイナリ ファイルです。

image

 

3. TLBIMP を使って TLB ファイルから .NET アセンブリを作成

TLB ファイルができたら、同じく Windows SDK に含まれる tlbimp を使うと .NET アセンブリを生成してくれます。

以下のコマンドを実行します。ここはノー エラーで通った。

e:\dropbox\sapfewse> tlbimp sapfewse.tlb  /out:sapfewse.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.1
Copyright (C) Microsoft Corporation.  All rights reserved.

TlbImp : Type library imported to e:\dropbox\sapfewse\sapfewse.dll

e:\dropbox\sapfewse>

無事、アセンブリである sapfewse.dll をゲットすることができました。

image

 

4. .NET アプリを書く

ここまでが事前準備です。では Visual Studio を起動して .NET アプリを書きます。例によって言語は C# を使いますが、VB でも F# でも動くはずです。

普通に C# の Window Forms Application プロジェクトを作成して下さい。
ソリューションのフォルダーに、.NET アセンブリを作ったときの作業用フォルダーを丸ごとコピーしておくとよいです。

image

Solution Explorer の References を右クリックし、Add Referene… を選択して下さい。
Add Reference ダイアログボックスの Browse タブから、先の手順で作った sapfewse.dll を選んで OK をクリックして下さい。をや、TLB ファイルはそのまま使えるのか。これは知らなかった。

image

名前空間 sapfewse が追加されました。Object Explorer で開いてみると、GuiApplication などのクラスが確認できます。

image

ボタンとテキスト ボックスを配置します。

image

ボタンをクリックしたときのイベント ハンドラーに、以下の 2 行を追加します。ええ、2 行だけです。
GuiApplication のインスタンスを作って、OpenConnection を呼ぶだけです。

sapfewse.GuiApplication SapGuiApp = new sapfewse.GuiApplication();
SapGuiApp.OpenConnection(textBox1.Text);

ソースはこんな感じになります。上の 2 行以外は何もいじっていません。

image

SAP GUI がインストールされている PC 上でプログラムを起動します。
テキスト ボックスに SAP GUI の接続エントリの名称を入力し、Logon をクリックすると接続できます。

この環境では、下記のように "NetWeaver 7.02" という名前のエントリを作ってあるので、"NetWeaver 7.02" と入力します。

image

image

Logon をクリックすると、"A script is opening a connection to system NetWeaver 7.02" という確認のポップアップが表示されます。もちろん OK をクリックします。

image

やけにクラシックな SAP GUI の画面が起動してきました。スキンの設定が変えられるかどうかは試していません。

image

何はともあれ、ログオンはできます。ツールバーとか変ですけど。

image

以上が C# から SAP GUI Scripting を操作するサンプルでした。SAP GUI を起動するサンプルでしたが、既存の SAP GUI セッションにアタッチして値の入力やトランザクションの実行を操作することもできます。むしろそういう使われ方をするほうが多いかもしれません。

オブジェクトのリファレンスは SDN で公開されています。以下のページから SAP GUI Scripting API をダウンロードして下さい。

SAP GUI Scripting
http://www.sdn.sap.com/irj/sdn/index?rid=/webcontent/uuid/007084d7-41f4-2a10-9695-d6bce1673c2f

例えば、サンプルで使った GuiApplication::OpenConnection はこんな感じです。

image