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の操作ができるのは便利だと思います。

新しくなったMZ-Tools前のページ

「Excel VBAの神様 ボクの人生を変えてくれた人」(大村あつし著)レビュー次のページ

関連記事

  1. Windows 10

    Microsoft Edgeを操作するVBAマクロ(WebDriver編)

    Microsoft Edge Dev Blogに「Bringing a…

  2. Windows 10

    Microsoft Edgeの拡張機能でHello Worldしてみた。

    「【Windows 10ユーザーズ・ワークベンチ】Edgeの拡張機能サ…

  3. Office関連

    続・Microsoft Edgeを操作するVBAマクロ(DOM編)

    以前VBAからMicrosoft Edgeを操作するマクロについて記事…

  4. Windows 10

    [Windows 10]Microsoft Edgeでブックマークレットを使う。

    2017/3/29 追記:新しく記事を書き直しました。…

コメント

  • コメント (0)

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

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

Time limit is exhausted. Please reload CAPTCHA.

※本ページはプロモーションが含まれています。

Translate

最近の記事

アーカイブ

PAGE TOP