「毎日、手作業で同じアプリを開いて、同じボタンをクリックして、別のアプリに転記するだけの不毛な作業がある…」
「このルーティンワークを、どうにか自動化して効率化できないか?」
そんな風に思ったことはありませんか?
Excelやブラウザの自動化(RPA)はよく見かけますが、会社独自の社内システムや、Windowsのデスクトップアプリの自動化となると、急に難易度が上がって見えます。
しかし「Windowsが画面をどう認識しているか」という大元の仕組みさえ分かってしまえば、WindowsのUIオートメーションに対応したアプリであれば、画面上のボタンや入力欄などをプログラムから取得し、自動操作できる場合があります。
この記事では、Windows標準の「電卓」を使って、画面全体から目当てのボタンを絞り込み、クリックするまでの手順をステップ形式で解説します。
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.ClassNamePowerShellの仕様で、ダブルクォーテーション( "")の中ではプロパティ(後ろの「.Current.Name」の部分)がうまく展開できないためです。
つまり「$RootElement」は「System.Windows.Automation.AutomationElement」となりますが、その後ろの「.Current.ClassName」が、そのまま「.Current.ClassName」という文字列として認識されているからです。
- $RootElement:System.Windows.Automation.AutomationElementとして展開
- .Current.ClassName:文字列として表示
正しく中身を表示させたいときは、全体を 「$()」 で囲みます。
Write-Host "$($RootElement.Current.ClassName)"ステップ2:直下の子ども(アプリのウィンドウ)を一覧表示する
大元(デスクトップ)が取得できたので、次は「1階層だけ下」の要素(Children)を取得してみましょう。
事前準備
パソコンの「電卓」アプリを起動しておいてください。
「Windowsキー+Rキー」で「ファイル名を指定して実行」を開き「calc」と入力することで、電卓を起動することができます。

実行するコード
次のコードを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」を使用しました。
- AutomationId(固有ID):アプリ側で設定されている場合、安定した識別子として利用できます。ただし、動的に生成されることもあり、起動のたびに変わる場合もあります。
- Name(名前):ボタンの表面に書かれている「7」や「保存」といったテキストです。
- ControlType(パーツの種類・型):その部品が「ボタン(Button)」なのか「入力欄(Document / Edit)」なのかという役割です。名前と組み合わせることで「同名の関係ないラベル」を誤操作するのを防ぎます。
自動化のワークフローは、どんなに複雑なシステムになっても基本的には同じです。
「ルート(デスクトップ)取得 ➡ アプリ枠の特定 ➡ 内部の要素を特定 ➡ 信号送信(クリックや文字入力)」 です。
この4ステップを軸に、ぜひ明日のルーティンワークの自動化にチャレンジしてみてください。



