6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-05-24

概要

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

実行環境

  • Microsoft Windows 11
  • PowerShell version 5.1
  • Microsoft Edge

開発環境

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

注意点

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

参照

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

PowerShellコード(汎用関数)

  • まずは以下のコードを「Windows PowerShell ISE」のスクリプトウィンドウに貼り付けます。このコードを用いると、「Windowの部分名」や「要素の名前」から操作したい要素を特定し、実際に要素をクリックしたり、値を入力することが容易になります。
汎用関数
# 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ファイルを配列に読み込み、それらの値を用いて要素に値を入力したり、クリックしたりします。
# 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コード(機能追加)

  • 以下のコードを汎用関数の中に追加することで、操作の種類を増やすことができます。
追加関数
# ラジオボタンの状態を変更する関数
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コード(要素の調査)

  • 以下のコードを用いることで、ウィンドウのすべての要素の特定のプロパティとパターンをデバッグできます。
要素の調査
# 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: 各関数において、要素取得に失敗または要素の操作に成功した際に「指定されたミリ秒数だけ待機」するコードを挿入しました。

6
5
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
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?