2017/8/18 追記:
当記事のコードは現在動作しなくなっているため、新しくコードを書き直しました。
これまで当ブログでは、C#でMicrosoft Edgeを操作するコードについていくつか記事を書いてきました。
- Microsoft Edgeを起動するC#コード
- //www.ka-net.org/blog/?p=6161
- Selenium(C#)でEdgeをいろいろ操作してみた。
- //www.ka-net.org/blog/?p=6165
- 起動中のMicrosoft EdgeからタイトルとURLを取得するC#コード(UI Automation編)
- //www.ka-net.org/blog/?p=6145
- 続・起動中のMicrosoft EdgeからタイトルとURLを取得するC#コード(UI Automation編)
- //www.ka-net.org/blog/?p=6148
- WebDriverを使わずMicrosoft Edgeを制御するC#コード
- //www.ka-net.org/blog/?p=6170
UI Automationを使ったりInternet Explorer_ServerクラスのウィンドウからIHTMLDocument2を取得したりしていたわけですが、MSDNフォーラムにあった質問「Get URLs of pages opened in MS-Edge Browser is not working in Windows 10 Home Edition(Upgraded from Windows 8.1) using C#」を発見し、改めてWindows 10 Home環境で上記記事のコードが使えるか確認したところ、意外なところで引っ掛かりました。
“Microsoft.mshtml が参照できない。”
これまで普通に使えていたので何とも思っていなかったのですが、どうやら開発環境かOffice用PIAが入っていないと使えないらしいのです。
「WebDriverを使わずMicrosoft Edgeを制御するC#コード」のコードをそのまま使うことができなくなったわけですが、キャストできないならdynamicで受ければ良いじゃない?、というわけでコードを書き直すことにしました。
using System; using System.Text; using System.Runtime.InteropServices; namespace Sample { class Program { [Flags] private enum SendMessageTimeoutFlags : uint { SMTO_NORMAL = 0x0000, SMTO_BLOCK = 0x0001, SMTO_ABORTIFHUNG = 0x0002, SMTO_NOTIMEOUTIFNOTHUNG = 0x0008, SMTO_ERRORONEXIT = 0x0020 } private delegate bool Win32Callback(IntPtr hWnd, ref IntPtr lParam); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool EnumWindows(Win32Callback lpEnumFunc, ref IntPtr lParam); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool EnumChildWindows(IntPtr hWndParent, Win32Callback lpEnumFunc, IntPtr lParam); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("oleacc.dll", PreserveSig=false)] [return: MarshalAs(UnmanagedType.Interface)] private static extern object ObjectFromLresult(UIntPtr lResult, [MarshalAs(UnmanagedType.LPStruct)] Guid refiid, IntPtr wParam); [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] private static extern uint RegisterWindowMessage(string lpString); [DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] private static extern IntPtr SendMessageTimeout(IntPtr windowHandle, uint Msg, IntPtr wParam, IntPtr lParam, SendMessageTimeoutFlags flags, uint timeout, out UIntPtr result); public static void Main(string[] args) { IntPtr hEdge = IntPtr.Zero; Win32Callback proc = new Win32Callback(EnumWindowsProc); EnumWindows(proc, ref hEdge); if (hEdge != IntPtr.Zero) { Win32Callback childProc = new Win32Callback(EnumChildProc); EnumChildWindows(hEdge, childProc, IntPtr.Zero); } Console.Write("Press any key to continue . . . "); Console.ReadKey(true); } //get a Edge window('Windows.UI.Core.CoreWindow' Class) private static bool EnumWindowsProc(IntPtr hWnd, ref IntPtr lParam) { IntPtr hChild = IntPtr.Zero; StringBuilder buf = new StringBuilder(1024); StringBuilder buf2 = new StringBuilder(1024); GetClassName(hWnd, buf, buf.Capacity); switch (buf.ToString()) { case "ApplicationFrameWindow": //normal window hChild = FindWindowEx(hWnd, IntPtr.Zero, "Windows.UI.Core.CoreWindow", "Microsoft Edge"); if (hChild != IntPtr.Zero) { lParam = hChild; return false; } break; case "Windows.UI.Core.CoreWindow": //minimized window hChild = FindWindowEx(hWnd, IntPtr.Zero, "Spartan XAML-To-Trident Input Routing Window", ""); GetWindowText(hWnd, buf2, buf2.Capacity); if (hChild != IntPtr.Zero && buf2.ToString() == "Microsoft Edge") { lParam = hWnd; return false; } break; } return true; } //get 'Internet Explorer_Server' window private static bool EnumChildProc(IntPtr hWnd, ref IntPtr lParam) { dynamic doc = null; StringBuilder buf = new StringBuilder(1024); GetClassName(hWnd, buf, buf.Capacity); if (buf.ToString() == "Internet Explorer_Server") { doc = GetHTMLDocumentFromWindow(hWnd); if (doc != null) { Console.WriteLine(doc.Title + ", " + doc.url); //get document title & url } } return true; } //get HTMLDocument object private static object GetHTMLDocumentFromWindow(IntPtr hWnd) { UIntPtr lRes; object doc = null; Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637"); uint nMsg = RegisterWindowMessage("WM_HTML_GETOBJECT"); if (nMsg != 0) { SendMessageTimeout(hWnd, nMsg, IntPtr.Zero, IntPtr.Zero, SendMessageTimeoutFlags.SMTO_ABORTIFHUNG, 1000, out lRes); if (lRes != UIntPtr.Zero) { doc = ObjectFromLresult(lRes, IID_IHTMLDocument, IntPtr.Zero); } } return doc; } } }
書き直すついでにUI Automationを使わないように変更もしています。
コードの解説
上記コードは「Microsoft Edgeを操作するVBAマクロ(DOM編)」でも書いている通り、Microsoft Edgeに含まれる「Internet Explorer_Server」クラスのウィンドウからIHTMLDocument2を取得し、DOM操作を行っています。
ここでEdgeのウィンドウがどのような構造になっているのかをSpy++で見てみると、
・非最小化時
(タイトル)- Microsoft Edge : ApplicationFrameWindow
― Microsoft Edge : Windows.UI.Core.CoreWindow
― “” : Spartan XAML-To-Trident Input Routing Window
― “” : Spartan Tab XAML-To-Trident Input Routing Window
― (タイトル) : TabWindowClass
― “” : Internet Explorer_Server
上図のようになっています。
ApplicationFrameWindowクラスのウィンドウの下にWindows.UI.Core.CoreWindowクラスのウィンドウがあって…、といった感じですが、Edgeが最小化されているときは下記のようになります。
・最小化時
(タイトル)- Microsoft Edge : ApplicationFrameWindow
― “” : ApplicationFrameTitleBarWindow
― “” : ApplicationFrameInputSinkWindow
上図の通り、ApplicationFrameWindowクラスのウィンドウの下にWindows.UI.Core.CoreWindowクラスのウィンドウはありません。
では、最小化時にはInternet Explorer_Serverクラスのウィンドウが取得できないかというとそうではなく、下記のようにEdgeのWindows.UI.Core.CoreWindowクラスのウィンドウはApplicationFrameWindowクラスとは別に存在しています。
Microsoft Edge : Windows.UI.Core.CoreWindow
― “” : Spartan XAML-To-Trident Input Routing Window
― “” : Spartan Tab XAML-To-Trident Input Routing Window
― (タイトル) : TabWindowClass
― “” : Internet Explorer_Server
したがって、上記コードではEnumWindowsでウィンドウを調べていき、ApplicationFrameWindowクラスのウィンドウだったらFindWindowExを使って子ウィンドウにWindows.UI.Core.CoreWindowクラスのウィンドウ(ウィンドウ名:Microsoft Edge)があるかどうかを判断(= 非最小化時)し、Windows.UI.Core.CoreWindowクラスのウィンドウだったらウィンドウ名が「Microsoft Edge」であるかどうか、また、子ウィンドウにSpartan XAML-To-Trident Input Routing Windowウィンドウがあるかどうかを判断(= 最小化時)して、Internet Explorer_Serverウィンドウの上位ウィンドウであるWindows.UI.Core.CoreWindowウィンドウを取得するようにしています。
Windows.UI.Core.CoreWindowウィンドウが取得できれば、あとはEnumChildWindowsでInternet Explorer_Serverクラスのウィンドウを取得していくだけです。
なぜEdgeにまだ「Internet Explorer_Server」が残っているのかは謎ですが、WebDriverのインストール不要でEdgeの操作ができるのは便利だと思います。
この記事へのコメントはありません。