モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

Windowsの近代的なHighDPIについて自分なりにまとめてみる。

 SylphyHorn を Per-monitor DPI 対応しようとしたら,Per-monitor DPI に不具合があってさらに調査進めていたらめっちゃ不具合見つかるという……

 .NET Core 3.1 WPF でも Per-monitor DPI に不具合あるっぽいし,いろいろ不具合見つかる模様…

1. Per-monitor DPI v2 API が追加されて。

 ここで話す High DPI は,Vista 以降の DPI システムについて。XP スケーリングモードを入れるととてもややこしくなりますので,割愛します(実は互換機能にて XP スケーリング使えるので厳密にいうと知っておくべきです)。

 まず,Windows 10 1607 以降では

  • Unaware
  • System Aware: Windows Vista+
  • Per-monitor Aware: Windows 8.1+
    • Per-monitor Aware + 非クライアントエリアスケーリング: Windows 10 Anniversary Update (1607)+
  • Per-monitor Aware (Version 2): Windows 10 Creators Update (1703)+
  • Unaware (GDI Scaled): Windows 10 1809+

という 5 つのモードが存在します。これを「DPI Awareness Context」と言います(今後も増える可能性があります)。

 順番に説明していきます。

1.1 Unaware

 DPI スケールは非対応です。つまり,アプリケーションは 100% (96 DPI) でレンダリングされます。

 DWM では GDI などで 100% でレンダラーされたものを各画面に合うように DPI スケールします。 もちろんビットマップ画像を拡大する処理ですので汚くなります (整数比の場合はドットが荒く表示されるだけだが)。

1.2 System Aware

 DPI スケールはプライマリーディスプレイの設定に対応です。つまり,アプリケーションは n (96n DPI) でレンダリングされます。

 ただし,システム側が下記の Per-monitor DPI に対応している場合,プライマリーディスプレイ以外では DWM 側で拡大や縮小されることがあります。

1.3 Per-monitor Aware (Version 1)

 Windows 8.1 から対応です。

 非クライアントエリアはシステム DPI 固定です。 (1607 除く)

 プロセス 1 つに対して,1 つの DPI Awareness Context を指定できます複数モードの混在は不可能)。

 プロセス 1 つに対して,1 つの DPI Awareness Context しか持つことができないため,アプリケーションはすべての画面を Per-monitor DPI に対応する必要があります。

1.4 Per-monitor Aware (Version 2)

 Windows 10 Creators Update (1703) から対応です。

 1607 (Anniversary Update) では EnableNonClientDpiScaling を呼び出すことによって,非クライアントエリアのスケーリングに対応(これは PM Aware V2 の標準機能です)。

 スレッド 1 つに対して,1 つの DPI Awareness Context を使用でき,スレッド中でモード変更することも可能です。例えば座標系 API に対して,

 ―[Per-monitor DPI v2 から Unaware へ]→GetCursorPos―[Unaware から Per-monitor DPI v2]→

といった実行中に自由自在に切り替えが可能。この場合,GetCursorPos は Unaware モードでの座標を取り扱うことになります。

1.5 Unaware (GDI Scaled)

 Windows 10 Redstone 5 (1809) から対応です。

 DWM 向けの GDI スケーリング DPI Awareness Context だと思っていいです。このモードのスケーリング方法は少し特殊です。

  iPhone Plus シリーズは 300% でレンダリングされた後,1/1.15 倍で縮小処理されますが,それと同じように,

  1. 画面のスケーリング率 n に対して,100 の倍数になるように GDI でレンダリングする (例. 225% → 300%)
  2. それを実際の表示範囲に DWM で縮小処理する。

 以下は iPhone のスケーリング回りの図です。

www.paintcodeapp.com

 このモードは特殊ですがとてもよくできていると思います。GDI 系のアプリはこの仕組みを使ってスケーリングするのはかなりお手軽です。詳しい仕組みは公式のブログをご覧ください。

blogs.windows.com

2. Windows 10 Anniversary Update (1607) で増えた API

 一番メジャーで使うと思われる GetDpiForWindow

docs.microsoft.com

 1607 向けに非クライアントエリアをきちんとスケーリングするための EnableNonClientDpiScaling

docs.microsoft.com

 システムパラメーター系を取得する SystemParametersInfoForDpi

docs.microsoft.com

 ほかにもありますが各自調べてください。部分的に Per-monitor DPI v2 に対応したいケースなどは Context をスイッチすることで実現できます。

3. WPF (.NET Core 3.1) で発覚している既知の不具合

 まだちゃんと調べたわけじゃないのですが,システム DPI 以外の環境でウィンドウを起動すると,xaml の Window に初期値 Width, Height がシステム DPI 換算での値になる不具合があるっぽいです。

 ソースコードも読んでいませんし,多くの実験サンプルを試したわけじゃないので間違っていたらすみません。後日時間があれば公式のソースコードを追いかけて,追記しておきます。

3.1 Windows 10 Anniversary Update [1607] 特有問題 (8/11 追記)

 非クライアントエリアの調整機能ですが,.NET Framework 4.8 以降しか実装されていません。Windows 10 Creators Update (1703) 以降では何もせずに調整されるため,Windows 10 Anniversary Update (1607) は Windows 8.1 と同じような扱いということで問題ありません。

まとめ

 DPI の実装を真面目にやるなら,以下の MSDN のページが一番まとまっていると思います。

docs.microsoft.com

  • 非クライアントエリアがないアプリケーションでは Windows 8.1 から正しく Per-monitor DPI に対応できます。
  • 非クライアントエリアがあるアプリケーションでは Windows 10 Anniversary Update (1607) から正しく Per-monitor DPI に対応できます。
  • Common Controls V6 を利用したアプリケーションでは Windows 10 Creators Update (1703) から正しく Per-monitor DPI に対応できます。