複数のディスプレイ間でウィンドウを移動させたい。座標系がどうのこうの。

きばらしに、ショートキー一発でアクティブウィンドウを別のディスプレイに移動させるアプリをつくっている。
そんなもの30分でできそうなものだが、残念なことに、System.Windows.Formsはてんで不完全だ。アクティブウィンドウを取得する手段すらない!

アクティブウィンドウの取得

GetForegroundWindowでウィンドウハンドルが取れる。

ディスプレイ情報の取得

これは.netのライブラリで対応してる。
Screen.AllScreensですべてのディプレイ情報が取得できる ――途中で解像度が変わるとまずいらしいが、今回の用途には問題ないと判断してスルー。
Screen.FromHandleで指定したウィンドウが所属するディスプレイが取得できる。

Screen[] screens=Screen.AllScreens;
Screen current=Screen.FromHandle( target_window );
//Screenオブジェクトの比較にはBoundsプロパティを使うとよい。
//DeviceNameはなぜか取得するたびに違ったりする!文字化けしてるし!無視!
int current_index = Array.Find(screens,delegate(Screen s){return s.Bounds==current.Bounds});
Screen next=screens[ (current_index + 1) % screens.Length ];

//ほげほげ

座標の取得と変換と計算

ウィンドウ操作のさい出てくる座標系は、次のようなものがある(と現在認識している。名前は適当に付けた)

絶対座標系
すべてのスクリーン領域を含む座標系
スクリーン絶対座標系
スクリーン左上を原点とする座標系
クライアント座標系
スクリーン領域からスタートメニュー等をのぞいた部分を基準とする。タスクバーが下にあるときはスクリーン絶対座標系と一致

ウィンドウ操作APIでは、これらのどれかが使用される……のだが、ドキュメントにもまともな記載がないこと多々。特にスクリーン絶対座標系とスクリーンクライアント座標系は多くの場合同一であるため、取り違えていてもバグが顕在しないことも多々ありタスクバーを上に配置しているひねくれ者を日々苛立たせている。うんこうんこ。

とりあえずSetWindowPlacementGetWindowPlacementは同一の座標系(おそらくは絶対座標系)を使用している。MoveWindowはそれとは別(おそらくクライアント座標系)を使っているため、不用意に混ぜると困る。また、マウスカーソルの位置を設定/取得するSystem.Windows.Forms.Cursor.Positionはクライアント座標系。

ウィンドウを他ディスプレイに移動させ、ウィンドウとの相対位置を保ったままマウスカーソルも追随させるには、次のような手順となる。

  1. 対象となるウィンドウのハンドル(tgtとする)を取得。API::GetActiveWindow
  2. tgtが所属しているディスプレイを取得(cur_scrとする)。System.Windows.Forms.Screen.FromHandle
  3. 存在するディスプレイを列挙。System.Windows.Forms.Screen.AllScreens(EnumDisplayDevicesを使ったほうが確実しかし面倒)
  4. 移動先のディスプレイを決定(tgt_scrとする)
  5. ウィンドウの現在位置を取得。API::GetWindowPlacement
  6. ウィンドウの移動先絶対座標を計算
    1. tgt_scr.Bound.Location - cur_scr.Bounds.Location でディスプレイ間の距離が求められる。
    2. 現在のウィンドウ座標にこのオフセットを加えればよい。
  7. ウィンドウの位置を設定。API::SetWindowPlacement
  8. クライアント座標の補正。
    1. WorkingArea.Location - Bounds.Locationが絶対座標とクライアント座標の相対位置になる。
    2. (tgt_scr.WorkingArea.Location - tgt_scr.Bound.Location) - (cur_scr.WorkingArea.Location - cur_scr.Bounds.Location)がクライアント座標の補正値となる
  9. マウス座標を取得。System.Windows.Forms.Cursor.Position
  10. マウス座標を設定。現在座標にディスプレイ間の距離+クライアント座標補正値を足してCursor.Positionにセット。

以上。

……以上ではない!この手順に基づいて実装してみたものの挙動が微妙に変だ。特にウィンドウがふたつのディスプレイ間にまたがって表示されてるときおかしい。ううう。