WindowsのGUI操作を自動化したいけど、運用上、UIAutomation Extensionsの導入はできない。
UIAutomation Extensionsを利用せずにGUI操作を自動化する方法について教えて欲しい。
こんなお悩みを解決します。
今回は、前回の記事の続きとなります。
【UIAutomation】PowerShellによるGUIの操作例
続きを見る
前回は、事例を取り上げ、対応するサンプルを示しました。
一方、これでは異なる目的で操作する際に別途追加の調査が必要になってしまいます。
今回は、操作対象のGUIアプリケーションに対し、アプリケーションを構成する要素の探索や実行可能な処理を呼び出せる関数を用意しました。
具体的な実装方法も併せて紹介するので、興味がある方は、最後までじっくりと読んでみてください。
また、本記事の実装は、利用者にとってより使いやすい形で更新していただいて構いません。
前提条件
前回に引き続き、今回も以下の環境で実行することを想定しています。
この環境以外においては期待通りに挙動しない可能性があります。ご了承ください。
- OSのエディション:Windows 10 Home
- バージョン:21H2
- PowerShellのバージョン:5.1.19041.1682
題材
実装だけでは、分かりづらい部分があると思います。
今回は、電卓の操作を自動化を対象に解説していきます。
今回実施したい具体的な処理は、以下のようになります。
- 電卓を起動する(関数電卓モードに設定済み)。
- \(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9\)を計算し、\(45\)という解を得る。
- 電卓を終了する。
私が作成した関数を用いて、アプリケーションの操作権限の取得方法、各ボタンの操作方法、アプリケーションの終了方法を解説していきます。
実装
ここでは、以下のステップに分けて順番に解説していきます。
- UIAutomationを利用する際の準備
- PatternとPatternごとに利用できるメソッドの解説
- GUI操作自動化時の補助関数の説明(私が作成したもの)
- 電卓を対象とした実装例の紹介
Step1:UIAutomationを利用する際の準備
この章で紹介する内容は、UIAutomationを利用する際に必要になる実装となります。
そのまま使いまわせるように実装していますので、参考にしてください。
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$uiAutomation = [System.Windows.Automation.AutomationElement]
$tree = [System.Windows.Automation.TreeScope]
$root = $uiAutomation::RootElement
最初の2行で、UIAutomation操作時に必要な.NET Frameworkクラスライブラリを読み込んでいます。
後半3行は、UIAutomationで要素を取得するためのクラス、階層構造を保持しているクラス、ルート要素を変数に保持しておきます。
$uiAutomation
は、電卓など、PowerShellを動作させているPC上で動作しているアプリケーションを取得する際に必要になります。
$tree
は、アプリケーションの階層構造を調べる際に必要になります。一般的なアプリケーションでは、表示される画面上の領域を区切って、領域ごとにレイヤーに分けて要素を管理しています。
このため、階層構造を順番に探索して目的の要素を探し出すことになり、階層構造を保持している情報が必要になります。
$root
は、アプリケーションを探す際に必要になります。
Step2:PatternとPatternごとに利用できるメソッドの解説
まず、Patternについて解説します。
Patternは、要素の属性に合わせて提供されるコントローラー要素になります。
例えば、ボタンであれば、コントローラー要素は「クリック」となります。
テキストフィールドであれば、コントローラー要素は「値設定・値取得」となります。
コントロールできる要素は決まっているため、すべての要素を取り扱えるように変数に保持しています。
$automationPatterns = [ordered]@{
DockPattern = [System.Windows.Automation.DockPattern]::Pattern
ExpandCollapsePattern = [System.Windows.Automation.ExpandCollapsePattern]::Pattern
GridPattern = [System.Windows.Automation.GridPattern]::Pattern
GridItemPattern = [System.Windows.Automation.GridItemPattern]::Pattern
InvokePattern = [System.Windows.Automation.InvokePattern]::Pattern
MultipleViewPattern = [System.Windows.Automation.MultipleViewPattern]::Pattern
RangeValuePattern = [System.Windows.Automation.RangeValuePattern]::Pattern
ScrollPattern = [System.Windows.Automation.ScrollPattern]::Pattern
ScrollItemPattern = [System.Windows.Automation.ScrollItemPattern]::Pattern
SelectionPattern = [System.Windows.Automation.SelectionPattern]::Pattern
SelectionItemPattern = [System.Windows.Automation.SelectionItemPattern]::Pattern
TablePattern = [System.Windows.Automation.TablePattern]::Pattern
TableItemPattern = [System.Windows.Automation.TableItemPattern]::Pattern
TextPattern = [System.Windows.Automation.TextPattern]::Pattern
TogglePattern = [System.Windows.Automation.TogglePattern]::Pattern
TransformPattern = [System.Windows.Automation.TransformPattern]::Pattern
ValuePattern = [System.Windows.Automation.ValuePattern]::Pattern
WindowPattern = [System.Windows.Automation.WindowPattern]::Pattern
}
これだけのPatternがありますが、私が把握している範囲は、以下の4つとなります。
把握しているPattern名 | 概要 | メソッド |
---|---|---|
ExpandCollapsePattern | メニューの展開・折りたたみを制御 | ・Expand():展開 ・Collapse():折りたたみ |
InvokePattern | 主にクリック操作を制御 (チェックボックスやボタンの押下など) | Invoke():クリック |
SelectionItemPattern | ListViewItemの要素を選択 ※他の用途は調査中 | Select():選択 |
TogglePattern | トグルスイッチの操作を制御 | Toggle():スイッチ操作 |
上記以外のパターンは、利用用途が分かり次第、追加していきたいと思います。
Step3:GUI操作自動化時の補助関数の説明(私が作成したもの)
ここでは、私が作成したGUI操作自動化時に利用する補助関数について説明します。
関数は以下の6つあります。
対象 | 概要 | 引数 |
---|---|---|
createCondition | 要素を探す際の条件を生成する | $params:要素名をkey、要素値をvalueにもつ連想配列 |
searchApps | Windows上で起動しているアプリケーションを探索する | $params:要素名をkey、要素値をvalueにもつ連想配列 |
getApp | Windows上で起動しているアプリケーションのうち、 条件に合致するアプリケーションを取得する | $params:要素名をkey、要素値をvalueにもつ連想配列 |
searchAllElements | アプリケーションを構成するすべての要素を取得する | $app:アプリケーション $params:要素名をkey、要素値をvalueにもつ連想配列 |
getElement | アプリケーションを構成する要素のうち、 条件に合致する要素を取得する | $app:アプリケーション $params:要素名をkey、要素値をvalueにもつ連想配列 |
checkActionPattern | 指定された要素に対し、有効なPattern名を取得する | $element:アプリケーションを構成する要素 |
createCondition
createCondition
関数は、アプリケーションやアプリケーションを構成する要素を探し出す際の条件を生成します。
アプリケーションや要素指定時は、具体的な条件を指定することを想定し、複数の条件の「and」を取るようにしています。
function createCondition([hashtable]$params) {
[System.Collections.ArrayList]$conditions = @()
$propertyCondition = [System.Windows.Automation.PropertyCondition]
$andCondition = [System.Windows.Automation.AndCondition]
$trueCondition = [System.Windows.Automation.Condition]::TrueCondition
$params.GetEnumerator() | ForEach-Object {
$key = $_.Key
$value = $_.Value
if ($value) {
switch -Exact ($key) {
"AcceleratorKey" {
$currentCondition = New-Object $propertyCondition($uiAutomation::AcceleratorKeyProperty, $value)
}
"AccessKey" {
$currentCondition = New-Object $propertyCondition($uiAutomation::AccessKeyProperty, $value)
}
"AutomationId" {
$currentCondition = New-Object $propertyCondition($uiAutomation::AutomationIdProperty, $value)
}
"BoundingRectangle" {
$currentCondition = New-Object $propertyCondition($uiAutomation::BoundingRectangleProperty, $value)
}
"ClassName" {
$currentCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, $value)
}
"ClickablePoint" {
$currentCondition = New-Object $propertyCondition($uiAutomation::ClickablePointProperty, $value)
}
"ControlType" {
$currentCondition = New-Object $propertyCondition($uiAutomation::ControlTypeProperty, $value)
}
"Culture" {
$currentCondition = New-Object $propertyCondition($uiAutomation::CultureProperty, $value)
}
"FrameworkId" {
$currentCondition = New-Object $propertyCondition($uiAutomation::FrameworkIdProperty, $value)
}
"HasKeyboardFocus" {
$currentCondition = New-Object $propertyCondition($uiAutomation::HasKeyboardFocusProperty, $value)
}
"HelpText" {
$currentCondition = New-Object $propertyCondition($uiAutomation::HelpTextProperty, $value)
}
"IsContentElement" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsContentElementProperty, $value)
}
"IsControlElement" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsControlElementProperty, $value)
}
"IsEnabled" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsEnabledProperty, $value)
}
"IsKeyboardFocusable" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsKeyboardFocusableProperty, $value)
}
"IsOffscreen" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsOffscreenProperty, $value)
}
"IsPassword" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsPasswordProperty, $value)
}
"IsRequiredForForm" {
$currentCondition = New-Object $propertyCondition($uiAutomation::IsRequiredForFormProperty, $value)
}
"ItemStatus" {
$currentCondition = New-Object $propertyCondition($uiAutomation::ItemStatusProperty, $value)
}
"ItemType" {
$currentCondition = New-Object $propertyCondition($uiAutomation::ItemTypeProperty, $value)
}
"LabeledBy" {
$currentCondition = New-Object $propertyCondition($uiAutomation::LabeledByProperty, $value)
}
"LocalizedControlType" {
$currentCondition = New-Object $propertyCondition($uiAutomation::LocalizedControlTypeProperty, $value)
}
"Name" {
$currentCondition = New-Object $propertyCondition($uiAutomation::NameProperty, $value)
}
"NativeWindowHandle" {
$currentCondition = New-Object $propertyCondition($uiAutomation::NativeWindowHandleProperty, $value)
}
"Orientation" {
$currentCondition = New-Object $propertyCondition($uiAutomation::OrientationProperty, $value)
}
"PositionInSet" {
$currentCondition = New-Object $propertyCondition($uiAutomation::PositionInSetProperty, $value)
}
"ProcessId" {
$currentCondition = New-Object $propertyCondition($uiAutomation::ProcessIdProperty, $value)
}
"RuntimeId" {
$currentCondition = New-Object $propertyCondition($uiAutomation::RuntimeIdProperty, $value)
}
"SizeOfSet" {
$currentCondition = New-Object $propertyCondition($uiAutomation::SizeOfSetProperty, $value)
}
default {
$currentCondition = $null
}
}
if ($currentCondition) {
[Void]$conditions.Add($currentCondition)
}
}
}
if ($conditions.Count -ge 2) {
$condition = New-Object $andCondition($conditions)
}
elseif ($conditions.Count -eq 1) {
$condition = $conditions[0]
}
else {
$condition = $trueCondition
}
return $condition
}
アプリケーションを識別する際に利用できる情報はすべて盛り込んでいますが、このうち良く利用するものは以下の3つになります。
- Name
- ClassName
- AutomationId
例えば、電卓のアプリケーションを取得する場合の条件は、以下のように指定することになります。
$params = @{
Name = "電卓"
ClassName = "ApplicationFrameWindow"
}
$cond = createCondition $params
searchApps
searchApps
関数は、制御対象のアプリケーション取得時に、どういったアプリケーションが制御可能かを調べる際に利用します。
今回の場合、電卓を制御したいため、名称の予測はつきますが、電卓以外(Webブラウザやexplorerなど)の場合に困ると考えました。
そこで、条件に合致するアプリケーション一覧を取得する関数を用意しました。
function searchApps([hashtable]$params) {
Start-Sleep -m 200
$condition = createCondition $params
$apps = $root.FindAll($tree::Children, $condition)
return $apps
}
上記の関数は、条件を指定しない($params = @{}
)とすると起動しているGUIアプリケーションすべてを対象に探索します。
getApp
getApp
関数は、特定のアプリケーションを取得するための関数となります。
以前実装したスクリプトを関数化したものとなります。
function getApp([hashtable]$params) {
$condition = createCondition $params
$app = $null
do {
Start-Sleep -m 200
$app = $root.FindFirst($tree::Children, $condition)
} while ($null -eq $app)
return $app
}
今回の電卓の例の場合、以下のように利用します。
$params = @{
Name = "電卓"
ClassName = "ApplicationFrameWindow"
}
$app = getApp $params
searchAllElements
searchAllElements
関数は、制御対象のアプリケーションに、どのような要素が含まれているかを探索する関数となります。
この関数は、後半で述べるcheckActionPattern
を組み合わせて利用します。
function searchAllElements([System.Windows.Automation.AutomationElement]$app, [hashtable]$params) {
$condition = createCondition $params
$elements = $app.FindAll($tree::Subtree, $condition)
return $elements
}
例えば、ボタンに該当する要素のみを抽出したい場合、以下のように利用します。
$params = @{
ClassName = "Button"
}
$elements = searchAllElements($app, $params)
getElement
getElement
関数は、制御対象のアプリケーションから特定の要素を取り出すための関数となります。
function getElement([System.Windows.Automation.AutomationElement]$app, [hashtable]$params) {
$condition = createCondition $params
$element = $app.FindFirst($tree::Subtree, $condition)
return $element
}
例えば、電卓の「1」の要素を取得したい場合、以下のように利用します。
$params = @{
ClassName = "Button" # Buttonであることは明白であるため、無くても良い
AutomationId = "num1Button"
}
$elements = getElement($app, $params)
checkActionPattern
checkActionPattern
関数は、対象となるPatternのうち、どのPatternが利用可能かを調べる関数となります。
処理としては、GetCurrentPattern
メソッドを用いた場合の例外の発生有無により判断しています。
function checkActionPattern([System.Windows.Automation.AutomationElement]$element) {
[System.Collections.ArrayList]$patterns = @()
$automationPatterns.GetEnumerator() | ForEach-Object {
$key = $_.Key
$value = $_.Value
try {
[Void]$element.GetCurrentPattern($value)
[Void]$patterns.Add($key)
}
catch {}
}
return $patterns
}
searchAllElements関数とcheckActionPattern関数の組み合わせ
電卓を操作する上での準備として、どのような要素があり、それぞれの要素に対しどのようなPatternが適用可能かを調査しておきます。
処理自体は、以下のコマンドで実現できます。
searchAllElements $app @{} | ForEach-Object {
$array = checkActionPattern $_
if ($array.Count -gt 0) {
$current = $_.Current
Write-Host ("Name: {0}, ClassName: {1}, AutomationId: {2}" -f $current.Name, $current.ClassName, $current.AutomationId)
Write-Host (" {0}" -f ($array -join ", "))
}
}
電卓起動直後に実行した結果は、以下のようになりました。
名称 | クラス名 | AutomationId | 実行可能なパターン |
---|---|---|---|
電卓 | ApplicationFrameWindow | - | TransformPattern, WindowPattern |
電卓 | ApplicationFrameTitleBarWindow | TitleBar | ValuePattern |
システム | - | - | ExpandCollapsePattern |
電卓 の最小化 | - | Minimize | InvokePattern |
電卓 を最大化する | - | Maximize | InvokePattern |
電卓 を閉じる | - | Close | InvokePattern |
電卓 | TextBlock | AppName | ScrollItemPattern, TextPattern |
- | LandmarkTarget | - | ScrollItemPattern |
式は です | - | CalculatorExpression | ScrollItemPattern |
表示は 0 です | - | CalculatorResults | InvokePattern, ScrollItemPattern |
履歴のポップアップを開く | Button | HistoryButton | InvokePattern, ScrollItemPattern |
角度演算子 | NamedContainerAutomationPeer | ScientificAngleOperators | ScrollItemPattern |
角度切り替え | Button | degButton | InvokePattern, ScrollItemPattern |
指数表記 | ToggleButton | ftoeButton | ScrollItemPattern, TogglePattern |
メモリ コントロール | NamedContainerAutomationPeer | MemoryPanel | ScrollItemPattern |
すべてのメモリをクリア | Button | ClearMemoryButton | InvokePattern, ScrollItemPattern |
メモリ呼び出し | Button | MemRecall | InvokePattern, ScrollItemPattern |
メモリ加算 | Button | MemPlus | InvokePattern, ScrollItemPattern |
メモリ減算 | Button | MemMinus | InvokePattern, ScrollItemPattern |
メモリ保存 | Button | memButton | InvokePattern, ScrollItemPattern |
メモリのポップアップを開く | Button | MemoryButton | InvokePattern, ScrollItemPattern |
指数演算子パネル | ListView | - | ScrollPattern, ScrollItemPattern |
三角関数 | ListViewItem | - | ScrollItemPattern |
三角関数 | ToggleButton | trigButton | ScrollItemPattern, TogglePattern |
三角関数 | TextBlock | - | ScrollItemPattern, TextPattern |
関数 | ListViewItem | - | ScrollItemPattern |
関数 | ToggleButton | funcButton | ScrollItemPattern, TogglePattern |
関数 | TextBlock | - | ScrollItemPattern, TextPattern |
逆関数 | ToggleButton | shiftButton | ScrollItemPattern, TogglePattern |
パイ | Button | piButton | InvokePattern, ScrollItemPattern |
オイラー数 | Button | eulerButton | InvokePattern, ScrollItemPattern |
ディスプレイ コントロール | NamedContainerAutomationPeer | DisplayControls | ScrollItemPattern |
クリア | Button | clearButton | InvokePattern, ScrollItemPattern |
バックスペース | Button | backSpaceButton | InvokePattern, ScrollItemPattern |
科学関数 | NamedContainerAutomationPeer | - | ScrollItemPattern |
2 乗 | Button | xpower2Button | InvokePattern, ScrollItemPattern |
平方根算出 | Button | squareRootButton | InvokePattern, ScrollItemPattern |
"X" を指数に | Button | powerButton | InvokePattern, ScrollItemPattern |
10 を指数に | Button | powerOf10Button | InvokePattern, ScrollItemPattern |
ログ | Button | logBase10Button | InvokePattern, ScrollItemPattern |
自然対数 | Button | logBaseEButton | InvokePattern, ScrollItemPattern |
逆数 | Button | invertButton | InvokePattern, ScrollItemPattern |
絶対値 | Button | absButton | InvokePattern, ScrollItemPattern |
指数近似曲線 | Button | expButton | InvokePattern, ScrollItemPattern |
剰余 | Button | modButton | InvokePattern, ScrollItemPattern |
始め丸かっこ | Button | openParenthesisButton | InvokePattern, ScrollItemPattern |
終わり丸かっこ | Button | closeParenthesisButton | InvokePattern, ScrollItemPattern |
階乗 | Button | factorialButton | InvokePattern, ScrollItemPattern |
標準演算子 | NamedContainerAutomationPeer | StandardOperators | ScrollItemPattern |
除算 | Button | divideButton | InvokePattern, ScrollItemPattern |
乗算 | Button | multiplyButton | InvokePattern, ScrollItemPattern |
マイナス | Button | minusButton | InvokePattern, ScrollItemPattern |
プラス | Button | plusButton | InvokePattern, ScrollItemPattern |
等号 | Button | equalButton | InvokePattern, ScrollItemPattern |
正 負 | Button | negateButton | InvokePattern, ScrollItemPattern |
数字パッド | NamedContainerAutomationPeer | NumberPad | ScrollItemPattern |
0 | Button | num0Button | InvokePattern, ScrollItemPattern |
1 | Button | num1Button | InvokePattern, ScrollItemPattern |
2 | Button | num2Button | InvokePattern, ScrollItemPattern |
3 | Button | num3Button | InvokePattern, ScrollItemPattern |
4 | Button | num4Button | InvokePattern, ScrollItemPattern |
5 | Button | num5Button | InvokePattern, ScrollItemPattern |
6 | Button | num6Button | InvokePattern, ScrollItemPattern |
7 | Button | num7Button | InvokePattern, ScrollItemPattern |
8 | Button | num8Button | InvokePattern, ScrollItemPattern |
9 | Button | num9Button | InvokePattern, ScrollItemPattern |
小数点 | Button | decimalSeparatorButton | InvokePattern, ScrollItemPattern |
関数電卓 電卓モード | TextBlock | Header | ScrollItemPattern, TextPattern |
- | - | NavView | ScrollItemPattern, SelectionPattern |
ナビゲーションを開く | Button | TogglePaneButton | InvokePattern, ScrollItemPattern |
- | TextBlock | PaneTitleTextBlock | ScrollItemPattern, TextPattern |
以降では、上記の情報を用いて実装をしていきます。
Step4:電卓を対象とした実装例の紹介
再掲となりますが、今回の実装内容は、以下に格納してあります。
今回の処理内容をおさらいします。
今回やりたいことは、以下のような内容でした。
- 電卓を起動する(関数電卓モードに設定済み)。
- \(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9\)を計算し、\(45\)という解を得る。
- 電卓を終了する。
これをPowerShellで実装すると以下のようになります。
# 1. 電卓を起動する
Start-Process "calc"
# 準備
$params = @{
Name = "電卓"
ClassName = "ApplicationFrameWindow"
}
# 起動した電卓の制御権限を取得する
$app = getApp $params
# 2. 「1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9」の計算
@("num1Button", "plusButton",
"num2Button", "plusButton",
"num3Button", "plusButton",
"num4Button", "plusButton",
"num5Button", "plusButton",
"num6Button", "plusButton",
"num7Button", "plusButton",
"num8Button", "plusButton",
"num9Button", "equalButton") | ForEach-Object {
# ボタン要素を取得
$element = getElement $app @{AutomationId = $_}
# invoke用の要素を取得
$button = $element.GetCurrentPattern($automationPatterns.InvokePattern)
# ボタン押下
$button.Invoke()
# 途中経過確認のための待機処理
Start-Sleep -m 150
}
# 結果確認のための待機処理
Start-Sleep -m 500
# 3. 電卓を終了する
$params = @{
Name = "電卓 を閉じる"
AutomationId = "Close"
}
# 閉じるボタン要素を取得
$element = getElement $app $params
# invoke用の要素を取得
$button = $element.GetCurrentPattern($automationPatterns.InvokePattern)
# ボタン押下
$button.Invoke()
電卓の構成要素とコントロール方法が分かっていれば、それほど難しくない内容でした。
まとめ
実装結果は、GitHubに格納しています。実装を確認したい方は、以下のリンクにアクセスしてください。
- アプリケーションを構成する要素によって、利用できるPatternが異なる。
- 利用できるPatternは、要素が持つGetCurrentPatternメソッドを介して確認できる。
- 実際にアプリケーションを起動させ、アプリケーションを構成する要素と利用可能なPatternの一覧を取得する関数を作成した。
- 電卓を題材に実装し、期待通りの結果が得られた。