C# - AutomationElementの情報を取得するツールをつくった(Windows10)

# 機能
クリックした座標にあるコントロールの情報（実行パスと`AutomationElementInformation`）を取得します。
（ウィンドウハンドルを取得して、該当座標を含む、子`AutomationElement`を再帰的に探します。重なっている場合はどれか１つになります。）

`AutomationElement`の使い方は、手前味噌ですが
[Windows10でBASIC認証画面にIDとパスワードを自動入力するソフトを作った](https://qiita.com/kob58im/items/f373df61e9ea0be7befa)などを参照ください。

# 注意事項
+ <b>マルチスクリーン環境での動作は未確認です。</b>
+ マウスのグローバルフックを使用しています。

# スクリーンショット

例：getボタンを押して、コマンドプロンプトのタイトルバーをクリックした場合。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/b70d633d-9a9b-918b-c4fa-d96d7a280972.png)

# ソースコード

```csharp

using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Automation;
using System.Windows.Forms;


public static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;
    }

    public const int WH_MOUSE_LL = 14;
    public delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SetWindowsHookEx(int idHook, HookProc callback, IntPtr hInstance, int threadId);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool UnhookWindowsHookEx(IntPtr hHook);
    
    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

    public const int WM_MOUSEMOVE   = 0x0200;
    public const int WM_LBUTTONDOWN = 0x0201;
    public const int WM_LBUTTONUP   = 0x0202;
    public const int WM_RBUTTONDOWN = 0x0204;
    public const int WM_RBUTTONUP   = 0x0205;
    public const int WM_MOUSEWHEEL  = 0x020A;
    public const int WM_MOUSEHWHEEL = 0x020E;

    [DllImport("user32.dll",SetLastError = true)]
    public static extern IntPtr WindowFromPoint(POINT point);

    [DllImport("user32.dll",SetLastError = true)]
    public static extern IntPtr GetAncestor(IntPtr hWnd, int gaFlags);
    public const int GA_PARENT    = 1;
    public const int GA_ROOT      = 2;
    public const int GA_ROOTOWNER = 3;
    
    [DllImport("User32.dll")]
    public static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("User32.dll")]
    public static extern void ReleaseDC(IntPtr hwnd, IntPtr dc);

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

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetProcessDPIAware();
}


public class AutomationTest : Form
{
    IntPtr _hHook;
    NativeMethods.HookProc _handler;
    GCHandle _hookProcHandle;
    NativeMethods.POINT _lastPoint;
    bool _hookExitReq;

    System.Windows.Forms.Timer timer;
    Button btn;
    TextBox txtProc;
    ListView lsv;

    AutomationTest()
    {
        NativeMethods.SetProcessDPIAware();

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 10;
        timer.Tick += (s,e)=>{Timer_Tick();};

        btn = new Button(){Text="get"};
        btn.Click += (s,e)=>{Btn_Click();};
        Controls.Add(btn);

        txtProc = new TextBox(){
            ReadOnly = true,
            Location = new System.Drawing.Point(100,0)
        };
        txtProc.KeyDown += (sender,e)=>{
            if (e.Control && e.KeyCode == Keys.A) { txtProc.SelectAll(); }
        };
        Controls.Add(txtProc);

        lsv = new ListView(){
            Location = new System.Drawing.Point(0, 40),
            FullRowSelect = true,
            GridLines = true,
            HideSelection = false,
            MultiSelect = false,
            View = View.Details
        };
        lsv.MouseDoubleClick += Lsv_MouseDoubleClick;
        lsv.Columns.AddRange(new ColumnHeader[]{
            new ColumnHeader(){Text="ClassName",Width=150},
            new ColumnHeader(){Text="AutomationId",Width=80},
            new ColumnHeader(){Text="ControlType",Width=100},
            new ColumnHeader(){Text="FrameworkId",Width=50},
            new ColumnHeader(){Text="Name",Width=150},
            new ColumnHeader(){Text="ItemType",Width=50},
            new ColumnHeader(){Text="X"     ,Width=50,TextAlign=HorizontalAlignment.Right},
            new ColumnHeader(){Text="Y"     ,Width=50,TextAlign=HorizontalAlignment.Right},
            new ColumnHeader(){Text="Width" ,Width=50,TextAlign=HorizontalAlignment.Right},
            new ColumnHeader(){Text="Height",Width=50,TextAlign=HorizontalAlignment.Right},
        });
        Controls.Add(lsv);

        this.Text = "AutomationElement Information Getter";
        this.ClientSize = new System.Drawing.Size(700, 500);

        FormClosed += (s,e)=>{UnHook();};

        Load += (s,e)=>{MyResize();};
        Resize += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};
    }

    void MyResize()
    {
        int w = ClientSize.Width - txtProc.Left;
        int h = ClientSize.Height - lsv.Top;
        if (w<50){w=50;}
        if (h<50){h=50;}
        lsv.Size = new System.Drawing.Size(ClientSize.Width, h);
        txtProc.Width = w;
    }

    void Timer_Tick()
    {
        UnHook();
        if ( !timer.Enabled ) {
            return;
        }
        timer.Stop();
        btn.Enabled = true;

        var p = new System.Windows.Point(_lastPoint.x, _lastPoint.y);

        IntPtr hWnd = NativeMethods.WindowFromPoint(_lastPoint);
        hWnd = NativeMethods.GetAncestor(hWnd, NativeMethods.GA_ROOT);
        var elem = AutomationElement.FromHandle(hWnd);

        {
            int pid;
            NativeMethods.GetWindowThreadProcessId(hWnd, out pid);
            var proc = System.Diagnostics.Process.GetProcessById(pid);
            txtProc.Text = proc.MainModule.FileName;
        }
        
        if ( elem != null) {
            lsv.BeginUpdate();
            try {
                AutomationElement.AutomationElementInformation elemInfo;
                do {
                    try {
                        elemInfo = elem.Current;
                    }
                    catch( ElementNotAvailableException ) {
                        return;
                    }

                    lsv.Items.Add(AeToListItem(elemInfo));
                    elem = FindNextElementFromPoint(elem, p);
                }
                while(elem != null);

                DrawPointAndRectToScreen(p, elemInfo.BoundingRectangle);
            }
            finally {
                lsv.EndUpdate();
            }
        }
    }

    AutomationElement FindNextElementFromPoint(AutomationElement elem, System.Windows.Point p)
    {
        var childElements = elem.FindAll(TreeScope.Children, Condition.TrueCondition);

        foreach(AutomationElement childElem in childElements) {
            AutomationElement.AutomationElementInformation elemInfo;
            try {
                elemInfo = childElem.Current;
            }
            catch( ElementNotAvailableException ) {
                return null;
            }

            if ( elemInfo.BoundingRectangle.Contains(p) ) {
                return childElem;
            }
        }
        return null;
    }

    ListViewItem AeToListItem(AutomationElement.AutomationElementInformation a)
    {
        System.Windows.Rect r = a.BoundingRectangle;

        return new ListViewItem(new string[]{
            a.ClassName,
            a.AutomationId,
            a.ControlType.ToString(),
            a.FrameworkId,
            a.Name,
            a.ItemType,
            r.X.ToString(),
            r.Y.ToString(),
            r.Width.ToString(),
            r.Height.ToString()
        });
    }

    void Btn_Click()
    {
        try {
            _hookExitReq = false;
            SetHook();
        }
        catch (System.ComponentModel.Win32Exception e) {
            MessageBox.Show(e.ToString());
            return;
        }

        btn.Enabled = false;
        lsv.Items.Clear();
    }

    void Lsv_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        ListViewHitTestInfo info = lsv.HitTest(e.Location);
        if ( info.SubItem != null ) {
            SubForm f = new SubForm(info.SubItem.Text);
            f.ShowDialog();
        }
    }

    void DrawPointAndRectToScreen(System.Windows.Point p, System.Windows.Rect rect)
    {
        IntPtr desktopDC = NativeMethods.GetDC(IntPtr.Zero);
        
        if (desktopDC == IntPtr.Zero) {
            // failed
            return;
        }

        try {
            var pen = new System.Drawing.Pen(System.Drawing.Color.Blue, 6.0f);
            using (var g = System.Drawing.Graphics.FromHdc(desktopDC)) {
                g.DrawLine(pen, (float)(p.X-5), (float)(p.Y-5), (float)(p.X+5), (float)(p.Y+5));
                g.DrawLine(pen, (float)(p.X-5), (float)(p.Y+5), (float)(p.X+5), (float)(p.Y-5));
                g.DrawRectangle(pen, (float)rect.X, (float)rect.Y, (float)rect.Width, (float)rect.Height);
            }
        }
        finally {
            NativeMethods.ReleaseDC(IntPtr.Zero, desktopDC);
        }
    }

    void SetHook()
    {
        IntPtr module = IntPtr.Zero;
        _handler = CallbackProc;
        _hookProcHandle = GCHandle.Alloc(_handler);
        _hHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, _handler, module, 0);

        if ( _hHook == IntPtr.Zero ) {
            // failed
            int errorCode = Marshal.GetLastWin32Error();
            _hookProcHandle.Free();
            _handler = null;
            throw new System.ComponentModel.Win32Exception(errorCode);
        }
    }

    void UnHook()
    {
        if ( _hHook != IntPtr.Zero ) {
            NativeMethods.UnhookWindowsHookEx(_hHook);
            _hHook = IntPtr.Zero;
            _hookProcHandle.Free();
            _handler = null;
        }
    }

    IntPtr CallbackProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if ( nCode < 0 || _hookExitReq ) {
            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
        else {
            if ( (long)wParam == NativeMethods.WM_MOUSEMOVE ) {
                return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
                // 注意：WM_MOUSEMOVE をキャンセルすると、カーソル表示が更新されない
            }
            else {
                if ( (long)wParam == NativeMethods.WM_LBUTTONDOWN ||
                     (long)wParam == NativeMethods.WM_RBUTTONDOWN ) {
                    _hookExitReq = true;
                    var p = Cursor.Position;
                    _lastPoint = new NativeMethods.POINT(){x=p.X, y=p.Y};
                    timer.Start();
                }
                // cancel
                return new IntPtr(1);
            }
        }
    }

    [STAThread]
    static void Main(string[] args)
    {
        Application.Run(new AutomationTest());
    }
}

internal class SubForm : Form
{
    internal SubForm(string text)
    {
        var txt = new TextBox(){
            Text = text,
            Multiline = true,
            ScrollBars = ScrollBars.Both,
            Dock = DockStyle.Fill
        };
        txt.KeyDown += (sender,e)=>{
            if (e.Control && e.KeyCode == Keys.A) { txt.SelectAll(); }
        };
        Controls.Add(txt);
    }
}
```


# コンパイルバッチ

```text:compile.bat

csc ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll ^
 %*
```