前回まででウインドウのサイズを変更するPowerShellスクリプト「window_resizer.ps1」を作成してきた。このスクリプトの本質は、Windows APT (user32.dll)のMoveWindow()関数だ。次の関数を呼び出すことで、ウインドウサイズを変更している。

bool MoveWindow(IntPtr ウインドウハンドラ, int X, int Y, int 幅, int 高さ, bool);

XとYの値を現在のウインドウと同じ値にし、幅と高さを変更すればサイズ変更を行うことができる。

この関数はほかの処理にも利用できる。今度は逆に、幅と高さはそのままに、XとYだけを変えてみよう。こうすると、サイズはそのままに表示される場所だけが変更される。要するに、ウインドウを移動させる処理になるのだ。今回はこのスクリプトを「window_move.ps1」として作成する。

window_move.ps1への書き換え

window_move.ps1は、ほとんどwindow_resizer.ps1を流用することができる。書き換えた結果は、次のようになる。

#!/usr/bin/env pwsh

#====================================================================
# 引数を処理
#====================================================================
Param(
    [String]$ProcessName="*",   # プロセス名
    [String]$WindowTitle=".*",  # ウインドウタイトル(正規表現)
    [Int32]$X="0",              # ウインドウ幅
    [Int32]$Y="0",              # ウインドウ高さ
    [Switch]$WindowProcessList  # ウインドウプロセス一覧を表示
)

#====================================================================
# ウインドウプロセスを一覧表示
#====================================================================
if ($WindowProcessList) {
    Get-Process                         |
    ? {$_.MainWindowHandle -ne 0 }              |
    Format-Table -Property Id,ProcessName,MainWindowTitle
    exit
}

#====================================================================
# ウインドウを移動する関数 Move-Window
#====================================================================
Add-Type @"
using System;
using System.Runtime.InteropServices;

public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

public class WinAPI
{
    // ウインドウの現在の座標データを取得する関数
    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    // ウインドウの座標を変更する関数
    [DllImport("user32.dll")]
    public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
}
"@

function Move-Window {
    param (
        $wh  # ウインドウハンドラ
    )

    # ウインドウ座標データ構造体
    $rc = New-Object RECT

    # ウインドウの現在の座標データを取得
    [WinAPI]::GetWindowRect($wh, [ref]$rc) > $null

    # 取得した座標データからウインドウの幅と高さを計算
    $width = $rc.Right - $rc.Left;
    $height = $rc.Bottom - $rc.Top;

    # ウインドウのサイズはそのままに、左上の場所を変更
    [WinAPI]::MoveWindow($wh, $X, $Y, $width, $height, $true) > $null
}

#====================================================================
# 対象となるウインドウを選択し、サイズを変更
#====================================================================
Get-Process -Name $processName |
    ? { $_.MainWindowHandle -ne 0 } |
    ? { $_.MainWindowTitle -match "$windowTitle" } |
    % {
        # ウインドウサイズを変更
        Move-Window($_.MainWindowHandle);
}

window_resizer.ps1とwindow_move.ps1の差分を見ると、書き換えた内容が理解しやすい。UNIX系ではこうしたファイルは通常「パッチファイル」と呼ばれ、「diff」というコマンドで作成する(逆に、パッチファイルは「patch」というコマンドでファイルに適用すると、ファイルを書き換えることができる)。

差分の読み方を簡単に説明しておくと、1行目に書いてあるのが元のファイル、2行目に書いてあるのが書き換えた先のファイル、それ以降は「-」から始まる行が元ファイルから削除した行、「+」が元ファイルへ追加した行、である。これを踏まえて読めば、どの部分をどのように書き換えたかがわかるはずだ。なお、この書き方は、diffのユニファイド形式と呼ばれることが多い。人間が比較的読みやすい形式である。

◆window_resizer.ps1とwindow_move.ps1の差分

--- window_resizer.ps1  2021-08-19 19:59:53.929113000 +0900
+++ window_move.ps1     2021-08-26 18:16:18.575664000 +0900
@@ -6,8 +6,8 @@
 Param(
        [String]$ProcessName="*",   # プロセス名
        [String]$WindowTitle=".*",  # ウインドウタイトル(正規表現)
-       [Int32]$Width="800",        # ウインドウ幅
-       [Int32]$Height="600",       # ウインドウ高さ
+       [Int32]$X="0",              # ウインドウ幅
+       [Int32]$Y="0",              # ウインドウ高さ
        [Switch]$WindowProcessList  # ウインドウプロセス一覧を表示
 )

@@ -22,7 +22,7 @@
 }

 #====================================================================
-# ウインドウサイズを変更する関数 Resize-Window
+# ウインドウを移動する関数 Move-Window
 #====================================================================
 Add-Type @"
 using System;
@@ -48,7 +48,7 @@
 }
 "@

-function Resize-Window {
+function Move-Window {
        param (
                $wh  # ウインドウハンドラ
        )
@@ -59,8 +59,12 @@
        # ウインドウの現在の座標データを取得
        [WinAPI]::GetWindowRect($wh, [ref]$rc) > $null

-       # 左上の場所はそのままに、ウインドウのサイズを変更
-       [WinAPI]::MoveWindow($wh, $rc.Left, $rc.Top, $width, $height, $true) > $null
+       # 取得した座標データからウインドウの幅と高さを計算
+       $width = $rc.Right - $rc.Left;
+       $height = $rc.Bottom - $rc.Top;
+
+       # ウインドウのサイズはそのままに、左上の場所を変更
+       [WinAPI]::MoveWindow($wh, $X, $Y, $width, $height, $true) > $null
 }

 #====================================================================
@@ -71,5 +75,5 @@
        ? { $_.MainWindowTitle -match "$windowTitle" } |
        % {
                # ウインドウサイズを変更
-               Resize-Window($_.MainWindowHandle);
+               Move-Window($_.MainWindowHandle);
 }

まず、パラメータを-ProcessName、-WindowTitle、-Width、-Height、-WindowProcessListから-ProcessName、-WindowTitle、-X、-Y、-WindowProcessListへ変更している。ウインドウを移動させたいので、引数として幅と高さではなく、X座標とY座標を取得している。X座標とY座標というのはディスプレイ左上から右への長さ(X座標)、ディスプレイ左上から下への長さ(Y座標)だ。ディスプレイ上の座標としてよく使われる指定方法である。

次に、サイズを変更する関数Resize-Windowを、場所を変更する関数Move-Windowへ書き換えている。名前を変更した以外は、Windows APIのMoveWindow()関数の使い方が変わっただけだ。

実行してみよう

作成したスクリプト(window_move.ps1)をパスの通ったフォルダにコピーし、実行してみよう。実行すると次のようになる。

スクリプト実行前のスクリーンショット

◆100, 100へ移動

windw_move -ProcessName msedge -X 100 -Y 100

スクリプト実行後

◆1200, 500へ移動

window_move -ProcessName msedge -X 1200 -Y 500

スクリプト実行後

◆1300, 50へ移動

window_move-ProcessName msedge -X 1300 -Y 50

スクリプト実行後

◆0, 600へ移動

window_move -ProcessName msedge -X 0 -Y 600

スクリプト実行後

作った通りに動作していることがわかる。ちょっとした書き換えだが、なかなか実用的なスクリプトだ。

応用例

これでウインドウの移動とサイズの変更ができるようになった。基本的な処理なのだが、これまでマウスを使うしかなかった処理をスクリプトで自動化できるというのは時短に大きく貢献するのだ。一度使い出すと癖になるくらい便利だったりする。

実用的な使い方は近いうちに取り上げるとして、今回はこのスクリプトの使用例を1つ取り上げておく。次のスクリプト(Alert.ps1)をご覧いただきたい。

while ($true) {
        window_move -ProcessName msedge -X 600 -Y 300
        Start-Sleep 1
        window_move -ProcessName msedge -X 500 -Y 250
        Start-Sleep 1
        window_move -ProcessName msedge -X 600 -Y 200
        Start-Sleep 1
        window_move -ProcessName msedge -X 700 -Y 250
        Start-Sleep 1
}

このスクリプトを実行すると、Microsoft Edgeが1秒ごとに、円を描くように時計回りで移動するようになる。何の役にも立たなそうだが、タイマーとして使うのは悪くない。仕事を切り上げないといけない時間になったら動作するように仕込んでおくと、いきなり目の前でEdgeが円を描きながら移動し始める。イラッとするのは間違いないが、作業を強制的に停止させる効果はあるだろう。

ある意味、嫌がらせのようなスクリプトなので実際に利用するかどうかはさておき、こういったことができる、ということは覚えておいて損はないだろう。