HoyoPlay、Steamなどの各種ランチャーを最小化して起動したかった

# 背景

PC起動時にHoyoPlayやSteamなどのランチャーを自動起動すると、ウィンドウが表示された状態で起動される。


閉じるボタンを押せば済む話だが、毎度のこととなると手間。
そこでPowerShellスクリプトを用いて起動直後に自動的に閉じるコードを書いた。

なお、コードはChatGPTで修正しながら書いたので細かい部分については目をつむってほしい。

## まず最終的なコードから
HoyoPlay、Steamを起動して、自動的に最小化するコードです。
タスクスケジューラーなどで管理者権限で実行して使う。

## コード
```powershell:launcher_mini_run.ps1
Add-Type -TypeDefinition @'
using System;
using System.Text;
using System.Runtime.InteropServices;

public class User32Ex {
    // EnumWindowsProc 用のデリゲート
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    // トップレベルウィンドウを列挙するための API
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    // ウィンドウのタイトルを取得する API
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

    // ウィンドウのクラス名を取得する API
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    // ウィンドウが可視状態かを判定する API
    [DllImport("user32.dll", SetLastError=false)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool IsWindowVisible(IntPtr hWnd);

    // ウィンドウメッセージ送信 (Close用)
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    // 取得しやすい形にまとめたヘルパーメソッド
    public static string GetWindowText(IntPtr hWnd) {
        StringBuilder sb = new StringBuilder(256);
        GetWindowText(hWnd, sb, sb.Capacity);
        return sb.ToString();
    }

    public static string GetClassName(IntPtr hWnd) {
        StringBuilder sb = new StringBuilder(256);
        GetClassName(hWnd, sb, sb.Capacity);
        return sb.ToString();
    }
}
'@

# 定数
$SW_MINIMIZE = 6
$WM_CLOSE = 0x0010

function CloseWindow {
    param (
        [string]$TargetClassName,
        [string]$TargetWindowTitle = $null,
        [int]$TimeoutSeconds = 10,
        [int]$TimeoutSeconds_after = 10
    )

    Write-Host "$TargetClassName ウィンドウを待機しています..."

    $startTime = Get-Date
    $afterTime = $null
    while ($true) {
        Start-Sleep -Seconds 1

        # EnumWindows でトップレベルウィンドウを列挙
        [User32Ex]::EnumWindows({
            param($hWnd, $lParam)

            if ([User32Ex]::IsWindowVisible($hWnd)) {
                $className = [User32Ex]::GetClassName($hWnd)
                $windowTitle = [User32Ex]::GetWindowText($hWnd)

                if ($className -eq $TargetClassName -and (!$TargetWindowTitle -or $windowTitle -like $TargetWindowTitle)) {
                    Write-Host "ウィンドウ (Handle: $hWnd, Title: '$windowTitle') を終了しています..."
                    Start-Sleep -Seconds 1
                    [User32Ex]::SendMessage($hWnd, $WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero)
                     $script:afterTime = Get-Date
                }
            }
            return $true
        }, [IntPtr]::Zero)

        if (((Get-Date) - $startTime).TotalSeconds -ge $TimeoutSeconds) {
            Write-Host "タイムアウト: $TimeoutSeconds 秒経過しました。"
            break
        }
        
        if ($script:afterTime -and ((Get-Date) - $script:afterTime).TotalSeconds -ge $TimeoutSeconds_after) {
            Write-Host "アフタータイムアウト: $TimeoutSeconds_after 秒経過しました。"
            break
        }
    }
}

Start-Process "C:\Program Files (x86)\Steam\steam.exe" 
Start-Process "D:\HoYoPlay\launcher.exe"


CloseWindow -TargetClassName "Qt51517QWindowIcon" -TargetWindowTitle "*HoYoPlay*" -TimeoutSeconds 10 -TimeoutSeconds_after 5
CloseWindow -TargetClassName "SDL_app" -TargetWindowTitle "*Steam*" -TimeoutSeconds 10 -TimeoutSeconds_after 5
```

コードの流れとしては
1.`Start-Process`でランチャーを起動
2.`CloseWindow`でウィンドウが表示されるのを待機して、見つかったら閉じる(=タスクトレイに収納される)

:::note info
動作が安定しない場合はタイムアウトの時間を伸ばすと動くかもしれない。
デフォルトは最大10秒間、1つ目のウィンドウが見つかったあとは5秒間待機する。
（Steamなどは複数のウィンドウが表示されるため、それを想定した動作）
:::
:::note info
ファイルパスについては適宜修正してください。
:::
:::note warn
注意点として、HoYoPlayについてはウィンドウを閉じたときに最小化(=タスクトレイに収納)するモードとランチャーを閉じるモードがあるので、「ウィンドウを最小化する」を選択しておくこと。
![{D70213E5-B761-47A5-BB12-DB5640E79ED7}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3872158/8cae653f-ebbe-29f8-ed83-17d2d2c6e35f.png)
:::

:::note warn
全くウィンドウが閉じられない場合、管理者権限で実行していない可能性があるので要確認
:::
## おまけ
現在開いているウィンドウのタイトルやクラス名を列挙するための補助スクリプト。
任意のランチャーやアプリを自動的に閉じたいときにクラス名やタイトルを調べるときに使う。

```powershell:listwindows.ps1
Add-Type -TypeDefinition @'
using System;
using System.Text;
using System.Runtime.InteropServices;

public class User32 {
    // EnumWindowsProc 用のデリゲート
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    // トップレベルウィンドウを列挙するための API
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    // ウィンドウのタイトルを取得する API
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

    // ウィンドウのクラス名を取得する API
    [DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=false)]
    public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    // ウィンドウが可視状態かを判定する API
    [DllImport("user32.dll", SetLastError=false)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool IsWindowVisible(IntPtr hWnd);

    // 取得しやすい形にまとめたヘルパーメソッド
    public static string GetWindowText(IntPtr hWnd) {
        StringBuilder sb = new StringBuilder(256);
        GetWindowText(hWnd, sb, sb.Capacity);
        return sb.ToString();
    }

    public static string GetClassName(IntPtr hWnd) {
        StringBuilder sb = new StringBuilder(256);
        GetClassName(hWnd, sb, sb.Capacity);
        return sb.ToString();
    }
}
'@

# 可視ウィンドウを列挙し、クラス名とタイトルを取得して一覧にする
$windows = New-Object System.Collections.Generic.List[PSObject]

[User32]::EnumWindows({
    param($hWnd, $lParam)

    if ([User32]::IsWindowVisible($hWnd)) {
        $title = [User32]::GetWindowText($hWnd)
        # タイトルが空でなければ情報を取得・格納
        if ($title -and $title.Trim().Length -gt 0) {
            $className = [User32]::GetClassName($hWnd)
            $windows.Add([PSCustomObject]@{
                Handle    = $hWnd
                ClassName = $className
                Title     = $title
            })
        }
    }

    # 列挙を続行
    return $true
}, [IntPtr]::Zero)

# 取得したウィンドウ一覧を出力
$windows | ForEach-Object {
    Write-Output "ハンドル   : $($_.Handle)"
    Write-Output "クラス名  : $($_.ClassName)"
    Write-Output "タイトル  : $($_.Title)"
    Write-Output "---------------------------"
}

# 終了せずに待機
Write-Host "処理が完了しました。Enterキーを押すと終了します。"
Read-Host "続行するにはEnterキーを押してください"

```

## まとめ

そもそもこういうランチャーは最小化状態で起動できるようにしてほしい。
調べるとそれらしいコマンドライン引数はあるものの、機能しなかったりする。

誰かの参考になりましたら幸いです。