広告 プログラミング

PowerShellによるGUI操作自動化

※本ページには、プロモーション(広告)が含まれています。

悩んでいる人
悩んでいる人

WindowsのGUI操作を自動化したいけど、運用上、UIAutomation Extensionsの導入はできない。

UIAutomation Extensionsを利用せずにGUI操作を自動化する方法について教えて欲しい。

こんなお悩みを解決します。

今回は、前回の記事の続きとなります。

あわせて読みたい
【UIAutomation】PowerShellによるGUIの操作例

続きを見る

前回は、事例を取り上げ、対応するサンプルを示しました。

一方、これでは異なる目的で操作する際に別途追加の調査が必要になってしまいます。

今回は、操作対象のGUIアプリケーションに対し、アプリケーションを構成する要素の探索や実行可能な処理を呼び出せる関数を用意しました。

具体的な実装方法も併せて紹介するので、興味がある方は、最後までじっくりと読んでみてください。

また、本記事の実装は、利用者にとってより使いやすい形で更新していただいて構いません。

前提条件

前回に引き続き、今回も以下の環境で実行することを想定しています。

この環境以外においては期待通りに挙動しない可能性があります。ご了承ください。

  • OSのエディション:Windows 10 Home
  • バージョン:21H2
  • PowerShellのバージョン:5.1.19041.1682

題材

実装だけでは、分かりづらい部分があると思います。

今回は、電卓の操作を自動化を対象に解説していきます。

今回実施したい具体的な処理は、以下のようになります。

  1. 電卓を起動する(関数電卓モードに設定済み)。
  2. \(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9\)を計算し、\(45\)という解を得る。
  3. 電卓を終了する。
電卓起動時の画面

私が作成した関数を用いて、アプリケーションの操作権限の取得方法、各ボタンの操作方法、アプリケーションの終了方法を解説していきます。

実装

ここでは、以下のステップに分けて順番に解説していきます。

  1. UIAutomationを利用する際の準備
  2. PatternとPatternごとに利用できるメソッドの解説
  3. GUI操作自動化時の補助関数の説明(私が作成したもの)
  4. 電卓を対象とした実装例の紹介

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():クリック
SelectionItemPatternListViewItemの要素を選択
※他の用途は調査中
Select():選択
TogglePatternトグルスイッチの操作を制御Toggle():スイッチ操作
機能を把握できているPattern

上記以外のパターンは、利用用途が分かり次第、追加していきたいと思います。

Step3:GUI操作自動化時の補助関数の説明(私が作成したもの)

ここでは、私が作成したGUI操作自動化時に利用する補助関数について説明します。

関数は以下の6つあります。

対象概要引数
createCondition要素を探す際の条件を生成する$params:要素名をkey、要素値をvalueにもつ連想配列
searchAppsWindows上で起動しているアプリケーションを探索する$params:要素名をkey、要素値をvalueにもつ連想配列
getAppWindows上で起動しているアプリケーションのうち、
条件に合致するアプリケーションを取得する
$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
電卓ApplicationFrameTitleBarWindowTitleBarValuePattern
システム--ExpandCollapsePattern
電卓 の最小化-MinimizeInvokePattern
電卓 を最大化する-MaximizeInvokePattern
電卓 を閉じる-CloseInvokePattern
電卓TextBlockAppNameScrollItemPattern, TextPattern
-LandmarkTarget-ScrollItemPattern
式は です-CalculatorExpressionScrollItemPattern
表示は 0 です-CalculatorResultsInvokePattern, ScrollItemPattern
履歴のポップアップを開くButtonHistoryButtonInvokePattern, ScrollItemPattern
角度演算子NamedContainerAutomationPeerScientificAngleOperatorsScrollItemPattern
角度切り替えButtondegButtonInvokePattern, ScrollItemPattern
指数表記ToggleButtonftoeButtonScrollItemPattern, TogglePattern
メモリ コントロールNamedContainerAutomationPeerMemoryPanelScrollItemPattern
すべてのメモリをクリアButtonClearMemoryButtonInvokePattern, ScrollItemPattern
メモリ呼び出しButtonMemRecallInvokePattern, ScrollItemPattern
メモリ加算ButtonMemPlusInvokePattern, ScrollItemPattern
メモリ減算ButtonMemMinusInvokePattern, ScrollItemPattern
メモリ保存ButtonmemButtonInvokePattern, ScrollItemPattern
メモリのポップアップを開くButtonMemoryButtonInvokePattern, ScrollItemPattern
指数演算子パネルListView-ScrollPattern, ScrollItemPattern
三角関数ListViewItem-ScrollItemPattern
三角関数ToggleButtontrigButtonScrollItemPattern, TogglePattern
三角関数TextBlock-ScrollItemPattern, TextPattern
関数ListViewItem-ScrollItemPattern
関数ToggleButtonfuncButtonScrollItemPattern, TogglePattern
関数TextBlock-ScrollItemPattern, TextPattern
逆関数ToggleButtonshiftButtonScrollItemPattern, TogglePattern
パイButtonpiButtonInvokePattern, ScrollItemPattern
オイラー数ButtoneulerButtonInvokePattern, ScrollItemPattern
ディスプレイ コントロールNamedContainerAutomationPeerDisplayControlsScrollItemPattern
クリアButtonclearButtonInvokePattern, ScrollItemPattern
バックスペースButtonbackSpaceButtonInvokePattern, ScrollItemPattern
科学関数NamedContainerAutomationPeer-ScrollItemPattern
2 乗Buttonxpower2ButtonInvokePattern, ScrollItemPattern
平方根算出ButtonsquareRootButtonInvokePattern, ScrollItemPattern
"X" を指数にButtonpowerButtonInvokePattern, ScrollItemPattern
10 を指数にButtonpowerOf10ButtonInvokePattern, ScrollItemPattern
ログButtonlogBase10ButtonInvokePattern, ScrollItemPattern
自然対数ButtonlogBaseEButtonInvokePattern, ScrollItemPattern
逆数ButtoninvertButtonInvokePattern, ScrollItemPattern
絶対値ButtonabsButtonInvokePattern, ScrollItemPattern
指数近似曲線ButtonexpButtonInvokePattern, ScrollItemPattern
剰余ButtonmodButtonInvokePattern, ScrollItemPattern
始め丸かっこButtonopenParenthesisButtonInvokePattern, ScrollItemPattern
終わり丸かっこButtoncloseParenthesisButtonInvokePattern, ScrollItemPattern
階乗ButtonfactorialButtonInvokePattern, ScrollItemPattern
標準演算子NamedContainerAutomationPeerStandardOperatorsScrollItemPattern
除算ButtondivideButtonInvokePattern, ScrollItemPattern
乗算ButtonmultiplyButtonInvokePattern, ScrollItemPattern
マイナスButtonminusButtonInvokePattern, ScrollItemPattern
プラスButtonplusButtonInvokePattern, ScrollItemPattern
等号ButtonequalButtonInvokePattern, ScrollItemPattern
正 負ButtonnegateButtonInvokePattern, ScrollItemPattern
数字パッドNamedContainerAutomationPeerNumberPadScrollItemPattern
0Buttonnum0ButtonInvokePattern, ScrollItemPattern
1Buttonnum1ButtonInvokePattern, ScrollItemPattern
2Buttonnum2ButtonInvokePattern, ScrollItemPattern
3Buttonnum3ButtonInvokePattern, ScrollItemPattern
4Buttonnum4ButtonInvokePattern, ScrollItemPattern
5Buttonnum5ButtonInvokePattern, ScrollItemPattern
6Buttonnum6ButtonInvokePattern, ScrollItemPattern
7Buttonnum7ButtonInvokePattern, ScrollItemPattern
8Buttonnum8ButtonInvokePattern, ScrollItemPattern
9Buttonnum9ButtonInvokePattern, ScrollItemPattern
小数点ButtondecimalSeparatorButtonInvokePattern, ScrollItemPattern
関数電卓 電卓モードTextBlockHeaderScrollItemPattern, TextPattern
--NavViewScrollItemPattern, SelectionPattern
ナビゲーションを開くButtonTogglePaneButtonInvokePattern, ScrollItemPattern
-TextBlockPaneTitleTextBlockScrollItemPattern, TextPattern
電卓を構成する要素一覧

以降では、上記の情報を用いて実装をしていきます。

Step4:電卓を対象とした実装例の紹介

再掲となりますが、今回の実装内容は、以下に格納してあります。

https://github.com/yuruto-free/powershell-ui-automation/blob/master/uiautomation_calculator_sample.ps1

今回の処理内容をおさらいします。

今回やりたいことは、以下のような内容でした。

  1. 電卓を起動する(関数電卓モードに設定済み)。
  2. \(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9\)を計算し、\(45\)という解を得る。
  3. 電卓を終了する。

これを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に格納しています。実装を確認したい方は、以下のリンクにアクセスしてください。

https://github.com/yuruto-free/powershell-ui-automation/blob/master/uiautomation_calculator_sample.ps1

  • アプリケーションを構成する要素によって、利用できるPatternが異なる。
  • 利用できるPatternは、要素が持つGetCurrentPatternメソッドを介して確認できる。
  • 実際にアプリケーションを起動させ、アプリケーションを構成する要素と利用可能なPatternの一覧を取得する関数を作成した。
  • 電卓を題材に実装し、期待通りの結果が得られた。

スポンサードリンク

-プログラミング
-,