• #AutoHotkey
  • #PowerShell
  • #Excel
  • #Stream Deck
  • #ウルトラワイドモニター
  • #ウィンドウ管理
開発misc-devメモ

Excel全ウィンドウをウルトラワイドモニター中央に一括配置する

ウルトラワイドモニターでExcelを使っていると、ブックが画面の左端や右端に散らばる。8つのブックを開いた状態でモニター中央(1920x1080、座標4600,180)に全部並べ直す作業を毎日やっていた。Stream Deckのボタン一発で片付けたい――そう思ってスクリプトを書き始めたが、ここから4段階の試行錯誤が始まった。


Step 1: バッチファイルにPowerShellを埋め込む → 行結合で壊れる

Stream Deckから呼ぶ最もシンプルな方法として、.batにPowerShellのhere-stringを埋め込んだ。

powershell -Command "$code = @'
Add-Type ...
'@; ..."

実行するとhere-stringがバッチの行結合で壊れ、構文エラーが出た。バッチファイルのエスケープルールとPowerShellのhere-stringは相性が悪い。

修正: .ps1を分離して、.batから powershell -ExecutionPolicy Bypass -File move-excel.ps1 で呼ぶ構成に切り替えた。構文エラーが消えて、スクリプト本体の開発に集中できるようになった。


Step 2: 1ウィンドウしか動かない → 全プロセスをループに変更

最初のPowerShellスクリプトではGet-Process EXCEL | Select -First 1で1つだけ取得していた。当然、8つのうち1つしか動かない。

修正: Select -First 1を外してGet-Process EXCELの全結果をループで回した。

Get-Process EXCEL | ForEach-Object {
    [Win32]::MoveWindow($_.MainWindowHandle, 4600, 180, 1920, 1080, $true)
}

ところが結果は変わらず、1つのウィンドウしか移動しない。ここで手が止まった。


Step 3: Excelは1プロセスで複数ブックを持つ → EnumWindowsで直接列挙

原因を調べて腑に落ちた。Excelは複数ブックを開いていてもOSのプロセスは1つしか立たない。Get-Processで列挙してもMainWindowHandleは1個だけ返る。

修正: Win32 APIのEnumWindowsを使い、ウィンドウクラス名XLMAINを持つウィンドウを直接列挙する方式に切り替えた。

# EnumWindowsのコールバックでXLMAINクラスを収集
[Win32]::EnumWindows({
    param($hwnd, $lParam)
    $cls = New-Object Text.StringBuilder 256
    [Win32]::GetClassName($hwnd, $cls, 256)
    if ($cls.ToString() -eq "XLMAIN") {
        # $hwndをリストに追加
    }
    return $true
}, [IntPtr]::Zero)

これで8つのExcelウィンドウハンドルが全て取得できた。MoveWindowをループで回すと、8つ全てが指定座標に移動した――が、サイズがおかしい。


Step 4: リサイズが効かない → SW_SHOWNORMALに変更

最大化や最小化されたウィンドウに対してMoveWindowを呼んでも、位置は変わるがサイズが元に戻らない。最初はShowWindow($hwnd, 9)(SW_RESTORE)を呼んでからMoveWindowしていたが、最大化状態のウィンドウは復元後に直前のサイズに戻るだけで、こちらが指定したサイズにはならなかった。

修正: ShowWindow($hwnd, 1)(SW_SHOWNORMAL)に変更した。SW_SHOWNORMALは最大化・最小化フラグを完全にクリアしてから通常状態に戻す。その後にMoveWindowを呼ぶと、指定サイズが正しく適用された。

[Win32]::ShowWindow($hwnd, 1)  # SW_SHOWNORMAL
[Win32]::MoveWindow($hwnd, 4600, 180, 1920, 1080, $true)

8つのExcelウィンドウが全て1920x1080でモニター中央に並んだ。


最終構成

StreamDeck → move-excel.bat → move-excel.ps1
  • move-excel.bat: powershell -ExecutionPolicy Bypass -File move-excel.ps1
  • move-excel.ps1: Add-TypeでWin32 API定義 → EnumWindowsでXLMAINクラス列挙 → ShowWindow(1) + MoveWindow

学んだこと

  • Excelのプロセスモデル: 複数ブックを開いてもGet-Processは1行しか返さない。ウィンドウ単位で操作するにはEnumWindows + クラス名フィルタが必要
  • SW_RESTOREとSW_SHOWNORMALの違い: SW_RESTORE(9)は「直前の状態に戻す」、SW_SHOWNORMAL(1)は「最大化/最小化フラグをクリアして通常表示」。リサイズを効かせたいなら後者
  • バッチとPowerShellの分離: here-stringの埋め込みは行結合で壊れる。最初から.ps1を分離した方が速い
  • デバッグ手順: 「動かない」の切り分けは、ハンドルが取れているか → 取れたハンドルの数は正しいか → API呼び出しの戻り値は何か、の順に追う