PowerShellで好きなウィンドウの位置とサイズを指定する方法

ネットで断片的な情報しか見つからなくて苦戦していたのですが、解決したので記事にしました。好きなウィンドウのタイトルを指定して位置とサイズを設定します。

### 目次
1. スクリプト
1. 使い方
1. スクリプトの解説

## スクリプト
先にスクリプト全体を示します。Set-WindowPosition.ps1という名前でファイルを作成して下のスクリプトをコピペしてください。

```powershell:Set-WindowPosition.ps1
param (
    [int32]$x,
    [int32]$y,
    [int32]$width,
    [Int32]$hight,
    [string]$windowNames
)

Add-Type @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
    
    [DllImport("user32.dll")]
    public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
    
    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
    
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    public const int SW_SHOWNORMAL = 1;
}
"@

$windowNameArray = $windowNames -split '\s+'

$enumCallback = {
    param ($hWnd, $lParam)

    $sb = New-Object System.Text.StringBuilder 256
    $wndText = [User32]::GetWindowText($hWnd, $sb, $sb.Capacity)

    if ($wndText -gt 0) {
        $windowTitle = $sb.ToString()

        foreach ($windowName in $windowNameArray) {
            if ($windowTitle -like "*$windowName*") {
                Write-Host "Window name: $windowTitle"
                Write-Host "Position:    ($x, $y)"
                Write-Host "Size:        ($width, $hight)"

                [User32]::ShowWindow($hWnd, [User32]::SW_SHOWNORMAL) | Out-Null
                [User32]::SetForegroundWindow($hWnd) | Out-Null
                [User32]::SetWindowPos($hWnd, [IntPtr]::Zero, $x, $y, $width, $hight, 0) | Out-Null
                break
            }
        }
    }
    return $true
}

[User32]::EnumWindows($enumCallback, [IntPtr]::Zero) | Out-Null
```

## 使い方
Windows PowerShellを開いて、Set-WindowPosition.ps1が置いてあるディレクトリに移動し、下のコマンドを打ち込めば完了です（`$`はプロンプトなので打たない）。こちらの例では、スクリーン左上から座標 (x, y) = (100, 100) の位置に縦横 500x500 の大きさで、「Chrome」をタイトルに含むウィンドウを配置します。

```terminal
$ & ".\Start-WindowAtPosition.ps1" -x 100 -y 100 -width 500 -hight 500 -windowNames Chrome
```

### 実行結果（標準出力）

```terminal
Window name: 新しいタブ - Google Chrome
Position:    (100, 100)
Size:        (500, 500)
```

ウィンドウのタイトルはタスクバーのアイコンにマウスポインタをかざしたときに浮き出てくる文字列のことです。Google Chromeの新しいウィンドウを開いた時の名前は「新しいタブ - Google Chrome」になります。`-windowNames`には半角スペース区切りで複数指定できます。

:::note warn
**ウィンドウタイトル指定時の注意点**
ウィンドウが読み込まれるまではタイトルが決まらないため、ウィンドウのロードが終わらないうちに上のスクリプトを実行してしまうと上手くいかない場合があります。

例えばGoogle Chromeで https://google.com を開いた時のタイトルの変遷は、筆者の環境だと

0.000s : なし
0.182s : 無題 - Google Chrome
0.506s : Google - Google Chrome

のようになりました。タイトルが変わってしまう対策として複数タイトルを指定しておくといいです。
:::

## スクリプトの解説

はじめに載せたスクリプトを少しずつ組み立てるつもりで解説していきます。

#### 画面上のすべてのウィンドウを列挙
Win32 APIから`EnumWindows`を読み込みます。

```powershell
Add-Type @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
}
"@

[User32]::EnumWindows($enumCallback, [IntPtr]::Zero)
```
`EnumWindows`は画面上のすべてのウィンドウを列挙します。列挙されたウィンドウに行う処理を第１引数にコールバック関数として渡します。第２引数で追加のデータを渡すことができますが、今回は使わないのでNULLを渡しています。

#### 目的のウィンドウを見つけたら位置とサイズを設定する
`EnumWindows`に渡す`$enumCallback`を書いて、各ウィンドウになんの処理をするか指定します。今回は指定したタイトルのウィンドウを見つけた場合に位置とサイズを設定するという処理を行います。そのため、ウィンドウタイトルを取得する`GetWindowText`とウィンドウの位置とサイズを指定する`SetWindowPos`を読み込みます。

```powershell
Add-Type @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
    
    [DllImport("user32.dll")]
    public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
    
    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
}
"@

$enumCallback = {
    param ($hWnd, $lParam)

    $sb = New-Object System.Text.StringBuilder 256
    $wndText = [User32]::GetWindowText($hWnd, $sb, $sb.Capacity)

    if ($wndText -gt 0) {
        $windowTitle = $sb.ToString()

        if ($windowTitle -like "*新しいタブ*") {
            [User32]::SetWindowPos($hWnd, [IntPtr]::Zero, 100, 100, 500, 500, 0)
        }
    }
    return $true
}

[User32]::EnumWindows($enumCallback, [IntPtr]::Zero)
```

`$enumCallback`はウィンドウハンドル`$hWnd`と追加で渡すデータ`$lParam`（今回は使いません）を引数にとります。`$sb`はタイトルを格納するための256文字の容量を持つオブジェクトです。`$hWnd`と`$sb`を`GetWindowText`に渡してウィンドウタイトルの文字数`$wndText`を取得します。文字数が0より大きい場合、if文の中身が実行されます。if文では、最初に`$windowTitle`を取得します。次のif文の中身は、`$windowTitle`に「新しいタブ」が含まれるときに実行されます。前後の`*`は任意の文字列を表します。「新しいタブ」が任意の文字列に囲まれることを表します。if文の中で位置とサイズを指定する`SetWindowPos`を実行します。引数はそれぞれ、ウィンドウハンドル、他のウィンドウとの前後関係の指定（今回は使わないのでNULL）、x座標、y座標、幅、高さ、その他のオプション（今回は使わないので0）を指定します。最後の`return $true`で次のウィンドウの処理に移ります。

#### スクリプトの外から値を渡せるようにする

```powershell
param (
    [int32]$x,
    [int32]$y,
    [int32]$width,
    [Int32]$hight,
    [string]$windowName
)

Add-Type @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
    
    [DllImport("user32.dll")]
    public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
    
    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
}
"@

$enumCallback = {
    param ($hWnd, $lParam)

    $sb = New-Object System.Text.StringBuilder 256
    $wndText = [User32]::GetWindowText($hWnd, $sb, $sb.Capacity)

    if ($wndText -gt 0) {
        $windowTitle = $sb.ToString()

        if ($windowTitle -like "*$windowName*") {
            [User32]::SetWindowPos($hWnd, [IntPtr]::Zero, $x, $y, $width, $hight, 0)
        }
    }
    return $true
}

[User32]::EnumWindows($enumCallback, [IntPtr]::Zero)
```

### 画面最大化、最小化時も動くようにする
今のままでも動きますが、実は画面を最大化や最小化していると位置とサイズの設定がされません。そこで、最大化や最小化を解除するために`ShowWindow`を追加します。ついでに画面を最前面に持ってくる`SetForegroundWindow`も追加します。

```powershell
param (
    [int32]$x,
    [int32]$y,
    [int32]$width,
    [Int32]$hight,
    [string]$windowName
)

Add-Type @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
    
    [DllImport("user32.dll")]
    public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
    
    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

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

    public const int SW_SHOWNORMAL = 1;
}
"@

$enumCallback = {
    param ($hWnd, $lParam)

    $sb = New-Object System.Text.StringBuilder 256
    $wndText = [User32]::GetWindowText($hWnd, $sb, $sb.Capacity)

    if ($wndText -gt 0) {
        $windowTitle = $sb.ToString()

        if ($windowTitle -like "*$windowName*") {
            [User32]::ShowWindow($hWnd, [User32]::SW_SHOWNORMAL)
            [User32]::SetForegroundWindow($hWnd)
            [User32]::SetWindowPos($hWnd, [IntPtr]::Zero, $x, $y, $width, $hight, 0)
        }
    }
    return $true
}

[User32]::EnumWindows($enumCallback, [IntPtr]::Zero)
```

`ShowWindow`の第2引数に1を指定すると最大化や最小化が解除されます。しかし、意味が分かりやすいように、`SW_SHOWNORMAL`という定数を導入しています。

#### 仕上げ
もうほとんど完成ですが、複数のタイトルを指定できるようにします。以下は最初に示したのと同じスクリプトです。

```powershell
param (
    [int32]$x,
    [int32]$y,
    [int32]$width,
    [Int32]$hight,
    [string]$windowNames
)

Add-Type @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll")]
    public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
    public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
    
    [DllImport("user32.dll")]
    public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
    
    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
    
    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
    
    [DllImport("user32.dll")]
    public static extern bool SetForegroundWindow(IntPtr hWnd);

    public const int SW_SHOWNORMAL = 1;
}
"@

$windowNameArray = $windowNames -split '\s+'

$enumCallback = {
    param ($hWnd, $lParam)

    $sb = New-Object System.Text.StringBuilder 256
    $wndText = [User32]::GetWindowText($hWnd, $sb, $sb.Capacity)

    if ($wndText -gt 0) {
        $windowTitle = $sb.ToString()

        foreach ($windowName in $windowNameArray) {
            if ($windowTitle -like "*$windowName*") {
                Write-Host "Window name: $windowTitle"
                Write-Host "Position:    ($x, $y)"
                Write-Host "Size:        ($width, $hight)"

                [User32]::ShowWindow($hWnd, [User32]::SW_SHOWNORMAL) | Out-Null
                [User32]::SetForegroundWindow($hWnd) | Out-Null
                [User32]::SetWindowPos($hWnd, [IntPtr]::Zero, $x, $y, $width, $hight, 0) | Out-Null
                break
            }
        }
    }
    return $true
}

[User32]::EnumWindows($enumCallback, [IntPtr]::Zero) | Out-Null
```

半角スペース区切りでタイトルを指定するので、`$windowName`を`$windowNames`に書き換えて、それを配列に分割したのが$windowNameArrayです。foreachでそれらの要素全部に処理を行います。最後に、標準出力に情報を表示するために `Write-Host`を使って完成です。実行結果が標準出力に表示されてほしくない行に`| Out-Null`をつけています。