SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

.Net 4.6.2以降でのWPFのPer-Monitor DPI対応

この記事はXAMLアドベントカレンダー 2016 14日目の記事です。

WPFでは、ボタンをはじめとする各種UI要素はベクターベースでの描画を行っています。
そのため、WinFormsやMFCなどのGDI系のUIフレームワークと違い、High-DPIな環境でもボケずにキレイな描画ができる、、、と言われていました。

Per-Monitor DPI

Win8.1からはPer-Monitor DPIという仕組みが導入され、ディスプレイごとに異なったDPI設定をすることができるようになっています。

Win8.1が出てきたころには、10inchほどのサイズでFullHD解像度のタブレットやノートPCなどが多数登場してきました。
それらの環境はピクセル密度が非常に高いので、標準で125%や150%のdpi設定になっているものが多数ありました。

このようなHigh-DPI環境のPCを外部ディスプレイに繋ぐ場合、外部ディスプレイ側はピクセル密度が高いものではないので普通の解像度にしたい、ということが多々あります。
そこで、ディスプレイごとに別々のdpiを設定できるようにPer-Monitor DPIという仕組みが導入されました。

Per-Monitor DPIの悪夢

Win8.1で導入されたPer-Monitor DPIは、正直なところ微妙な出来だったと思います。

WPFベクター形式でのUI描画なので、このようなディスプレイごとにDPIが異なる場合も、ボケずにちゃんと描画できると思っていました。。。。
しかし、現実はそうではありませんでした。

何も考えずに作ったWPFアプリは、Per-Monitor DPI環境ではボケボケ表示となってしまいます。
(厳密に言うと、Per-Monitor DPI有効にして、プライマリ以外のディスプレイで表示した場合)

また、WPFだけでなくエクスプローラなどのOS標準のUIなどもボケた表示となっていました。

WPFアプリでも、GetDpiForMonitor関数などを使いP/Invokeを多用するコードをかけば、このPer-Monitor DPIに対応することもできます。   しかし、OS標準のUIなどもPer-Monitor DPI対応は微妙だったので、個人的にはWin8.1のPer-Monitor DPIは使えない技術として目を背けていました。

.Net 4.6.2でのWPFのPer-Monitor DPI対応の改善

https://blogs.msdn.microsoft.com/dotnet/2016/08/02/announcing-net-framework-4-6-2/#wpf

こんな微妙な立ち位置だったWPFとPer-Monitor DPIですが、とうとう.Net4.6.2でフレームワーク側の対応が行われました。

Anniversary Update以降の環境向けの場合、WPFアプリは簡単にPer-Monitor DPI対応することができます。

ターゲットフレームワークを.Net 4.6.2としたWPFアプリ

.Net4.6.2をターゲットとしたWPFアプリは、マニフェストファイルに以下の記述を追加しておくと、Windows10 Anniversary Update以降の環境では、Per-Monitor DPIに対応した状態で動作します。

app.manifest
マニフェストファイルのapplication/windowsSetting以下に、dpiAwarenessという要素を追加し以下のように記述します。

  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"> PerMonitor</dpiAwareness>
    </windowsSettings>
  </application>
ターゲットフレームワークの設定

プロジェクトのプロパティで、ターゲットフレームワークを以下のようにビルドしておけばOK。
f:id:minami_SC:20161214080742p:plain

こうすると、ディスプレイをまたいでもボケたりせず、適切なDPI値でのUI表示が行われます。

.Net Framework 4.6.2 Developer Pack

VS2015の標準の状態では、ターゲットフレームワークには4.6.2の選択肢が出てきません。

そんな時は、↓の.Net Framework 4.6.2 Developer Packをインストールすると、ターゲットとして4.6.2を選べるようになります。 https://www.microsoft.com/ja-jp/download/details.aspx?id=53321

ターゲットフレームワークが、.Net 4.6.2より前のWPFアプリ

続いて、ターゲットフレームワークを4.6.2に上げたくない場合の対応方法です。 (アプリの実行環境に極力制限をかけたくない場合などでしょうか。。。)

ターゲットフレームワークを.Net4.6.2にしなくても、以下の設定をすることでPer-Monitor DPI対応ができます。 * 前述のapp.manifestファイルの設定 * App.configに↓のようにAppContextSwitchOverridesという項目を追加

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
    <runtime>
        <AppContextSwitchOverrides value = "Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
    </runtime>
</configuration>

※補足
ただし、Per-Monitor DPI対応が有効になるのは、Win10 Anniversary Update以降の環境で実行した場合のみです。

Win10 Anniversary Updateより前のOSの場合

Anniversary Update以前のWin10(1507、1511など)や、Win8/8.1、Win7と言ったOSでは、.Net 4.6.2で追加されたdpiAwarenessの設定は機能しません。
これらのOSでPer-Monitor DPIに対応したい場合は、今までと同じようにP/Invokeを使った対処をする必要があります。

NCA(Non Client Area)のスケーリングが行われない問題

dpiAwarenessの設定でPer-Monitor DPI対応を行っても、ウィンドウのタイトルバーなどのNCA領域は正しくスケーリングされませんでした。

f:id:minami_SC:20161214080912p:plain
左側がWPFアプリ/右側がエクスプローラ表示です。
並べてみると、WPFアプリのタイトルバーはdpiに合わせてスケーリングされず、元のdpiでのピクセル数のままとなっていることがわかります。

この辺は、また将来のアップデートで改善されるといいのですが・・・

まとめ

.Net4.6.2で追加されたこの方法は、コードを書かずにマニフェストなどの設定ファイルをいじるだけで対応できるお手軽なものです。

Win10だったら基本的に最新のバージョンに更新されますし、今となってはWin8/8.1はかなりの少数派でしょう。
また、Win7世代のタブレット/ノートPCはHigh-DPI設定にしたくなるような、高解像度な端末は少なかったと思います。

いっそのことWin10 Anniversarry Update以降のみ、Per-Monitor DPIまでの対応をする、 という割り切った判断もありなのかな、と感じました。