Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9318

UI AutomationでWindowsプログラムの自動化などしてみる

$
0
0

WindowsのGUIで出来たプログラムを評価していて、頻度の低い問題にぶち当たったとします。例えば下記の処理を100回繰り返すと1回ぐらい例外で落ちるんですーみたいなの。

  1. EXECUTEボタンをクリック
  2. 処理
  3. 終了するとENDボタンをクリック

これをさすがに手でやる訳にもいかないので自動的にWindows様にやって頂けると助かります。UI Automationを使うとそれが出来るらしいので、調べて実装してみました。

これ系はざっとググると今(2020年2月)と事情が違う情報もあったりします。なので、今どうなのという参考にちょっとでもなってくれれば幸いです。

UI Automationって

以下、公式から引用です。

>UI オートメーション は、デスクトップ上のほとんどの ユーザー インターフェイス (UI) 要素へのプログラムによるアクセスを提供し、スクリーン リーダーなどの補助技術製品が UI に関する情報をエンド ユーザーに提供したり、標準入力方式以外の方法で UI を操作したりできるようにします。 また、UI オートメーション は、自動テスト スクリプトが UIと対話できるようにします。

https://docs.microsoft.com/ja-jp/dotnet/framework/ui-automation/ui-automation-overview

例えばボタンをクリックしたりとか、メニューの位置が取得出来ます。これらの機能を使うことで、諸々の操作を自動的にやれるという事なようです。

今回の自動実行シナリオ

実際に作って試した方が早かろうという事で、以下のシナリオでの自動実行を行ってみました。

  1. 電卓を起動します
  2. 123456 ÷ 5 と計算させます(プログラムでキーを叩いて計算させます)
  3. その結果をコピーします(該当パーツへのフォーカスとキー入力)
  4. メモ帳を起動します
  5. 結果をペーストします(該当パーツへのフォーカスとキー入力)
  6. 更にメニューからバージョンを表示させます(マウスクリックによる動作とキー入力)
  7. 「notepadの内容を確認して、enterして下さい」と表示
  8. enterが入力されたら、電卓アプリを終了させます

開発・実行環境について

以下の環境でしか確認はしていません。。

  • Windows10 Professional(64bit)
  • VisualStdio2019
  • .NET Framework 4.7.2

電卓アプリについて

あとこないだ知ったのですが、Windows10をクリーンインストールすると電卓が標準で入って来ない事があるみたいです。その場合はストアから入手出来ます。以下

https://www.microsoft.com/ja-jp/p/windows-%E9%9B%BB%E5%8D%93/9wzdncrfhvn5#activetab=pivot:overviewtab

今回はこれを使っています。calcで起動出来る所は従来と同じですが、内部仕様は結構変わっているみたいです。なので、古いUI Automationのサンプルだとそのままでは動かなかったりします。

自動実行に関するコードはこんな感じ

上記のシナリオで必要とされる機能を関数化し。それを集めてクラス化したコードを以下に。

usingSystem;usingSystem.Collections.Generic;usingSystem.Diagnostics;usingSystem.Linq;usingSystem.Runtime.InteropServices;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;usingSystem.Windows.Automation;usingSystem.Windows.Forms;namespaceConsoleApp1{// UI Automation関連の処理で使う関数を集めてみました// 個別に抜き出して使うことも鑑み、各関数の独立を意図的に高める記述をしています。publicclassUIAutomationLib{readonlystringModuleName="UIAutomationLib";// UI automation系以外に、Win32APIも使いますのでその宣言。 [DllImport("USER32.dll",CallingConvention=CallingConvention.StdCall)]staticexternvoidSetCursorPos(intX,intY);[DllImport("USER32.dll",CallingConvention=CallingConvention.StdCall)]staticexternvoidmouse_event(intdwFlags,intdx,intdy,intcButtons,intdwExtraInfo);// マウスイベント// 定義は以下に//  https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mouse_event//privateconstintMOUSEEVENTF_LEFTDOWN=0x2;privateconstintMOUSEEVENTF_LEFTUP=0x4;//指定したタイトルの文字列が含まれているプロセスを取得//一個目を戻すだけなので、複数対応はしていません。publicProcessUpdateTargetProcess(stringtitle){Processprocess=null;foreach(ProcesspinProcess.GetProcesses()){if(p.MainWindowTitle.Contains(title)){process=p;break;}}if(process==null){MessageBox.Show(title+"のプロセスが見つかりません。",ModuleName);}returnprocess;}//指定されたプロセスのMainFramに関するAutomationElementを取得publicAutomationElementGetMainFrameElement(Processp){returnAutomationElement.FromHandle(p.MainWindowHandle);}//指定された名前のButtonをクリックします//(例外対策はしていませんので注意)publicvoidPushButtonByName(AutomationElementelement,stringname){InvokePatternbutton=FindElementsByName(element,name).First().GetCurrentPattern(InvokePattern.Pattern)asInvokePattern;button.Invoke();}//指定されたAutomationIdのButtonをクリックします//(例外対策はしていませんので注意)publicvoidPushButtonById(AutomationElementelement,stringAutomationId){InvokePatternbutton=FindElementById(element,AutomationId).GetCurrentPattern(InvokePattern.Pattern)asInvokePattern;button.Invoke();}//指定されたAutomationIdのパーツをクリックします//(例外対策はしていませんので注意。clockableじゃないパーツ叩くと多分落ちるw)publicvoidClickElement(AutomationElementelement,stringAutomationId){AutomationElementtarget=FindElementById(element,AutomationId);System.Windows.Pointp=target.GetClickablePoint();SetCursorPos((int)p.X,(int)p.Y);mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);}//指定されたAutomationElementにキーボード叩いた体で文字列を送り込みます//(対象はTextBoxなどを想定)////focusはキー叩く前に該当パーツにマウスを移動するかどうか //  制御コードなどは以下を参考に// // https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.sendkeys?view=netframework-4.8// publicvoidKeyin(boolfocus,AutomationElementelement,stringtext){if(focus){element.SetFocus();}Thread.Sleep(200);SendKeys.SendWait(text);Thread.Sleep(200);}// 指定されたautomationIdに一致するAutomationElementを取得publicAutomationElementFindElementById(AutomationElementrootElement,stringautomationId){returnrootElement.FindFirst(TreeScope.Element|TreeScope.Descendants,newPropertyCondition(AutomationElement.AutomationIdProperty,automationId));}// 指定された名前に一致するAutomationElement達をIEnumerableで戻します、publicIEnumerable<AutomationElement>FindElementsByName(AutomationElementrootElement,stringname){returnrootElement.FindAll(TreeScope.Element|TreeScope.Descendants,newPropertyCondition(AutomationElement.NameProperty,name)).Cast<AutomationElement>();}}}

コードの簡単な説明

  • 関数一覧
関数名説明
UpdateTargetProcess指定したタイトルの文字列が含まれているプロセスクラスを取得
GetMainFrameElement指定されたプロセスクラスのMainFramに関するAutomationElementを取得
PushButtonByName指定された名前のButtonをクリックします
PushButtonById指定されたAutomationIdのButtonをクリックします
ClickElement指定されたAutomationIdのパーツを左クリックします
Keyin指定されたAutomationElementにキー入力で文字列を送り込みます
FindElementById指定されたautomationIdに一致するAutomationElementを取得
FindElementsByName指定された名前に一致するAutomationElement達をIEnumerableで取得

私がC#に関しては素人から毛を抜いたような人なので、そんなに難しいコードが理解できるハズもないため、MSDNのAPI仕様を見つつ上記のコードを見れば、まぁ何となくは分かるのではないかと思いますが、いくつか補足します。

  • AutomationElementというのは各リソースに対応するUI Automationに関する要素です(Element直訳なのかな)。例えばフレームとかボタンとかメニューとかです。それぞれにAutomationElementが割り振られていて、制御する際にそれを使うという理解で多分大丈夫な気がします…
  • PushButtonByXXX系ですが、ボタンに関するAutomationElementは関数内で取得しますので、親フレームのAutomationElementを与えれば良いです。電卓みたいに簡単な構造であればMainFramに関するAutomationElementで良いです。
  • ボタンをクリックする場合はボタン上のテキスト(電卓なら1とか2とか…)で指定する方法と、そのボタンのAutomationId(AutomationElement毎に割り振られるユニークな文字列)とを用意しました。AutomationIdの取得方法は後述します、
  • ClickElementは指定されたAutomationIdの場所を割り出してフォーカスを当ててクリックします。Clickableなリソースでないとエラーになります(すみません対策してません)ので注意です。
  • Keyinは指定された文字列をキー入力します。CTRL系とかも入力可能です。キーインの前にそのリソースにフォーカスを当てるかどうかは選択出来ます(既にフォーカスが当たっている場合はfalseにする運用イメージです)
  • Find系はAutomationElementからAutomationElementを取る時などに使う事を想定しています。

このコードを使う場合に必要な設定

上記のコードを使う場合にはVisualStdioの「参照」→「参照の追加」→「アセンブリ」で以下を追加します。

  • System.Windows.Forms
  • UIAutomationClient
  • UIAutomationTypes
  • WindowsBase(CUIの場合にこれの追加が必要)

AutomationIdの取得方法

UI Automationの機構を利用して、何かを制御する場合にAutomationIdを指定する方が楽なケースもあります。例えば電卓の「÷」は名前を ÷ にしてもダメです。名前で行く場合には 除算 としないとダメでした。こうなるともうAutomationIdを指定した方が楽だと思います。

で、これらの情報をどう収集するかというと、 Automation Spyというツールを使って調べるのがどうも定番みたいです、下記にURLを起きます。私のChromeだとここ、危険サイト扱いになってますね…

https://archive.codeplex.com/?p=uiautomation

とはいえこのツール、今は開発が終わっていようです。なのでAutomationId周辺情報を取得するツールを作ってみました。GitHubにて公開しています。使い方などはGitHubの説明を参照下さい、このツールで制御対象のAutomationId等は取得できます、電卓の÷が除算だというのも私はこのツールで調べました。

https://github.com/khamada611/check_automationID

  • 電卓の「÷」を調べた例。Nameが表示と違うんですよね…

pro1.PNG

自動実行のコードを記述

それでは上記のクラスを用いて、前述したシナリオに沿って動作するDOSアプリのソースコードを示します。以下です。

usingSystem;usingSystem.Collections.Generic;usingSystem.Diagnostics;usingSystem.Linq;usingSystem.Text;usingSystem.Threading;usingSystem.Threading.Tasks;usingSystem.Windows.Automation;namespaceConsoleApp1{classProgram{staticvoidMain(string[]args){//// 自動実行のコードはこんな風にも書けます。// UIAutomationLibui=newUIAutomationLib();// 電卓を起動しますProcesscalc=Process.Start(@"calc");// 起動待ちThread.Sleep(2000);// 電卓のMainFRameのAutomationElementを取得calc=ui.UpdateTargetProcess("電卓");// 更新AutomationElementcalcElement=ui.GetMainFrameElement(calc);// 電卓操作ui.PushButtonById(calcElement,"clearButton");ui.PushButtonByName(calcElement,"1");ui.PushButtonByName(calcElement,"2");ui.PushButtonByName(calcElement,"3");ui.PushButtonByName(calcElement,"4");ui.PushButtonByName(calcElement,"5");ui.PushButtonByName(calcElement,"6");ui.PushButtonById(calcElement,"divideButton");ui.PushButtonByName(calcElement,"5");ui.PushButtonById(calcElement,"equalButton");// 結果のテキストを取り出し、CTRL-Cでクリップボードにコピーします。AutomationElementResultElement=ui.FindElementById(calcElement,"CalculatorResults");ui.Keyin(true,ResultElement,"^c");// ^ = CTRL// notepadを起動させます。Processnotepad=Process.Start(@"notepad");// 起動待ちThread.Sleep(2000);// 電卓のMainFRameのAutomationElementを取得notepad=ui.UpdateTargetProcess("メモ帳");// 更新AutomationElementnotepadElement=ui.GetMainFrameElement(notepad);// で、ペーストします。ui.Keyin(true,notepadElement,"^v");// ^ = CTRL// さらにメニューをクリック操作してバージョンを出します。stringnotepadHelpMenuId="Item 5";// 「メニュー」のAutomationIdui.ClickElement(notepadElement,notepadHelpMenuId);ui.Keyin(false,notepadElement,"a");// 確認のメッセージです。Console.WriteLine("notepadの内容を確認して、<enter>して下さい(電卓は消しますがnotepadhaは残します)");Console.ReadKey();// 電卓プロセスを終了させますcalc.CloseMainWindow();}}}

一直線なシナリオになりますので、コメントとその下の手続きを見てもらえればおおよそは理解できるのではないかと思います。

VisualStdioを使って、上記2つのソースコードを入れ、前述した参照の設定を行うことで実際に確認も出来るかと思います。その際、電卓はプログラムで落とすので放置しておいて下さい(<enter>する前に電卓手動で落とすとプログラムが正常に終わりません)

1点補足しますと、今の電卓だとUpdateTargetProcess関数の処理、つまり制御を行う前に現状のプロセスを再度取得する必要があるです。私の環境でここをコメントアウトすると死にます。少し古いWeb情報ですとこれが不要なのですが、今の電卓アプリには必要みたいです。

参考

これらのコードを組んだり動かしたりするのに、以下のサイトを参考にさせて頂きました。各サイトの皆様ありがとうございました。

最後に

Windowsで自動実行を行う場合の定番は電卓になるのですが(笑)、ストアアプリになった関係か結構電卓の実装仕様が変わってしまっています。その為ちょっと前の情報がそのままでは使えなくなっているので、スキルの無い私は動かすまで苦労しました。2020年2月の時点ではこれで恐らく動くと思います。同じような感じで調べている方の参考になれば幸いです。

あと、当然ですがこの情報を使用した際の損害は誰も請け負ってくれません。そこはお願いします。


Viewing all articles
Browse latest Browse all 9318

Latest Images

Trending Articles