Windows と C++
Windows 8.1 用の高 DPI アプリケーションを作成する
Windows 8 では、Windows ランタイム (WinRT) に基づいた、Windows アプリ用の新しいプログラミング モデルが導入されました。このモデルを使用すると、ユーザーは PC 設定を使用して画面要素のサイズを動的に変更できます。このオプションの内容は、PC 設定で確認できます (図 1 参照)。私が使用しているデスクトップ PC では、このオプションは [既定] と [大] でした。Surface Pro では、このオプションは [小] と [既定] でした。オプションの内容はデバイスによって異なり、具体的には、取り付けているディスプレイの垂直方向の解像度によって異なります。さらに重要な点として、Windows ストア アプリはこのオプションが変更されるたびに通知を受けるため、現在の倍率を反映するよう動的にレンダリング コードを更新できます。
図 1 Windows ストア アプリに影響を与える Windows 8.1 の PC 設定
ただし Windows 8 では、デスクトップ アプリケーションの倍率は静的なまま変更されませんでした。デスクトップ アプリケーションは以前と同じくシステム DPI 設定で管理され、変更を適用するには、ユーザーがサインアウトして再度サインインし、すべてのアプリケーションを事実上シャットダウンしてから再起動する必要がありました。このオプションは Windows 8.1 にもありますが、より細かい [小]、[中]、[大]、および [特大] のオプションが用意されています (図 2 参照)。このオプションを見ると、地元のコーヒー ショップを思い出します。このページのオプションの内容も、取り付けているディスプレイによって異なります。 たとえば、私が使用している Surface Pro では、[小]、[中]、および [大] のみが表示されます。
図 2 デスクトップ アプリケーションに影響を与える Windows 8.1 以前の PC 設定
言うまでもなく、この二重人格じみた設定は (Windows 8 ではよくありますが)、ユーザーはもちろん開発者にとっても非常に紛らわしい設定です。Windows 8.1 ではこの紛らわしさがきちんと解決されたわけではありませんが、ついにデスクトップ アプリケーションでも Windows ストア アプリと同じように DPI スケールを動的に調整できるようになりました。このため、ユーザーはすべてのアプリケーションをシャットダウンしてログオンし直す必要がなくなりました。ただし、Windows 8.1 はずっと進化しており、マルチモニターの構成に新機能が加わっています。
図 2 のウィンドウは Windows 8 当時のウィンドウとほとんど同じですが、Windows 8.1 で小さなチェック ボックスが追加されています。このチェック ボックスは図 2 ではオンになっていますが、既定ではオフです。[すべてのディスプレイで同じ拡大率を使用する] というチェック ボックス名から、モニターごとに異なる倍率を適用する機能という、Windows 8.1 の別の新機能を推測できます。このチェック ボックス名は少し紛らわしいと言えます。なぜなら、このチェック ボックスをオフにすると、モニターを 1 台しか使用していないユーザーにも効果があるためです。チェック ボックスをオフにすると、ユーザーは倍率を動的に変更できるようになります。このチェック ボックスをオンにすると、DPI 動作のレガシ (互換) モードが有効になります。したがって、モニターが複数あるかどうかにかかわらず、またさらに重要な点としてユーザーが通常使用しているモニターの数に関係なく、開発者はこれらの新しいオプションに対処することになります。新しいオプションは、好むと好まざるとにかかわらず、アプリケーションに影響を与えます。このチェック ボックスをオフにすると図 3 のウィンドウが表示されます。繰り返しますが、Windows 8.1 ではこれが既定のウィンドウです。
図 3 デスクトップの動的なスケール調整とモニターごとのスケール調整に影響を与える Windows 8.1 の PC 設定
考えてみると、図 2 と図 3 のウィンドウにそれほど違いはありません。前者は 4 つのラジオ ボタンを使用し、後者は 4 段階で指定できるスライダーを使用しています。明確な違いは、スライダーによる変更がすぐに、少なくとも [適用] を押すと、適用される点のみです。これは、少なくともユーザーにとっては、Windows ストア アプリのスケーリング オプションとほとんど同じ動作です。一方で、選択するラジオ ボタンの変更は、ユーザーが次回サインインするときに初めて適用されます。
スライダーまたはラジオ ボタンで指定できる 4 つの値は、4 つの DPI の倍率に対応しています (図 4 参照)。ここで開発者として注意させていただきますが、具体的な DPI 値にこだわりすぎないようにしてください。これらの DPI 値は、画面の解像度またはピクセル密度を反映するよう設計されていますが、実際には、フォーム ファクターや画面までの距離など、多くの要素が DPI 値に影響を及ぼします。そのため、最終的に使用する有効な DPI 値は、実際のインチ数とほとんど関係ありません。また、これらの 4 つのオプションは最大限の可能性を示しており、特定の PC で使用できる倍率はディスプレイの垂直方向の解像度によって異なります。
図 4 システムの倍率
DPI | パーセント |
96 | 100 |
120 | 125 |
144 | 150 |
192 | 200 |
図 5 に、この関係を示します。倍率の制限は、UI 要素がディスプレイの下部からはみ出さないように設定されています。ディスプレイの垂直方向の解像度が 900 行未満の場合は、いずれのオプションも使用できないため、100% の倍率を使用するしかありません。ディスプレイの垂直方向の解像度が増えるにつれて使用できるオプションが多くなり、垂直方向の解像度が 1,440 行以上になると 4 つのオプションをすべて使用できます。ただし、これらのオプションは、すべてのモニターのスケーリングに同じように影響を与えるわけではありません。これが、モニターごとの DPI スケールという概念の由来です。物理ディスプレイの垂直方向の解像度とネイティブ DPI の両方が OS によって考慮されるため、この概念はそれほどわかりやすくありません。
図 5 垂直方向の解像度別スケーリング オプション
解像度 | スケーリング オプション |
… ~ 900 | 100% |
900 ~ 1,079 | 100% – 125% |
1,080 ~ 1,439 | 100% – 125% – 150% |
1,440 ~ ... | 100% – 125% – 150% – 200% |
デスクトップ アプリケーションの開発者の方は、影響を及ぼす可能性がある倍率が 2 つになったことに注意してください。2 つの倍率とは、システム DPI の倍率とモニターごとの DPI の倍率です。システム DPI の倍率は、図 4 の値のいずれかに対応します (Windows Phone などのデバイスは除きます)。また、ログオンしている間は一定のまま変化しません。システム DPI 値は、図 2 の 1 つ目のラジオ ボタン、または図 3 のスライダーの位置に基づいています。
システム DPI 値を取得するには、まず、デスクトップ デバイス コンテキストを取得します。つまり昔ながらの GDI API を取得することになりますが、GDI レンダリングとは関係なく、これは過去に関する豆知識にすぎません。まず、デスクトップ デバイス コンテキストを示すハンドルを取得するには、ウィンドウ ハンドルではなく nullptr を指定して、GetDC 関数を呼び出します。この特殊な値は、特定のウィンドウではなくデスクトップ全体のデバイス コンテキストが必要であることを示しています。
auto dc = GetDC(nullptr);
当然ながら、処理が完了したらこのハンドルを解放する必要があります。
ReleaseDC(nullptr, dc);
1 つ目のパラメーターは、デバイス コンテキストで参照しているウィンドウのハンドルです。繰り返しますが、nullptr 値はデスクトップを示します。これでデバイス コンテキストを取得したので、次のように GetDeviceCaps 関数を使用して x 軸と y 軸のシステム DPI の倍率を取得できます。
auto x = GetDeviceCaps(dc, LOGPIXELSX);
auto y = GetDeviceCaps(dc, LOGPIXELSY);
取得した値が x 軸と y 軸で異なっていると、プリンタで指定される倍率が水平方向と垂直方向でしばしば異なっていた暗黒時代を思い出します。画素が正方形ではないディスプレイを目にしたことはありませんが、特殊なグラフィック カードを開発している一部の業界には存在すると聞いています。LOGPIXELSX と LOGPIXELSY は、デスクトップの水平方向と垂直方向での、論理インチあたりのピクセル数を表します。繰り返しますが、これは論理インチであり、実際のインチ数とは異なります。また、これらの値はシステム DPI 値なので、モニターの相対的なサイズに関係なく、デスクトップを表示するすべてのモニターで同じ値になります。ここに問題があります。
8 インチの画面を搭載した Dell Venue 8 Pro タブレットを 30 インチの Dell UltraSharp モニターに接続すると、難しい選択を迫られることになります。また、私はデスクトップ PC のグラフィック カードに 2 ~ 3 台のまったく異なるモニターを接続することがよくあります。システム幅の DPI 倍率では画面がはみ出すことはありませんが、各モニターの相対的なサイズや解像度に最適な、DPI の倍率を指定する必要があります。これこそが、Windows 8.1 に用意されているモニターごとの DPI スケール設定機能です。
Windows 8.1 には、3 つの DPI 対応レベルが用意されています。これは、[Process Explorer] ウィンドウを表示するとよくわかります (図 6 参照)。ご覧のとおり、コマンド プロンプトなどの一部のアプリケーションは DPI にまったく対応していません。Windows 7 および Windows 8 用に作成されたアプリケーションのほとんどはシステム DPI に対応しているか、少なくともそのように表示されています。たとえば、図 6 では、Microsoft Outlook と電卓が挙げられます。3 つ目のレベルはモニターごとの DPI 対応で、これが理想的な対応レベルです。たとえば、図 6 では、Internet Explorer と Microsoft Word が挙げられます。興味深いことに、Word は現在のところ実際にはモニターごとの DPI 対応ではありません。ただし、ここでは混乱を避けるために Word のディスプレイのスケーリングを無効にしています。これにはプロセスの DPI 対応をオーバーライドする効果があるため、デスクトップ ウィンドウ マネージャーによるウィンドウのスケーリングが行われなくなります。スケーリングの無効化は、互換性オプションを使用して実行しました。
図 6 DPI 対応と考えられるアプリケーションを示す Process Explorer
ここでの主なポイントは、アプリケーションをモニターごとの DPI 対応にしてそれに応じたスケーリングを行う方がよいことです。具体的な方法については、これから説明します。
アプリケーションによって、指定されている DPI 対応レベルはそれぞれ異なります。そこで、デスクトップ ウィンドウ マネージャー (さまざまなアプリケーション ウィンドウを合成するサービス) では、すべてのウィンドウがある一定のスケールで表示されるように、さまざまなアプリケーション ウィンドウのスケール方法をアプリケーション別の DPI 対応設定に基づいて判断します。アプリケーションがモニターごとの DPI 対応とされている場合、デスクトップ ウィンドウ マネージャーはウィンドウのスケールを調整せず、スケールをアプリケーション自体で調整できると判断します。アプリケーションがシステム DPI 対応の場合、デスクトップ ウィンドウ マネージャーは、前述の GetDeviceCaps 関数から返されたシステム DPI の倍率でウィンドウがレンダリングされたと想定して、ウィンドウのスケールを調整します。また、アプリケーションが DPI 非対応の場合、デスクトップ ウィンドウ マネージャーは、ウィンドウが従来の 96 DPI でレンダリングされたと想定して、それに応じてスケールを調整します。
適切に機能するモニターごとの DPI 対応アプリケーションを実際に作成する方法を考える前に、対応の指定に必要な処理について説明しましょう。Windows Vista では、呼び出しプロセスを DPI 対応にできる SetProcessDPIAware 関数が導入されました。この関数は引数を受け取らず、言ってみれば DPI 対応を有効にするだけの関数でした。当時はモニターごとの DPI 対応が存在しなかったため、DPI 対応は二者択一状態でした。つまり、DPI に対応するか対応しないかのいずれかだけでした。Windows 8.1 では、この対応レベルをより細かく指定できる SetProcessDpiAwareness という新しい関数が導入されました。
VERIFY_(S_OK, SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE));
この関数は、プロセスの DPI 対応を指定のレベルに設定します。定数を使用すると、DPI 非対応、システム DPI 対応、モニターごとの DPI 対応という 3 つの状態を指定できます。また、指定のプロセスについてこの定数値を照会できる、GetProcessDpiAwareness という関数もあります。ただし、コードを使用して対応レベルを設定する手法には、多数の欠点があります。アプリケーション有効期間の初期段階でこれらの関数を呼び出す場合は、注意する必要があります。この手法は、実用的でない場合や、実行可能ファイルや DLL が混在しているときにさまざまな問題が発生する場合があります。この件については、もっと優れた解決策が必要です。
アプリケーションには、マニフェスト ファイルを組み込んだりバイナリと共に配布したりすることができます。マニフェスト ファイルには、コードの実行開始前にアプリケーション プロセス準備方法を特定するためにシェルまたは Windows ローダーで使用できる情報が含まれています。Windows シェルは、さまざまな目的でこの情報を使用します。たとえば、セキュリティ機能、アセンブリ依存関係の情報、そしてもちろん、DPI 対応の設定に使用します。
こうしたマニフェスト ファイルは単なるテキスト ファイルで、従来の Win32 リソースとして実行可能ファイルに組み込まれているか、単に実行可能ファイルとまとめて提供されます。たとえば、Windows SDK に付属しているマニフェスト ツールで次のコマンドを実行すると、Word のマニフェスト ファイルを簡単に確認できます。
mt -inputresource:"C:\ ... \WINWORD.EXE" -out:manifest.xml
スペースを節約するために、完全なパスは省略しました。コマンドが成功すれば、現在のディレクトリに XML ドキュメントが保存されます。図 7 は、このドキュメントの内容を示しています。このような場合以外には確認することもない Word 実行可能ファイルに関する情報の真ん中に、XML 要素の dpiAware とその値 true が表示されています。
図 7 Microsoft Word のマニフェスト ファイル
アプリケーションにマニフェスト ファイルがなく、プログラムによる対応レベル設定手法を使用していない場合、このアプリケーションは DPI 非対応と見なされます。アプリケーションにマニフェスト ファイルがあっても dpiAware 要素が含まれていない場合も、DPI 非対応と見なされます。Windows 7 では、DPI 対応であるとシステムが判断するには dpiAware 要素の存在を確認するだけで十分です。dpiAware 要素に指定されている値も、そもそもこの要素に値が指定されているかどうかも関係ありません。Window 8 では、もう少し具体的な情報が必要で、dpiAware 要素の値が T で始まる必要があります (つまり、"True" が指定されていると想定します)。大文字か小文字かは関係ありません。
これで DPI 非対応のアプリケーションとシステム DPI 対応のアプリケーションを特定できますが、モニターごとの DPI 対応のアプリケーションについてはどうしたらよいでしょうか。この機能が初めて開発されたとき、dpiAware 要素には Per Monitor という新しい値が採用されました。しかし、Windows 7 と Windows 8 の両方を対象にシステム DPI 対応をサポートするだけでなく Windows 8 のモニターごとの DPI 対応もサポートする、単一のバイナリをアプリケーション開発者が作成できるように、True/PM という新しい値が採用されました。この値を使用すると、バイナリを Windows 7 と Windows 8 ではシステム DPI 対応として受け入れ、Windows 8.1 ではモニターごとの DPI 対応として受け入れることができます。もちろん、サポート対象が Windows 8.1 のみの場合は、引き続き Per Monitor を使用することもできます。
残念ながら Visual C++ 2013 ではこの新しい値をサポートしていませんが、この値を指定することは可能です。Visual C++ プロジェクト ビルドの一部であるマニフェスト ツールには、追加のマニフェスト ファイルをマージするオプションがあるため、適切な dpiAware 要素と値を含んだテキスト ファイルを作成し、ビルドにマージするだけです。必要な XML は次のコードのとおりです。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns=
"https://schemas.microsoft.com/SMI/2005/WindowsSettings">
True/PM</dpiAware>
</windowsSettings>
</application>
</assembly>
マニフェスト ファイルを作成したら、プロジェクトの設定を更新するだけです (図 8 参照)。このマニフェストを追加のマニフェスト ファイルとして設定し、[DPI 認識] を [なし] に設定します。
図 8 マニフェスト ツールのオプション
アプリケーションのウィンドウをレンダリングするときには、適切な DPI の倍率を使用する必要があります。GDI グラフィックやユーザー コントロールに深く関連しているレガシ アプリケーションに取り組んでいる場合、そのアプリケーションをモニターごとの DPI 対応にしようとするとおそらく多数の課題が生じます。これは、マイクロソフトの Windows チームや Office チームも直面している課題です。ただし、このコラムを読み続けている方は、Direct2D で対処できることをご存じでしょう。Direct2D は DPI スケールに関するすべてのニーズに対処し、物理ピクセルと DPI の倍率から独立した論理的な座標系を提供します。もちろん、Direct2D を効果的に利用するには、特定のレンダー ターゲットに使用する DPI 値を指定する必要があります。
従来の Direct2D アプリケーションは Direct2D ファクトリから DPI 値を取得する、つまり前述の GetDeviceCaps 関数を呼び出すだけでした。ただし、現在はこれだけでは不十分です。既に説明したように、今では DPI 値は動的に変化する可能性があり、GetDeviceCaps 関数が返す値は、システム DPI 対応アプリケーションのレンダリングに適している場合にのみ役立ちます。
代わりに、Direct2D レンダー ターゲットの作成直後に、ウィンドウに最も近いモニターの DPI 値を照会する必要があります。
auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONEAREST);
このモニター ハンドルを使用すると、GetDpiForMonitor 関数を呼び出して、このモニターの DPI 値を取得できます。
auto x = unsigned {};
auto y = unsigned {};
VERIFY_(S_OK, GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &x, &y));
特定のモニターに対する実際の DPI 値は、必ずしも図 4 のオプションと正確に一致しているとは限りません。繰り返しますが、実際の DPI 値は、解像度、ディスプレイの物理 DPI、画面までの想定距離など、さまざまな要因に応じて変わります。最後に、Direct2D レンダー ターゲットの SetDpi メソッドを呼び出します。これで、Direct2D によって必要に応じてコンテンツが適切にスケーリングされます。
既に説明したとおり、以上の処理は動的に発生する場合があります。アプリケーションの実行中に、ユーザーが突然図 3 のウィンドウでスケールを変更したり、DPI の倍率が異なる別のモニターにウィンドウをドラッグしたりする場合が考えられます。このような場合、Windows シェルによって、WM_DPICHANGED と呼ばれる新しいウィンドウ メッセージがモニターごとの DPI 対応アプリケーション ウィンドウに送信されます。
メッセージ ハンドラー内部では、MonitorFromWindow 関数と GetDpiForMonitor 関数を再度呼び出して現在のモニターの実際の DPI 値を取得してから、再度 SetDpi メソッドを使用して Direct2D レンダー ターゲットを更新できます。別の方法としては、メッセージの WPARAM で x 軸と y 軸の両方の DPI 値をパックすれば、後は下位ワードと上位ワードを抽出するだけです。
auto x = LOWORD(wparam);
auto y = HIWORD(wparam);
ただし、WM_DPICHANGED メッセージの LPARAM は欠かせない存在です。この LPARAM は、新しく適用されたスケールに基づく、ウィンドウの新しい位置とサイズを指定します。このため、Direct2D でコンテンツのスケーリングを処理しながら、ユーザーがデスクトップ上での実際のウィンドウ サイズを調整し、適切に配置できるようになります。メッセージの LPARAM は、RECT 構造体のポインターです。
auto rect = *reinterpret_cast<RECT *>(lparam);
続いて、SetWindowPos 関数を呼び出して、ウィンドウの位置とサイズを更新できます。
VERIFY(SetWindowPos(window,
0, // No relative window
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
SWP_NOACTIVATE | SWP_NOZORDER));
必要なコードは以上です。Direct2D を採用する場合、モニターごとの DPI 対応に対処するために少し遠回りすることになります。この DPI 対応を実現しているアプリケーションはほとんどないため、このようなアプリケーションを作成すれば注目を集めることになるでしょう。Windows Presentation Foundation (WPF) アプリケーションを既に作成している場合は、bit.ly/IPDN3p (英語) のサンプルも役に立ちます。このサンプルは、この記事で説明したのと同じ原理を WPF コード ベースに統合する方法を示しています。
図 9 は、モニターごとの DPI 対応アプリケーションの例を示しています。WM_DPICHANGED メッセージに基づいて、コンテンツとウィンドウ サイズの両方のスケールが自動的に調整されます。絵文字は、ウィンドウが現在表示されているモニターを示しています。さらに対話的な例については、bit.ly/1fgTifi (英語) で Pluralsight のコースをご覧ください。このアプリケーションのサンプル コードも入手できます。
図 9 モニターごとの DPI 対応アプリケーション
Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローできます。
この記事のレビューに協力してくれた技術スタッフの James Clarke (マイクロソフト) に心より感謝いたします。