先日の めとべや東京勉強会 #2 にて WPF での Per-Monitor DPI 対応アプリのデモをしましたが、アプリが完成したので公開します。
XamClaudia
https://github.com/Grabacr07/XamClaudia
間もなく Windows 8.1 公開ですね!
ということで、Windows 8.1 の新機能である Per-Monitor DPI の解説と対応方法の紹介をします。
High DPI と WPF
昨今のタブレット PC などは、本体の小型化と同時にモニターの高精細化が進んでおり、1 ドットの物理的なサイズがどんどん小さくなっています。たとえば、Surface Pro (10.6”, Full-HD) の 1 ドットのサイズは約 0.12 mm です。
そのため、Windows の High DPI 設定が既定で 125 % や 150 % になっている PC も増え、必然的にアプリ側も High DPI 対応は必須と言える状況になりました。
DPI 仮想化
この High DPI に対応していないアプリは High DPI 環境でどうなるかというと、DPI 仮想化と呼ばれる Windows の機能により、自動的にスケーリングされます。いわゆる救済策のようなもの。
ただし、ピクセル単位でのスケーリングになるため、単純に画像を拡大縮小したのと同じで、ボケた表示になってしまうのが残念なところ。所詮は救済策です。
WPF は気にしない!
以前の投稿にも書いたとおり、WPF アプリは DPI 設定と一致するよう自動的にスケーリングされます。DPI 仮想化とは違い、しっかりスケーリング後のサイズに合わせてキレイに描画されます。
そのため、Windows デスクトップ アプリは、WPF で開発していれば基本的には自前で High DPI 対応する必要がなく、とても楽です。
Windows 8.1 の新機能 “Per-Monitor DPI”
Windows 8.1 では、新たに “Per-Monitor DPI” という機能が追加され、モニターごとに異なる DPI を使用できるようになりました。
High DPI 設定されたタブレットと一般的なサブ モニターを接続したとき、その必要がないサブ モニターまで High DPI 設定となり、非常に使いづらかった… のですが、Windows 8.1 からその悩みが解消されます。待ち望んでいた機能です。
Per-Monitor DPI に対応したアプリは、モニターをまたいだタイミング (= DPI 設定が変わったタイミング) でウィンドウをスケーリングし、そのモニターの DPI 設定にマッチしたサイズで表示することができるようになり、可視性が大幅に向上します。
この Per-Monitor DPI に対応したアプリを開発する方法は後述します。
Per-Monitor DPI 環境での各アプリの挙動
Per-Monitor DPI に対応していないアプリは、前述の DPI 仮想化により、モニターをまたいだタイミングで Windows が自動的にスケーリングします。ただし、High DPI のときと同じで、ボケた表示となってしまいます。
High DPI および Per-Monitor DPI に対応したアプリと、対応していないアプリが、それぞれどのような挙動になるか、例を 2 パターンほど挙げてみました。
プライマリ モニターが 200 %、セカンダリ モニターが 100 % の環境
割とよくありそうな環境です。タブレット PC 本体をプライマリ モニターとし、デスク据え置きのディスプレイをセカンダリ モニターとして接続した場合など。
プライマリ モニター (DPI 200 %) で起動したとき | セカンダリ モニター (DPI 100 %) へウィンドウを移動したとき | |
High DPI 非対応 | DPI 仮想化により、200 % のサイズに拡大されて表示 (ボケる) | DPI 仮想化により 100 % のサイズに縮小されて表示 (ちょうど 100 % なのでボケない) |
High DPI 対応 Per-Monitor DPI 非対応 (WPF など) |
自身の High DPI 対応処理で、200 % のサイズで表示 | DPI 仮想化により 100 % のサイズに縮小されて表示 (元が 200 % に合わせたサイズなのでボケる) |
High DPI および Per-Monitor DPI 対応 |
自身の High DPI 対応処理で、200 % のサイズで表示 | 自身の Per-Monitor DPI 対応処理で、100 % のサイズで表示 |
プライマリ モニターが 100 %、セカンダリ モニターが 200 % の環境
上記の逆。あんまりないかも?
プライマリ モニター (DPI 100 %) で起動したとき | セカンダリ モニター (DPI 200 %) へウィンドウを移動したとき | |
High DPI 非対応 | そのまま表示 | DPI 仮想化により 200 % のサイズに拡大されて表示 (ボケる) |
High DPI 対応 Per-Monitor DPI 非対応 (WPF など) |
そのまま表示 | DPI 仮想化により 200 % のサイズに拡大されて表示 (ボケる) |
High DPI および Per-Monitor DPI 対応 |
そのまま表示 | 自身の Per-Monitor DPI 対応処理で、200 % のサイズで表示 |
赤字部分が、DPI 仮想化によりボケて表示される部分です。
ここで問題なのは、explorer.exe をはじめとした Windows 本体の一部のアプリケーションや WPF アプリすべてが、2 番目の「High DPI 対応、Per-Monitor DPI 非対応」に分類されることです。
High DPI 環境では DPI 設定に合わせて自動的にスケーリングしてくれて楽だった WPF アプリも、Per-Monitor DPI 環境では DPI 仮想化によりボケて表示されてしまうのです。なんで High DPI 環境はキレイにスケーリングできるのに Per-Monitor DPI には対応してくれないんだと文句を言いたい。非常に残念。
Per-Monitor DPI 対応方法
ということで、Per-Monitor DPI 対応方法です。
コードは C# + WPF ですが、C++ でもやることは変わらない、はず。
モニターの DPI を取得する
まず、ウィンドウがどのモニターに属しているかを判別します。
MonitorFromWindow function
http://msdn.microsoft.com/en-us/library/windows/desktop/dd145064(v=vs.85).aspx
次に、モニターの DPI 設定値を取得します。
Windows 8.1 で、モニターの DPI 設定を取得する関数が追加されました。MonitorFromWindow 関数で得た hmonitor (IntPtr) を指定し、そのモニターの DPI 設定値 (96, 120, 144, 192, など) を取得できます。
GetDpiForMonitor function
http://msdn.microsoft.com/library/windows/desktop/dn280510.aspxMONITOR_DPI_TYPE enumeration
http://msdn.microsoft.com/ja-JP/library/windows/desktop/dn280511.aspx
WPF で実装する場合は P/Invoke 祭りですね。。。
internal class NativeMethods { [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MonitorDefaultTo dwFlags); [DllImport("SHCore.dll", CharSet = CharSet.Unicode, PreserveSig = false)] public static extern void GetDpiForMonitor(IntPtr hmonitor, MonitorDpiType dpiType, ref uint dpiX, ref uint dpiY); }
GetDpiForMonitor 関数の MonitorDpiType の意味は、次のとおり。
MonitorDpiType 値 | GetDpiForMonitor 関数で取得される DPI 値の意味 |
MDT_Effective_DPI | DWM が使用する DPI 値。 96, 120, 144 など。Per-Monitor DPI でスケーリングするときは、これを使用する。 |
MDT_Angular_DPI | よくわからん。どなたか教えてください (´・_・`) |
MDT_Raw_DPI | Windows の DPI 設定に依らない、デバイスの物理的な DPI (たぶん)。 たとえば、Acer ICONIA W7 は 11.6″ の 1920 x 1080 で 190 ppi になるが、MDT_Raw_DPI を指定したところ 190 が返ってきた。 |
MDT_Default | = MDT_Effective_DPI |
これらをふまえ、ウィンドウ (HwndSource) から DPI 設定値を取得するコードは、以下のようになります。
public static Dpi GetDpi(this HwndSource hwndSource, MonitorDpiType dpiType = MonitorDpiType.Default) { if (!IsSupported) return Dpi.Default; var hmonitor = NativeMethods.MonitorFromWindow( hwndSource.Handle, MonitorDefaultTo.MONITOR_DEFAULTTONEAREST); uint dpiX = 1, dpiY = 1; NativeMethods.GetDpiForMonitor(hmonitor, dpiType, ref dpiX, ref dpiY); return new Dpi(dpiX, dpiY); }
DPI の変更通知を受け取る
また、ウィンドウが使用すべき DPI 設定が変わったことを示すウィンドウ メッセージが飛んでくるようになりました (主にウィンドウがモニターをまたいだタイミングなど)。
WM_DPICHANGED
http://msdn.microsoft.com/en-us/library/windows/desktop/dn312083(v=vs.85).aspx
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == (int)WindowMessage.WM_DPICHANGED) { var dpiX = wParam.ToHiWord(); var dpiY = wParam.ToLoWord(); this.ChangeDpi(new Dpi(dpiX, dpiY)); handled = true; } return IntPtr.Zero; }
アプリケーションが Per-Monitor DPI 対応であることを宣言する
そして、忘れてはならないのが、マニフェストです。
アプリケーションが Per-Monitor DPI に対応していることを宣言します。
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <dpiAware>True/PM</dpiAware> </asmv3:windowsSettings> </asmv3:application>
従来まで、dpiAware で指定できる値は True と False の 2 つでしたが、Windows 8.1 から更に 2 つ加わっています。それぞれの意味は下記のとおり。
dpiAware 値 | 意味 |
False | High DPI 非対応。 High DPI 環境では DPI 仮想化により自動スケーリングされる。 |
True | High DPI 対応。 Per-Monitor DPI には非対応なので、Windows 8.1 でモニターをまたぐと DPI 仮想化により自動スケーリングされる。 |
Per-Monitor (新規追加) |
Per-Monitor DPI 対応。 Windows Vista ~ 8 では、False を指定したときと同じ挙動。 |
True/PM (新規追加) |
Per-Monitor DPI 対応。 Windows Vista ~ 8 では、True を指定したときと同じ挙動。 |
マニフェストで Per-Monitor DPI に対応していることを宣言すると、DPI 仮想化による自動スケーリング (= 非対応アプリ向けの救済措置) が行われなくなります。
モニターの DPI 値に合わせて、ウィンドウとコンテンツをスケーリングさせれば完成です。
WPF の場合は、ウィンドウのリサイズと共に、ウィンドウのコンテンツを ScaleTransform でスケーリングさせてやればよいでしょう (ほかに良い感じの実装方法があったらご教示頂きたく)。
サンプル アプリ
というわけで、サンプルアプリを作りました。以前 XAML でクラウディアさんを描きました が、そのときのアプリを Per-Monitor DPI に対応させたものです。
XamClaudia
https://github.com/Grabacr07/XamClaudia
XamClaudia プロジェクトが XAML で描いたクラウディアさんを表示するだけのアプリ、PerMonitorDpi プロジェクトが、Per-Monitor DPI に対応するためのライブラリ、になります。
Per-Montor DPI に対応させたいウィンドウを、PerMonitorDpi.Views.PerMonitorDpiWindow クラスから継承させるだけです。
ちなみに XAML のクラウディアさんは XamClaudia.WPF/Views/Claudia.xaml にありますが、頂点数多めでかなり雑な仕上がりですので、あくまでネタと思ってください。。。
実行結果
そして、肝心の実行結果がこちら。プライマリ モニターが DPI 125 %、セカンダリ モニターが DPI 100 % の環境で (それしか手元になかった…)、ウィンドウをセカンダリー モニターに移動したときのスクリーンショットになります。
Per-Monitor DPI に対応せず、DPI 仮想化によってスケーリングされたもの (クリックで拡大)。
(NT バージョンが 6.2 と出てるのは、マニフェストを埋め込まなかったので Environment.OSVersion が Windows 8 と同じ 6.2 を返しているためであり、実行環境はもちろん Windows 8.1 です)
そして、Per-Monitor DPI に対応し、スケーリング処理を行ったもの (クリックで拡大)。
お わ か り 頂 け た だ ろ う か 。
上の方がボケてますよね? 下の方がくっきりですよね?
DPI 200 % のような環境を用意できればもっとわかりやすかったのでしょうが、手元にその環境を用意できなかったのでご容赦ください。。。
なお、お手元の環境で実行結果だけ見たいという方向けに、Per-Monitor DPI 対応版、非対応版の両方を同梱したサンプルを置いておきます。お試しください。
Per-Monitor DPI Aware / Unaware application sample
http://grabacr.net/wp-content/uploads/dpi-aware-unaware-sampleapps.zip
.NET Framework 4.5 があれば動きます。たぶん。
Windows 8.1 Pro 以外での動作確認は取ってません!
まとめ
というわけで、Windows 8.1 の便利機能 “Per-Monitor DPI” の解説と、なぜか対応していない WPF アプリで自前で対応する方法の紹介でした。
DPI 仮想化によるボケたスケーリング (救済措置) は、「画面解像度がディスプレイの適正解像度と合っていない状態で使っている」ような気持ち悪さがあり、私は到底我慢できない… ので、なら自分で対応してしまおう、という。
せっかくいい機能なので、もうちょっとちゃんと対応してほしかった…
Windows 8.2 (?) に期待。
記事の内容やサンプル コードに関するご指摘、ご質問等は、この記事のコメントか Twitter までお願いします。
それでは、よい P/Invoke ライフを。