適材適所

パソコン作業の自動化・効率化のための情報を発信するブログ(VBA,PowerShellなど)

PowerShellでウィンドウの移動(MoveWindow関数)

Windowsのウィンドウをコマンドから移動させたい

ウィンドウを移動させるときは、user.dllのMoveWindow関数を使います。今回はpowershellです。

手っ取り早く結果だけ知りたい人は、「まとめ」を見てくださいな。

MoveWindow関数(とその他)を使う準備

C#のソースを書き、Add-Typeコマンドレットで追加します。
後で使う関数たち(3つ)はMoveWindow関数を使うだけなら不要なものです。
後で使うのでここに入れています。

$source= @"
 using System;
 using System.Runtime.InteropServices;

 public static class Win32{
 [DllImport("user32.dll")]
 [return: MarshalAs(UnmanagedType.Bool)]
 public static extern bool MoveWindow(IntPtr hWnd, intX, int Y, int nWidth, int nHeight, bool bRepaint);

##後で使う関数たち
  [DllImport("user32.dll")]
 public static extern int GetWindowDC(int hWnd);

 [DllImport("gdi32.dll")]
 public static extern int GetDeviceCaps(IntPtr hdc, int index);

 [DllImport("user32.dll")]
 public static extern bool SetProcessDPIAware();
}
"@
Add-Type -TypeDefinition $source

MoveWindow関数の引数

MoveWindow関数の引数は6つです。
1つ目の引数は移動したいウィンドウのウィンドウハンドルです。
2、3番目の引数は移動先の位置です。
4、5番目の引数は移動後のサイズです。
最後の引数は基本的に$trueを指定します。

ウィンドウハンドルの取得

ウィンドウハンドルの取得はGet-Processコマンドレットで行うのが楽です。ただし、このコマンドレットをそのまま使うとバックグラウンドプロセスで動いているプログラムも一覧に表示されてしまうことと、肝心のウィンドウのタイトルが表示されないので、次のようにフィルターとパイプをしてあげると欲しい情報がいい感じに得られます。

PS C:\> get-process|?{$_.MainWindowTitle -ne ""}|select MainWindowTitle,MainWindowHandle


MainWindow                  TitleMainWindowHandle
---------------                  ----------------
無題 - メモ帳                  1508268
Windows PowerShell               4787404
Windows シェル エクスペリエンス ホスト     66310

今回はメモ帳を動かしてみたいと思います。
ウィンドウハンドルは1508268になります。

どのくらいの大きさか

 2~5番目の引数は数値になります。数値の単位はピクセルです。1ピクセルの大きさはデバイスによって異なります。適当に動かしてみてサイズ感を理解するのも有効なのですが、今回はちゃんと調べてみます。

1ピクセルのサイズを調べる

(少しだけ込み入った話になります。面倒な方は読み飛ばしてもらっても大丈夫です。)

まず、ディスプレイの情報を取得する必要があります。
ディスプレイのdpiを取得します。dpiとはdot per inch、1インチ当たりのピクセル数です。ここで、Add-Typeコマンドレットで色々と追加したものを使います。

GetDeviceCaps関数を使うとディスプレイのdpiが取得できます。
第一引数はウィンドウハンドル、第二引数は取得したいものの種類を定数で指定します。定数はあらかじめ決まっているので調べます。今回は88と89です。

GetDeviceCaps関数の第一引数のウィンドウハンドルはGetWindowDCという関数で取得できます。GetWindowDC関数は引数に0を指定すると、ウィンドウ全体のハンドルを取得できます。

PS C:\> [Win32]::GetWindowDC(0)
-318695421

これでウィンドウのdpiを取得する準備が整いました。つまり、今使っているウィンドウが「1インチに含まれるピクセル数」を得る準備が整いました。

PS C:\> [Win32]::SetProcessDPIAware()
True
PS C:\> [Win32]::GetDeviceCaps([Win32]::GetWindowDC(0),88)
120
PS C:\> [Win32]::GetDeviceCaps([Win32]::GetWindowDC(0),90)
120

試してもらうとわかるのですが、GetDeviceCapsの第二引数に88と90を指定した場合、dpiは変わりません。縦と横で1インチ当たりのピクセル数が変わってしまってはおかしなことになるからですかね・・・?

最初に[Win32]::SetProcessDPIAware()を実行しているのは、これを実行しないと、常に96という数値を返すからだそうです。

ここでようやく1インチに含まれるピクセル数がわかりました。120ということは1インチに120個のピクセルがあるということです。 1インチ=2.54㎝です。
2.54㎝に120個のピクセルが含まれています。
2.54㎝/120個≒0.021166666666666..㎝/1個≒0.02㎝/1個
つまり、私の環境だと1ピクセルあたり0.02㎝だということがわかりました。

使ってみる

では実際にMoveWindow関数でウィンドウを動かしてみます。
今回のターゲットのウィンドウはメモ帳です。メモ帳をメインウィンドウの左上にぴったり合わせ、大きさを10㎝四方くらいにしてみたいと思います。

もう一度ハンドルを取得してみます。

PS C:\> get-process|?{$_.MainWindowTitle -ne ""}|select MainWindowTitle,MainWindowHandle


MainWindow                  TitleMainWindowHandle
---------------                  ----------------
無題 - メモ帳                  1508268
Windows PowerShell               4787404
Windows シェル エクスペリエンス ホスト     66310

ここでもう一度MoveWindow関数の引数を確認します。

public static extern bool MoveWindow(IntPtr hWnd, intX, int Y, int nWidth, int nHeight, bool bRepaint);

hWnd=1508268
x(横位置)=0
y(縦位置)=0
nWidth(横ピクセル)=100/0.2=500
nHeight(縦ピクセル)=100/0.2=500

xとyは0がメインウィンドウの左上になります。
理論上はこれでメモ帳の位置が左上で10㎝四方になるはずです。

いざ実行!

PS C:\> [Win32]::MoveWindow(1508268,0,0,568,568,$true)
True

f:id:shinmai_papa:20190706134907p:plain

f:id:shinmai_papa:20190706134902p:plain あれ、10㎝四方にならない・・・(´・ω・`)

しかも左が少し空いている・・・(´・ω・`)

ここまで偉そうに語ってきましたが、計算があってないのか、どこか理解が誤っているところがあるのか・・・。

やっぱり何度か試してもらって状況に合わせてもらうのが一番いいようです。

まとめ

ということでおさらいです。

$source= @"
  using System;
  using System.Runtime.InteropServices;
  public static class Win32{

  [DllImport("user32.dll")]
  [return: MarshalAs(UnmanagedType.Bool)]
  public static extern bool MoveWindow(IntPtr hWnd, intX, int Y, int nWidth, int nHeight, bool bRepaint);
}
"@
Add-Type -TypeDefinition $source

を実行し、

get-process|?{$_.MainWindowTitle -ne ""}|select MainWindowTitle,MainWindowHandle

でハンドルを取得し、引数1にハンドル、2に横位置、3に縦位置、4に横サイズ、5に縦サイズ、最後は$trueで

PS C:\> [Win32]::MoveWindow(1508268,0,0,568,568,$true)

とすると、ウィンドウを移動することができました。 なにかの参考になれば。

参考サイト

GetDeviceCaps GetDeviceCapsが常にDPI96を返す問題と解決方法について · GitHub