適材適所

WindowsやPowerShellやネットワーク、IBMなどのシステム系の話やポイ活など気になったことも載せているブログです。

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

PowerShellでWindows APIを使ってウィンドウを移動させる方法の紹介です。

UI Automationを使った方法はこちら↓

www.tekizai.net

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

ウィンドウを移動させるときは、WindowsのAPIであるuser.dllのMoveWindow関数を使うことで実現できます。

C#やCでやるほどでもない・・・というときは、PowerShellを使うとお手軽で簡単です。

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

PowerShellなら、C#のネイティブなソースをコンパイルし、起動することができます。

C#のソースを書き、Add-Typeコマンドレットで追加します。

MoveWindow関数を使うために、次のようなC#の命令を書いて、Add-Typeで追加してあげます。

 
$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, int X, 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. 移動先のx位置
  3. 移動先のy位置
  4. 移動後の幅
  5. 移動後の高さ
  6. 画面の再描写をするかどうか(基本的に$trueを指定)

ウィンドウハンドルというのは、Windowsの中で、そのウィンドウひとつひとつに結びつけられた識別子(その実態は数字)です。

Windows関数の定義だと、hWnd型と書かれています。

このウィンドウハンドルに自分が操作したいウィンドウの値をセットしてあげることが、MoveWindow関数の肝です。

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

ウィンドウハンドルの取得はGet-Processコマンドレットを使うと簡単に知ることができます。

しかし、このコマンドレットの出力結果をそのまま使うとバックグラウンドプロセスで動いているプログラムも一覧に表示されてしまうことと、

肝心のウィンドウのタイトルが表示されないので、次のようにフィルターとパイプをしてあげると欲しい情報がいい感じに得られます。

 
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を指定すると、ウィンドウ全体のハンドルを取得できます。

 
[Win32]::GetWindowDC(0)
-318695421

これでウィンドウのdpiを取得する準備が整いました。

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

これらを組み合わせると・・・

 
[void][Win32]::SetProcessDPIAware()
[Win32]::GetDeviceCaps([Win32]::GetWindowDC(0),88)
[Win32]::GetDeviceCaps([Win32]::GetWindowDC(0),90)
120
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㎝四方くらいにしてみたいと思います。

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

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

出力

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

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

public static extern bool MoveWindow(IntPtr hWnd, int X, 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㎝四方になるはずです。

ここまでできれば、あとはやってみるのみ。いざ実行!

 
[void][Win32]::MoveWindow(1508268,0,0,568,568,$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, int X, 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で

 
[Win32]::MoveWindow(1508268,0,0,568,568,$true)

とすると、ウィンドウを移動することができました。

なにかの参考になれば。

というわけで、ここまでお読み頂き、ありがとうございました。

参考サイト

GetDeviceCaps

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