【PowerShell】Windowsアプリ自動化入門!電卓操作で学ぶUIオートメーションの基本

geralt-keyboard プログラミング

「毎日、手作業で同じアプリを開いて、同じボタンをクリックして、別のアプリに転記するだけの不毛な作業がある…」

「このルーティンワークを、どうにか自動化して効率化できないか?」

そんな風に思ったことはありませんか?

Excelやブラウザの自動化(RPA)はよく見かけますが、会社独自の社内システムや、Windowsのデスクトップアプリの自動化となると、急に難易度が上がって見えます。

しかし「Windowsが画面をどう認識しているか」という大元の仕組みさえ分かってしまえば、WindowsのUIオートメーションに対応したアプリであれば、画面上のボタンや入力欄などをプログラムから取得し、自動操作できる場合があります。

この記事では、Windows標準の「電卓」を使って、画面全体から目当てのボタンを絞り込み、クリックするまでの手順をステップ形式で解説します。

UIオートメーションの基本的な使い方を一緒に学んでいきましょう。

UIオートメーションをまずは試してみたい!というときは次の記事をチェック

【初心者向け】PowerShellでUI自動化!インストール不要のRPA入門
追加ツールは一切不要!Windows標準のPowerShellを使い、初心者でも5分でできるUI自動化(UIオートメーション)のチュートリアルを現役エンジニアが解説。メリット・デメリットから堅牢なコードの書き方、エラー対策まで網羅した決定版です。

準備編:プログラミングの「最初のおまじない」

自動化を始める前に、PowerShell ISEを開いて、Windowsの画面をコントロールするためのライブラリ(機能)を読み込みます。

PowerShell ISEの起動方法は次の記事を参考にしてください。

PowerShellの基本!!フォルダーとファイルの操作
PowerShellとはPowerShellは、Windowsを効率的に操作するためのコマンドラインシェル、スクリプト言語です。Windowsにはプレインストールされており、従来のコマンドプロンプトよりも強力で、Windowsの管理の自動化…

次の1行を、PowerShell ISEの画面に貼り付けてください。

Add-Type -AssemblyName UIAutomationClient

これで、Windowsが持つUI自動化システム(UIオートメーション)を使うための準備が整いました。

【重要概念】UIオートメーションの正体は「巨大なツリー構造」

Windowsは、画面に映るすべてのパーツを「大きなツリー構造」のように管理しています。

ボタンも、入力欄も、アプリのウィンドウも、すべてが親子関係で結ばれています。

そして、ツリー構造の一番大元にあたるのが「パソコンのデスクトップ画面」です。

これをプログラミングの世界では「ルート(Root)」と呼びます。

これから行う自動化の作業は、この「デスクトップ(ルート)」から、ツリー構造の枝葉をたどって「目的のボタンや入力欄」を検索して見つけていきます。

ステップ1:大元(ルートエレメント)を捕まえる

最初のステップです。

ツリー構造の頂点である「デスクトップ」を取得して、Windowsからどう見えているかを確認してみます。

📋 実行するコード

次のコードをPowerShell ISEに貼り付けて実行してみましょう。

Add-Type -AssemblyName UIAutomationClient

# 大元となる「ルート(画面全体)」を取得して変数に入れる
$RootElement = [Windows.Automation.AutomationElement]::RootElement

# 取得した大元の情報を画面に表示してみる
Write-Host "◆ 表示名 (Name)       : " -NoNewline; Write-Host "$($RootElement.Current.Name)" -ForegroundColor Yellow
Write-Host "◆ クラス名 (ClassName): " -NoNewline; Write-Host "$($RootElement.Current.ClassName)" -ForegroundColor Yellow

実行すると、次のように表示されました。

◆ 表示名 (Name)       : 
◆ クラス名 (ClassName): #32769

クラス名は「#32769」となっていますが、これがデスクトップウィンドウになります。

コードにある「.Current」ってなに?

UI オートメーションでは、取得した要素(ボタンやウィンドウ)のプロパティ(名前や種類など)を見るときに、「いま現在の最新状態(Current)」のデータ を指定してアクセスする仕組みになっています。

  • $RootElement.Current.Name = ルート要素の現在の「表示名」
  • $RootElement.Current.ClassName = ルート要素の現在の「クラス名」

という意味になります。

要素の情報を出すときは「 .Current.〇〇 」というように書きます。

PowerShellの文字列

画面に文字を表示するとき「Write-Host “$RootElement.Current.ClassName”」 と書いて実行すると、次のように表示されます。

System.Windows.Automation.AutomationElement.Current.ClassName

PowerShellの仕様で、ダブルクォーテーション( "")の中ではプロパティ(後ろの「.Current.Name」の部分)がうまく展開できないためです。

つまり「$RootElement」は「System.Windows.Automation.AutomationElement」となりますが、その後ろの「.Current.ClassName」が、そのまま「.Current.ClassName」という文字列として認識されているからです。

Write-Host “$RootElement.Current.ClassName”の場合
  • $RootElement:System.Windows.Automation.AutomationElementとして展開
  • .Current.ClassName:文字列として表示

正しく中身を表示させたいときは、全体を 「$()」 で囲みます。

Write-Host "$($RootElement.Current.ClassName)"

ステップ2:直下の子ども(アプリのウィンドウ)を一覧表示する

大元(デスクトップ)が取得できたので、次は「1階層だけ下」の要素(Children)を取得してみましょう。

事前準備

パソコンの「電卓」アプリを起動しておいてください。

「Windowsキー+Rキー」で「ファイル名を指定して実行」を開き「calc」と入力することで、電卓を起動することができます。

【保存版】ファイル名を指定して実行(Win+R)で作業効率化!Windowsの便利コマンド大全
「ファイル名を指定して実行(Win+R)」の基本から、コマンド実行の仕組み、履歴の保存場所と削除方法まで初心者にもわかりやすく解説!

実行するコード

次のコードをPowerShell ISEに貼り付けて実行してみましょう。

Add-Type -AssemblyName UIAutomationClient

# 大元となる「ルート(画面全体)」を取得して変数に入れる
$RootElement = [Windows.Automation.AutomationElement]::RootElement

Write-Host "==============================================" -ForegroundColor Cyan
Write-Host " ステップ2:『直下の子要素(Children)』一覧" -ForegroundColor Cyan
Write-Host "==============================================" -ForegroundColor Cyan

# 範囲を「Children(子ども限定)」にして、条件なし(TrueCondition)で取得
$ChildElements = $RootElement.FindAll(
    [Windows.Automation.TreeScope]::Children, 
    [Windows.Automation.Condition]::TrueCondition
)

# 見つかった子要素をループ処理で一覧表示
$Count = 1
foreach ($Child in $ChildElements) {
    Write-Host " [#$Count] 型: " -NoNewline
    Write-Host ("{0,-10}" -f $Child.Current.LocalizedControlType) -ForegroundColor Cyan -NoNewline
    Write-Host " | 名前: " -NoNewline
    Write-Host "'$($Child.Current.Name)'" -ForegroundColor Yellow
    $Count++
}

上記コードの次の部分で、大元(デスクトップ)の一階層だけ下の要素を取得し、変数「$ChildElements」に格納しています。

# 範囲を「Children(子ども限定)」にして、条件なし(TrueCondition)で取得
$ChildElements = $RootElement.FindAll(
    [Windows.Automation.TreeScope]::Children, 
    [Windows.Automation.Condition]::TrueCondition
)

TreeScope と TrueCondition ってなに?

[Windows.Automation.TreeScope]::Children は「検索する範囲」を指定します。

Children(子ども)を指定することで、孫やひ孫まで探しに行かず、「デスクトップのすぐ直下(1階層下)」だけを探すようにしています。

[Windows.Automation.Condition]::TrueCondition は「検索する条件」を指定しています。

TrueCondition は、「どの要素でも条件を満たす」と判定されるので、すべての要素を取得しています。

実行すると、何が見える?

次のようにデスクトップの一つ下の階層の要素が表示されます。以下は一例です。

==============================================
 ステップ2:『直下の子要素(Children)』一覧
==============================================
 [#1] 型: ウィンドウ      | 名前: ''
 [#2] 型: ウィンドウ      | 名前: ''
 [#3] 型: ウィンドウ      | 名前: 'Windows PowerShell メイン ウィンドウ'
 [#4] 型: ウィンドウ      | 名前: 'UI オートメーションの概要 - .NET Framework | Microsoft Learn - Google Chrome'
 [#5] 型: ウィンドウ      | 名前: '電卓'
 [#6] 型: ウィンドウ      | 名前: 'Program Manager'

そのリストの中に「型:ウィンドウ | 名前:'電卓' という行が見つかれば成功です。

ステップ3:目当ての「電卓」アプリを指定して取得する

デスクトップに並ぶアプリの中から、今回の対象である「電卓」アプリを特定して、取得します。

これから書くコードのポイント

ステップ2で取得したアプリの中から、名前(Name)が「電卓」 かつ 種類(ControlType)が「ウィンドウ」 であるものを探して取得します。

実行するコード

Add-Type -AssemblyName UIAutomationClient

# 大元となる「ルート(画面全体)」を取得して変数に入れる
$RootElement = [Windows.Automation.AutomationElement]::RootElement

# 範囲を「Children(子ども限定)」にして、条件なし(TrueCondition)で取得
$ChildElements = $RootElement.FindAll(
    [Windows.Automation.TreeScope]::Children, 
    [Windows.Automation.Condition]::TrueCondition
)

Write-Host "==============================================" -ForegroundColor Cyan
Write-Host " ステップ3:電卓アプリ(ウィンドウ)の取得" -ForegroundColor Cyan
Write-Host "==============================================" -ForegroundColor Cyan

# ステップ2で取得した一覧の中から「電卓」を探して変数に取得
$TargetApp = $null
foreach ($Element in $ChildElements) {
    if ($Element.Current.ControlType.ProgrammaticName -eq "ControlType.Window" -and $Element.Current.Name -eq "電卓") {
        $TargetApp = $Element
        break
    }
}

# 電卓が見つかったかどうかのチェック
if ($TargetApp -eq $null) {
    Write-Host "【エラー】電卓アプリが見つかりません。起動してからもう一度試してください。" -ForegroundColor Red
    exit
}

# 成功したら画面に表示
Write-Host "成功:目的のアプリ枠を確保しました!" -ForegroundColor Green
Write-Host "◆ アプリ名: $($TargetApp.Current.Name)"
Write-Host "◆ クラス名: $($TargetApp.Current.ClassName)"

上記コードの以下の部分が「電卓」を取得するコードになります。

取得した要素を一つ一つ「コントロールタイプがウィンドウか?」、「名前が電卓か?」をチェックし、合致するものを変数「$TargetApp」に格納しています。

# ステップ2で取得した一覧の中から「電卓」を探して変数にセット
$TargetApp = $null
foreach ($Element in $ChildElements) {
    if ($Element.Current.ControlType.ProgrammaticName -eq "ControlType.Window" -and $Element.Current.Name -eq "電卓") {
        $TargetApp = $Element
        break
    }
}

なぜ ControlType もチェックするの?

コードの中で「 $Element.Current.ControlType.ProgrammaticName -eq “ControlType.Window” 」という条件を入れています。

これは「名前が電卓であること」に加えて「それがちゃんと『ウィンドウ(アプリ枠)』であること」 をダブルチェックするためです。

Nameだけで検索すると、同じ名前を持つ別のUI要素まで候補になる可能性があります。

そこでControlTypeも組み合わせ「名前が電卓で、なおかつウィンドウである要素」に絞り込んでいます。

実行すると、何が見える?

電卓が正しく起動していれば次のように成功メッセージが表示されます。

==============================================
 ステップ3:電卓アプリ(ウィンドウ)の特定
==============================================
成功:目的のアプリ枠を確保しました!
◆ アプリ名: 電卓
◆ クラス名: ApplicationFrameWindow

これで「電卓アプリ全体」をしっかりとプログラムで捕まえることができました。

ステップ4:電卓の中身(パーツ)を探索する

「電卓」が取得できたので、次はいよいよ電卓の内側を見てみます。

電卓の中に入っているボタンやテキスト枠を取得して、その中から目的の「7」ボタンを特定します。

実行するコード

Add-Type -AssemblyName UIAutomationClient

# 大元となる「ルート(画面全体)」を取得して変数に入れる
$RootElement = [Windows.Automation.AutomationElement]::RootElement

# 範囲を「Children(子ども限定)」にして、条件なし(TrueCondition)で取得
$ChildElements = $RootElement.FindAll(
    [Windows.Automation.TreeScope]::Children, 
    [Windows.Automation.Condition]::TrueCondition
)

# ステップ2で取得した一覧の中から「電卓」を探して変数にセット
$TargetApp = $null
foreach ($Element in $ChildElements) {
    if ($Element.Current.ControlType.ProgrammaticName -eq "ControlType.Window" -and $Element.Current.Name -eq "電卓") {
        $TargetApp = $Element
        break
    }
}

# 電卓が見つかったかどうかのチェック
if ($TargetApp -eq $null) {
    Write-Host "【エラー】電卓アプリが見つかりません。起動してからもう一度試してください。" -ForegroundColor Red
    exit
}

Write-Host "==============================================" -ForegroundColor Cyan
Write-Host " ステップ4:電卓内部の探索" -ForegroundColor Cyan
Write-Host "==============================================" -ForegroundColor Cyan

# 電卓の中にあるすべてのパーツ(子孫:Descendants)をまとめて取得
$AppChildren = $TargetApp.FindAll(
    [Windows.Automation.TreeScope]::Descendants,
    [Windows.Automation.Condition]::TrueCondition
)

# 電卓の中にどんなパーツがあるのか抜粋表示(最初の10個)
Write-Host "--- 電卓内部のパーツ一覧 ---" -ForegroundColor Gray
$ItemCount = 1
foreach ($Child in $AppChildren) {
    Write-Host " [#$ItemCount] 型: " -NoNewline
    Write-Host ("{0,-10}" -f $Child.Current.LocalizedControlType) -ForegroundColor Cyan -NoNewline
    Write-Host " | 名前: '$($Child.Current.Name)'" -ForegroundColor Yellow
    $ItemCount++
}
Write-Host "--------------------------------------------------`n" -ForegroundColor Gray

電卓の中の要素を取得するときのFindAll()メソッドの [Windows.Automation.TreeScope] で「Descendants」を指定しています。

これは、要素の孫やひ孫まで取得するときに使います。

  • Children(直下の子ども): 1階層だけ下を探す(ステップ2・3でアプリの「外枠」を探すときに使用)。
  • Descendants(配下の全子孫): 孫やひ孫まで含めて探す(ステップ4でアプリ内部の「細かなボタン」を探すときに使用)。

実行すると、何が見える?

実行すると、次のように電卓内のパーツが表示されます。

==============================================
 ステップ4:電卓内部の探索
==============================================
--- 電卓内部のパーツ一覧 ---
 [#1] 型:            | 名前: '電卓'
 [#2] 型: メニュー バー    | 名前: 'システム'
 [#3] 型: メニュー項目     | 名前: 'システム'
 [#4] 型: ボタン        | 名前: '電卓 の最小化'
 [#5] 型: ボタン        | 名前: '電卓 を最大化する'
 [#6] 型: ボタン        | 名前: '電卓 を閉じる'
 [#7] 型: ウィンドウ      | 名前: '電卓'
 [#8] 型: text       | 名前: '電卓'
 [#9] 型: custom     | 名前: ''
 [#10] 型: button     | 名前: 'ナビゲーションを開く'
         (中略)
 [#66] 型: button     | 名前: '7'
 [#67] 型: button     | 名前: '8'
 [#68] 型: button     | 名前: '9'
 [#69] 型: button     | 名前: '小数点'
 [#70] 型: group      | 名前: '履歴とメモリの一覧'
 [#71] 型: tab        | 名前: ''
 [#72] 型: tab item   | 名前: 'まだ履歴がありません。 履歴'
 [#73] 型: text       | 名前: 'まだ履歴がありません。'
 [#74] 型: tab item   | 名前: 'メモリに何も保存されていません。 メモリ'
 [#75] 型: text       | 名前: '関数電卓 電卓モード'
 [#76] 型: ウィンドウ      | 名前: ''
--------------------------------------------------

ステップ5:目当ての「7」のボタンを特定する

電卓のパーツから目的の「7」のボタンがあったことが確認できたので、「7」のボタンを取得してみましょう。

📋 実行するコード

Add-Type -AssemblyName UIAutomationClient

# 大元となる「ルート(画面全体)」を取得して変数に入れる
$RootElement = [Windows.Automation.AutomationElement]::RootElement

# 範囲を「Children(子ども限定)」にして、条件なし(TrueCondition)で取得
$ChildElements = $RootElement.FindAll(
    [Windows.Automation.TreeScope]::Children, 
    [Windows.Automation.Condition]::TrueCondition
)

# ステップ2で取得した一覧の中から「電卓」を探して変数にセット
$TargetApp = $null
foreach ($Element in $ChildElements) {
    if ($Element.Current.ControlType.ProgrammaticName -eq "ControlType.Window" -and $Element.Current.Name -eq "電卓") {
        $TargetApp = $Element
        break
    }
}

# 電卓が見つかったかどうかのチェック
if ($TargetApp -eq $null) {
    Write-Host "【エラー】電卓アプリが見つかりません。起動してからもう一度試してください。" -ForegroundColor Red
    exit
}

# 電卓の中にあるすべてのパーツ(子孫:Descendants)をまとめて取得
$AppChildren = $TargetApp.FindAll(
    [Windows.Automation.TreeScope]::Descendants,
    [Windows.Automation.Condition]::TrueCondition
)

Write-Host "==============================================" -ForegroundColor Cyan
Write-Host " ステップ5:『7』ボタンの取得" -ForegroundColor Cyan
Write-Host "==============================================" -ForegroundColor Cyan

# パーツ一覧の中から「名前が『7』」かつ「種類が『ボタン』」を探す
$TargetButton = $null
foreach ($Child in $AppChildren) {
    if ($Child.Current.ControlType.ProgrammaticName -eq "ControlType.Button" -and $Child.Current.Name -eq "7") {
        $TargetButton = $Child
        break
    }
}

if ($TargetButton -eq $null) {
    Write-Host "【エラー】電卓の中に『7』のボタンが見つかりませんでした。" -ForegroundColor Red
    exit
}

Write-Host "目的のパーツを捕捉 : '$($TargetButton.Current.Name)' ボタンをロックオン!" -ForegroundColor Green

実行すると、何が見える?

コードを実行すると、次のように表示されます。

==============================================
 ステップ5:『7』ボタンの取得
==============================================
目的のパーツを捕捉 : '7' ボタンをロックオン!

これで「画面全体」→「電卓アプリ」→「7のボタン」と段階を踏んで狙い通りの要素までたどり着くことができました。

ステップ6:取得した「7」のボタンをクリックする

「7」のボタンが取得できたので、ボタンをクリックしてみます。

UIオートメーションでボタンをクリックする場合、マウスを動かすのではなく「ボタンに直接『クリックされた』という信号を送る」という方法を使います。

📋 実行するコード

Add-Type -AssemblyName UIAutomationClient

# 大元となる「ルート(画面全体)」を取得して変数に入れる
$RootElement = [Windows.Automation.AutomationElement]::RootElement

# 範囲を「Children(子ども限定)」にして、条件なし(TrueCondition)で取得
$ChildElements = $RootElement.FindAll(
    [Windows.Automation.TreeScope]::Children, 
    [Windows.Automation.Condition]::TrueCondition
)

# ステップ2で取得した一覧の中から「電卓」を探して変数にセット
$TargetApp = $null
foreach ($Element in $ChildElements) {
    if ($Element.Current.ControlType.ProgrammaticName -eq "ControlType.Window" -and $Element.Current.Name -eq "電卓") {
        $TargetApp = $Element
        break
    }
}

# 電卓が見つかったかどうかのチェック
if ($TargetApp -eq $null) {
    Write-Host "【エラー】電卓アプリが見つかりません。起動してからもう一度試してください。" -ForegroundColor Red
    exit
}

# 電卓の中にあるすべてのパーツ(子孫:Descendants)をまとめて取得
$AppChildren = $TargetApp.FindAll(
    [Windows.Automation.TreeScope]::Descendants,
    [Windows.Automation.Condition]::TrueCondition
)

# パーツ一覧の中から「名前が『7』」かつ「種類が『ボタン』」を探す
$TargetButton = $null
foreach ($Child in $AppChildren) {
    if ($Child.Current.ControlType.ProgrammaticName -eq "ControlType.Button" -and $Child.Current.Name -eq "7") {
        $TargetButton = $Child
        break
    }
}

if ($TargetButton -eq $null) {
    Write-Host "【エラー】電卓の中に『7』のボタンが見つかりませんでした。" -ForegroundColor Red
    exit
}

Write-Host "==============================================" -ForegroundColor Cyan
Write-Host " ステップ6:特定したパーツを自動操作します" -ForegroundColor Cyan
Write-Host "==============================================" -ForegroundColor Cyan

# ボタンをクリックするための専用機能(InvokePattern)を呼び出す
$InvokePattern = $TargetButton.GetCurrentPattern([Windows.Automation.InvokePattern]::Pattern)

Write-Host "➡ 『7』のボタンを自動クリックします!" -ForegroundColor Yellow

# 信号を送信!
$InvokePattern.Invoke()

Write-Host "【成功】自動クリックが完了しました。電卓を確認してください!" -ForegroundColor Green

コードを実行すれば、電卓の画面に「7」と入力されたはずです。

Pattern(パターン) と Invoke(インボーク)

Windowsのパーツ(要素)には、それぞれできる操作が決まっています。

ボタンなら「押す(Invoke)」、入力欄なら「文字を書く(Value)」、チェックボックスなら「切り替える(Toggle)」といった具合です。

「GetCurrentPattern([Windows.Automation.InvokePattern]::Pattern)」で、ターゲットのパーツから「クリック操作を行うための専用リモコン」を取り出す処理を行っています。

その後「$InvokePattern.Invoke()」で、取り出したリモコンのボタンを押しています。

まとめ:自分で新しいアプリを自動化するための「3大プロパティ」

今回、電卓のパーツを検索する手がかりとして使ったのが、Windowsが各パーツに割り振っている「プロパティ」です。

社内システムなどを自動化するときは、各パーツの「3大プロパティ」に注目して探してみてください。

今回の記事では「Name+ControlType」を使用しました。

  1. AutomationId(固有ID):アプリ側で設定されている場合、安定した識別子として利用できます。ただし、動的に生成されることもあり、起動のたびに変わる場合もあります。
  2. Name(名前):ボタンの表面に書かれている「7」や「保存」といったテキストです。
  3. ControlType(パーツの種類・型):その部品が「ボタン(Button)」なのか「入力欄(Document / Edit)」なのかという役割です。名前と組み合わせることで「同名の関係ないラベル」を誤操作するのを防ぎます。

自動化のワークフローは、どんなに複雑なシステムになっても基本的には同じです。

「ルート(デスクトップ)取得 ➡ アプリ枠の特定 ➡ 内部の要素を特定 ➡ 信号送信(クリックや文字入力)」 です。

この4ステップを軸に、ぜひ明日のルーティンワークの自動化にチャレンジしてみてください。

AIで仕事を効率化する方法|初心者でも今日からできる活用術
AIで仕事を効率化する方法を初心者向けに解説します。AIは完璧な成果物ではなく「60点の下書き」を作らせるのがコツ。メール作成を例に、プロンプトの書き方や業務自動化の始め方、活用時の注意点まで分かりやすく紹介します。
タイトルとURLをコピーしました