2016/09/18

WPFのPer-Monitor DPIサポート(その3)

先月のWindows 10 Anniversary Updateに合わせて.NET Framework 4.6.2がリリースされ、WPFのPer-Monitor DPI機能も正式公開になりました。この機能の正式名はとくにないようですが、区別のため、ここではBuilt-in Scalingと呼びます。


1. 条件


このエントリへの質問に対するRohit Agrawal(WPFチームの人)の回答と
例によってDeveloper Guideをまず参照。
まとめると、Built-in Scalingの前提条件は以下のとおりです。
  • OSがWindows 10 Anniversary Update(Redstone 1)以降であること
  • Target Frameworkが.NET Framework 4.6.2以降であること
  • マニフェストのdpiAwarenessがPerMonitorであること
最初に対応OSについて。Windows 8.1については触れられてませんが、サンプル等を見るとWindows 10 Anniversary Updateより前はまとめてSystem DPI Awareにしてしまいなよ、という扱いなので、Windows 8.1のPer-Monitor DPIについてはさっくり放置する方針のようです。

つまり、Windows 8.1については従来どおり独自対応で行くしかないという方向で確定と。まあMicrosoft全社挙げてWindows 10推しの現況では、それ以前のOSに対してリソースの割り当てがないのだろうと思います。今となってはWindows 8.1は過渡期のOSであることは否定できない感じはします。

次に新しいdpiAwarenessについて。従来のdpiAwareに対して、新しくマニフェスト(app.manifest)に導入されました。この二つは併用が可能で、OSがWindows 10 Anniversary Update以降で、かつdpiAwarenessが指定されていれば、この指定の方が有効になる、ということのようです。

例として、サンプルでの指定方法。

上のdpiAwarenessではPer-Monitor DPI Aware、下のdpiAwareではSystem DPI Awareの指定になっています。この場合、コメントにあるとおり、Windows 10 Anniversary Update以降ではPer-Monitor DPI Awareとなりますが、Windows 8.1ではdpiAwareのみが効いてSystem DPI Awareとなり、仮想スケーリングがかかります。

なぜわざわざdpiAwarenessを追加したのかを推測するに、dpiAwareでtrue/PM(またはPer-Monitor)で指定する方法のままだと、(独自対応なしでは)Windows 8.1上で全くスケーリングがかからなくなってしまうので、それを避けたかったのかなと思いますが、何か要らぬ手間がかかっている気がします。

また、これらの基本条件に対して、コンフィグ(App.config)の方でSwitch.System.Windows.DoNotScaleForDpiChangesを指定すると、その値によって設定をオーバーライドできます。
  • True - 上記の条件を満たしていてもBuilt-in Scalingを無効にする
  • False - Target Frameworkが4.6.2より前であってもBuilt-in Scalingを有効にする
例として、有効にする場合(supportedRuntimeは無関係)。

これの使いどころとしては、Target Frameworkは4.6.2に上げたくないけど、Built-in Scalingは使いたいような場合でしょうか。DPIに応じて変化するような独自コントロールがあるのでもなければ、4.6.2の機能がなくても困りませんし。

条件としては以上です。

2. 独自対応との兼ね合い


結局、Windows 8.1上で仮想スケーリングに甘んじたくなければ、引き続き独自対応が必要になります。あるいは、Windows 10以降も独自対応で通したい場合もあるかもしれません。

独自対応との兼ね合いでありそうな問題としては、
  • Built-in Scalingが不要な場合の抑止

    これは実は簡単で、WM_DPICHANGEDが来たときのハンドラー内で「handled = true」として処理済みにしてしまえば、Built-in Scalingは発動しないようです。 

  • DPI変化の伝播ルートの統一

    .NET Framework 4.6.2より前では、VisualTreeのルートたるWindowから傘下のコントロールにDPI変化を伝えるためには、独自に伝播ルートを張る必要がありました。これが4.6.2以降でBuilt-in Scalingが発動したときは、各コントロールのOnDpiChangedメソッドかImage等のDpiChangedイベントで自動的に伝播されます。

    であれば、独自対応の場合でもこのルートを利用した方が楽ですが、この伝播はVisualTreeHelper.SetRootDpiメソッドで発生させることができます。したがって、独自対応でスケーリングさせると同時に、これを実行するようにすることで伝播ルートを統一できます。
ということで、Windows 8.1上だけ独自対応にし、Windows 10 Anniversary Update以降はBuilt-in Scalingに任せるように切り替えることは意外と難しくないです。

自作ライブラリでは、XAMLから指定可能なWillForbearScalingIfUnnecessaryプロパティでこれが可能なようにしています。

3. Non-client areaの新API


やや蛇足になりますが、Anniversary Updateの後にNon-client area(NCA、ウィンドウの枠のクローム部分)のスケーリングに関してエントリが出ています。
既存デスクトップアプリのPer-Monitor DPI対応を促すために遅まきながらWin32 APIを追加するという話ですが、これまで手つかずだったNCAのスケーリングの話もあります。

WPFに関しては、
For the Windows 10 Anniversary Update WPF is being updated to support automatic NCA scaling.

とありますが、実際に試してみると「いや、効いてないし……」という状態。

そこで、NCAのスケーリングを有効にする新APIのEnableNonClientDpiScalingを実行しようとすると、WPFでウィンドウハンドルを取得できるタイミングとしてはおそらく最早のWindow.OnSourceInitializedメソッド内では成功しないし、エラーコードにも有意な情報がなく。

以下のスレッドのコメントによれば、WM_NCCREATEが来たときのハンドラーで実行すればいいらしいですが、
WPFでウィンドウメッセージを捕捉するにはウィンドウハンドルが必要なので、いずれにしてもWindow.OnSourceInitializedメソッドより早くはできなくて、このときにはこのメッセージは通り過ぎた後らしくて捕捉できない、と手が出せない状態。

まあ出せるものから出していくという姿勢は否定しませんが、既にとっ散らかる兆候を見せているので、出来上がったときにはなるべくまとまった形になっているといいな、というのがささやかな希望です。

[追記]

EnableNonClientDpiScalingの効果は、WinFormsであれば簡単に確認できます。