RPA時代に向けてUIAutomationを実装する

# 0. 概要
UI AutomationというWindowsのlibraryを使うことで、Windowsフォームを自由に操作することが出来る。これを説明していきたい。


# 1. 環境
まずはUI Automation Spyというソフトウェアをインストールする。
もうバイナリの配布はなくソースコードのみの提供なので、
以下で再配布しているバイナリをダウンロードする。
https://www.code-lab.net/?page_id=21648

その後、UI Automation Spyを起動してStartを押す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95636/a1f54122-badb-51d0-23d0-8fced5aebb09.png)

これにより各フォームのID番号等を得られる。

# 2. プログラムからID番号を得る

一般的に以下のようなコードからフォームの要素を得ることが出来る。
しかし、UI Automationは全ての子要素を取得しない。これはTree Scopeに何の値を設定してもである。

``` C#
        // 指定したID属性に一致するAutomationElementを返します
        private static AutomationElement FindElementById(AutomationElement rootElement, string automationId)
        {
            return rootElement.FindFirst(
                TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.AutomationIdProperty, automationId));
        }

        // 指定したName属性に一致するAutomationElementをすべて返します
        private static IEnumerable<AutomationElement> FindElementsByName(AutomationElement rootElement, string name)
        {
            return rootElement.FindAll(
                TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.NameProperty, name))
                .Cast<AutomationElement>();
        }

        // 指定したName属性に一致するボタン要素をすべて返します
        private static IEnumerable<AutomationElement> FindButtonsByName(AutomationElement rootElement, string name)
        {
            const string BUTTON_CLASS_NAME = "Button";
            return from x in FindElementsByName(rootElement, name)
                   where x.Current.ClassName == BUTTON_CLASS_NAME
                   select x;
        }

        // 指定したControlType属性に一致する要素をすべて返します
        private static AutomationElementCollection FindElementsByControlType(AutomationElement rootElement, ControlType controlType)
        {
            return rootElement.FindAll(
                TreeScope.Element | TreeScope.Descendants,
                new PropertyCondition(AutomationElement.ControlTypeProperty, controlType));
        }
```

このため、以下のコードからProcessの要素を得る。

``` C#
        private static IEnumerable<AutomationElement> FindInRawView(AutomationElement root)
        {
            TreeWalker rawViewWalker = TreeWalker.RawViewWalker;
            Queue<AutomationElement> queue = new Queue<AutomationElement>();
            queue.Enqueue(root);
            while (queue.Count > 0)
            {
                var element = queue.Dequeue();
                yield return element;

                var sibling = rawViewWalker.GetNextSibling(element);
                if (sibling != null)
                {
                    queue.Enqueue(sibling);
                }

                var child = rawViewWalker.GetFirstChild(element);
                if (child != null)
                {
                    queue.Enqueue(child);
                }
            }
        }
```

サンプルコードとして、以下で電卓の計算結果である表示領域から値を持ってくることができる。

``` C#
                foreach (var x in FindInRawView(mainForm))
                {
                    if (x.Current.AutomationId.Contains("NormalOutput"))
                    {
                        Console.WriteLine("Detected");
                        Console.WriteLine(x.Current.Name);
                    }
                }
```

基本的に全探査してくれるのでIF文を外せば全てのフォームを得られるはずである。


# 2. プロセスにアタッチする

よくある手法としては、ProcessStartさせて帰ってきたID番号を取得する方法であるが、複雑なプログラムだと親から子が呼ばれ…等を繰り返すのでプロセスIDが変わることがしばしばある。そこで、起動中のものにアタッチする。

``` C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;
using System.Runtime.InteropServices;

namespace RPA
{
    class Program
    {

        [DllImport("user32.dll")]
        public static extern IntPtr FindWindowEx(IntPtr parentWindow, IntPtr previousChildWindow, string windowClass, string windowTitle);

        [DllImport("user32.dll")]
        private static extern IntPtr GetWindowThreadProcessId(IntPtr window, out int process);
        private static IntPtr[] GetProcessWindows(int process)
        {
            IntPtr[] apRet = (new IntPtr[256]);
            int iCount = 0;
            IntPtr pLast = IntPtr.Zero;
            do
            {
                pLast = FindWindowEx(IntPtr.Zero, pLast, null, null);
                int iProcess_;
                GetWindowThreadProcessId(pLast, out iProcess_);
                if (iProcess_ == process) apRet[iCount++] = pLast;
            } while (pLast != IntPtr.Zero);
            System.Array.Resize(ref apRet, iCount);
            return apRet;
        }

        private static AutomationElement mainForm;

        static void Main(string[] args)
        {
            Process process = null;
            try
            {
                foreach (Process p in Process.GetProcesses())
                {
                    if (p.MainWindowTitle.Contains("電卓"))
                    {
                        Console.WriteLine("Detected");
                        Console.WriteLine(p.MainWindowTitle);
                        Console.WriteLine(p.MainWindowHandle);
                        process = p;
                    }
                }

                mainForm = AutomationElement.FromHandle(process.MainWindowHandle);
                
                var btnClear = FindElementsByName(mainForm, "1").First()
                    .GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
                btnClear.Invoke();
         }
      }
    }
```