広告 プログラミング

【UIAutomation】PowerShellによるGUIの操作例

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

悩んでいる人

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

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

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

今回は、PowerShellを用いたGUIの操作例を紹介します。

普段から、PowerShellを用いて作業の効率化を行っていますが、時々、PowerShellからGUIの操作をしたくなる時があります。

PowerShellからGUIを操作するツールとして、UIAutomation Extensionsというものがありますが、2014年以降、更新がされていません。

また、私の場合、ツールを配布することもあるため、環境依存の実装は極力避けることが望ましいです。

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

前提条件

今回、動作確認も含めて、以下の環境で実行することを想定しています。

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

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

UIAutomationとは?

まず、UIAutomationについて触れておきます。

Microsoft UIオートメーションには、以下のような説明が記載されています。

Microsoft UI オートメーションは、Windows Presentation Foundation (WPF) をサポートするすべてのオペレーティング システムで利用可能な、Microsoft Windows の新しいアクセシビリティ フレームワークです。

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

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

上記には、標準入力(マウスやキーボードによる操作)以外の方法で、デスクトップ上のUI(すなわち、GUI)へのプログラムによるアクセスを提供する、とあります。

つまり、UIAutomationを用いることで、プログラムによりGUIを操作できることになります。

今回の活用事例

今回は、Windowsメニューからたどれる「設定→ネットワークとインターネット」で表示される画面を対象とした事例を紹介します。

事例として紹介とする画面

大まかな処理フローを以下に示します。

ここで、上記の図を初期状態とします。

  1. 左側のリストから「プロキシ」を押下
  2. 「プロキシ サーバーを使う」のトグルスイッチを押下(トグルスイッチがONに切り替わる)
  3. 画面下部にある「保存」を押下(同一名称のボタンが上側にもあるため、区別が必要)
  4. 「プロキシ サーバーを使う」のトグルスイッチを押下(トグルスイッチがOFFに切り替わる)
  5. 「設定」の画面をクローズ
1.「プロキシ」を押下
2.「トグルスイッチ」を押下(OFF→ON)
3.「保存」を押下
4.「トグルスイッチ」を押下(ON→OFF)

Step0:事前準備

PowerShellでUIAutomationを利用する際は、事前準備が必要となります。GitHubにあるコードの該当箇所を以下に示します。

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes

# GUI操作のための準備
$uiAutomation = [System.Windows.Automation.AutomationElement]
$tree = [System.Windows.Automation.TreeScope]
$propertyCondition = [System.Windows.Automation.PropertyCondition]
$andCondition = [System.Windows.Automation.AndCondition]
$selectionItemPattern = [System.Windows.Automation.SelectionItemPattern]::Pattern
$togglePattern = [System.Windows.Automation.TogglePattern]::Pattern
$invokePattern = [Windows.Automation.InvokePattern]::Pattern
$root = $uiAutomation::RootElement

# Step1: 「設定」からネットワークの状態を開く
Start-Process "ms-settings:network-status"

# Step2: 「設定」の操作権限を取得
$app = $null
$namePropertyCondition = New-Object $propertyCondition($uiAutomation::NameProperty, "設定")
$classNamePropertyCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, "ApplicationFrameWindow")
$condition = New-Object $andCondition($namePropertyCondition, $classNamePropertyCondition)
do {
    Start-Sleep -m 200
    $app = $root.FindFirst($tree::Children, $condition)
} while ($null -eq $app)
Start-Sleep -m 300

Windowsでは、デスクトップ(モニタに映っている画面領域)をルートとし、ルートの子要素、孫要素、というように木構造でプロセスが管理されています。

このため、プログラムからGUIを操作する場合は、操作対象(今回の場合、「設定」の画面)を条件を与えて探索する必要があります。

ルート要素の取得や探索時の条件の設定に対応するため、以下の処理が必要になります。

$uiAutomation = [System.Windows.Automation.AutomationElement]
$tree = [System.Windows.Automation.TreeScope]
$propertyCondition = [System.Windows.Automation.PropertyCondition]
$andCondition = [System.Windows.Automation.AndCondition]

また、トグルスイッチやボタンの操作には、該当する操作パターンを与え、操作対象要素に適した処理を行う必要があります。

以下は、この時の操作パターンの一例となります。

$selectionItemPattern = [System.Windows.Automation.SelectionItemPattern]::Pattern # 左側の列にある「プロキシ」押下時に使用
$togglePattern = [System.Windows.Automation.TogglePattern]::Pattern # トグルスイッチ操作時に使用
$invokePattern = [Windows.Automation.InvokePattern]::Pattern # 「保存」ボタン押下時に使用

では、実際にデスクトップから「設定」画面を探索する部分までを説明します。

該当するコードは以下のようになります。

# Step1: 「設定」からネットワークの状態を開く
Start-Process "ms-settings:network-status"

# Step2: 「設定」の操作権限を取得
$app = $null
$namePropertyCondition = New-Object $propertyCondition($uiAutomation::NameProperty, "設定")
$classNamePropertyCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, "ApplicationFrameWindow")
$condition = New-Object $andCondition($namePropertyCondition, $classNamePropertyCondition)
do {
    Start-Sleep -m 200
    $app = $root.FindFirst($tree::Children, $condition)
} while ($null -eq $app)
Start-Sleep -m 300

まず、ms-settings:network-statusにより、「設定→ネットワークとインターネット」で表示される画面を開きます。

余談ですが、ファイル名を指定して実行にms-settings:network-statusと入力してOKを押下すると、同じ画面を開くことができます。

次に、以下の処理を実行し、デスクトップに表示されているアプリケーションの一覧を取得します。

$root.FindAll($tree::Children, [System.Windows.Automation.Condition]::TrueCondition) | forEach {
    $current = $_.Current
    Write-Host ("Name: {0}, ClassName: {1}" -f $current.Name, $current.ClassName)
}

出力結果の例は、以下のようになります。

Name: UIAutomation_sample.ps1 - workspace - Visual Studio Code, ClassName: Chrome_WidgetWin_1
Name: 設定, ClassName: ApplicationFrameWindow
Name: workspace, ClassName: CabinetWClass

上記の結果から、設定画面を取得するために、以下のような条件で探索を行います。

$namePropertyCondition = New-Object $propertyCondition($uiAutomation::NameProperty, "設定") # Nameが「設定」となっているものが対象
$classNamePropertyCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, "ApplicationFrameWindow") # ClassNameが「ApplicationFrameWindow」となっているものが対象
$condition = New-Object $andCondition($namePropertyCondition, $classNamePropertyCondition) # 上記の両方の条件に当てはまるものを探索
do {
    Start-Sleep -m 200
    $app = $root.FindFirst($tree::Children, $condition)
} while ($null -eq $app)

以上により、操作対象のGUIの制御権限を取得できました。

Step1:左側のリストから「プロキシ」を押下

Step0の結果を用いてGUIを操作していきます。まずは、「プロキシ」を押下する処理を記載していきます。

先ほどと同じように表示されている画面にどのような要素があるかを確認していきます。

表示されている要素の一覧を取得するため、以下のコードを実行します。(以降、探索コードと呼びます)

$app.FindAll($tree::Subtree, [System.Windows.Automation.Condition]::TrueCondition) | forEach {
    $current = $_.Current
    Write-Host ("Name: {0}, ClassName: {1}" -f $current.Name, $current.ClassName)
}

実行結果は以下のようになります。

Name: 設定, ClassName: ApplicationFrameWindow
Name: 設定, ClassName: ApplicationFrameTitleBarWindow
Name: システム, ClassName:
Name: システム, ClassName:
Name: 設定 の最小化, ClassName:
Name: 設定 を最大化する, ClassName:
Name: 設定 を閉じる, ClassName:
Name: 設定, ClassName: Windows.UI.Core.CoreWindow
Name: 設定, ClassName: TextBlock
Name: ホーム, ClassName: Button
Name: ホーム, ClassName: TextBlock
Name: , ClassName: LandmarkTarget
Name: 検索ボックス、設定の検索, ClassName: TextBox
Name: 設定の検索, ClassName: TextBlock
Name: ネットワークとインターネット, ClassName: TextBlock
Name: , ClassName: ListView
Name: 状態, ClassName: ListViewItem
Name: 状態, ClassName: TextBlock
Name: イーサネット, ClassName: ListViewItem
Name: イーサネット, ClassName: TextBlock
Name: ダイヤルアップ, ClassName: ListViewItem
Name: ダイヤルアップ, ClassName: TextBlock
Name: VPN, ClassName: ListViewItem
Name: VPN, ClassName: TextBlock
Name: プロキシ, ClassName: ListViewItem
Name: プロキシ, ClassName: TextBlock
Name: , ClassName: LandmarkTarget
Name: 状態, ClassName: TextBlock
Name: , ClassName: ScrollViewer
Name: ネットワークの状態, ClassName: GroupItem
Name: ネットワークの状態, ClassName: TextBlock
Name: イーサネット, ClassName: TextBlock
Name: プライベート ネットワーク, ClassName: TextBlock
Name: インターネットに接続されています, ClassName: TextBlock
Name: 制限付きのデータ通信プランをお使いの場合は、このネットワークを従量制課金接続に設定するか、またはその他のプロパティを変更できます。, ClassName: TextBlock
Name: イーサネット, ClassName: TextBlock
Name: 144.95 GB, ClassName: TextBlock
Name: 過去 30 日から, ClassName: TextBlock
Name: , ClassName: TextBlock
Name: イーサネット のプロパティ, ClassName: Button
Name: イーサネット のデータ使用状況, ClassName: Button
Name: 利用できるネットワークの表示, ClassName: Button
Name: 利用できるネットワークの表示, ClassName: TextBlock
Name: 周囲の接続オプションを表示します。, ClassName: TextBlock
Name: ネットワークの詳細設定, ClassName: GroupItem
Name: ネットワークの詳細設定, ClassName: TextBlock
Name: アダプターのオプションを変更する, ClassName: Button
Name: アダプターのオプションを変更する, ClassName: TextBlock
Name: ネットワーク アダプターを表示して接続設定を変更します。, ClassName: TextBlock
Name: ネットワークと共有センター, ClassName: Button
Name: ネットワークと共有センター, ClassName: TextBlock
Name: 接続先のネットワークについて、共有するものを指定します。, ClassName: TextBlock
Name: ネットワークのトラブルシューティング ツール, ClassName: Button
Name: ネットワークのトラブルシューティング ツール, ClassName: TextBlock
Name: ネットワークの問題を診断し、解決します。, ClassName: TextBlock
Name: ハードウェアと接続のプロパティを表示する, ClassName: Hyperlink
Name: ハードウェアと接続のプロパティを表示する, ClassName: TextBlock
Name: Windows ファイアウォール, ClassName: Hyperlink
Name: Windows ファイアウォール, ClassName: TextBlock
Name: ネットワークのリセット, ClassName: Hyperlink
Name: ネットワークのリセット, ClassName: TextBlock
Name: ヘルプを表示, ClassName: GroupItem
Name: ヘルプを表示, ClassName: Hyperlink
Name: ヘルプを表示, ClassName: TextBlock
Name: フィードバックの送信, ClassName: GroupItem
Name: フィードバックの送信, ClassName: Hyperlink
Name: フィードバックの送信, ClassName: TextBlock
Name: 垂直, ClassName: ScrollBar
Name: , ClassName: ApplicationFrameInputSinkWindow

非常に長いですが、途中に以下のような記載があることが分かります。

Name: 状態, ClassName: ListViewItem
Name: 状態, ClassName: TextBlock
Name: イーサネット, ClassName: ListViewItem
Name: イーサネット, ClassName: TextBlock
Name: ダイヤルアップ, ClassName: ListViewItem
Name: ダイヤルアップ, ClassName: TextBlock
Name: VPN, ClassName: ListViewItem
Name: VPN, ClassName: TextBlock
Name: プロキシ, ClassName: ListViewItem
Name: プロキシ, ClassName: TextBlock

ここから、「プロキシ」は、Nameがプロキシかつ、ClassNameがListViewItemとなっているもの」を探せば良さそうです。

探索後に押下処理をしますが、ここで注意点があります。

ListViewItemを選択する際は、SelectionItemPatternというパターンからコントローラーを取得し、Selectメソッドを用いる必要があります。

上記をもとに実装した結果は、以下のようになります。

# $proxyItemに「Nameがプロキシかつ、ClassNameがListViewItemとなっているもの」の探索結果を格納
$namePropertyCondition = New-Object $propertyCondition($uiAutomation::NameProperty, "プロキシ")
$classNamePropertyCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, "ListViewItem")
$condition = New-Object $andCondition($namePropertyCondition, $classNamePropertyCondition)
$proxyItem = $app.FindFirst($tree::Subtree, $condition)

# 以下、押下処理
$selectionItem = $proxyItem.GetCurrentPattern($selectionItemPattern) # SelectionItemPatternのコントローラーを取得
$selectionItem.Select() # Selectメソッドの呼び出し

Step2:「プロキシ サーバーを使う」のトグルスイッチを押下

先程と同様に、探索コードを用いて表示されている画面内の要素を一覧化すると以下のようになります。

Name: 設定, ClassName: ApplicationFrameWindow
Name: 設定, ClassName: ApplicationFrameTitleBarWindow   
Name: システム, ClassName:
Name: システム, ClassName:
Name: 戻る, ClassName:
Name: 設定, ClassName: ApplicationFrameTitleBarWindow   
Name: 設定 の最小化, ClassName:
Name: 設定 を最大化する, ClassName:
Name: 設定 を閉じる, ClassName:
Name: 設定, ClassName: Windows.UI.Core.CoreWindow       
Name: 設定, ClassName: TextBlock
Name: ホーム, ClassName: Button
Name: ホーム, ClassName: TextBlock
Name: , ClassName: LandmarkTarget
Name: 検索ボックス、設定の検索, ClassName: TextBox      
Name: 設定の検索, ClassName: TextBlock
Name: ネットワークとインターネット, ClassName: TextBlock
Name: , ClassName: ListView
Name: 状態, ClassName: ListViewItem
Name: 状態, ClassName: TextBlock
Name: イーサネット, ClassName: ListViewItem
Name: イーサネット, ClassName: TextBlock
Name: ダイヤルアップ, ClassName: ListViewItem
Name: ダイヤルアップ, ClassName: TextBlock   
Name: VPN, ClassName: ListViewItem
Name: VPN, ClassName: TextBlock
Name: プロキシ, ClassName: ListViewItem      
Name: プロキシ, ClassName: TextBlock
Name: , ClassName: LandmarkTarget
Name: プロキシ, ClassName: TextBlock
Name: , ClassName: ScrollViewer
Name: 自動プロキシ セットアップ, ClassName: GroupItem
Name: 自動プロキシ セットアップ, ClassName: TextBlock
Name: イーサネットまたは Wi-Fi 接続にプロキシ サーバーを使います。これらの設定は、VPN 接続には適用されません。, ClassName: TextBlock        
Name: 設定を自動的に検出する, ClassName: ToggleSwitch
Name: 設定を自動的に検出する, ClassName: TextBlock
Name: セットアップ スクリプトを使う, ClassName: ToggleSwitch
Name: セットアップ スクリプトを使う, ClassName: TextBlock
Name: スクリプトのアドレス, ClassName: TextBlock
Name: スクリプトのアドレス, ClassName: TextBox
Name: 保存, ClassName: Button
Name: 手動プロキシ セットアップ, ClassName: GroupItem
Name: 手動プロキシ セットアップ, ClassName: TextBlock
Name: イーサネットまたは Wi-Fi 接続にプロキシ サーバーを使います。これらの設定は、VPN 接続には適用されません。, ClassName: TextBlock        
Name: プロキシ サーバーを使う, ClassName: ToggleSwitch
Name: プロキシ サーバーを使う, ClassName: TextBlock
Name: アドレス, ClassName: TextBlock
Name: アドレス, ClassName: TextBox
Name: ポート, ClassName: TextBlock
Name: ポート, ClassName: TextBox
Name: 次のエントリで始まるアドレス以外にプロキシ サーバーを使います。エントリを区切るにはセミコロン (;) を使います。, ClassName: TextBlock  
Name: 次のエントリで始まるアドレス以外にプロキシ サーバーを使います。エントリを区切るにはセミコロン (;) を使います。, ClassName: TextBox    
Name: ローカル (イントラネット) のアドレスにはプロキシ サーバーを使わない, ClassName: CheckBox
Name: ローカル (イントラネット) のアドレスにはプロキシ サーバーを使わない, ClassName: TextBlock
Name: 保存, ClassName: Button
Name: ヘルプを表示, ClassName: GroupItem
Name: ヘルプを表示, ClassName: Hyperlink
Name: ヘルプを表示, ClassName: TextBlock
Name: フィードバックの送信, ClassName: GroupItem
Name: フィードバックの送信, ClassName: Hyperlink
Name: フィードバックの送信, ClassName: TextBlock
Name: 垂直, ClassName: ScrollBar
Name: , ClassName: ApplicationFrameInputSinkWindow

上記の結果から、以下の情報が読み取れます。

Name: プロキシ サーバーを使う, ClassName: ToggleSwitch
Name: プロキシ サーバーを使う, ClassName: TextBlock

ここから「Nameがプロキシ サーバーを使うかつ、ClassNameがToggleSwitch」となっているものを探索すればよいことが分かります。

先程と同様に、探索後の押下処理において、ToggleSwitchの切り替えには、TogglePatternというパターンからコントローラーを取得し、Toggleメソッドを用いる必要があります。

上記をもとに実装した結果は以下のようになります。

# 「Nameがプロキシ サーバーを使うかつ、ClassNameがToggleSwitch」となっているものを探索
$namePropertyCondition = New-Object $propertyCondition($uiAutomation::NameProperty, "プロキシ サーバーを使う")
$classNamePropertyCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, "ToggleSwitch")
$condition = New-Object $andCondition($namePropertyCondition, $classNamePropertyCondition)
$proxyStatusItem = $app.FindFirst($tree::Subtree, $condition)
# 以下、切り替え処理
$toggleSwitch = $proxyStatusItem.GetCurrentPattern($togglePattern) # TogglePatternからコントローラーを取得
$toggleSwitch.Toggle() # Toggleメソッドの呼び出し

Step3:画面下部にある「保存」を押下

今回で、探索処理は最後となります。ただ、複数対象がある場合の例となるので、これまで通り順を追って確認します。

先に示した探索コードは、余計な情報が多く出てくるかつ、今回の探索対象は「保存」ボタンであることが分かっているため、以下のように修正します。

ここで、Name、ClassName、IsEnabled以外の属性(attribute)を知りたい場合は$currentをそのまま出力してください。

$condition = New-Object $propertyCondition($uiAutomation::NameProperty, "保存")
$app.FindAll($tree::Subtree, $condition ) | forEach {
    $current = $_.Current
    Write-Host ("Name: {0}, ClassName: {1}, IsEnabled: {2}" -f $current.Name, $current.ClassName, $current.IsEnabled)
    # $current を表示したい場合は、以下のコメントを外す
    #Write-Host $current
}

上記を実行すると以下のような結果が表示されます。

Name: 保存, ClassName: Button, IsEnabled: False
Name: 保存, ClassName: Button, IsEnabled: True

この違いは、画面上に表示される結果が下記のようになっているためです。

Step3における取得結果の違い

今回は、下側のボタンが対象となるため、該当するデータのうちIsEnabled=$trueとなっているものを抽出することになります。

以上より、押下対象のボタンを探索する処理は以下のようになります。

$namePropertyCondition = New-Object $propertyCondition($uiAutomation::NameProperty, "保存")
$classNamePropertyCondition = New-Object $propertyCondition($uiAutomation::ClassNameProperty, "Button")
$condition = New-Object $andCondition($namePropertyCondition, $classNamePropertyCondition)
$saveButton = ($app.FindAll($tree::Subtree, $condition) | Where-Object {$_.Current.IsEnabled}) # FindAllにより条件に合致する要素をすべて取得。そして、Where-Objectによりフィルタリング

また、ボタン押下時はInvokePatternというパターンからコントローラーを取得し、Invokeメソッドを用いて押下処理を行います。

該当する処理は以下のようになります。

$invokeButton = $saveButton.GetCurrentPattern($invokePattern) # InvokePatternからコントローラーを取得
$invokeButton.Invoke() # Invokeメソッドの呼び出し

Step4:「プロキシ サーバーを使う」のトグルスイッチを押下

Step2でToggleSwitchのコントローラーを取得しているので、以下のコマンドを実行するだけとなります。

$toggleSwitch.Toggle()

Step5:「設定」の画面をクローズ

最後は、最初に開いた「設定」画面をクローズします。

これは、プロセスの終了により実現できます。Start-Processでプロセスを起動したため、Stop-Processでプロセスを終了します。

この時の注意点として、プログラムで制御するときよりも早く人手でクローズした場合、すでに終了しようとしているプロセスに対し、プロセスを終了させようとするため、エラーが発生します。

あまり好ましい扱いではないですが、ここでは、-ErrorAction Ignoreを付与することで、エラーを無視するという対応をしております。

ErrorActionの詳細オプションは、コチラのページで紹介されています。

UIAutomationを用いてGUI操作も自動化しよう!

実装結果は、GitHubに格納しています。先に、実装を見たい方は、コチラを参照してください。

今回の結論は、以下のようになります。

  • UIAutomationを使うとマウスやキーボードを操作せずに、プログラムからGUIの操作ができる。
  • 「設定→ネットワークとインターネット」で表示される画面に対し、簡単なGUI操作を行う事例を紹介する。
  • デスクトップに展開されているGUIアプリ一覧は、コマンド①で取得できる。
  • 制御対象のGUIアプリを構成する要素一覧は、コマンド②で取得できる。
  • ListViewItem、ToggleSwitch、Buttonは、それぞれ以下のPatternを指定し、関係するメソッドを呼び出すことで、状態を更新できる。
    • ListViewItem:SelectionItemPattern、Selectメソッド
    • ToggleSwitch:TogglePattern、Toggleメソッド
    • Button:InvokePattern、Invokeメソッド
  • 期待通りに動作していることを確認した。今後は、具体的なユースケースを想定した上で、細かな機能について紹介する。

コマンド①

$root.FindAll($tree::Children, [System.Windows.Automation.Condition]::TrueCondition) | forEach {
    $current = $_.Current
    Write-Host ("Name: {0}, ClassName: {1}" -f $current.Name, $current.ClassName)
}

コマンド②

前提条件:コマンド①で見つけたデータをもとに、制御対象を$app変数に保持していること。

$app.FindAll($tree::Subtree, [System.Windows.Automation.Condition]::TrueCondition) | forEach {
    $current = $_.Current
    Write-Host ("Name: {0}, ClassName: {1}" -f $current.Name, $current.ClassName)
}

スポンサードリンク



-プログラミング
-,