Windows 10

起動中のMicrosoft EdgeからタイトルとURLを取得するC#コード(DOM編)

2017/8/18 追記:
当記事のコードは現在動作しなくなっているため、新しくコードを書き直しました。


これまで当ブログでは、C#でMicrosoft Edgeを操作するコードについていくつか記事を書いてきました。

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

AutomateMicrosoftEdgeCS_DOM_01

上図のようになっています。
ApplicationFrameWindowクラスのウィンドウの下にWindows.UI.Core.CoreWindowクラスのウィンドウがあって…、といった感じですが、Edgeが最小化されているときは下記のようになります。

・最小化時

(タイトル)- Microsoft Edge : ApplicationFrameWindow
 ― “” : ApplicationFrameTitleBarWindow
 ― “” : ApplicationFrameInputSinkWindow

AutomateMicrosoftEdgeCS_DOM_02

上図の通り、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

AutomateMicrosoftEdgeCS_DOM_03

したがって、上記コードでは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の操作ができるのは便利だと思います。

関連記事

  1. Windows 10

    「ファイル名を指定して実行」からMicrosoft Edgeを起動する

    以前書いた記事で、Microsoft EdgeをVBScriptから起…

  2. Windows 10

    Microsoft Edgeを操作するVBScript

    「Microsoft Edgeを操作するVBAマクロ(WebDrive…

  3. Windows関連

    右クリックメニューからフォルダを管理者権限で開く(コマンド プロンプト)

    フォルダをShiftキーを押しながら右クリックすると、「コマンド ウィ…

  4. Windows 10

    [Windows 10]OneDriveを無効にする。

    前回の記事で、Windows 10のナビゲーションウィンドウにあるOn…

コメント

  • コメント (0)

  • トラックバックは利用できません。

  1. この記事へのコメントはありません。

Time limit is exhausted. Please reload CAPTCHA.

最近の記事

アーカイブ

RapidSSL_SEAL-90x50
PAGE TOP