1. 検証アプリ
少し前にWindows 8.1 PreviewのPer-Monitor DPI環境下でDPI仮想化(DPI Virtualization)のかかったウィンドウのスクリーン座標を正確に取得できないことがあるのに気づいたので、検証アプリを作ってみた。
- 上2つの"Chaser"は下の"Runner"のスクリーン座標を追いかけるためのアプリで、
- "Process Name"にあるプロセス名のプロセスを見つけて、そのウィンドウの座標をWin32のGetWindowRect関数を使って表示する("GetWindowRect Left"がウィンドウの左上角のX座標、"GetWindowRect Top"が同じくY座標)。
- "Process DPI Awareness"はそのプロセスのDPI Awarenessを示す。
- タイトルバーにはこのChaser自身のDPI Awarenessを表示。
- "Process Name"にあるプロセス名のプロセスを見つけて、そのウィンドウの座標をWin32のGetWindowRect関数を使って表示する("GetWindowRect Left"がウィンドウの左上角のX座標、"GetWindowRect Top"が同じくY座標)。
- 下の"Runner"はドラッグして動かしてみるためのアプリで、
- Runner自身の座標をWin32のGetWindowRect関数とForm.Locationプロパティを使って表示する(これらは常に一致する、はず)。
- "Return Zero"はスクリーンの原点(左上角)にウィンドウを移動させる。原点はいかなる場合でも一致することの確認用。
- "Move One Right"はウィンドウをドラッグによらず、右に座標1つ分だけずらす。
- タイトルバーにはこのRunnerのDPI Awarenessを表示。
2. 検証結果
モニターは、1680x1050(メイン)と1024x768(セカンド)のマルチモニター。
これを元の96DPIの環境から、Per-Monitor DPI環境(メインは120DPIになり、セカンドは96DPIのまま)に設定後、サインアウトまたは再起動する前の、新しいDPIがまだデスクトップに反映されてない状態で、Chaser (Per-Monitor Aware)とChaser (System Aware)を起動しておく。
ここでRunner (System Aware)を起動し、メインからセカンドに移る直前まで動かした状態が以下。
正確な座標を示しているのはChaser (Per-Monitor Aware)で、Runner (System Aware)はDPI仮想化がかかっていて、拡大された分だけ座標は小さな値になっている。Chaser (System Aware)はRunner (System Aware)と同じで、正確でない。なお、Chaser (System Aware)にDPI仮想化がかかっているが、検証には関係ない。
このまま右にドラッグするとセカンドに移るが、敢えてMove One Rightで座標1つ分だけずらすとこうなる。
Runner (System Aware)のウィンドウが左上に飛んでいる。「何これ?」という感じだが、DPI仮想化の効果が切れ、座標も元に戻って、そこに移動している。三者の示す座標は同じ。普通にウィンドウを動かしている限りは陥らない状態だが、システム的にはこうなるということ。
この状態、あるいは最初の状態からドラッグして右に動かし、セカンドの左端に合わせたところ。
メインの幅は1680なので、三者とも座標は正確。ということは、Runner (System Aware)とChaser (System Aware)にとっては、座標が大きくジャンプしたことになる。その間の座標はどこに行ったかというと、上の状態のとおり。
さて、次にRunner (Per-Monitor Aware)を起動して、同じようにメインからセカンドに移る直前まで動かす。
Chaser (Per-Monitor Aware)とRunner (Per-Monitor Aware)は正確な座標を示しているが、Chaser (System Aware)は先ほどと同じく小さくなった値を示している。
同じようにMove One Rightで座標1つ分だけずらす。
Runner (Per-Monitor Aware)はそのまま波乱なく移動。Chaser (System Aware)は値がジャンプした。
このままセカンドの左端まで動かす。
三者とも正確な座標で一致。
これで結論は見えてきたが、確認のため一旦サインアウトして新しいDPIがデスクトップに反映された状態にし、同じようにChaser (Per-Monitor Aware)とChaser (System Aware)を起動しておく。
まずRunner (System Aware)を起動。
ここまでは三者とも一致して正確。
Move One Rightで座標1つ分だけずらす。
またRunner (System Aware)のウィンドウが飛んでいる。その座標をChaser (Per-Monitor Aware)は正確に捉えているが、Runner (System Aware)とChaser (System Aware)はそのまま右にずれたかのように認識している。ウィンドウにはDPI仮想化が縮小する方向にかかっている。
セカンドの左端まで動かす。
Chaser (Per-Monitor Aware)は正確だが、Runner (System Aware)の座標はDPI仮想化で縮小した分だけ大きな値になっている。Chaser (System Aware)も同様。
最後にRunner (Per-Monitor Aware)を起動。
ここまでは三者とも一致。
Move One Rightで座標1つ分だけずらす。
Chaser (Per-Monitor Aware)とRunner (Per-Monitor Aware)は至って正確だが、Chaser (System Aware)は座標がジャンプした。
セカンドの左端まで動かす。
Chaser (Per-Monitor Aware)とRunner (Per-Monitor Aware)は正確で、Runner (Per-Monitor Aware)にDPI仮想化もかかっていないが(当然)、Chaser (System Aware)の座標は大きくなった値のままずれている。
3. まとめ
以上から、とりあえずの結論。
- アプリのDPI AwarenessがPer-Monitor Awareに設定されていない場合、Per-Monitor DPI環境下ではそのプロセスで実行したGetWindowsRect関数が正確な値を返さないことがある。これは対象のプロセスが自分のアプリか他のアプリか(そして、その、他のアプリがPer-Monitor Awareに設定されているか否か)には関係ない。
- これは対象のアプリにDPI仮想化がかかっているか否かにも直接関係ない(この存在を示唆するものだとは思うが)。
- GetWindowsRect関数以外でも同様のことは起こり得る(GetClientRect関数でも起こることは確認)。
- したがって、ケースバイケースだが、自分のアプリのUIをPer-Monitor DPIに合わせて積極的に変えたりしない場合でも、スクリーン座標に関係する処理があればPer-Monitor Awareに設定した方が安全、と思われる。
[追記]
Windows 8.1 RTMで試した結果も同じだったので、これが仕様らしいと確定。ついでに、Previewでは動作の怪しかったSetProcessDpiAwarenessが正しく機能するようになっていた。
4. コード
検証アプリは.NET Framework 4.5のWindowsフォームアプリで、Visual Studio 2013 Previewで作成。言語はC#。
Chaserの主要部分。
private Timer timerChaser; private Process processRunner = null; private void ChaserForm_Load(object sender, EventArgs e) { timerChaser = new Timer(); timerChaser.Tick += new System.EventHandler(this.CheckRunner); timerChaser.Interval = 400; timerChaser.Enabled = true; } private void CheckRunner(object sender, EventArgs e) { if (String.IsNullOrWhiteSpace(this.textBox_ProcessName.Text)) return; GetProcess(this.textBox_ProcessName.Text); CheckLocation(); CheckDpiAwareness(); } // Get process of Runner. private void GetProcess(string nameRunner) { if (!String.IsNullOrWhiteSpace(nameRunner)) { try { processRunner = Process.GetProcessesByName(nameRunner).FirstOrDefault(); } catch (Exception ex) { Debug.WriteLine("Failed to get process. " + ex.Message); } } } // Check location of Runner. private void CheckLocation() { try { if ((processRunner != null) && (processRunner.MainWindowHandle != IntPtr.Zero)) { W32.RECT rct; bool result = W32.GetWindowRect(processRunner.MainWindowHandle, out rct); if (result) { this.textBox_GetWindowRectLeft.Text = rct.Left.ToString(); this.textBox_GetWindowRectTop.Text = rct.Top.ToString(); } else { throw new Win32Exception(Marshal.GetLastWin32Error()); } } else { this.textBox_GetWindowRectLeft.Text = ""; this.textBox_GetWindowRectTop.Text = ""; } } catch (Exception ex) { this.textBox_GetWindowRectLeft.Text = "Failed"; this.textBox_GetWindowRectTop.Text = "Failed"; Debug.WriteLine("Failed to get window rectangle. " + ex.Message); } } // Check DPI awareness of Runner. private void CheckDpiAwareness() { if (!OsVersion.IsEightOneOrNewer()) return; try { if ((processRunner != null) && (processRunner.Handle != IntPtr.Zero)) { W32.PROCESS_DPI_AWARENESS awareness; int result = W32.GetProcessDpiAwareness(processRunner.Handle, out awareness); if (result == 0) // If S_OK { this.textBox_DpiAwareness.Text = DpiAwareness.NameAwareness(awareness); } else { throw new Exception(result.ToString()); } } } catch (Exception ex) { this.textBox_DpiAwareness.Text = "Failed"; Debug.WriteLine("Failed to get DPI awareness. " + ex.Message); } }Runnerの主要部分。
private void RunnerForm_Move(object sender, EventArgs e) { CheckLocation(); this.textBox_LocationX.Text = this.Location.X.ToString(); this.textBox_LocationY.Text = this.Location.Y.ToString(); } // Check location of this Runner. private void CheckLocation() { IntPtr handleThis = Process.GetCurrentProcess().MainWindowHandle; if (handleThis != IntPtr.Zero) { try { W32.RECT rct; bool result = W32.GetWindowRect(handleThis, out rct); if (result) { this.textBox_GetWindowRectLeft.Text = rct.Left.ToString(); this.textBox_GetWindowRectTop.Text = rct.Top.ToString(); } else { throw new Win32Exception(Marshal.GetLastWin32Error()); } } catch (Exception ex) { this.textBox_GetWindowRectLeft.Text = "Failed"; this.textBox_GetWindowRectTop.Text = "Failed"; Debug.WriteLine("Failed to get window rectangle. " + ex.Message); } } } private void button_ReturnZero_Click(object sender, EventArgs e) { this.Location = Point.Empty; } private void button_MoveOneRight_Click(object sender, EventArgs e) { this.Location = new Point(this.Location.X + 1, this.Location.Y); }完全なコードはここからダウンロードできる。