「Office 非搭載PC」向け　PowerShellでUIAutomationを使ったブラウザ操作(RPA) ver.1.0

# 概要
* 自動化ツールをインストールせずに、Windowsに標準搭載されている「UI Automation」機能を使って、ブラウザを自動操作するコードです。たとえば、「Office 非搭載PC」でブラウザでの作業を自動化することが可能です。
* この内容は、以下の動画で使われているものです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/4LVbTqbv990?si=JERWzoAQ4pualR2b" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 実行環境
* Microsoft Windows 11
* PowerShell version 5.1
* Microsoft Edge

# 開発環境
* Windows PowerShell ISE
（Windowsに標準搭載されているPowerShellの統合開発環境）
※「UI Automation PowerShell Extensions」は不要です。


# 注意点
* プログラムの実行については、すべて自己責任で行ってください。実行により発生した、いかなる直接的または間接的被害について、作者はその責任を負いません。
* このコードにおいては、読み込み待機処理やエラー処理は加味しきれていません。
* Win32やWPFなど、従来の古いウィンドウシステムを操作する場合は、要素をうまく取得できない可能性があります。

# 参照
* 今回のコードは、以下の「System.Windows.Automation」を用いています。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.automation?view=windowsdesktop-8.0

# PowerShellコード（汎用関数）
* まずは以下のコードを「Windows PowerShell ISE」のスクリプトウィンドウに貼り付けます。このコードを用いると、「Windowの部分名」や「要素の名前」から操作したい要素を特定し、実際に要素をクリックしたり、値を入力することが容易になります。

```powershell:汎用関数
# UI オートメーション関連のアセンブリを読み込む
Add-Type -AssemblyName "UIAutomationClient"  # UIAutomationClientアセンブリを追加
Add-Type -AssemblyName "UIAutomationTypes"   # UIAutomationTypesアセンブリを追加

# UI オートメーション関連の型を定義する
$AutomationElement = [System.Windows.Automation.AutomationElement]  # AutomationElementクラスを定義
$TreeScope = [System.Windows.Automation.TreeScope]  # TreeScope列挙型を定義
$Condition = [System.Windows.Automation.Condition]  # Conditionクラスを定義
$InvokePattern = [System.Windows.Automation.InvokePattern]  # InvokePatternクラスを定義
$ValuePattern = [System.Windows.Automation.ValuePattern]  # ValuePatternクラスを定義
$SelectionItemPattern = [System.Windows.Automation.SelectionItemPattern] 
$TogglePattern = [System.Windows.Automation.TogglePattern] 
$ExpandCollapsePattern = [System.Windows.Automation.ExpandCollapsePattern] 

$TreeWalker=[System.Windows.Automation.TreeWalker]


# ウィンドウの全要素の配列を取得する関数
function GetElementsfromWindow {
    Param(
        $RootWindowName = $null,     # ルートウィンドウの名前（部分一致）
        $waitMilliseconds = 300      # ウィンドウが見つからない場合の待機時間（ミリ秒）
    )

    # ルート要素から全ての子要素を取得
    $childrenElements = $AutomationElement::RootElement.FindAll($TreeScope::Children, $Condition::TrueCondition)
    
    # ルートウィンドウが見つかるまで繰り返す
    Do {
        # 全ての子要素について処理
        foreach ($element in $childrenElements) {
            # 要素の名前がルートウィンドウの名前に部分一致する場合
            if ($element.GetCurrentPropertyValue($AutomationElement::NameProperty) -like "*$RootWindowName*") {
                # サブツリー内の全要素を取得して返す
                return $element.FindAll($TreeScope::Subtree, $Condition::TrueCondition)
            }
        }

        # 指定された名前の部分が含まれるウィンドウが見つからない場合
        Write-Host "部分名 '${RootWindowName}' のウィンドウが見つかりません。"   
        # 指定されたミリ秒数だけ待機
        Start-Sleep -Milliseconds $waitMilliseconds 
        
    } While ($true)
}



# 要素を特定する関数
function FindElement {
    Param(
        $RootWindowName = $null,   # ルートウィンドウの名前（部分一致）
        $PropertyType,             # プロパティの種類
        $Identifier,               # 識別子
        $waitMilliseconds = 300      # ウィンドウが見つからない場合の待機時間（ミリ秒）        
    )

    # ルートウィンドウから要素を取得して繰り返し処理
    Do {
        foreach ($element in GetElementsfromWindow -RootWindowName $RootWindowName) {
            # 要素が指定されたプロパティの識別子と一致する場合
            if ($element.GetCurrentPropertyValue($AutomationElement::$PropertyType) -eq $Identifier) {
                # 要素を返す
                return $element
            }
        }
        # 指定されたプロパティの識別子が見つからない場合
        Write-Host " '$Identifier' の '$PropertyType' 要素が見つかりません。"  
        # 指定されたミリ秒数だけ待機
        Start-Sleep -Milliseconds $waitMilliseconds 
        
    } While ($true)
}



# クリック操作をする関数
function InvokeElement {
    Param(
        $RootWindowName,    # ルートウィンドウの名前
        $PropertyType,      # プロパティの種類
        $Identifier         # 識別子
    )

    # 識別子を元に要素を検索する
    $element = FindElement -RootWindowName $RootWindowName -PropertyType $PropertyType -Identifier $Identifier
    # $element の名前などをデバッグする
    # Write-Host "Element Name: $($element.Current.Name)"
    # Write-Host "Control Type: $($element.Current.ControlType.ProgrammaticName)"

    # 要素に対して Invoke パターンを実行する
    $element.GetCurrentPattern($InvokePattern::Pattern).Invoke()

    Start-Sleep -Milliseconds 300
}


# 値を入力する関数
function SetValueNextElement {
    Param(
        $RootWindowName,    # ルートウィンドウの名前
        $PropertyType,      # プロパティの種類
        $Identifier,        # 識別子
        $inputValue        # 入力する値
    )

    # 識別子を元に要素を検索する
    $element = FindElement -RootWindowName $RootWindowName -PropertyType $PropertyType -Identifier $Identifier

    # 次の要素を取得する
    $nextSiblingElement = $TreeWalker::ControlViewWalker.GetNextSibling($element)

    # 次の要素に対して値を設定する
    $nextSiblingElement.GetCurrentPattern($ValuePattern::Pattern).SetValue($inputValue)
    Start-Sleep -Milliseconds 300    
}





###################################################
# ↓↓↓↓↓↓↓ この行以降に自動操作スクリプトを記述する。 ↓↓↓↓↓↓↓ #
###################################################

```

# PowerShellコード（自動操作スクリプトの例）
* 動画で用いた、自動操作スクリプトの例です。CSVファイルのパスを指定して、CSVファイルを配列に読み込み、それらの値を用いて要素に値を入力したり、クリックしたりします。

```powershell
# CSVファイルのパスを指定します
$csvFilePath = "C:\Users\TestFolder\input_test01\元データ.csv"

# CSVファイルを配列に読み込みます
$csvArray = Import-Csv -Path $csvFilePath



InvokeElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "開始"



# 配列の中身を1行ずつ処理します
foreach ($row in $csvArray) {
    # ヘッダーが「苗字」の場合の処理
    if ($row.苗字) {
        Write-Output "苗字: $($row.苗字)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "苗字" -inputValue $($row.苗字)
    }
    
    # ヘッダーが「名前」の場合の処理
    if ($row.名前) {
        Write-Output "名前: $($row.名前)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "名前" -inputValue $($row.名前)
    }
    
    
    if ($row.会社名) {
        Write-Output "会社名: $($row.会社名)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "会社名" -inputValue $($row.会社名)
    }
    
    if ($row.部署) {
        Write-Output "部署: $($row.部署)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "部署" -inputValue $($row.部署)
    }
    
    if ($row.住所) {
        Write-Output "住所: $($row.住所)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "住所" -inputValue $($row.住所)
    }
    
    if ($row.メールアドレス) {
        Write-Output "メールアドレス: $($row.メールアドレス)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "メールアドレス" -inputValue $($row.メールアドレス)
    }
    
    if ($row.電話番号) {
        Write-Output "電話番号: $($row.電話番号)"
        SetValueNextElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "電話番号" -inputValue $($row.電話番号)
    }

    InvokeElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "登録"


}
```

# PowerShellコード（機能追加）
* 以下のコードを汎用関数の中に追加することで、操作の種類を増やすことができます。

```powershell:追加関数
# ラジオボタンの状態を変更する関数
function SelectionItemElement {
    Param(
        $RootWindowName,    # ルートウィンドウの名前
        $PropertyType,      # プロパティの種類
        $Identifier        # 識別子
    )

    # 識別子を元に要素を検索する
    $element = FindElement -RootWindowName $RootWindowName -PropertyType $PropertyType -Identifier $Identifier

    # 要素の SelectionItem パターンを適用して選択状態にする
    $element.GetCurrentPattern($SelectionItemPattern::Pattern).Select()
    Start-Sleep -Milliseconds 300    
}




# チェックボックスの状態を変更する関数
function ToggleElement {
    Param(
        $RootWindowName,    # ルートウィンドウの名前
        $PropertyType,      # プロパティの種類
        $Identifier        # 識別子
    )

    # 識別子を元に要素を検索する
    $element = FindElement -RootWindowName $RootWindowName -PropertyType $PropertyType -Identifier $Identifier

    # 要素の Toggle パターンを適用してチェックボックスの状態を変更する
    $element.GetCurrentPattern($TogglePattern::Pattern).Toggle()
    Start-Sleep -Milliseconds 300    
}


# コンボボックスを開く関数
function ExpandNextElement {
    Param(
        $RootWindowName,    # ルートウィンドウの名前
        $PropertyType,      # プロパティの種類
        $Identifier        # 識別子
    )

    # 識別子を元に要素を検索する
    $element = FindElement -RootWindowName $RootWindowName -PropertyType $PropertyType -Identifier $Identifier

    # 次の要素を取得する
    $nextSiblingElement = $TreeWalker::ControlViewWalker.GetNextSibling($element)

    # 次の要素に対して ExpandCollapse パターンを適用してコンボボックスを開く
    $nextSiblingElement.GetCurrentPattern($ExpandCollapsePattern::Pattern).Expand()
    Start-Sleep -Milliseconds 300    
}

```
# PowerShellコード（要素の調査）
* 以下のコードを用いることで、ウィンドウのすべての要素の特定のプロパティとパターンをデバッグできます。

```powershell:要素の調査
# UI オートメーション関連のアセンブリを読み込む
Add-Type -AssemblyName "UIAutomationClient"  # UIAutomationClientアセンブリを追加
Add-Type -AssemblyName "UIAutomationTypes"   # UIAutomationTypesアセンブリを追加

# UI オートメーション関連の型を定義する
$AutomationElement = [System.Windows.Automation.AutomationElement]  # AutomationElementクラスを定義
$TreeScope = [System.Windows.Automation.TreeScope]  # TreeScope列挙型を定義
$Condition = [System.Windows.Automation.Condition]  # Conditionクラスを定義


# ウィンドウの全要素の配列を取得する関数
function GetElementsfromWindow {
    Param(
        $RootWindowName = $null,     # ルートウィンドウの名前（部分一致）
        $waitMilliseconds = 300      # ウィンドウが見つからない場合の待機時間（ミリ秒）
    )

    # ルート要素から全ての子要素を取得
    $childrenElements = $AutomationElement::RootElement.FindAll($TreeScope::Children, $Condition::TrueCondition)
    
    # ルートウィンドウが見つかるまで繰り返す
    Do {
        # 全ての子要素について処理
        foreach ($element in $childrenElements) {
            # 要素の名前がルートウィンドウの名前に部分一致する場合
            if ($element.GetCurrentPropertyValue($AutomationElement::NameProperty) -like "*$RootWindowName*") {
                # サブツリー内の全要素を取得して返す
                return $element.FindAll($TreeScope::Subtree, $Condition::TrueCondition)
            }
        }

        # 指定された名前の部分が含まれるウィンドウが見つからない場合
        Write-Host "部分名 '${RootWindowName}' のウィンドウが見つかりません。"   
        # 指定されたミリ秒数だけ待機
        Start-Sleep -Milliseconds $waitMilliseconds 
        
    } While ($true)
}


# 要素を特定する関数
function SearchElement {
    Param(
        $RootWindowName = $null,   # ルートウィンドウの名前（部分一致）
        $PropertyType,             # プロパティの種類
        $Identifier               # 識別子
    )

    # カウンターの初期化
    $counter = 0

    # 出力の開始を表示
    Write-Host "---------出力開始---------"

    # GetElementsfromWindow関数で取得した各要素に対してループを実行
    foreach ($element in GetElementsfromWindow -RootWindowName $RootWindowName) {
    
        # カウンターの値を増加
        $counter++
        Write-Host ""
        Write-Host "$($counter)"
        Write-Host "Name: $($element.Current.Name)"
        Write-Host "LocalizedControlType: $($element.Current.LocalizedControlType)"
        
        # サポートされているパターンの出力
        $patterns = $element.GetSupportedPatterns()

        # 各サポートされているパターンに対してループを実行
        foreach ($pattern in $patterns) {
            # パターンの名前を表示
            Write-Host "Supported Pattern: $($pattern.ProgrammaticName)"
        }

        # 指定したプロパティの値が識別子と一致する場合の処理
        if ($element.GetCurrentPropertyValue($AutomationElement::$PropertyType) -eq $Identifier) {
            Read-Host "Enterで続行"
        }
    }

    # 出力の終了を表示
    Write-Host "---------出力終了---------"  
}



###################################################
# ↓↓↓↓↓↓↓ この行以降に要素調査スクリプトを記述する。 ↓↓↓↓↓↓↓ #
###################################################




SearchElement -RootWindowName "Rpa" -PropertyType "NameProperty" -Identifier "苗字"

```

# コードの修正履歴
例）yyyy/mm/dd:〇〇について修正
2024/05/24: 各関数において、要素取得に失敗または要素の操作に成功した際に「指定されたミリ秒数だけ待機」するコードを挿入しました。