PowerShellで特定のアプリケーションダイアログにエスケープキーを送信する方法（自動起動機能付き）

## はじめに

本記事では、PowerShellを使用して特定のアプリケーションのダイアログにエスケープキーを送信し、閉じる処理を実行するスクリプトを紹介します。さらに、アプリケーションが起動していない場合に自動で起動する機能も追加しています。

## スクリプトの実装

以下は、指定したプロセスのダイアログにエスケープキーを送信し、必要に応じてアプリケーションを自動起動するPowerShellスクリプトです。

```powershell
Add-Type @"
    using System;
    using System.Runtime.InteropServices;
    public class User32 {
        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        [DllImport("user32.dll")]
        public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
    }
"@

Add-Type -AssemblyName System.Windows.Forms

# 設定
$targetProcessName = "対象プロセス名"  # 例: "notepad" (メモ帳の場合)
$dialogTitle = "ダイアログのタイトル"  # 例: "名前を付けて保存"
$checkInterval = 10  # チェック間隔（秒）
$appPath = "C:\Path\To\Your\Application.exe"  # アプリケーションの実行ファイルパス

function Start-TargetApplication {
    if (-not (Get-Process | Where-Object { $_.ProcessName -eq $targetProcessName })) {
        Write-Host "$(Get-Date) - アプリケーションを起動します。"
        Start-Process $appPath
        Start-Sleep -Seconds 10  # アプリケーションの起動を待つ
    }
}

function Get-TargetProcess {
    return Get-Process | Where-Object { $_.ProcessName -eq $targetProcessName } | Select-Object -First 1
}

Write-Host "スクリプトを実行中です。終了するにはこのウィンドウを閉じてください。"

while ($true) {
    Start-TargetApplication

    $targetProcess = Get-TargetProcess
    if ($null -eq $targetProcess) {
        Write-Host "$(Get-Date) - 対象プロセスが見つかりません。再試行します。"
        Start-Sleep -Seconds $checkInterval
        continue
    }

    # ダイアログのハンドルを取得
    $dialogHandle = [User32]::FindWindow("#32770", $dialogTitle)  # "#32770"は標準的なWindowsダイアログのクラス名

    if ($dialogHandle -ne [IntPtr]::Zero) {
        # ダイアログのプロセスIDを取得
        $dialogProcessId = 0
        [User32]::GetWindowThreadProcessId($dialogHandle, [ref]$dialogProcessId)

        # プロセスIDが一致するか確認
        if ($dialogProcessId -eq $targetProcess.Id) {
            Write-Host "$(Get-Date) - ダイアログを検出しました。エスケープキーを送信します。"

            # ウィンドウを前面に表示
            [User32]::SetForegroundWindow($dialogHandle)
            [User32]::ShowWindow($dialogHandle, 9)  # 9はSW_RESTORE

            # エスケープキーの送信
            [System.Windows.Forms.SendKeys]::SendWait("{ESC}")

            Start-Sleep -Seconds 1

            # ダイアログが閉じたかどうかを確認
            $dialogStillOpen = [User32]::FindWindow("#32770", $dialogTitle) -ne [IntPtr]::Zero
            if (!$dialogStillOpen) {
                Write-Host "$(Get-Date) - ダイアログが正常に閉じました。"
            } else {
                Write-Host "$(Get-Date) - ダイアログが閉じませんでした。"
            }
        }
    }

    Start-Sleep -Seconds $checkInterval
}
```

## スクリプトの説明

1. **`Add-Type`**: Windows API関数を利用するために、`User32`クラスを作成します。
2. **設定**: `$targetProcessName`、`$dialogTitle`、`$checkInterval`、`$appPath`に対象のプロセス名、ダイアログタイトル、チェック間隔、アプリケーションのパスを設定します。
3. **`Start-TargetApplication`関数**: 対象のアプリケーションが起動していない場合、自動的に起動します。
4. **`Get-TargetProcess`関数**: 対象のプロセスを取得します。
5. **メインループ**:
   - アプリケーションの起動確認
   - ダイアログのハンドル取得
   - エスケープキーの送信
   - ダイアログが閉じたかの確認
6. **ループでダイアログの存在を監視**: 指定された間隔でダイアログの存在を確認し続けます。

## プロセス名、ダイアログタイトル、クラス名の調査方法

### 1. プロセス名の確認方法
プロセス名は、以下の手順で確認できます。
- **タスクマネージャーを開く**: タスクバーを右クリックし、「タスクマネージャー」を選択します。
- **プロセスタブで確認**: 実行中のプロセス名を確認します。

### 2. ダイアログタイトルの確認方法
ダイアログのタイトルは、ダイアログを開いているアプリケーションを見て確認できます。ダイアログの上部に表示されるタイトルバーにタイトルが表示されます。

### 3. クラス名の確認方法

PowerShellを使用して、現在表示されているすべてのウィンドウのクラス名を取得する方法を紹介します。このスクリプトは、Windows APIを利用してウィンドウ情報を収集します。

## スクリプト

```powershell
Add-Type @"
    using System;
    using System.Runtime.InteropServices;
    public class Win32 {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

        public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool IsWindowVisible(IntPtr hWnd);
    }
"@

$windows = New-Object System.Collections.ArrayList

$enumWindowsCallback = {
    param($hwnd, $lParam)

    if ([Win32]::IsWindowVisible($hwnd)) {
        $classNameBuilder = New-Object System.Text.StringBuilder(256)
        [Win32]::GetClassName($hwnd, $classNameBuilder, $classNameBuilder.Capacity)
        $className = $classNameBuilder.ToString()

        $titleBuilder = New-Object System.Text.StringBuilder(256)
        [Win32]::GetWindowText($hwnd, $titleBuilder, $titleBuilder.Capacity)
        $title = $titleBuilder.ToString()

        if (![string]::IsNullOrEmpty($className) -or ![string]::IsNullOrEmpty($title)) {
            $null = $windows.Add([PSCustomObject]@{
                Handle = $hwnd
                ClassName = $className
                Title = $title
            })
        }
    }

    return $true
}

$delegate = [Win32+EnumWindowsProc]$enumWindowsCallback
[Win32]::EnumWindows($delegate, [IntPtr]::Zero)

$windows | Format-Table -AutoSize

Write-Host "合計ウィンドウ数: $($windows.Count)"
```

## 説明

1. まず、必要なWin32 API関数（EnumWindows, GetClassName, GetWindowText, IsWindowVisible）を定義します。

2. `EnumWindows`関数を使用して、すべてのトップレベルウィンドウを列挙します。

3. 各ウィンドウに対して以下の処理を行います：
   - ウィンドウが可視かどうかをチェック
   - クラス名を取得
   - ウィンドウタイトルを取得
   - クラス名またはタイトルが空でない場合、リストに追加

4. 収集したウィンドウ情報をテーブル形式で表示します。

5. 最後に、見つかったウィンドウの総数を表示します。

## 注意点

- このスクリプトを実行するには管理者権限が必要な場合があります。
- セキュリティ設定によっては、一部のウィンドウ情報にアクセスできない可能性があります。
- ウィンドウの数が多い場合、出力が長くなる可能性があります。必要に応じて、出力をファイルにリダイレクトしたり、特定のクラス名やタイトルでフィルタリングしたりすることをお勧めします。

## ウィンドウハンドルの検索方法

ウィンドウハンドルを検索するには、以下の方法があります：

### 1. プロセス名からウィンドウハンドルを取得

```powershell
Add-Type @"
    using System;
    using System.Runtime.InteropServices;
    public class Win32 {
        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    }
"@

$processName = "notepad"  # 例: メモ帳
$process = Get-Process | Where-Object { $_.ProcessName -eq $processName } | Select-Object -First 1

if ($process) {
    $hwnd = $process.MainWindowHandle
    Write-Host "ウィンドウハンドル: $hwnd"
} else {
    Write-Host "指定されたプロセスが見つかりません。"
}
```

### 2. ウィンドウタイトルからウィンドウハンドルを取得

```powershell
Add-Type @"
    using System;
    using System.Runtime.InteropServices;
    public class Win32 {
        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
    }
"@

$windowTitle = "無題 - メモ帳"  # 例: メモ帳のタイトル
$hwnd = [Win32]::FindWindow([NullString]::Value, $windowTitle)

if ($hwnd -ne [IntPtr]::Zero) {
    Write-Host "ウィンドウハンドル: $hwnd"
} else {
    Write-Host "指定されたタイトルのウィンドウが見つかりません。"
}
```

### 3. すべてのウィンドウを列挙してハンドルを取得

```powershell
Add-Type @"
    using System;
    using System.Runtime.InteropServices;
    public class Win32 {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

        public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
    }
"@

$windows = New-Object System.Collections.ArrayList

$enumWindowsCallback = {
    param($hwnd, $lParam)

    $sb = New-Object System.Text.StringBuilder(256)
    [Win32]::GetWindowText($hwnd, $sb, $sb.Capacity)
    $title = $sb.ToString()

    if (![string]::IsNullOrEmpty($title)) {
        $null = $windows.Add([PSCustomObject]@{
            Handle = $hwnd
            Title = $title
        })
    }

    return $true
}

# ScriptBlockをデリゲートに変換
$delegate = [Win32+EnumWindowsProc]$enumWindowsCallback

# EnumWindowsを呼び出し
[Win32]::EnumWindows($delegate, [IntPtr]::Zero)

if ($windows.Count -gt 0) {
    $windows | Format-Table -AutoSize
} else {
    Write-Host "ウィンドウが見つかりませんでした。"
}
```

## 注意点
- **セキュリティ設定**: アプリケーションのセキュリティ設定によって、外部からの入力（キーストロークなど）が拒否されることがあります。
- **アプリケーションの特性**: アプリケーションによっては、ダイアログのタイトルやクラス名が異なるため、事前に確認が必要です。


## まとめ
PowerShellを用いることで、特定のアプリケーションのダイアログにエスケープキーを送信し、閉じる処理を自動化できます。上記の手法を用いて、様々なアプリケーションの操作を効率化しましょう。