WPF UI オートメーション-メニュー操作1

Garmin Connect IQ-コマンドラインでのビルド・シミュレーター起動でお勉強する環境が整ったので、いろいろと調べながらサンプルプログラムを作っている。

まずは、メニュー操作を習得するため、シミュレーターのメニュー一覧を出すプログラムを作成した。

動きとしては、以下のよう感じ。

  1. メイン画面にあるメニューバーを取り出す
  2. メニューバーに登録されているメニュー項目を取り出す
  3. メニュー項目が展開可能だったら展開し、展開されたメニューの中のメニュー項目を取り出す
  4. 3を全メニュー項目に対して再帰的に実行する。

メニューかどうかについては、AutomationElementのControlTypeがMenuBar、MenuItem、Menuかで判断している。
展開可能かどうかは、(bool)obj.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)で判断している。

見つけた項目は、メニュー名等をツリー状で管理し、それを最後出力している。

シミュレーターは、UpdateTargetProcessで起動済みのものをタイトル名で検索している。

以下、ソースコード。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Windows.Automation;

namespace UiAutomation1
{
    /// <summary>
    /// メニューツリーを構築するためのツリー管理クラス
    /// </summary>
    class TreeNode<T>
    {
        public List<TreeNode<T>> Children = new List<TreeNode<T>>();

        public T Item { get; set; }

        public TreeNode(T item)
        {
            Item = item;
        }

        public TreeNode<T> AddChild(T item)
        {
            TreeNode<T> nodeItem = new TreeNode<T>(item);
            Children.Add(nodeItem);
            return nodeItem;
        }

        /// <summary>
        /// ツリーの内容表示
        /// </summary>
        public void Print(string header)
        {
            string output;
            if (header.Length == 0) {
                output = Item.ToString();
            }
            else {
                output = header + "-" + Item.ToString();
            }
            Console.WriteLine(output);
            foreach (var item in Children) {
                item.Print(output);
            }
        }
    }

    enum MenuType
    {
        MenuBar,        // 一番トップのやつ
        ExpandMenu,     // メニューが展開するやつ
        MenuItem,       // 単独のメニュー
    }

    struct MenuData
    {
        public MenuType Type;
        public string Name;
        public string Id;
        public AutomationElement Element;

        override public string ToString()
        {
            return Name;
        }
    }

    class Program
    {
        [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
        public static extern bool AllocConsole();

        private const int MaxRetryNum = 1000;

        //指定したタイトルの文字列が含まれているプロセスを取得
        //一個目を戻すだけなので、複数対応はしていません。
        // UWPの場合、起動直後タイトルが未決定の場合があるので、リトライを入れてみる
        static public Process UpdateTargetProcess(string title)
        {
            for (int i = 0; i < MaxRetryNum; i++) {
                foreach (Process p in Process.GetProcesses()) {
                    if (p.MainWindowTitle.Contains(title)) {
                        return p;
                    }
                }
                Thread.Sleep(10);
            }
            return null;
        }

        static void Main(string[] args)
        {
            AllocConsole();

            TreeNode<MenuData> menu = new TreeNode<MenuData>(new MenuData() { Type = MenuType.MenuBar });

            var proc = UpdateTargetProcess("Connect IQ Device Simulator");

            // トップのメニューを探す
            var root = AutomationElement.FromHandle(proc.MainWindowHandle);
            var children = root.FindAll(TreeScope.Children, Condition.TrueCondition).Cast<AutomationElement>();
            AutomationElement menuTop = null;
            foreach (var obj in children) {
                if (obj.Current.ControlType == ControlType.MenuBar) {
                    var item = new MenuData();
                    item.Name = obj.Current.Name;
                    item.Type = MenuType.MenuBar;
                    item.Id = obj.Current.AutomationId;
                    item.Element = obj;
                    menuTop = obj;
                    menu.Item = item;
                }
            }
            if (menu.Item.Type == MenuType.MenuBar && menuTop is not null) {
                FindChildren(menu, menuTop, root);
            }

            menu.Print("");
            var line = Console.ReadLine();
        }

        /// <summary>
        /// メニューの子供を見つけ、expand可能なアイテムの場合その子供を探す<br/>
        /// ポップされたメニューはトップの子供として展開されるため、検索するためトップ要素も渡すようにしている
        /// </summary>
        /// <param name="appTop">ポップアップしたメニューを探すためのトップ要素</param>
        static void FindChildren(TreeNode<MenuData> menu, AutomationElement menuTop, AutomationElement appTop) {
            // メニュー項目の子要素をまず検索する
            var children = menuTop.FindAll(TreeScope.Children, Condition.TrueCondition).Cast<AutomationElement>();
            foreach (var obj in children) {
                if (obj.Current.ControlType == ControlType.MenuItem || obj.Current.ControlType == ControlType.Menu) {
                    var item = new MenuData();
                    item.Name = obj.Current.Name;
                    if ((bool)obj.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) {
                        item.Type = MenuType.ExpandMenu;
                    }
                    else {
                        item.Type = MenuType.MenuItem;
                    }
                    item.Id = obj.Current.AutomationId;
                    item.Element = obj;
                    menu.AddChild(item);
                }
            }

            // 展開可能なメニューを展開していく
            foreach (var obj in menu.Children) {
                if (obj.Item.Type == MenuType.ExpandMenu) {
                    if (obj.Item.Element.Current.IsEnabled) {
                        var expand = obj.Item.Element.GetCurrentPattern(ExpandCollapsePattern.Pattern) as ExpandCollapsePattern;
                        expand.Expand();
                        // 以下Childrenで探せるかと思ったのだがだめだったので、Descendantsに変更してある
                        children = appTop.FindAll(TreeScope.Descendants, new PropertyCondition(AutomationElement.NameProperty, obj.Item.Name)).Cast<AutomationElement>();
                        foreach (var obj1 in children) {
                            if (obj1.Current.ControlType == ControlType.Menu) {
                                FindChildren(obj, obj1, appTop);
                            }
                        }
                    }
                }
            }
        }
    }
}

 

コメント

タイトルとURLをコピーしました