PowershellでInternetExplorerを操作する

# IEの起動
```powershell
$url = "http://hogehoge.com"

# シェルを取得
$shell = New-Object -ComObject Shell.Application

# IE起動
$ie = New-Object -ComObject InternetExplorer.Application

# 可視化
$ie.Visible = $true

# HWNDを記憶
$hwnd = $ie.HWND

# URLオープン(キャッシュ無効)
$ie.Navigate($url,4)

# IE再取得
while ($ie.Document -isnot [mshtml.HTMLDocumentClass]) {
    $ie = $shell.Windows() | ? {$_.HWND -eq $hwnd}
}
```

IEの保護モードがオフの時は、`-ComObject`に`InternetExplorer.ApplicationMedium`を指定するか、管理者モードで起動しないと、URLを開いた後にIEが操作できなくなる。しかし、保護モードかどうかを知るのが難しく、管理者モードで起動できるとも限らない。

最後のループはそれを解消するためのもので、`$shell.Windows()`からIEを取得しなおすと保護モードがオフでもIEが操作できる。ループにしたのは、`$ie.Document`が取得できない場合があるため。

# DOMの操作

```powershell
function OverrideMethod ([mshtml.HTMLDocumentClass]$Document) {
    $doc = $Document | Add-Member -MemberType ScriptMethod -Name "getElementById" -Value {
        param($Id)
        [System.__ComObject].InvokeMember(
            "getElementById",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $Id
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force -PassThru

    $doc | Add-Member -MemberType ScriptMethod -Name "getElementsByClassName" -Value {
        param($ClassName)
        [System.__ComObject].InvokeMember(
            "getElementsByClassName",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $ClassName
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force

    $doc | Add-Member -MemberType ScriptMethod -Name "getElementsByTagName" -Value {
        param($TagName)
        [System.__ComObject].InvokeMember(
            "getElementsByTagName",
            [System.Reflection.BindingFlags]::InvokeMethod,
            $null,
            $this,
            $TagName
        ) | ? {$_ -ne [System.DBNull]::Value}
    } -Force

    return $doc
}

$document = OverrideMethod($ie.Document)

$button = $document.getElementById("login_button")
$button.click()
```

Powershellから`getElementById()`などを実行すると、メソッドは存在しているのにエラーになる場合がある。こういう場合は、`InvokeMember()`経由で実行するようメソッドをオーバーライドする。

メソッドのオーバーライドは`Add-Member`に`-Force`を指定すればできる。ただし、もとのオブジェクトを直接オーバーライドするわけではなく、いったんPSObjectでラップしてそのメソッドをオーバーライドしている。そのため、最初のオーバーライド時に`-PassThru`を指定してラップしたオブジェクトを受け取る必要がある。

なお、`getElementById()`などは該当するノードがないと、`$null`ではなく`[System.DBNull]::Value`を返してくるので、扱いやすくするため除外している。

# 読み込み待ち

```powershell
while ($ie.busy -or $ie.readystate -ne 4) {
    Start-Sleep -Milliseconds 100
}
```

読み込み待ちのサンプルとして上記のようなコードをよく見かけるが、これだとページのリダイレクトやJavaScriptによる動的生成に対応できない。以下のように、実際に操作したいオブジェクトを取得してみる方が確実。

```powershell
while ($true) {
    $button = $document.getElementById("login_button")

    # 画面が表示された
    if ($button) {break}
    
    Start-Sleep -Milliseconds 100
}

$button.click()
```

# 既存のウインドウを取得する

```powershell
$url = "http://hogehoge.com"

$ie = $shell.Windows() |
? {$_.Name -eq "Internet Explorer"} |
? {$_.LocationURL -like $url} |
Select-Object -First 1
```

# ウインドウの最大化
```powershell
$dll_info = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
Add-Type -MemberDefinition $dll_info -Name NativeMethods -Namespace Win32

[Win32.NativeMethods]::ShowWindowAsync($ie.HWND, 3) | Out-Null
```

# ウインドウをアクティブ化する

```powershell
Add-Type -AssemblyName Microsoft.VisualBasic

$window_process = Get-Process -Name "iexplore" | ? {$_.MainWindowHandle -eq $ie.HWND}

[Microsoft.VisualBasic.Interaction]::AppActivate($window_process.ID) | Out-Null
```

※`Out-Null`は本当は不要なのだが、行が表示されなくなるので仕方なく付けた。二重コロンがダメ?

# タブを前面に表示する

```powershell
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms

while ($true) {
    # ウインドウプロセス取得
    while ($true) {
        $window_process = Get-Process -Name "iexplore" | ? {$_.MainWindowHandle -eq $ie.HWND}

        if ($window_process) {break}

        Start-Sleep -Milliseconds 100
    }
    
    # ウインドウをアクティブ化
    [Microsoft.VisualBasic.Interaction]::AppActivate($window_process.ID)

    if ($window_process.MainWindowTitle -ne "$($ie.Document.title) - Internet Explorer") {
        # タブ切替
        [System.Windows.Forms.SendKeys]::SendWait("^{TAB}")
    } else {
        # タブが前面に表示されている
        break
    }
}
```

ウインドウプロセスの`MainWindowTitle`と`$ie.Document.title`を比較すれば、タブが前面に表示されているかが分かる。あとは目的のタブが前面に表示されるまでタブを切り替えれば良い。

ウインドウプロセスはタブを切り替える度に取得し直さなければならず、タブを切り替えてから間がないと取得に失敗する。

タブの切替はショートカットキーで行わなければならず、念のため毎回ウインドウをアクティブ化している。

# キーイベントに反応するフォームの入力

```powershell
Add-Type -AssemblyName Microsoft.VisualBasic

$username = "user1"

$input = $document.getElementById("user")

# ウインドウのアクティブ化
[Microsoft.VisualBasic.Interaction]::AppActivate($window_process.ID) | Out-Null

# 入力欄のクリア
$input.value = ""

# フォーカスを当てる
$input.focus()

# キー入力
[System.Windows.Forms.SendKeys]::SendWait($username) | Out-Null

# 入力が反映されたか確認
while ($input.value -ne $username) {
    Start-Sleep -Milliseconds 100
}
```

キーイベントに反応してバリデーションチェックが行われ送信ボタンが押せるようになるフォームの場合、`value`に直接値を代入しても送信ボタンが押せるようにならないため、キー入力を再現する必要がある。

事前に値が入力されているとその後に追加されてしまうので、まず入力欄をクリアした後にキー入力する。また、文字列のキー入力は反映されるのに時間がかかるので、最後のループで入力が反映されたことを確認している。

※`Out-Null`は本当は不要なのだが、行が表示されなくなるので仕方なく付けた。