1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

マウスをかざすだけでRPAコードを自動生成する無料ツール(PowerShellとUI Automationで作成)

1
Last updated at Posted at 2026-08-10

はじめに

業務効率化のためにRPAを導入したいけれど、高価なツールは買えないし、環境構築も面倒……。そんな悩みを解決するために、Windowsに標準搭載されている PowerShellUI Automation を組み合わせて、「完全無料のRPAコード自動生成ツール」を作成しました。

本ツールを実行すると、操作したい画面のボタンや入力欄にマウスをかざしてキーを押すだけで、UI要素の情報を取得し、自動でRPA実行用のPowerShellスクリプトを生成 してクリップボードにコピーしてくれます。

注意
  • プログラムの実行については、すべて自己責任で行ってください。実行により発生した、いかなる直接的または間接的被害について、作者はその責任を負いません。
  • テスト環境で動作確認をしてください。
  • ブラウザで使う場合は通信先のサーバーへの負荷、サイトポリシーに留意してください。
  • 静的なサイトで主に実施しているため、未知の状況によってエラーが発生する可能性があります。

【概要の動画】

【ツールの使い方】

ツールの特徴

  1. 環境構築不要: Windows標準の機能(PowerShell 5.1以上 & .NET Framework)だけで動作します。
  2. 直感的な記録: マウスをかざして ShiftEnter を押すだけで、クリック、テキスト入力、値の取得などのアクションを判定・記録します。
  3. 可変要素への対応: 実行時にIDや名前が変わってしまう要素でも、「固定のテキスト(アンカー)」を基準にした相対位置で記録する機能を搭載しています。

ソースコード

以下のコードを Generate-RPA.ps1 などの名前で保存し、PowerShellから実行してください。


# ==============================================================================
# UI Automation RPAコード自動生成ツール 
# ==============================================================================

# [アセンブリ読み込み] UI操作自動化(UI Automation)やダイアログ表示(VisualBasic)、フォーム制御に必要なDLLをロードします
# ツール実行に必要な.NETのライブラリ群をPowerShellセッションに読み込みます
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes, System.Windows.Forms, WindowsBase, Microsoft.VisualBasic

# --- Windowsフォームの見た目をモダン(OS標準)スタイルにする ---
# 生成するダイアログフォームのUIをOSの標準的なビジュアルスタイルに設定します
[System.Windows.Forms.Application]::EnableVisualStyles()
# ----------------------------------------------------------------------

# [条件分岐] キーボード入力をバックグラウンド検出するC#クラス(KeyboardHelper_V3)が未定義の場合のみ定義処理に入ります
# 現在のセッションに 'KeyboardHelper_V3' というクラスが存在しないか判定します
if (-not ([System.Management.Automation.PSTypeName]'KeyboardHelper_V3').Type) {
    # C#のコードを文字列(ヒアドキュメント)として変数に格納します
    $csharpCode = @"
    // Systemネームスペースをインポートします
    using System;
    // DllImportを使用するためにInteropServicesをインポートします
    using System.Runtime.InteropServices;
    // キーボード入力を監視するためのクラスを公開します
    public class KeyboardHelper_V3 {
        // user32.dllから、指定したキーが押されているか判定する関数をインポートします
        [DllImport("user32.dll")]
        public static extern short GetAsyncKeyState(int vKey);
    }
"@
    # 変数に格納したC#コードをコンパイルし、PowerShellで使用できる型として追加します。エラー時は処理を停止します。
    Add-Type -TypeDefinition $csharpCode -ErrorAction Stop
}

# ==============================================================================
# [定数定義] マジックナンバーの定数化
# ==============================================================================
# Shiftキーの仮想キーコードを定義します(通常記録のトリガーとして使用)
$script:VK_SHIFT   = 0x10 # Shiftキー (通常記録)
# Enterキーの仮想キーコードを定義します(強制値取得モード・アンカー指定のトリガーとして使用)
$script:VK_RETURN  = 0x0D # Enterキー (強制GetNameモード記録・アンカー手動指定) 
# Ctrlキーの仮想キーコードを定義します(記録終了およびコード出力のトリガーとして使用)
$script:VK_CONTROL = 0x11 # Ctrlキー (出力・終了) 

# GetAsyncKeyState関数でキーが現在押されている状態を判定するためのビットマスクです
$script:KEY_PRESSED_MASK = 0x8000

# 待機・タイムアウト・リトライ回数関連
# メインの監視ループの待機時間(ミリ秒)を設定します
$script:MAIN_LOOP_INTERVAL_MS   = 100
# アンカー手動指定時のキー入力待ちの待機時間(ミリ秒)を設定します
$script:KEY_WAIT_INTERVAL_MS    = 50
# 要素取得時の最大リトライ回数を設定します
$script:ELEMENT_RETRY_MAX       = 3
# 要素取得が失敗した際の再試行までの待機時間(ミリ秒)を設定します
$script:ELEMENT_RETRY_WAIT_MS   = 300
# コンボボックスを展開した後に項目が表示されるまで待機する時間(ミリ秒)を設定します
$script:COMBO_EXPAND_WAIT_MS    = 300
# コードを生成したあと、ツールが終了するまでの待機時間(ミリ秒)を設定します
$script:CODE_GEN_DELAY_MS       = 100

# 探索上限関連
# 手動でアンカーを指定した際、対象要素を探す範囲(兄弟要素の最大ステップ数)を設定します
$script:MAX_MANUAL_ANCHOR_STEPS = 30
# 自動でアンカーを探す際、テキスト要素を探す範囲(兄弟要素の最大ステップ数)を設定します
$script:MAX_AUTO_ANCHOR_STEPS   = 10

# フォーム関連(コンボボックス選択用ダイアログ)
# 入力フォームの幅を設定します
$script:FORM_SIZE_W = 360
# 入力フォームの高さを設定します
$script:FORM_SIZE_H = 160
# 案内ラベルのX座標を設定します
$script:LBL_POS_X   = 15
# 案内ラベルのY座標を設定します
$script:LBL_POS_Y   = 15
# コンボボックスのX座標を設定します
$script:CMB_POS_X   = 15
# コンボボックスのY座標を設定します
$script:CMB_POS_Y   = 40
# コンボボックスの幅を設定します
$script:CMB_SIZE_W  = 310
# コンボボックスの高さを設定します
$script:CMB_SIZE_H  = 25
# OKボタンのX座標を設定します
$script:BTN_POS_X   = 135
# OKボタンのY座標を設定します
$script:BTN_POS_Y   = 80
# OKボタンの幅を設定します
$script:BTN_SIZE_W  = 75
# OKボタンの高さを設定します
$script:BTN_SIZE_H  = 25

# ==============================================================================
# [関数定義] メインループ内の処理を機能単位で分割
# ==============================================================================

# マウスカーソル位置のUI要素とウィンドウ情報を取得する関数の定義です
function Get-UIAutomationElementFromCursor {
    <# マウスカーソル位置のUIAutomation要素と、その親ウィンドウ情報を取得します #>
    # 現在のマウスカーソルの座標からPointオブジェクトを作成します
    $point = New-Object System.Windows.Point([System.Windows.Forms.Cursor]::Position.X, [System.Windows.Forms.Cursor]::Position.Y)
    # 対象要素を格納する変数を初期化します
    $targetElement = $null

    # 取得が失敗(Document全体が取れるなど)した場合に備えてリトライ上限までループします
    for ($retry = 1; $retry -le $script:ELEMENT_RETRY_MAX; $retry++) {
        # 指定した座標にあるUI要素を取得します
        $targetElement = [System.Windows.Automation.AutomationElement]::FromPoint($point)
        # 取得した要素がDocument(画面全体)でなければ正常取得とみなしループを抜けます
        if ($targetElement.Current.ControlType -ne [System.Windows.Automation.ControlType]::Document) { break }
        # Documentの場合は指定ミリ秒待機してから再度取得を試みます
        Start-Sleep -Milliseconds $script:ELEMENT_RETRY_WAIT_MS
    }

    # リトライしてもDocument全体しか取得できなかった場合の処理です
    if ($targetElement.Current.ControlType -eq [System.Windows.Automation.ControlType]::Document) {
        # ユーザーに警告メッセージを赤色で表示します(ウィンドウがアクティブでない可能性など)
        Write-Host "【警告】Document全体が取得されました。ブラウザ等をクリックしてアクティブにしてから再度記録してください。" -ForegroundColor Red
        # 取得失敗として$nullを返します
        return $null
    }

    # 親要素をたどるためのControlViewWalkerオブジェクトを取得します
    $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
    # 探索の開始点として現在の対象要素をセットします
    $current = $targetElement
    # 現在の要素がWindow(ウィンドウ)になるまで親階層をたどり続けるループです
    while ($current -ne $null -and $current.Current.ControlType -ne [System.Windows.Automation.ControlType]::Window) {
        # 1つ上の親要素を取得して$currentを更新します
        $current = $walker.GetParent($current)
    }
    
    # 記録用ウィンドウ名を格納する変数を初期化します
    $stepWindowName = ""
    # ウィンドウ要素を格納する変数を初期化します
    $targetWindow = $null
    # ウィンドウ要素が見つかった場合の処理です
    if ($current) {
        # ウィンドウ名に含まれる不要なページ数やプロファイル名(「および他 1 ページ」など)を正規表現で削除します
        $stepWindowName = $current.Current.Name `
            -replace "\s*および他\s*\d+\s*ページ.*$", "" `
            -replace "\s*-\s*(個人|仕事|InPrivate)(?=\s*-).*$", "" `
            -replace "\s*-[^-]+$", ""
        
        # 不要部分を削除したウィンドウ要素を変数に格納します
        $targetWindow = $current
    }

    # ターゲット要素、ターゲットウィンドウ、ウィンドウ名の3つの情報をハッシュテーブルで返却します
    return @{
        TargetElement = $targetElement
        TargetWindow  = $targetWindow
        WindowName    = $stepWindowName
    }
}

# ウィンドウ内で対象要素が何番目(Index)に当たるかを特定する関数の定義です
function Get-ElementIndex {
    # 要素、ウィンドウ、要素名の3つをパラメータとして受け取ります
    param($TargetElement, $TargetWindow, $TargetName)
    <# ウィンドウ内で対象要素が何番目(Index)かを特定します #>
    # インデックスを格納する変数を0で初期化します
    $index = 0
    # 対象ウィンドウが取得できている場合の処理です
    if ($TargetWindow) {
        # 名前(Name)とコントロールタイプ(ControlType)が一致する条件(AndCondition)を作成します
        $condition = New-Object System.Windows.Automation.AndCondition(
            (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $TargetName)),
            (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, $TargetElement.Current.ControlType))
        )
        # 条件に一致する要素をウィンドウの全ての子孫要素から探し出します
        $allMatchingElements = $TargetWindow.FindAll([System.Windows.Automation.TreeScope]::Descendants, $condition)
        # 操作対象要素の一意なID(RuntimeId)をカンマ区切りの文字列にして取得します
        $targetRuntimeId = $TargetElement.GetRuntimeId() -join ","
        # 見つかった同名・同型の要素の配列をループで確認します
        for ($i = 0; $i -lt $allMatchingElements.Count; $i++) {
            # 配列内の要素のRuntimeIdが、操作対象要素のRuntimeIdと一致するか判定します
            if (($allMatchingElements[$i].GetRuntimeId() -join ",") -eq $targetRuntimeId) {
                # 一致した場合、そのループの回数(インデックス番号)を変数に保存します
                $index = $i
                # 見つかったのでループを終了します
                break
            }
        }
    }
    # 特定したインデックス(見つからなかった場合は0)を返却します
    return $index
}

# アンカー(目印)となる要素の情報(名前、方向、距離)を取得する関数の定義です
function Get-AnchorElementInfo {
    # ターゲット要素、要素名、強制GetNameモードかどうかのフラグをパラメータとして受け取ります
    param($TargetElement, $TargetName, $IsGetNameMode)
    <# アンカー(目印)となる要素を、手動指定または自動探索で決定します #>
    # アンカー要素の名前を空文字で初期化します
    $anchorName = ""
    # 対象要素から見たアンカーの相対方向(Prev/Next)を空文字で初期化します
    $anchorDirection = ""
    # アンカー要素までの距離(ステップ数)を0で初期化します
    $anchorSteps = 0
    # 兄弟要素をたどるためのControlViewWalkerオブジェクトを取得します
    $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker

    # 要素名が空、または強制GetNameモード(Enterキー押下)の場合の処理です
    if ([string]::IsNullOrWhiteSpace($TargetName) -or $IsGetNameMode) {
        # 強制GetNameモード(手動でのアンカー指定モード)が有効な場合
        if ($IsGetNameMode) {
            # シアン色で操作の案内メッセージを表示します
            Write-Host "`n【アンカー手動指定】対象の可変要素を取得しました。" -ForegroundColor Cyan
            # 次に固定テキスト等にマウスを合わせるよう案内します
            Write-Host "  -> 次に、アンカー(目印)となる固定テキスト等の要素にマウスを合わせ、" -ForegroundColor Cyan
            # 再度Enterキーを押すよう促します
            Write-Host "     再度 [Enter] キーを押してください..." -ForegroundColor Cyan
            
            # Enterキーが離されるのを待機
            # GetAsyncKeyStateでEnterキーの押下状態を調べ、押されている間はループで待機します
            while (([KeyboardHelper_V3]::GetAsyncKeyState($script:VK_RETURN) -band $script:KEY_PRESSED_MASK) -ne 0) { 
                # 指定ミリ秒待機します
                Start-Sleep -Milliseconds $script:KEY_WAIT_INTERVAL_MS 
            }

            # 取得したアンカー要素を格納する変数を初期化します
            $anchorElement = $null
            # ユーザーが再度Enterキーを押すまで待機する無限ループです
            while ($true) {
                # ループの負荷軽減のための待機です
                Start-Sleep -Milliseconds $script:KEY_WAIT_INTERVAL_MS
                # Enterキーが押されたか判定します
                if (([KeyboardHelper_V3]::GetAsyncKeyState($script:VK_RETURN) -band $script:KEY_PRESSED_MASK) -ne 0) {
                    # 現在のマウスカーソル位置の座標オブジェクトを作成します
                    $p2 = New-Object System.Windows.Point([System.Windows.Forms.Cursor]::Position.X, [System.Windows.Forms.Cursor]::Position.Y)
                    # その座標にあるUI要素(アンカー要素)を取得します
                    $anchorElement = [System.Windows.Automation.AutomationElement]::FromPoint($p2)
                    # 取得できたらループを抜けます
                    break
                }
            }

            # アンカー要素が無事に取得できた場合の処理です
            if ($anchorElement) {
                # アンカー要素の名前を変数に格納します
                $anchorName = $anchorElement.Current.Name
                # 取得したアンカー名を表示し、相対位置の計算を開始するメッセージを出力します
                Write-Host "  -> アンカー [$anchorName] を取得しました。相対位置を計算中... " -NoNewline
                
                # ターゲット要素のRuntimeIdをカンマ区切りの文字列で取得します
                $targetRuntimeId = $TargetElement.GetRuntimeId() -join ","
                # 見つかったかどうかのフラグを初期化します
                $found = $false
                
                # アンカーから見てターゲットがNext(次)の兄弟要素内にあるか検索します
                # まずアンカーの次の兄弟要素を取得します
                $tempNode = $walker.GetNextSibling($anchorElement)
                # ステップ数を1で初期化します
                $steps = 1
                # ノードが存在し、かつ最大ステップ数以下の間ループします
                while ($tempNode -ne $null -and $steps -le $script:MAX_MANUAL_ANCHOR_STEPS) {
                    # 探索中の要素のRuntimeIdがターゲットと一致するか判定します
                    if (($tempNode.GetRuntimeId() -join ",") -eq $targetRuntimeId) {
                        # ターゲットから見てアンカーはPrev(前)の方向にあることを記録します
                        $anchorDirection = "Prev" # ターゲットから見てアンカーはPrev方向にある
                        # アンカーまでのステップ数を記録します
                        $anchorSteps = $steps
                        # 見つかったフラグを$trueにします
                        $found = $true
                        # 探索を終了します
                        break
                    }
                    # 次の兄弟要素へ進みます
                    $tempNode = $walker.GetNextSibling($tempNode)
                    # ステップ数を加算します
                    $steps++
                }

                # アンカーから見てターゲットがPrev(前)にあるか検索します
                # Next方向の探索で見つからなかった場合の処理です
                if (-not $found) {
                    # まずアンカーの前の兄弟要素を取得します
                    $tempNode = $walker.GetPreviousSibling($anchorElement)
                    # ステップ数を1にリセットします
                    $steps = 1
                    # ノードが存在し、かつ最大ステップ数以下の間ループします
                    while ($tempNode -ne $null -and $steps -le $script:MAX_MANUAL_ANCHOR_STEPS) {
                        # 探索中の要素のRuntimeIdがターゲットと一致するか判定します
                        if (($tempNode.GetRuntimeId() -join ",") -eq $targetRuntimeId) {
                            # ターゲットから見てアンカーはNext(次)の方向にあることを記録します
                            $anchorDirection = "Next" # ターゲットから見てアンカーはNext方向にある
                            # アンカーまでのステップ数を記録します
                            $anchorSteps = $steps
                            # 見つかったフラグを$trueにします
                            $found = $true
                            # 探索を終了します
                            break
                        }
                        # 前の兄弟要素へ進みます
                        $tempNode = $walker.GetPreviousSibling($tempNode)
                        # ステップ数を加算します
                        $steps++
                    }
                }

                # ターゲットとの相対位置関係が計算できた場合の処理です
                if ($found) {
                    # 緑色で「完了」と表示します
                    Write-Host "完了" -ForegroundColor Green
                # 計算できなかった(兄弟ノードでなかった)場合の処理です
                } else {
                    # 赤色で「失敗」と表示します
                    Write-Host "失敗" -ForegroundColor Red
                    # 兄弟ノードに見つからなかったため、実行時に逆引きが失敗するリスクを警告します
                    Write-Host "【警告】アンカー要素と対象要素が同じ階層(兄弟ノード)に見つかりませんでした。実行時の逆引きが失敗する可能性があります。" -ForegroundColor Red
                }

                # Enterが離されるのを待機
                # ユーザーが2回目のEnterキーから指を離すまでループで待機します
                while (([KeyboardHelper_V3]::GetAsyncKeyState($script:VK_RETURN) -band $script:KEY_PRESSED_MASK) -ne 0) { 
                    # 指定ミリ秒待機します
                    Start-Sleep -Milliseconds $script:KEY_WAIT_INTERVAL_MS 
                }
            }
        # 強制GetNameモードではない(通常記録で名前が空だった)場合の処理です
        } else {
            # 従来の自動探索フォールバック(Shift押下で名前が空だった場合)
            # アンカーを自動検索中であるメッセージを表示します
            Write-Host " アンカーを自動検索中... " -NoNewline
            
            # ターゲット要素の前の兄弟要素を取得します
            $prevNode = $walker.GetPreviousSibling($TargetElement)
            # 自動検索の最大ステップ数までループします
            for ($i = 1; $i -le $script:MAX_AUTO_ANCHOR_STEPS; $i++) {
                # 要素がなければループを終了します
                if ($prevNode -eq $null) { break }
                # その要素がテキストで、かつ名前が空でない場合アンカーとみなします
                if ($prevNode.Current.ControlType -eq [System.Windows.Automation.ControlType]::Text -and -not [string]::IsNullOrWhiteSpace($prevNode.Current.Name)) {
                    # アンカー名を変数に格納します
                    $anchorName = $prevNode.Current.Name
                    # ターゲットから見てPrev(前)の方向にあることを記録します
                    $anchorDirection = "Prev"
                    # ステップ数を記録します
                    $anchorSteps = $i
                    # 探索を終了します
                    break
                }
                # 前の兄弟要素へ進みます
                $prevNode = $walker.GetPreviousSibling($prevNode)
            }
            # Prev方向でアンカーが見つからなかった場合の処理です
            if ([string]::IsNullOrWhiteSpace($anchorName)) {
                # ターゲット要素の次の兄弟要素を取得します
                $nextNode = $walker.GetNextSibling($TargetElement)
                # 自動検索の最大ステップ数までループします
                for ($i = 1; $i -le $script:MAX_AUTO_ANCHOR_STEPS; $i++) {
                    # 要素がなければループを終了します
                    if ($nextNode -eq $null) { break }
                    # その要素がテキストで、かつ名前が空でない場合アンカーとみなします
                    if ($nextNode.Current.ControlType -eq [System.Windows.Automation.ControlType]::Text -and -not [string]::IsNullOrWhiteSpace($nextNode.Current.Name)) {
                        # アンカー名を変数に格納します
                        $anchorName = $nextNode.Current.Name
                        # ターゲットから見てNext(次)の方向にあることを記録します
                        $anchorDirection = "Next"
                        # ステップ数を記録します
                        $anchorSteps = $i
                        # 探索を終了します
                        break
                    }
                    # 次の兄弟要素へ進みます
                    $nextNode = $walker.GetNextSibling($nextNode)
                }
            }
            # 自動検索が完了したことを表示します
            Write-Host "完了"
        }
    # ターゲット名が存在し、通常モードの場合はアンカー計算が不要なためスキップします
    } else {
        # 計算完了のメッセージを表示します
        Write-Host "計算完了"
    }

    # 取得したアンカー名、方向、ステップ数のハッシュテーブルを返却します
    return @{
        AnchorName = $anchorName
        AnchorDirection = $anchorDirection
        AnchorSteps = $anchorSteps
    }
}

# コンボボックスの選択項目をユーザーに選ばせるダイアログフォームを表示する関数の定義です
function Show-ComboBoxDialog {
    # 選択肢の配列をパラメータとして受け取ります
    param($ComboItems)
    <# コンボボックスの選択値を聞き出すためのダイアログフォームを表示します #>
    # ユーザーが選択(または入力)した値を格納する変数を初期化します
    $actionValue = ""
    # 新しいWindowsフォームオブジェクトを作成します
    $form = New-Object System.Windows.Forms.Form
    # フォームのタイトルを設定します
    $form.Text = "項目の選択"
    # フォームのサイズ(幅と高さ)を設定します
    $form.Size = New-Object System.Drawing.Size($script:FORM_SIZE_W, $script:FORM_SIZE_H)
    # フォームを画面の中央に配置します
    $form.StartPosition = 'CenterScreen'
    # フォームを常に最前面に表示するようにします
    $form.TopMost = $true
    # フォームの枠線を固定ツールウィンドウのスタイルにします
    $form.FormBorderStyle = 'FixedToolWindow'
    
    # テキストを表示するラベルオブジェクトを作成します
    $label = New-Object System.Windows.Forms.Label
    # ラベルに操作案内のテキストを設定します
    $label.Text = "取得した一覧から選択、または手入力してください:"
    # ラベルの配置位置(X, Y座標)を設定します
    $label.Location = New-Object System.Drawing.Point($script:LBL_POS_X, $script:LBL_POS_Y)
    # ラベルのサイズをテキストに合わせて自動調整します
    $label.AutoSize = $true
    # ラベルをフォームに追加します
    $form.Controls.Add($label)

    # 選択用のコンボボックスオブジェクトを作成します
    $cmb = New-Object System.Windows.Forms.ComboBox
    # コンボボックスの配置位置(X, Y座標)を設定します
    $cmb.Location = New-Object System.Drawing.Point($script:CMB_POS_X, $script:CMB_POS_Y)
    # コンボボックスのサイズ(幅と高さ)を設定します
    $cmb.Size = New-Object System.Drawing.Size($script:CMB_SIZE_W, $script:CMB_SIZE_H)
    # コンボボックスのスタイルを、手入力も可能なドロップダウンに設定します
    $cmb.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDown 
    # パラメータで受け取った選択肢をコンボボックスに追加します
    $cmb.Items.AddRange([string[]]$ComboItems)
    # デフォルトで最初の項目(インデックス0)を選択状態にします
    $cmb.SelectedIndex = 0
    # コンボボックスをフォームに追加します
    $form.Controls.Add($cmb)

    # OKボタンオブジェクトを作成します
    $okButton = New-Object System.Windows.Forms.Button
    # ボタンの表示テキストを「OK」に設定します
    $okButton.Text = "OK"
    # ボタンの配置位置(X, Y座標)を設定します
    $okButton.Location = New-Object System.Drawing.Point($script:BTN_POS_X, $script:BTN_POS_Y)
    # ボタンのサイズ(幅と高さ)を設定します
    $okButton.Size = New-Object System.Drawing.Size($script:BTN_SIZE_W, $script:BTN_SIZE_H)
    # ボタンを押した際のダイアログの戻り値を「OK」に設定します
    $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
    # ボタンをフォームに追加します
    $form.Controls.Add($okButton)
    
    # フォーム上でEnterキーを押したときに反応するデフォルトボタンをOKボタンに設定します
    $form.AcceptButton = $okButton

    # モーダルダイアログとしてフォームを表示し、ユーザーの操作結果を変数に格納します
    $result = $form.ShowDialog()
    # ユーザーがOKボタンを押してダイアログを閉じた場合の処理です
    if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
        # コンボボックスで選択(または入力)されたテキストを変数に保存します
        $actionValue = $cmb.Text
    }
    # フォームのメモリリソースを解放します
    $form.Dispose()
    
    # 確定したアクション値を返却します
    return $actionValue
}

# ターゲット要素の属性から、実行するアクション(クリックや入力など)と値を決定する関数の定義です
function Get-ActionTypeAndValue {
    # ターゲット要素と強制GetNameモードのフラグをパラメータとして受け取ります
    param($TargetElement, $IsGetNameMode)
    <# コントロールタイプとパターンに基づき、実行するアクション名とアクション値を決定します #>
    # ターゲット要素のコントロールタイプ名を取得し、「ControlType.」という文字列を削除して整形します
    $targetControlType = $TargetElement.Current.ControlType.ProgrammaticName.Replace("ControlType.", "")
    # ターゲット要素がサポートするUI Automationの操作パターンを取得します
    $patterns = $TargetElement.GetSupportedPatterns()
    # デフォルトのアクションを「Click(クリック)」に設定します
    $action = "Click"
    # アクション実行に使う値を空文字で初期化します
    $actionValue = ""

    # 強制GetNameモード(Enterキーでの記録)の場合の処理です
    if ($IsGetNameMode) {
        # アクションを「GetName(値の取得)」に設定します
        $action = "GetName"
    # 要素がコンボボックスの場合の処理です
    } elseif ($targetControlType -eq "ComboBox") {
        # アクションを「ComboBoxSelect(コンボボックス項目選択)」に設定します
        $action = "ComboBoxSelect"
        # 選択肢リストを格納する配列を初期化します
        $comboItems = @()

        # ListItem(リスト項目)コントロールを検索するための条件を作成します
        $itemCondition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, [System.Windows.Automation.ControlType]::ListItem)
        # まず直下の子要素からリスト項目を検索します
        $itemElements = $TargetElement.FindAll([System.Windows.Automation.TreeScope]::Children, $itemCondition)
        
        # 子要素に見つからなかった場合の処理です
        if ($itemElements.Count -eq 0) {
            # 子孫要素全体からリスト項目を検索します
            $itemElements = $TargetElement.FindAll([System.Windows.Automation.TreeScope]::Descendants, $itemCondition)
        }

        # 展開パターンを格納する変数を初期化します
        $expandPattern = $null
        # まだ項目が見つからず、要素が展開(ExpandCollapse)パターンをサポートしている場合の処理です
        if ($itemElements.Count -eq 0 -and ($patterns.Id -contains [System.Windows.Automation.ExpandCollapsePattern]::Pattern.Id)) {
            # エラーを無視するtry-catchブロックです
            try {
                # 展開パターンを取得します
                $expandPattern = $TargetElement.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern) -as [System.Windows.Automation.ExpandCollapsePattern]
                # 現在折りたたまれている状態か判定します
                if ($expandPattern.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Collapsed) {
                    # コンボボックスを展開します
                    $expandPattern.Expand()
                    # 展開アニメーションが終わって項目が表示されるまで待機します
                    Start-Sleep -Milliseconds $script:COMBO_EXPAND_WAIT_MS 
                    # 展開後、再度子孫要素からリスト項目を検索します
                    $itemElements = $TargetElement.FindAll([System.Windows.Automation.TreeScope]::Descendants, $itemCondition)
                }
            } catch {}
        }

        # 見つかったリスト項目を1つずつループで確認します
        foreach ($item in $itemElements) {
            # 項目の名前が空でなければ配列に追加します
            if (-not [string]::IsNullOrWhiteSpace($item.Current.Name)) {
                # 選択肢の配列に項目名を追加します
                $comboItems += $item.Current.Name
            }
        }

        # 展開パターンを使用し、かつ現在展開されたままになっている場合の処理です
        if ($expandPattern -ne $null -and $expandPattern.Current.ExpandCollapseState -ne [System.Windows.Automation.ExpandCollapseState]::Collapsed) {
            # エラーを無視してコンボボックスを折りたたみます
            try { $expandPattern.Collapse() } catch {}
        }

        # 取得した選択肢リストから重複する項目を削除します
        $comboItems = $comboItems | Select-Object -Unique

        # 選択肢が1つ以上取得できた場合の処理です
        if ($comboItems.Count -gt 0) {
            # 選択ダイアログを表示してユーザーに選ばせ、結果をアクション値に格納します
            $actionValue = Show-ComboBoxDialog -ComboItems $comboItems
        # 選択肢が自動取得できなかった場合の処理です
        } else {
            # テキスト入力用のダイアログを表示し、ユーザーに項目名を手入力させます
            $actionValue = [Microsoft.VisualBasic.Interaction]::InputBox("項目の自動取得に失敗しました。`nコンボボックスで選択する項目名を手入力してください。", "コンボボックス項目の設定", "")
        }
    # 要素がコンボボックス以外、かつ通常記録モードの場合の判定処理です
    } else {
        # 選択(SelectionItem)パターンをサポートしている場合
        if ($patterns.Id -contains [System.Windows.Automation.SelectionItemPattern]::Pattern.Id) {
            # アクションを「Select(選択)」に設定します
            $action = "Select"
        # トグル(Toggle)パターンをサポートしている場合(チェックボックスなど)
        } elseif ($patterns.Id -contains [System.Windows.Automation.TogglePattern]::Pattern.Id) {
            # アクションを「Toggle(切り替え)」に設定します
            $action = "Toggle"
        # 展開(ExpandCollapse)パターンをサポートしている場合(ツリーなど)
        } elseif ($patterns.Id -contains [System.Windows.Automation.ExpandCollapsePattern]::Pattern.Id) {
            # アクションを「ExpandCollapse(展開/折りたたみ)」に設定します
            $action = "ExpandCollapse"
        # 値の入出力(Value)パターンをサポートしている場合(テキストボックスなど)
        } elseif ($patterns.Id -contains [System.Windows.Automation.ValuePattern]::Pattern.Id) {
            # Valueパターンを取得します
            $valPattern = $TargetElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
            # 要素が読み取り専用ではない(入力可能)場合の処理です
            if (-not $valPattern.Current.IsReadOnly) { 
                # アクションを「Value(値入力)」に設定します
                $action = "Value" 
            # 要素が読み取り専用(ラベル等の値表示)の場合の処理です
            } else {
                # アクションを「GetName(値取得)」に設定します
                $action = "GetName"
            }
        # 呼び出し(Invoke)パターンをサポートしている場合(ボタンなど)
        } elseif ($patterns.Id -contains [System.Windows.Automation.InvokePattern]::Pattern.Id) {
            # アクションを「Invoke(実行)」に設定します
            $action = "Invoke"
        # コントロールタイプが単なるテキスト(Text)の場合
        } elseif ($targetControlType -eq "Text") {
            # アクションを「GetName(値取得)」に設定します
            $action = "GetName"
        }

        # アクションが「Value(値入力)」と判定された場合の処理です
        if ($action -eq "Value") {
            # テキスト入力ダイアログを表示し、RPA実行時に入力する値をユーザーに指定させます
            $actionValue = [Microsoft.VisualBasic.Interaction]::InputBox("対象要素に入力するテキストを指定してください。`n※生成されたコード上で後から変更することも可能です。", "RPA入力値の設定", "")
        }
    }

    # 決定したアクションの種類とアクション値のハッシュテーブルを返却します
    return @{ Action = $action; ActionValue = $actionValue }
}

# 1回分のアクション記録処理を行い、スクリプトのコード文字列を生成する関数の定義です
function Process-RecordAction {
    # 強制GetNameモードかどうかをパラメータとして受け取ります
    param([bool]$IsGetNameMode)
    <# 1要素分の記録処理を取りまとめ、RPAのステップ文字列を生成して返却します #>
    
    # 強制GetNameモードの場合の処理です
    if ($IsGetNameMode) {
        # 記録中であることを黄色でコンソールに表示します
        Write-Host "`n[Enter] 強制GetNameモード (可変要素の逆引き) で記録中..." -ForegroundColor Yellow
    # 通常モードの場合の処理です
    } else {
        # 通常記録中であることを黄色でコンソールに表示します
        Write-Host "`n[Shift] 通常記録中..." -ForegroundColor Yellow
    }

    # エラーを捕捉するためのtry-catchブロックを開始します
    try {
        # カーソル位置のUI要素情報を取得します
        $elementInfo = Get-UIAutomationElementFromCursor
        # 取得に失敗した場合は何も返さずに終了します
        if (-not $elementInfo) { return $null }

        # 取得したターゲット要素を変数に格納します
        $targetElement = $elementInfo.TargetElement
        # 取得したウィンドウ要素を変数に格納します
        $targetWindow = $elementInfo.TargetWindow
        # 取得したウィンドウ名を変数に格納します
        $stepWindowName = $elementInfo.WindowName
        
        # ターゲット要素の名前を取得します
        $targetName = $targetElement.Current.Name
        # ターゲット要素のコントロールタイプ名を取得し整形します
        $targetControlType = $targetElement.Current.ControlType.ProgrammaticName.Replace("ControlType.", "")

        # 要素情報を計算中であることを表示します
        Write-Host " 要素情報を計算中... " -NoNewline

        # 要素がウィンドウ内で何番目にあたるか(インデックス)を取得します
        $index = Get-ElementIndex -TargetElement $targetElement -TargetWindow $targetWindow -TargetName $targetName
        
        # アンカー(目印)要素の情報を取得します
        $anchorInfo = Get-AnchorElementInfo -TargetElement $targetElement -TargetName $targetName -IsGetNameMode $IsGetNameMode
        # アンカーの名前を変数に格納します
        $anchorName = $anchorInfo.AnchorName
        # アンカーの方向を変数に格納します
        $anchorDirection = $anchorInfo.AnchorDirection
        # アンカーからの距離(ステップ数)を変数に格納します
        $anchorSteps = $anchorInfo.AnchorSteps
        
        # アクションの種類と値を取得します
        $actionInfo = Get-ActionTypeAndValue -TargetElement $targetElement -IsGetNameMode $IsGetNameMode
        # アクションの種類を変数に格納します
        $action = $actionInfo.Action
        # アクションの値を変数に格納します
        $actionValue = $actionInfo.ActionValue
        
        # GetNameモード時は要素名が実行時に変動することを前提とし、「名前を記録しない(*)」ようにする
        # GetNameモードならターゲット名を「*(可変)」にし、そうでなければ実際の名前を使用します
        $stepTargetName = if ($IsGetNameMode) { "*" } else { $targetName }

        # --- 生成時に構文エラーにならないようダブルクォーテーションをエスケープ処理 ---
        # アクション値に含まれるダブルクォーテーションをPowerShell用にエスケープします
        $safeActionValue = $actionValue -replace '"', '`"'
        # ターゲット名に含まれるダブルクォーテーションをエスケープします
        $safeTargetName  = $stepTargetName -replace '"', '`"'
        # ウィンドウ名に含まれるダブルクォーテーションをエスケープします
        $safeWindowName  = $stepWindowName -replace '"', '`"'
        # アンカー名に含まれるダブルクォーテーションをエスケープします
        $safeAnchorName  = $anchorName -replace '"', '`"'

        # アクションがGetName(値取得)の場合のコード生成処理です
        if ($action -eq "GetName") {
            # 取得した値を変数に代入し、画面に表示するRPAステップのコード文字列を組み立てます
            $stepCode = "`$extractedValue = Execute-UIRpaStep -TargetWindowName `"$safeWindowName`" -TargetName `"$safeTargetName`" -TargetType `"$targetControlType`" -Index $index -Action `"$action`" -ActionValue `"$safeActionValue`" -AnchorName `"$safeAnchorName`" -AnchorDirection `"$anchorDirection`" -AnchorSteps $anchorSteps`r`nWrite-Host `"  => 取得した値: `$extractedValue`" -ForegroundColor Cyan"
        # それ以外のアクションの場合のコード生成処理です
        } else {
            # UI操作を実行するRPAステップのコード文字列を組み立てます
            $stepCode = "Execute-UIRpaStep -TargetWindowName `"$safeWindowName`" -TargetName `"$safeTargetName`" -TargetType `"$targetControlType`" -Index $index -Action `"$action`" -ActionValue `"$safeActionValue`" -AnchorName `"$safeAnchorName`" -AnchorDirection `"$anchorDirection`" -AnchorSteps $anchorSteps"
        }
        
        # アンカー名が指定されている(空でない)場合の処理です
        if (-not [string]::IsNullOrWhiteSpace($anchorName)) {
            # アンカーからの相対位置で記録した旨を緑色で成功メッセージとして表示します
            Write-Host "`n => 記録成功: 手動指定アンカー [$anchorName] からの相対位置で記録 ($targetControlType) -> $action" -ForegroundColor Green
        # アンカー名が指定されていない場合の処理です
        } else {
            # 強制GetNameモードの場合の処理です
            if ($IsGetNameMode) {
                # アンカーなしの可変要素として記録した旨を表示します
                Write-Host "`n => 記録成功: [アンカーなし可変要素] ($targetControlType) -> $action" -ForegroundColor Green
                # 値が変動する要素にアンカーがないことに対する警告を黄色で表示します
                Write-Host "   (警告) アンカー要素が指定されていません。値が変動する場合、実行時にエラーになる可能性があります。" -ForegroundColor Yellow
            # 通常モードの場合の処理です
            } else {
                # ターゲット名とアクションの種類を表示し、記録成功を通知します
                Write-Host "`n => 記録成功: $targetName ($targetControlType) -> $action" -ForegroundColor Green
            }
        }

        # 組み立てた1ステップ分のコード文字列を返却します
        return $stepCode

    # 例外が発生した場合の処理です
    } catch {
        # エラーの内容を赤色でコンソールに表示します
        Write-Host "エラーが発生しました: $_" -ForegroundColor Red
        # エラー時は$nullを返却します
        return $null
    }
}

# 蓄積されたシナリオステップ配列を元に、最終的なPowerShellスクリプトを生成する関数の定義です
function Generate-RpaScript {
    # シナリオステップの配列をパラメータとして受け取ります
    param($ScenarioSteps)
    <# 蓄積されたシナリオステップ配列を元に、最終的なPowerShellスクリプトを生成しクリップボードにコピーします #>
    
    # コード生成処理を開始する旨をシアン色で表示します
    Write-Host "`n[Ctrl] コードを生成しています..." -ForegroundColor Cyan 
    # 配列に格納されたステップを改行コードで連結して1つのテキストブロックにします
    $stepsText = $ScenarioSteps -join "`r`n"

    # RPAスクリプトの共通ヘッダー(定数定義や実行関数など)をヒアドキュメントで定義します
    $TemplateCode = @'
# =========================================================
# 自動生成されたRPAコード
# =========================================================
# --- System.Windows.Forms のロードを追加 ---
# 実行に必要なアセンブリをロードします
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes, System.Windows.Forms
# エラー発生時にスクリプトを停止する設定にします
$ErrorActionPreference = "Stop"

# --- 実行時の定数 ---
# RPA実行時の要素検索の最大リトライ回数を定義します
$script:RPA_MAX_RETRIES = 30
# リトライ時の待機時間(ミリ秒)を定義します
$script:RPA_RETRY_INTERVAL_MS = 300
# 短い待機時間(ミリ秒)を定義します
$script:RPA_SHORT_DELAY_MS = 100
# 長い待機時間(ミリ秒)を定義します
$script:RPA_LONG_DELAY_MS = 500
# コンボボックス展開時の待機時間(ミリ秒)を定義します
$script:RPA_EXPAND_DELAY_MS = 200
# フォールバック(代替)検索時の最大ステップ数を定義します
$script:RPA_FALLBACK_SEARCH_STEPS = 5

# RpaWin32Helperクラスが未定義の場合にC#コードを追加する処理です
if (-not ([System.Management.Automation.PSTypeName]'RpaWin32Helper').Type) {
    # Win32APIを呼び出すためのC#コードを定義します
    $NativeCode = @"
    // Systemネームスペースをインポートします
    using System;
    // DllImportを使用するためにInteropServicesをインポートします
    using System.Runtime.InteropServices;
    // RpaWin32Helperクラスを公開します
    public class RpaWin32Helper {
        // マウスカーソル位置を設定するAPIをインポートします
        [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y);
        // マウスイベントをシミュレートするAPIをインポートします
        [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);
        // ウィンドウをフォアグラウンドに設定するAPIをインポートします
        [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
        // ウィンドウの表示状態を変更するAPIをインポートします
        [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
        // ウィンドウをZオーダーのトップに持ってくるAPIをインポートします
        [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
        // 現在のフォアグラウンドウィンドウを取得するAPIをインポートします
        [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
        // ウィンドウのスレッドプロセスIDを取得するAPIをインポートします
        [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr ProcessId);
        // 現在のスレッドIDを取得するAPIをインポートします
        [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
        // スレッド入力処理をアタッチするAPIをインポートします
        [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
        // ウィンドウが最小化されているか判定するAPIをインポートします
        [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
        
        // マウス左ボタン押下の定数です
        public const uint MOUSEEVENTF_LEFTDOWN = 0x02;
        // マウス左ボタン離しの定数です
        public const uint MOUSEEVENTF_LEFTUP = 0x04;
        // ウィンドウを元のサイズに戻す定数です
        public const int SW_RESTORE = 9;
        
        // 指定座標をクリックするメソッドです
        public static void Click(int x, int y) {
            // マウスカーソルを移動させます
            SetCursorPos(x, y);
            // 左ボタンを押下します
            mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
            // 左ボタンを離します
            mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
        }

        // ウィンドウをアクティブにするメソッドです
        public static void ActivateWindow(IntPtr hwnd) {
            // 最小化されている場合は元に戻します
            if (IsIconic(hwnd)) { ShowWindow(hwnd, SW_RESTORE); }
            // 現在の最前面ウィンドウハンドルを取得します
            IntPtr fgHwnd = GetForegroundWindow();
            // 既に最前面なら何もしません
            if (fgHwnd == hwnd) { return; }
            
            // 最前面ウィンドウのスレッドIDを取得します
            uint fgThread = GetWindowThreadProcessId(fgHwnd, IntPtr.Zero);
            // 自分のスレッドIDを取得します
            uint myThread = GetCurrentThreadId();
            
            // スレッドが異なる場合の処理です
            if (fgThread != myThread) {
                // スレッド入力をアタッチします
                AttachThreadInput(myThread, fgThread, true);
                // ウィンドウをトップに持っていきます
                BringWindowToTop(hwnd);
                // ウィンドウをフォアグラウンドに設定します
                SetForegroundWindow(hwnd);
                // スレッド入力のアタッチを解除します
                AttachThreadInput(myThread, fgThread, false);
            // スレッドが同じ場合の処理です
            } else {
                // ウィンドウをトップに持っていきます
                BringWindowToTop(hwnd);
                // ウィンドウをフォアグラウンドに設定します
                SetForegroundWindow(hwnd);
            }
        }
    }
"@
    # C#コードをコンパイルして型を追加します
    Add-Type -TypeDefinition $NativeCode -ErrorAction Stop
}

# 1つのRPAステップを実行する関数を定義します
function Execute-UIRpaStep {
    # 実行に必要な各パラメータを受け取ります
    param($TargetWindowName, $TargetName, $TargetType, $Index, $Action, $ActionValue, $AnchorName = "", $AnchorDirection = "", $AnchorSteps = 0)
    
    # デスクトップ全体を示すRootElementを取得します
    $rootElement = [System.Windows.Automation.AutomationElement]::RootElement
    # リトライ回数を0で初期化します
    $retryCount = 0
    # 最大リトライ回数を設定します
    $maxRetries = $script:RPA_MAX_RETRIES 
    
    # 実行開始の区切り線を表示します
    Write-Host "`n========================================================" -ForegroundColor Cyan
    # アンカー名が指定されている場合の処理です
    if (-not [string]::IsNullOrWhiteSpace($AnchorName)) {
        # ステップ数(距離)が指定されている場合の処理です
        if ($AnchorSteps -gt 0) {
            # 方向の文字列を日本語表現に変換します
            $directionStr = if ($AnchorDirection -eq "Prev") { "後(Next)" } else { "前(Prev)" }
            # アンカーからの相対位置で要素を操作する旨を表示します
            Write-Host " [Step開始] アンカー: [$AnchorName] の $AnchorSteps 個 $directionStr にある要素 ($TargetType) を操作 ($Action)" -ForegroundColor Cyan
        # 周辺探索の場合の処理です
        } else {
            # アンカー周辺の要素を操作する旨を表示します
            Write-Host " [Step開始] アンカー: [$AnchorName] 周辺の要素 ($TargetType) を操作 ($Action)" -ForegroundColor Cyan
        }
    # アンカー指定がない場合の処理です
    } else {
        # 対象名が可変指定(*)の場合の処理です
        if ($TargetName -eq "*") {
            # アンカーなしの可変要素を操作する旨を表示します
            Write-Host " [Step開始] 要素: [可変(アンカーなし)] ($TargetType) を操作 ($Action)" -ForegroundColor Cyan
        # 対象名が固定の場合の処理です
        } else {
            # 指定された名前の要素を操作する旨を表示します
            Write-Host " [Step開始] 要素: [$TargetName] ($TargetType) を操作 ($Action)" -ForegroundColor Cyan
        }
    }
    # 実行開始の区切り線を表示します
    Write-Host "========================================================" -ForegroundColor Cyan

    # リトライ上限回数まで要素検索と操作を繰り返すループです
    while ($retryCount -lt $maxRetries) {
        # リトライ回数をカウントアップします
        $retryCount++
        # エラー発生時にリトライするためのtryブロックです
        try {
            # ウィンドウの検索中であることを表示します
            Write-Host "[$retryCount / $maxRetries 回目] ウィンドウ `"$TargetWindowName`" を検索中..." -ForegroundColor DarkYellow
            
            # ウィンドウを検索するための条件を作成します
            $winCondition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, [System.Windows.Automation.ControlType]::Window)
            # ルート直下の子要素から全ウィンドウを取得します
            $windows = $rootElement.FindAll([System.Windows.Automation.TreeScope]::Children, $winCondition)
            # 名前が一致する最初のウィンドウを取得します
            $targetWindow = $windows | Where-Object { $_.Current.Name -match [regex]::Escape($TargetWindowName) } | Select-Object -First 1

            # ウィンドウが見つからなかった場合の処理です
            if (-not $targetWindow) { 
                # 待機メッセージを表示します
                Write-Host "  -> [待機] ウィンドウが見つかりません。" -ForegroundColor DarkGray
                # 指定時間待機してループの先頭に戻ります
                Start-Sleep -Milliseconds $script:RPA_RETRY_INTERVAL_MS; continue 
            }

            # 見つかったウィンドウのハンドルを取得します
            $hwnd = [IntPtr]$targetWindow.Current.NativeWindowHandle
            # ハンドルが有効な場合の処理です
            if ($hwnd -ne [IntPtr]::Zero) {
                # ウィンドウをアクティブにします
                [RpaWin32Helper]::ActivateWindow($hwnd)
                # 短い時間待機します
                Start-Sleep -Milliseconds $script:RPA_SHORT_DELAY_MS
            }

            # ターゲットのコントロールタイプの値をリフレクションで取得します
            $controlTypeField = [System.Windows.Automation.ControlType].GetField($TargetType)
            # コントロールタイプオブジェクトを取得します
            $controlType = $controlTypeField.GetValue($null)
            # 操作対象要素の変数を初期化します
            $targetElement = $null

            # 対象要素の検索開始メッセージを表示します
            Write-Host "  -> 対象要素を検索中..." -ForegroundColor DarkYellow

            # 手動指定されたアンカー名が存在する場合の処理です
            if (-not [string]::IsNullOrWhiteSpace($AnchorName)) {
                
                # 手動指定の場合、名目前提でアンカーを検索する条件を作成します
                $anchorCondition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $AnchorName)
                # ウィンドウ内からアンカー要素を検索します
                $anchorElement = $targetWindow.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $anchorCondition)
                
                # アンカー要素が見つかった場合の処理です
                if ($anchorElement) {
                    # ツリーをたどるためのWalkerを取得します
                    $walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
                    
                    # アンカーからのステップ数が指定されている場合の処理です
                    if ($AnchorSteps -gt 0) {
                        # 現在のノードをアンカーに設定します
                        $currentNode = $anchorElement
                        # 記録時とは逆方向にたどって対象要素を探すための方向を決定します
                        $searchDirection = if ($AnchorDirection -eq "Prev") { "Next" } else { "Prev" }
                        
                        # 指定ステップ数だけ兄弟要素をたどるループです
                        for ($i = 1; $i -le $AnchorSteps; $i++) {
                            # ノードが途切れた場合はループを抜けます
                            if ($currentNode -eq $null) { break }
                            # Next方向にたどる場合
                            if ($searchDirection -eq "Next") {
                                $currentNode = $walker.GetNextSibling($currentNode)
                            # Prev方向にたどる場合
                            } else {
                                $currentNode = $walker.GetPreviousSibling($currentNode)
                            }
                        }
                        # たどった先のコントロールタイプが一致する場合
                        if ($currentNode -ne $null -and $currentNode.Current.ControlType -eq $controlType) {
                            # それを対象要素とします
                            $targetElement = $currentNode
                        }
                    }

                    # オフセット情報が無い、または正確な位置で見つからなかった場合の代替検索処理です
                    if (-not $targetElement) {
                        # アンカーの次の兄弟要素を取得します
                        $nextNode = $walker.GetNextSibling($anchorElement)
                        # 代替検索の最大ステップ数だけループします
                        for ($i = 1; $i -le $script:RPA_FALLBACK_SEARCH_STEPS; $i++) {
                            # 要素がなければループを抜けます
                            if ($nextNode -eq $null) { break }
                            # コントロールタイプが一致した場合は対象要素とします
                            if ($nextNode.Current.ControlType -eq $controlType) {
                                $targetElement = $nextNode
                                break
                            }
                            # 次の兄弟要素へ進みます
                            $nextNode = $walker.GetNextSibling($nextNode)
                        }
                        # Next方向で見つからなかった場合
                        if (-not $targetElement) {
                            # アンカーの前の兄弟要素を取得します
                            $prevNode = $walker.GetPreviousSibling($anchorElement)
                            # 代替検索の最大ステップ数だけループします
                            for ($i = 1; $i -le $script:RPA_FALLBACK_SEARCH_STEPS; $i++) {
                                # 要素がなければループを抜けます
                                if ($prevNode -eq $null) { break }
                                # コントロールタイプが一致した場合は対象要素とします
                                if ($prevNode.Current.ControlType -eq $controlType) {
                                    $targetElement = $prevNode
                                    break
                                }
                                # 前の兄弟要素へ進みます
                                $prevNode = $walker.GetPreviousSibling($prevNode)
                            }
                        }
                    }
                }
            } 
            
            # 対象名が可変「*」などでまだ見つかっていない場合の処理です
            if (-not $targetElement) {
                # 対象名が可変の場合はコントロールタイプのみで条件を作成します
                if ($TargetName -eq "*") {
                    $condition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, $controlType)
                # 固定名の場合は名前とコントロールタイプの両方で条件を作成します
                } else {
                    $condition = New-Object System.Windows.Automation.AndCondition(
                        (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $TargetName)),
                        (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty, $controlType))
                    )
                }

                # インデックスが0で名前が固定の場合の処理です
                if ($Index -eq 0 -and $TargetName -ne "*") {
                    # 最初に見つかった要素を対象とします
                    $targetElement = $targetWindow.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition)
                # インデックス指定がある、または可変名の場合の処理です
                } else {
                    # 条件に合うすべての要素を取得します
                    $elements = $targetWindow.FindAll([System.Windows.Automation.TreeScope]::Descendants, $condition)
                    # 要素数がインデックスより大きい場合、そのインデックスの要素を取得します
                    if ($elements.Count -gt $Index) { 
                        $targetElement = $elements[$Index] 
                    # 要素は存在するがインデックスが合わず可変名の場合の代替処理です
                    } elseif ($elements.Count -gt 0 -and $TargetName -eq "*") {
                        # 最初の要素を取得します
                        $targetElement = $elements[0]
                    }
                }
            }

            # 対象要素がどうしても見つからなかった場合の処理です
            if (-not $targetElement) { 
                # 待機メッセージを表示します
                Write-Host "  -> [待機] 対象要素が見つかりません。" -ForegroundColor DarkGray
                # 指定時間待機してループの先頭に戻ります
                Start-Sleep -Milliseconds $script:RPA_RETRY_INTERVAL_MS; continue 
            }

            # 要素が発見できた旨を表示します
            Write-Host "  -> 要素を発見しました。アクションの準備をします..." -ForegroundColor DarkYellow
            # 少し待機します
            Start-Sleep -Milliseconds $script:RPA_SHORT_DELAY_MS

            # 実行するアクションの内容を表示します
            Write-Host "  -> アクション '$Action' を実行します..." -ForegroundColor DarkYellow

            # コンボボックス選択アクションの場合の処理です
            if ($Action -eq "ComboBoxSelect") {
                # 展開されたかのフラグを初期化します
                $expanded = $false
                # 展開パターンをサポートしている場合
                if ($targetElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.ExpandCollapsePattern]::Pattern.Id) {
                    # 展開パターンを取得します
                    $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern) -as [System.Windows.Automation.ExpandCollapsePattern]
                    # 折りたたまれている場合は展開します
                    if ($pattern.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Collapsed) {
                        $pattern.Expand()
                        $expanded = $true
                    }
                # トグルパターンをサポートしている場合
                } elseif ($targetElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.TogglePattern]::Pattern.Id) {
                    # トグルパターンを実行して展開します
                    $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) -as [System.Windows.Automation.TogglePattern]
                    $pattern.Toggle()
                    $expanded = $true
                # どちらのパターンも無い場合
                } else {
                    # 要素の矩形領域を取得します
                    $rect = $targetElement.Current.BoundingRectangle
                    # 領域が有効なら
                    if (-not $rect.IsEmpty -and $rect.Left -gt -10000 -and $rect.Width -gt 0 -and $rect.Height -gt 0) {
                        # 中心のX座標を計算します
                        $centerX = [int]($rect.Left + ($rect.Width / 2))
                        # 中心のY座標を計算します
                        $centerY = [int]($rect.Top + ($rect.Height / 2))
                        # 物理クリックで展開します
                        [RpaWin32Helper]::Click($centerX, $centerY)
                        $expanded = $true
                    }
                }

                # 展開した場合はアニメーション待ちのために少し長めに待機します
                if ($expanded) { Start-Sleep -Milliseconds $script:RPA_LONG_DELAY_MS }

                # 選択する項目名を条件にして要素を検索します
                $itemCondition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $ActionValue)
                # コンボボックス内から検索します
                $itemElement = $targetElement.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $itemCondition)
                
                # 見つからなかった場合の処理です
                if (-not $itemElement) {
                    # 名前が一致し、かつ画面上に表示されている要素を条件にします
                    $visibleCondition = New-Object System.Windows.Automation.AndCondition(
                        (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::NameProperty, $ActionValue)),
                        (New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::IsOffscreenProperty, $false))
                    )
                    # ウィンドウ全体から検索します
                    $itemElement = $targetWindow.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $visibleCondition)
                }

                # 項目要素が見つかった場合の処理です
                if ($itemElement) {
                    # アクション完了フラグを初期化します
                    $actionDone = $false

                    # 選択パターンをサポートしている場合
                    if ($itemElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.SelectionItemPattern]::Pattern.Id) {
                        # 選択パターンを取得します
                        $itemPattern = $itemElement.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern) -as [System.Windows.Automation.SelectionItemPattern]
                        # 選択を実行し成功フラグを立てます
                        try { $itemPattern.Select(); $actionDone = $true } catch {}
                    # 呼び出しパターンをサポートしている場合
                    } elseif ($itemElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.InvokePattern]::Pattern.Id) {
                        # 呼び出しパターンを取得します
                        $itemPattern = $itemElement.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
                        # 実行して成功フラグを立てます
                        try { $itemPattern.Invoke(); $actionDone = $true } catch {}
                    }
                    
                    # 念のため物理クリックも試みます
                    $rect = $itemElement.Current.BoundingRectangle
                    # 要素が画面内にあり領域が有効なら
                    if (-not $itemElement.Current.IsOffscreen -and -not $rect.IsEmpty -and $rect.Left -gt -10000 -and $rect.Width -gt 0 -and $rect.Height -gt 0) {
                        # 中心のX座標を計算します
                        $centerX = [int]($rect.Left + ($rect.Width / 2))
                        # 中心のY座標を計算します
                        $centerY = [int]($rect.Top + ($rect.Height / 2))
                        # クリックを実行し成功フラグを立てます
                        [RpaWin32Helper]::Click($centerX, $centerY)
                        $actionDone = $true
                    }

                    # 全ての方法で操作できなかった場合
                    if (-not $actionDone) {
                        # 警告メッセージを表示します
                        Write-Host "  -> (警告) 項目のパターン選択・物理クリックのいずれも実行できませんでした。" -ForegroundColor Yellow
                    }

                    # 要素を展開していた場合は元に戻す処理です
                    if ($expanded) {
                        # 少し待機します
                        Start-Sleep -Milliseconds $script:RPA_EXPAND_DELAY_MS
                        # 展開パターンがある場合
                        if ($targetElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.ExpandCollapsePattern]::Pattern.Id) {
                            # 展開パターンを取得します
                            $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern) -as [System.Windows.Automation.ExpandCollapsePattern]
                            # 現在展開されていれば折りたたみます
                            if ($pattern.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Expanded) {
                                try { $pattern.Collapse() } catch {}
                            }
                        }
                    }

                # 項目要素が見つからなかった場合の処理です
                } else {
                    # 直接のテキスト入力を試みる旨を警告表示します
                    Write-Host "  -> (警告) 項目 '$ActionValue' が見つからないため、直接のテキスト入力を試行します。" -ForegroundColor Yellow
                    # 入力処理完了フラグを初期化します
                    $handled = $false
                    # Valueパターンをサポートしている場合
                    if ($targetElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.ValuePattern]::Pattern.Id) {
                        # Valueパターンを取得します
                        $valPattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
                        # 読み取り専用でなければ値を設定します
                        if (-not $valPattern.Current.IsReadOnly) {
                            $valPattern.SetValue($ActionValue)
                            $handled = $true
                        }
                    }
                    # 直接入力もできなかった場合
                    if (-not $handled) {
                        # エラーをスローして失敗とします
                        throw "コンボボックスの項目 '$ActionValue' を特定・選択できませんでした。"
                    }
                }

            # --- ValuePatternでの入力処理にクリック&SendKeysのフォールバックを追加 ---
            # 値入力(Value)アクションの場合の処理です
            } elseif ($Action -eq "Value") {
                # 値設定完了フラグを初期化します
                $valueSet = $false
                
                # 事前にフォーカスを当てておく処理です(必須要件のコントロール対策)
                try {
                    # フォーカスを設定します
                    $targetElement.SetFocus()
                    # 少し待機します
                    Start-Sleep -Milliseconds $script:RPA_SHORT_DELAY_MS
                } catch {}

                # API経由での値設定を試みます
                try {
                    # Valueパターンを取得します
                    $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
                    # 値を設定し、成功フラグを立てます
                    $pattern.SetValue($ActionValue)
                    $valueSet = $true
                # 失敗した場合の処理です
                } catch {
                    # 警告メッセージを表示します
                    Write-Host "  -> (警告) ValuePatternでのバックグラウンド入力に失敗しました。物理クリックとキーボード操作を試行します。" -ForegroundColor Yellow
                }
                
                # API入力がエラーを返した場合のキー入力代替処理です
                if (-not $valueSet) {
                    # 矩形領域を取得します
                    $rect = $targetElement.Current.BoundingRectangle
                    # 領域が有効なら
                    if (-not $rect.IsEmpty -and $rect.Left -gt -10000 -and $rect.Width -gt 0 -and $rect.Height -gt 0) {
                        # 中心のX座標を計算します
                        $centerX = [int]($rect.Left + ($rect.Width / 2))
                        # 中心のY座標を計算します
                        $centerY = [int]($rect.Top + ($rect.Height / 2))
                        # 対象要素をクリックして強制的にアクティブにします
                        [RpaWin32Helper]::Click($centerX, $centerY)
                        # 少し待機します
                        Start-Sleep -Milliseconds $script:RPA_SHORT_DELAY_MS
                    }
                    
                    # Ctrl+A を送信して既存テキストを全選択します
                    [System.Windows.Forms.SendKeys]::SendWait("^{a}")
                    # 少し待機します
                    Start-Sleep -Milliseconds 50
                    # Deleteキーを送信して削除します
                    [System.Windows.Forms.SendKeys]::SendWait("{DELETE}")
                    # 少し待機します
                    Start-Sleep -Milliseconds 50

                    # 入力値が空でない場合
                    if (-not [string]::IsNullOrEmpty($ActionValue)) {
                        # SendKeys用の特殊記号をエスケープ処理します
                        $escapedValue = $ActionValue -replace '([+^%~()[\]{}])', '{$1}'
                        # エスケープした文字列をキーボード入力として送信します
                        [System.Windows.Forms.SendKeys]::SendWait($escapedValue)
                    }
                }
            # ---------------------------------------------------------------------------------

            # 呼び出し(Invoke)アクションの場合の処理です
            } elseif ($Action -eq "Invoke") {
                # Invokeパターンを取得します
                $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
                # パターンを実行します
                $pattern.Invoke()
            # 選択(Select)アクションの場合の処理です
            } elseif ($Action -eq "Select") {
                # SelectionItemパターンを取得します
                $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern) -as [System.Windows.Automation.SelectionItemPattern]
                # パターンを実行します
                $pattern.Select()
            # 切り替え(Toggle)アクションの場合の処理です
            } elseif ($Action -eq "Toggle") {
                # Toggleパターンを取得します
                $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.TogglePattern]::Pattern) -as [System.Windows.Automation.TogglePattern]
                # パターンを実行します
                $pattern.Toggle()
            # 展開/折りたたみ(ExpandCollapse)アクションの場合の処理です
            } elseif ($Action -eq "ExpandCollapse") {
                # ExpandCollapseパターンを取得します
                $pattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ExpandCollapsePattern]::Pattern) -as [System.Windows.Automation.ExpandCollapsePattern]
                # 折りたたまれている場合は展開します
                if ($pattern.Current.ExpandCollapseState -eq [System.Windows.Automation.ExpandCollapseState]::Collapsed) {
                    $pattern.Expand()
                # 展開されている場合は折りたたみます
                } else {
                    $pattern.Collapse()
                }
            # クリック(Click)アクションの場合の処理です
            } elseif ($Action -eq "Click") {
                # 矩形領域を取得します
                $rect = $targetElement.Current.BoundingRectangle
                # 領域が有効なら
                if (-not $rect.IsEmpty -and $rect.Left -gt -10000 -and $rect.Width -gt 0 -and $rect.Height -gt 0) {
                    # 中心のX座標を計算します
                    $centerX = [int]($rect.Left + ($rect.Width / 2))
                    # 中心のY座標を計算します
                    $centerY = [int]($rect.Top + ($rect.Height / 2))
                    # クリックを実行します
                    [RpaWin32Helper]::Click($centerX, $centerY)
                }
            # 値取得(GetName)アクションの場合の処理です
            } elseif ($Action -eq "GetName") {
                # 要素の名前を取得します
                $retrievedName = $targetElement.Current.Name
                # 名前が空で、Valueパターンをサポートしている場合
                if ([string]::IsNullOrWhiteSpace($retrievedName) -and ($targetElement.GetSupportedPatterns().Id -contains [System.Windows.Automation.ValuePattern]::Pattern.Id)) {
                    # Valueパターンを取得します
                    $valPattern = $targetElement.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern) -as [System.Windows.Automation.ValuePattern]
                    # Valueの値を取得して名前に代入します
                    $retrievedName = $valPattern.Current.Value
                }
            }
            
            # 処理が正常に完了したことを表示します
            Write-Host "  => [完了] 処理が正常に実行されました。" -ForegroundColor Green
            # 少し待機します
            Start-Sleep -Milliseconds $script:RPA_SHORT_DELAY_MS
            
            # アクションがGetNameだった場合は取得した値を返却します
            if ($Action -eq "GetName") {
                return $retrievedName
            }
            
            # 処理が成功したためリトライループを抜けます
            break 
            
        # 実行中に例外が発生した場合の処理です
        } catch {
            # エラー内容を表示して再試行を通知します
            Write-Host "  -> [エラー] 実行中に例外が発生しました。再試行します... - $_" -ForegroundColor Red
            # リトライ待機時間を置きます
            Start-Sleep -Milliseconds $script:RPA_RETRY_INTERVAL_MS
        }
    }
    
    # リトライ回数が上限に達した場合の処理です
    if ($retryCount -ge $maxRetries) {
        # エラーメッセージをスローしてスクリプトを終了させます
        throw "【タイムアウト】30秒間待機しましたが、要素の操作を完了できませんでした。(Target:$TargetName, Anchor:$AnchorName)"
    }
}

# 実行開始時のヘッダーメッセージを出力します
Write-Host "`n========================================" -ForegroundColor Magenta
# メッセージ本文を出力します
Write-Host "  RPAシナリオの実行を開始します..." -ForegroundColor Magenta
# ヘッダーメッセージの区切り線を出力します
Write-Host "========================================" -ForegroundColor Magenta

# --- 実行シナリオ ---
'@

    # RPAスクリプトの最後に出力するフッターコードをヒアドキュメントで定義します
    $FooterCode = @'
# 完了メッセージの区切り線を出力します
Write-Host "`n========================================" -ForegroundColor Magenta
# 全ての処理が完了したことを出力します
Write-Host "  全ての処理が完了しました。" -ForegroundColor Magenta
# 完了メッセージの区切り線を出力します
Write-Host "========================================" -ForegroundColor Magenta
'@
        
    # ヘッダーコード、記録されたステップ群、フッターコードを結合して最終的なスクリプト文字列を作成します
    $finalCode = $TemplateCode + "`r`n" + $stepsText + "`r`n" + $FooterCode
    # 作成したスクリプトをクリップボードにコピーします
    Set-Clipboard -Value $finalCode

    # コード生成が成功したことを緑色で表示します
    Write-Host "--- コード生成成功 ---" -ForegroundColor Green
    # クリップボードにコピーされたことをシアン色で表示します
    Write-Host "シナリオコードをクリップボードにコピーしました!" -ForegroundColor Cyan
}

# ==============================================================================
# [コンソール出力] 操作方法の表示
# ==============================================================================
# 操作案内のヘッダー区切り線を表示します
Write-Host "==========================================================" -ForegroundColor Cyan
# ツールのタイトルと対応モードを表示します
Write-Host " RPAコード自動生成ツール " -ForegroundColor Cyan
# Shiftキーによる通常アクション記録の操作方法を説明します
Write-Host " 1. 対象にマウスを合わせ [Shift] キーを押す (通常アクション記録)"
# Enterキーによる強制GetNameモードの操作方法を説明します
Write-Host " 2. 対象にマウスを合わせ [Enter] キーを押す (強制GetNameモード記録)"
# Ctrlキーによるコード生成と終了の操作方法を説明します
Write-Host " 3. 記録後 [Ctrl] キーを押す (コード一括生成して自動終了)" 
# Shiftキーでも値が取得できる場合があることの補足説明です
Write-Host " ※テキスト枠等はShiftでも自動で値取得になりますが、変動する値"
# 変動する値の場合はEnterキーを推奨する説明です
Write-Host "   や名前を取得したい場合は [Enter] を使用してください。"
# Enterキー押下時の動作(手動アンカー指定)について説明します
Write-Host "   ([Enter]の場合、手動でアンカーを指定して相対位置から逆引きします)"
# 操作案内のフッター区切り線を表示します
Write-Host "==========================================================" -ForegroundColor Cyan

# ==============================================================================
# [メインループ] 監視と実行
# ==============================================================================
# 記録した各ステップのコードを格納する配列を初期化します
$scenarioSteps = @()
# 前回のループでShiftキーが押されていたかのフラグを初期化します
$shiftWasPressed = $false
# 前回のループでEnterキーが押されていたかのフラグを初期化します
$enterWasPressed = $false 
# 前回のループでCtrlキーが押されていたかのフラグを初期化します
$ctrlWasPressed  = $false 

# キー入力を監視し続けるための無限ループです
while ($true) {
    # 負荷軽減のためメインループ内で指定時間待機します
    Start-Sleep -Milliseconds $script:MAIN_LOOP_INTERVAL_MS

    # 現在Shiftキーが押下されているか判定します
    $shiftPressed = ([KeyboardHelper_V3]::GetAsyncKeyState($script:VK_SHIFT) -band $script:KEY_PRESSED_MASK) -ne 0
    # 現在Enterキーが押下されているか判定します
    $enterPressed = ([KeyboardHelper_V3]::GetAsyncKeyState($script:VK_RETURN) -band $script:KEY_PRESSED_MASK) -ne 0 
    # 現在Ctrlキーが押下されているか判定します
    $ctrlPressed  = ([KeyboardHelper_V3]::GetAsyncKeyState($script:VK_CONTROL) -band $script:KEY_PRESSED_MASK) -ne 0 

    # Shiftキーが前回押されておらず、今回押された瞬間(エッジトリガ)を検出します
    $shiftTrigger = ($shiftPressed -and -not $shiftWasPressed)
    # Enterキーが前回押されておらず、今回押された瞬間を検出します
    $enterTrigger = ($enterPressed -and -not $enterWasPressed)

    # ShiftキーまたはEnterキーが新しく押された場合の処理です
    if ($shiftTrigger -or $enterTrigger) {
        # アクションの記録処理を実行し、生成されたステップのコードを受け取ります
        $stepCode = Process-RecordAction -IsGetNameMode $enterTrigger
        # コードが正常に生成された場合
        if ($stepCode) {
            # シナリオ配列にステップコードを追加します
            $scenarioSteps += $stepCode
        }
    }

    # Ctrlキーが新しく押された場合の処理です(終了およびコード出力)
    if ($ctrlPressed -and -not $ctrlWasPressed) { 
        # 記録されたステップが1つ以上あるか確認します
        if ($scenarioSteps.Count -gt 0) {
            # 蓄積されたステップをもとにRPAスクリプトを生成します
            Generate-RpaScript -ScenarioSteps $scenarioSteps
            # 終了前に少し待機します
            Start-Sleep -Milliseconds $script:CODE_GEN_DELAY_MS
            # 無限ループを抜けてツールを終了します
            break
        }
    }

    # 次回のループ判定のために、現在のShiftキー状態を保存します
    $shiftWasPressed = $shiftPressed
    # 次回のループ判定のために、現在のEnterキー状態を保存します
    $enterWasPressed = $enterPressed
    # 次回のループ判定のために、現在のCtrlキー状態を保存します
    $ctrlWasPressed  = $ctrlPressed 
}


使い方(記録のステップ)

ツールを起動すると、バックグラウンドでキーボード入力の監視が始まります。以下の手順で操作を記録します。

  1. 通常のアクション記録(Shiftキー)
    クリックしたいボタンやテキストを入力したい枠にマウスカーソルを合わせ、Shift キーを押します。要素の種類(ボタン、コンボボックス等)を自動判別し、必要な入力値などをポップアップで聞いてきます。

  2. 値の取得・可変要素の逆引き(Enterキー)
    表示されているテキストを取得したい場合や、要素の名前が毎回変動する場合に使用します。
    Enter キーを押すと、「アンカー手動指定モード」になり、目印となる固定テキスト(ラベルなど)を再度 Enter で指定することで、「目印の次にある要素」という形で相対的に記録します。

  3. コードの生成と終了(Ctrlキー)
    一連の操作を記録し終えたら、Ctrl キーを押します。
    記録されたステップをもとに、単独で動作するRPA用のPowerShellスクリプトが生成され、クリップボードにコピーされます。あとは任意の .ps1 ファイルに貼り付けて実行するだけです!

自動生成されるコードの仕組みについて

生成されるコードには、Win32APIを呼び出して対象ウィンドウを確実に最前面(アクティブ)にする処理や、UI Automationのパターン(InvokePatternValuePattern)を用いた裏側での操作、どうしても操作できない場合の物理クリックへのフォールバック(代替処理)などが組み込まれています。

そのため、生成されたコードは堅牢に動作します。

さいごに

高価なRPAツールを使わなくても、Windows標準の技術だけでここまで高度な自動化が可能です。毎日の定型作業の自動化に、ぜひ活用してみてください!
ご質問や改良アイデアなどがあれば、お気軽にコメントをお願いします。

参考資料

2023年11月10日

2023年12月09日

2024年02月04日

2024年05月24日

2025年02月15日

2025年03月16日

2025年07月04日

2025年07月06日

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?