WindowsのDPIスケーリング処理について一考察

WindowModePatchで実装するのに大分困っていたDPIスケーリング処理(DPIAwareness)についてメモ書き代わりに処理について書いておきます。ちなみに、英語の読み間違いを防ぐためにいっておくと、日本語的には「DPIによるスケーリング(解像度変更)処理」というべきなのでしょうが、英語では「DPIAwareness」となっており、Awarenessの意味は「気がつく」「知る」なので、直訳すると「DPIについてプログラム側が気がついているかどうかを処理する」という意味になってしまいます。これが私が処理について誤解した原因なのですが…。

 

DPIスケーリングを指定する3つのモード

はい、まずは私が翻訳をミスったポイントです。簡単に調べてみるとほかの日本語サイトではちょっと解説してあったのでなんともいえないのですが。基本的にはプログラム側が指定できるDPIスケーリングのモードは3つあります。それが

  1. DPI処理について関知しない(Unaware)
  2. DPI処理をアプリケーション初期化時にのみ行い、後はシステムに任せる(System aware)
  3. DPI処理を完全にアプリケーション側で行う。途中で(ウィンドウの位置移動によって表示を行うモニタが変更されるといった原因で)変更される場合にはメッセージを送るので、そのメッセージを確認してアプリケーション側が処理を行う(Per Monitor DPI aware)

となります。ちょっと注意してほしいのが1.のUnawareパターンですが、これはつまり「アプリケーション側は何もやらなくてもシステム側(すなわちOS側)が勝手にやるので気にしなくてもいいよ」という意味です。プログラム側からこれを設定すると表示上は勝手にスケーリングしてしまうのでWindowModePatchでは非常に都合が悪いモードになっています。通常のアプリケーションでは面倒であれば1.のパターンを使うのもありだと思います。まあ、今時のゲームであれば解像度調整機能は必須なので1.のUnawareを使うことはまずないと思います。

 

DPIスケーリング処理は通常はマニフェスト側で設定する

Microsoft側としてはこちらを想定しているようです。というのも、この後愚痴のように出てきますがアプリケーションが起動してからだとスケーリングモードの変更がプロセス単位でできないことが多いのです。(これについては推察になりますが、おそらくWindowModePatchと同じように座標を設定/取得する関数の処理をスケーリングに合わせて変更する、というものが割り込むことが「想定されるのか否か」によって初期化の方法が変わるのために難しいのではないか?というものです。)まあ、通常のアプリケーションならマニフェスト指定が簡単でしょうね。

やり方としては

  1. VisualStudioなどのビルドツールにおいてマニフェストの設定で「DPIの認識」を設定する
  2. マニフェスト内のwindowSettingsセクション内にdpiAware値およびdpiAwareness値を設定する

というものです。前者の方が勝手に作ってくれる分簡単ですね。なお、マニフェストにDPIに関する設定がないときはシステム側が勝手にスケーリングをするDPIUnaware状態となりますので注意してください。

 

アプリケーション側からの設定はOSごとに方法が異なる

まず紹介されている方法として自身のプロセス全体にDPIに関する設定をかける方法。MSDNでは

  • SetProcessDPIAware関数(Vista~Win8.0)
  • SetProcessDPIAwareness関数(Win8.1~Win10 1607以前)
  • SetProcessDpiAwarenessContext関数(Win10 1607以降)

によりできる、と書いてあるので、該当するフラグをつかって設定すれば良さそうなのですが、私がデバッグした限りにおいてこれらがうまく設定できた試しがない、というのが悲しいところでした。書いてある注意点としては「ウィンドウが作成される前にこの関数を使って設定するように」と書いてあるはずなのですが、やってみてもすべて「Access Denied」(すでにDPIのモードが設定されているため設定不可能)ではねられて終わりでした。なぜなんだよ~と思いながら書いています。

というわけで仕方がないので…。Win8.1以前では対応策がこれ以上ないようですが、Win10以降であればスレッド単位、もしくはウィンドウ単位でDPIAwarenessが設定できるのでそちらを使います。スレッド単位の場合はCreateWindowEx関数などウィンドウ作成系の関数の前に呼び出せばモード変更が可能なようです。使う関数は

  • GetThreadDpiAwarenessContext関数
  • SetThreadDpiAwarenessContext関数

になります。一応Get側で取得して予定しているモード値と異なっているときSet側で設定する、という動きになります。Win10以外のOSにも対応する場合はuser32.dllにインポートされているのでたいていの場合GetModuleHandle関数からGetProcAddress関数で取得することになります。

面倒なのはこれらで使うDPI_AWARENESS_CONTEXTという値です。

というのは、WindowsSDKのバージョンの関係で自分で定義する場合が多いこととこれら2つの関数が返すDPI_AWARENESS_CONTEXTの値はMSDNで定義されている各値と同じものを返さない場合がある、ということです。前者についてはDPI_AWARENESS_CONTEXTは列挙子ではなくハンドル扱い、と書かれていますのでUINT_PTR型などとしないとGetProcAddress関数で処理を持ってきたときに見事な失敗となりますし、後者はマニフェストや(この後紹介する)実行ファイルのの互換性設定でDPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2相当の値を設定してあるからといってGetThreadDpiAwarenessContext関数はMSDNでDPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2として定義されている値である(-4)ではなく別の値を返すパターンがあるということです。これが判定にはやっかいすぎる現象だと思います。設定する場合の値はMSDNの値が正しいようですが…。

なお、WindowModePatchでは前者はちゃんとハンドルとして定義する構文を借りて値を再定義していますし、後者についてはGetAwarenessFromDpiAwarenessContext関数を一度通してDPI_AWARENESS列挙子にすることでモードを判定するようにしています。この辺も実装に手間取った理由だったりします。

 

あとは必要に応じてDPI変更時の処理を組み込むだけ

DPIを変更する必要がある場合はWM_DPICHANGEDメッセージがウィンドウプロシージャに飛んできますのでこれを見てウィンドウの大きさや文字の大きさなどを制御します。このあたりは今回の考察範囲からは外れますので詳しく書きません。

 

使用者側から見るのであれば実行ファイルのプロパティから設定可能

Win10系であればこれが可能です。Win8.1系でもできるかな?やり方としては対象アプリケーションの実行ファイルのプロパティを開き「互換性」タブから「高DPI設定の変更」を選択して誰がスケーリングをするのか設定が可能です。ちなみにこれを使うと単にウィンドウモード時の解像度が低いだけなら無理矢理解像度を引き上げることが可能だったりします。メニューバーまで拡大されてしまうのはご愛敬といったところですか。WindowModePatchのような自由度はないですが、OSの機能ですのでうまく使えるのであれば損はないかな~と思ったりもしています。

 

ゲームの場合は直接解像度を見るので気にしない方がよい機能かな

と思います。といっても、名前の入力などでダイアログを出す必要がある場合は対応する必要もあるかもしれません、とだけ付け加えておきましょう。


コメントを残す

メールアドレスが公開されることはありません。

この記事のトラックバック用URL