Windows での高 DPI デスクトップ アプリケーションの開発
このコンテンツは、ディスプレイ スケール ファクター (1 インチあたりのドット数、DPI) の変更を動的に処理するようにデスクトップ アプリケーションを更新しようとしている開発者を対象としています。これにより、アプリケーションをどのようなディスプレイでも鮮明にレンダリングできるようにします。
まず、新しい Windows アプリを最初から作成する場合は、ユニバーサル Windows プラットフォーム (UWP) アプリケーションを作成することを強くお勧めします。 UWP アプリケーションは、実行されているディスプレイごとに自動的に、そして動的にスケーリングされます。
以前の Windows プログラミング技術 (未加工の Win32 プログラミング、Windows フォーム、Windows Presentation Framework (WPF) など) を使用したデスクトップ アプリケーション は、開発者の追加作業なしに DPI スケーリングを自動的に処理することができません。 このような作業を行わないと、多くの一般的な使用シナリオでアプリケーションがぼやけたり、サイズが間違って表示されたりします。 このドキュメントでは、デスクトップ アプリケーションを正しくレンダリングするためのアップデートについて、そのコンテキストと情報を提供します。
ディスプレイ スケール ファクター & DPI
ディスプレイ技術の進歩に伴い、ディスプレイ パネル メーカーは、パネル上の物理的スペースの各ユニットに詰め込む画素数を増やしてきました。 その結果、現代のディスプレイ パネルの 1 インチあたりのドット数 (DPI) は、これまでよりもはるかに高くなっています。 以前は、ほとんどのディスプレイには、物理的スペースの線形インチあたり 96 ピクセル (96 DPI) でしたが、2017年には、300 DPI 以上のディスプレイが容易に入手できます。
ほとんどのレガシ デスクトップ UI フレームワークには、プロセスのライフタイムにディスプレイ DPI が変更されないことを前提として組み込まれています。 この前提はもはや当てはまらず、ディスプレイ DPI はアプリケーション プロセスのライフタイムを通じて数回変更されるのが一般的です。 ディスプレイのスケール ファクター/DPI が変更されるな一般的なシナリオには、次のようなものがあります。
- 各ディスプレイに異なるスケール ファクターがあり、アプリケーションを 1 つのディスプレイから別のディスプレイ (4K や 1080p ディスプレイなど) に移動される複数モニターのセットアップ
- 高 DPI ノート PC と低 DPI 外部ディスプレイのドッキングとドッキング解除 (またはその逆)
- 高 DPI ノート PC/タブレットから低 DPI デバイス (またはその逆) へのリモート デスクトップ接続
- アプリケーションの実行中にディスプレイスケールファクターの設定を変更する
これらのシナリオでは、UWP アプリケーションは自動的に新しい DPI に対して再描画されます。 デフォルトでは、開発者が追加作業をしなければ、デスクトップ アプリケーションは再描画されません。 DPI の変更に対応するためにこの追加の作業を行わないデスクトップ アプリケーションは、ユーザーにはぼやけたり、サイズが間違って表示されたりすることがあります。
DPI 認識モード
デスクトップ アプリケーションは、DPI スケーリングをサポートしているかどうかを Windows に通知する必要があります。 既定では、システムはデスクトップ アプリケーションの DPI 非対応とみなし、ウィンドウをビットマップストレッチします。 次のいずれかの使用可能な DPI 対応モードを設定することで、アプリケーションは DPI スケーリングの処理方法を Windows に明示的に通知できます。
DPI 未対応
DPI 非対応のアプリケーションは、固定 DPI 値 96 (100%) でレンダリングされます。 これらのアプリケーションをディスプレイ スケールが 96 DPI を超える画面で実行すると、Windows はアプリケーションのビットマップを予想される物理サイズに拡大します。 これにより、アプリケーションがぼやけて表示されます。
システム DPI 対応
システム DPI 対応のデスクトップ アプリケーションは、通常、ユーザーがサインインした時点のプライマリ接続モニターの DPI を受け取ります。 初期化時に、そのシステム DPI 値を使用して、UI を適切にレイアウトします (コントロールのサイズ設定、フォント サイズの選択、アセットの読み込みなど)。 そのため、システム DPI 対応のアプリケーションは、その 1 つの DPI でレンダリングされるディスプレイ上では、Windows によって DPI スケーリング (ビットマップ ストレッチ) されません。 アプリケーションが別のスケール ファクターを持つディスプレイに移動された場合、またはスケール ファクターが変更された場合、Windows はアプリケーションのウィンドウをビットマップスケールし、ぼやけて表示します。 そのため、システム DPI 対応のデスクトップ アプリケーションは、1 つのディスプレイ スケール ファクターでのみ鮮明にレンダリングされ、DPI が変更されるたびにぼやけてしまいます。
Per-Monitor および Per-Monitor (V2) の DPI 対応
デスクトップ アプリケーションは、Per-Monitor DPI 対応モードを使用するように更新することをお勧めします。これにより、DPI が変更されるたびにすぐに正しくレンダリングできるようになります。 アプリケーションがこのモードで実行することを Windows に報告する場合、Windows は DPI が変更されても、アプリケーションをビットマップ ストレッチせず、代わりにアプリケーション ウィンドウにWM_DPICHANGED を送信します。 その後、アプリケーションの全責任で、新しい DPI のサイズ変更自体を処理します。 デスクトップ アプリケーションで使用されるほとんどの UI フレームワーク (Windows コモン コントロール (comctl32)、Windows フォーム、Windows Presentation Framework など) は自動 DPI スケーリングをサポートしていないため、開発者自身がウィンドウのコンテンツのサイズを変更して再配置する必要があります。
アプリケーション自体が登録できる Per-Monitor 対応には、バージョン 1 とバージョン 2 (PMv2) の 2 つのバージョンがあります。 プロセスを PMv2 対応モードで実行するように登録すると、次のような結果になります。
- DPI が変更されたときに通知を受け取るアプリケーション (トップレベルと子の両方の HWND)
- 各ディスプレイの未加工のピクセルが表示されるアプリケーション
- アプリケーションが Windows によってビットマップ スケーリングされない
- 自動非クライアント領域 (ウィンドウ キャプション、スクロール バーなど)Windows による DPI スケーリング
- Win32 ダイアログ (CreateDialog から) は、Windows によって自動的に DPI スケーリングされる
- 一般的なコントロール (チェックボックス、ボタンの背景など) のテーマで描画されたビットマップ資産が、適切な DPI スケール ファクターで自動的にレンダリングされる
Per-Monitor v2 対応モードで実行している場合、DPI が変更されたときにアプリケーションに通知されます。 アプリケーションが新しい DPI に合わせてサイズを変更しない場合、アプリケーション UI は (前の DPI 値と新しい DPI 値の差に応じて) 小さすぎたり大きすぎたりして表示されます。
Note
Per-Monitor V1 (PMv1) の対応は非常に限定的です。 アプリケーションでは PMv2 を使用することをお勧めします。
次の表は、さまざまなシナリオでアプリケーションがどのようにレンダリングされるかを示しています。
DPI 認識モード | 導入された Windows バージョン | アプリケーションの DPI ビュー | DPI の変更に対する動作 |
---|---|---|---|
非対応 | 該当なし | すべてのディスプレイは 96 DPI です | ビットマップストレッチ (ぼかし) |
System | Vista | すべてのディスプレイの DPI は同じ (現在のユーザー セッションが開始されたときのプライマリ ディスプレイの DPI) | ビットマップストレッチ (ぼかし) |
Per-Monitor | 8.1 | アプリケーション ウィンドウが主に配置されているディスプレイの DPI |
|
Per-Monitor V2 | Windows 10 Creators Update (1703) | アプリケーション ウィンドウが主に配置されているディスプレイの DPI |
DPI の自動スケーリング:
|
Per-Monitor (V1) DPI 対応
Windows 8.1 では、Per-Monitor V1 DPI 対応モード (PMv1) が導入されました。 この DPI 対応モードは非常に限定的で、次に示す機能のみを提供します。 デスクトップ アプリケーションでは、Windows 10 1703 以降でサポートされている Per-Monitor v2 対応モードを使用することをお勧めします。
Per-Monitor 対応の初期サポートでは、次のアプリケーションのみが提供されました。
- トップレベル HWND には DPI の変更が通知され、新しい推奨サイズが提供されます
- Windows でアプリケーションの UI がビットマップ ストレッチされない
- アプリケーションでは、すべてのディスプレイが物理ピクセルで表示されます (仮想化を参照)
Windows 10 1607 以降では、PMv1 アプリケーションは、WM_NCCREATE 中に EnableNonClientDpiScaling を呼び出して、Windows にウィンドウの非クライアント領域を正しくスケーリングするように要求することもできます。
UI フレームワーク/テクノロジによる Per-Monitor DPI スケーリングのサポート
次の表は、Windows 10 1703 時点で、さまざまな Windows UI フレームワークによって提供される Per-Monitor DPI 対応サポートのレベルを示しています。
フレームワーク / テクノロジ | サポート | OS バージョン | DPI スケーリングは次によって処理されます | もっと読む |
---|---|---|---|---|
ユニバーサル Windows プラットフォーム (UWP) | 完全 | 1607 | UI フレームワーク | ユニバーサル Windows プラットフォーム (UWP) |
未加工の Win32/共通コントロール V6 (comctl32.dll) |
|
1703 | アプリケーション | GitHub サンプル |
Windows フォーム | 一部のコントロールに対する自動 Per-Monitor DPI スケーリングの制限 | 1703 | UI フレームワーク | Windows フォームの高 DPI サポート |
Windows Presentation Foundation (WPF) | ネイティブ WPF アプリケーションは、他のフレームワークでホストされている WPF を DPI スケールし、WPF でホストされている他のフレームワークは自動的にスケーリングしません | 1607 | UI フレームワーク | GitHub サンプル |
GDI | なし | 該当なし | アプリケーション | 「GDI の高 DPI スケーリング」を参照してください |
GDI+ | なし | 該当なし | アプリケーション | 「GDI の高 DPI スケーリング」を参照してください |
MFC | なし | 該当なし | アプリケーション | 該当なし |
既存アプリケーションの更新
既存のデスクトップ アプリケーションを更新して、DPI スケーリングを適切に処理するには、少なくともその UI の重要な部分が DPI の変更に対応するように更新される必要があります。
ほとんどのデスクトップ アプリケーションは、システム DPI 対応モードで実行されます。 システム DPI 対応アプリケーションは、通常、プライマリ ディスプレイ (Windows セッションの開始時にシステム トレイが配置されていたディスプレイ) の DPI 合わせてスケーリングします。 DPI が変更されると、Windows によってこれらのアプリケーションの UI をビットマップ ストレッチされますが、多くの場合、その後 UI がぼやけます。 システム DPI 対応アプリケーションを Per-Monitor DPI 対応に更新する場合、UI レイアウトを処理するコードは、アプリケーションの初期化中だけでなく、DPI 変更通知 (Win32 の場合は WM_DPICHANGED) を受信するたびに実行されるように更新する必要があります。 これには通常、UI を 1 回だけスケーリングする必要があるという、コード内の前提条件を見直す必要があります。
また、Win32 プログラミングの場合、多くの Win32 API には DPI やディスプレイ コンテキストがないため、システム DPI に対する相対値のみが返されます。 コードを grep してこれらの API の一部を検索し、DPI 対応のバリアントに置き換えると便利です。 DPI 対応のバリアントを持つ一般的な API の一部を次に示します。
単一 DPI バージョン | Per-Monitor バージョン |
---|---|
GetSystemMetrics | GetSystemMetricsForDpi |
AdjustWindowRectEx | AdjustWindowRectExForDpi |
SystemParametersInfo | SystemParametersInfoForDpi |
GetDpiForMonitor | GetDpiForWindow |
また、DPI が一定であるを想定してハードコーディングされたサイズをコードベースで検索し、DPI スケーリングを正しく考慮したコードに置き換えることをお勧めします。 これらの提案をすべて組み込んだ例を次に示します。
例:
次の例は、子 HWND を作成する場合の簡略化された Win32 ケースを示しています。 CreateWindow の呼び出しでは、アプリケーションが 96 DPI (USER_DEFAULT_SCREEN_DPI
定数) で実行されていることを前提としており、より高い DPI ではボタンのサイズも位置も正しくありません。
case WM_CREATE:
{
// Add a button
HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
50,
50,
100,
50,
hWnd, (HMENU)NULL, NULL, NULL);
}
更新されたコードは次のとおりです。
- 親ウィンドウの DPI に合わせて子 HWND の位置とサイズをスケーリングするウィンドウ作成コード DPI
- 子 HWND の位置変更とサイズ変更による DPI の変更への対応
- ハードコーディングされたサイズが削除され、DPI の変更に対応するコードに置き換えられました
#define INITIALX_96DPI 50
#define INITIALY_96DPI 50
#define INITIALWIDTH_96DPI 100
#define INITIALHEIGHT_96DPI 50
// DPI scale the position and size of the button control
void UpdateButtonLayoutForDpi(HWND hWnd)
{
int iDpi = GetDpiForWindow(hWnd);
int dpiScaledX = MulDiv(INITIALX_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
int dpiScaledY = MulDiv(INITIALY_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
int dpiScaledWidth = MulDiv(INITIALWIDTH_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
int dpiScaledHeight = MulDiv(INITIALHEIGHT_96DPI, iDpi, USER_DEFAULT_SCREEN_DPI);
SetWindowPos(hWnd, hWnd, dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, SWP_NOZORDER | SWP_NOACTIVATE);
}
...
case WM_CREATE:
{
// Add a button
HWND hWndChild = CreateWindow(L"BUTTON", L"Click Me",
WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
0,
0,
0,
0,
hWnd, (HMENU)NULL, NULL, NULL);
if (hWndChild != NULL)
{
UpdateButtonLayoutForDpi(hWndChild);
}
}
break;
case WM_DPICHANGED:
{
// Find the button and resize it
HWND hWndButton = FindWindowEx(hWnd, NULL, NULL, NULL);
if (hWndButton != NULL)
{
UpdateButtonLayoutForDpi(hWndButton);
}
}
break;
システム DPI 対応アプリケーションを更新する場合、一般的な手順をいくつか次に示します。
- アプリケーション マニフェスト (または使用される UI フレームワークに応じて他の方法) を使用して、プロセスを Per-Monitor DPI 対応 (V2) としてマークします。
- UI レイアウト ロジックを再利用可能にし、DPI の変更 (Windows (Win32) プログラミングの場合は WM_DPICHANGED) が発生したときに再利用できるようにアプリケーション初期化コードから移動します。
- DPI に依存するデータ (DPI/フォント/サイズなど) を更新する必要がないと想定するコードを無効にします。 プロセスの初期化時にフォント サイズと DPI 値をキャッシュすることは、非常に一般的な方法です。 モニターごとの DPI 対応になるようにアプリケーションを更新する場合は、新しい DPI が検出されるたびに DPI に依存するデータを再評価する必要があります。
- DPI が変更されたら、新しい DPI のビットマップ資産を再読み込み (または再ラスター化) するか、必要に応じて、現在読み込まれている資産を適切なサイズにビットマップ ストレッチします。
- Per-Monitor DPI 対応ではない API を grep し、Per-Monitor DPI 対応の API に置き換えます (該当する場合)。 例: GetSystemMetrics を GetSystemMetricsForDpi に置き換えます。
- マルチディスプレイ/マルチ DPI システムでアプリケーションをテストします。
- 適切な DPI スケールに更新できないアプリケーションのトップレベル ウィンドウの場合は、混合モードの DPI スケーリング (後述) を使用して、システムによるこれらのトップレベル ウィンドウのビットマップ ストレッチを許可してください。
混合モード DPI スケーリング (サブプロセス DPI スケーリング)
アプリケーションを更新して、Per-Monitor DPI 対応をサポートする場合、アプリケーション内のすべてのウィンドウを一度に更新するのは現実的ではない場合や不可能な場合があります。 これは、すべての UI の更新とテストに必要な時間と労力、または実行する必要がある UI コードをすべて所有していない (アプリケーションがサードパーティの UI を読み込む場合) ことが原因である可能性があります。 このような場合、Windows では、アプリケーション ウィンドウの一部 (トップレベルのみ) を元の DPI 認識モードで実行しながら、その間に UI のより重要な部分を更新する時間とエネルギーに集中させることで、Per-Monitor 認識の世界に入りやすくする方法が提供されます。
次の図は、既存のモード ("セカンダリ ウィンドウ") で他のウィンドウを実行している間に、Per-Monitor DPI 認識で実行するように、メイン アプリケーション UI (図の "メイン ウィンドウ") を更新します。
Windows 10 Anniversary Update (1607) 以前では、プロセスの DPI 対応モードはプロセス全体のプロパティでした。 Windows 10 Anniversary Update 以降では、このプロパティはトップレベル ウィンドウごとに設定できるようになりました。 (子 ウィンドウは、引き続き親のスケーリング サイズに合わせる必要があります)。トップレベル ウィンドウは、親を持たないウィンドウとして定義されます。 これは通常、最小化、最大化、閉じるボタンを備えた "標準" ウィンドウです。 サブプロセス DPI 認識のシナリオは、プライマリ UI の更新に時間とリソースを集中しながら、Windows によってセカンダリ UI をスケーリング (ビットマップ ストレッチ) することです。
サブプロセス DPI 認識を有効にするには、ウィンドウ作成呼び出しの前後に SetThreadDpiAwarenessContext を呼び出します。 作成されたウィンドウは、SetThreadDpiAwarenessContext で設定した DPI 認識に関連付けられます。 2 番目の呼び出しを使用して、現在のスレッドの DPI 認識を復元します。
サブプロセス DPI スケーリングを使用すると、アプリケーションの DPI スケーリングの一部を Windows に任せることができますが、アプリケーションの複雑さが増す可能性があります。 このアプローチの欠点と、それがもたらす複雑さの本質を理解することが重要です。 サブプロセス DPI 認識の詳細については、「混合モード DPI スケーリングと DPI 認識 API」を参照してください。
変更のテスト
アプリケーションを更新して Per-Monitor DPI 認識できるようにした後は、混合 DPI 環境でアプリケーションが DPI の変更に適切に対応できるかを検証することが重要です。 具体的なテストの内容には、次のようなものがあります。
- 異なる DPI 値のディスプレイ間でアプリケーション ウィンドウを行き来させる
- さまざまな DPI 値のディスプレイでアプリケーションを起動する
- アプリケーションの実行中にモニターのスケール ファクターを変更する
- プライマリ ディスプレイとして使用するディスプレイを変更し、 Windows からサインアウトしてから、再びサインインした後にアプリケーションを再テストします。 これは、ハードコーディングされたサイズ/ディメンションを使用するコードを見つける場合に特に役立ちます。
一般的な落とし穴 (Win32)
WM_DPICHANGED で提供されている推奨される四角形を使用しない
Windows がアプリケーション ウィンドウに WM_DPICHANGED メッセージを送信するとき、このメッセージには、ウィンドウのサイズを変更するために使用する必要がある、推奨される四角形が含まれます。 次のようにアプリケーションは、この四角形を使用してサイズを変更することが重要です。
- ディスプレイ間をドラッグするときに、マウス カーソルがウィンドウ上の同じ相対位置に留まるようにする
- 1 つの DPI 変更によって後続の DPI 変更がトリガーされ、さらに別の DPI 変更がトリガーされるという、アプリケーション ウィンドウが再帰的な DPI 変更サイクルに陥るのを防ぎます。
Windows が WM_DPICHANGED メッセージで提供する推奨される四角形を使用できないようにするアプリケーション固有の要件がある場合は、WM_GETDPISCALEDSIZE を参照してください。 このメッセージは、上記の問題を回避しながら、DPI 変更後に使用する必要があるサイズを Windows に伝えるために使用できます。
仮想化に関するドキュメントの欠如
HWND またはプロセスが DPI 非対応またはシステム DPI 対応として実行されている場合は、Windows によってビットマップ ストレッチされる可能性があります。 このような場合、Windows は DPI に依存する情報をスケーリングし、一部の API から呼び出し元スレッドの座標空間に変換します。 たとえば、DPI 非対応スレッドが高 DPI ディスプレイで実行中に画面サイズを照会する場合、Windows はあたかも画面が 96 DPI 単位であるかのようにアプリケーションに与えられた応答を仮想化します。 または、システム DPI 対応スレッドが現在のユーザーのセッションの開始時に使用されていた DPI とは異なる DPI でディスプレイと対話している場合、Windows は、元の DPI スケール ファクターで実行されている場合に HWND が使用する座標空間に対して、一部の API 呼び出しを DPI スケールします。
デスクトップ アプリケーションを DPI スケールに適切に更新する場合、スレッド コンテキストに基づいてどの API 呼び出しが仮想化された値を返すことができるかを把握するのが困難になる場合があります。この情報は現在、Microsoft によって十分に文書化されていません。 DPI 非対応またはシステム DPI 対応のスレッド コンテキストからシステム API を呼び出すと、戻り値が仮想化される可能性があることに注意してください。 そのため、スレッドが画面または個々のウィンドウを操作するときに想定される DPI コンテキストで実行されていることを確認してください。 SetThreadDpiAwarenessContext を使用してスレッドの DPI コンテキストを一時的に変更する場合は、アプリケーションの他の場所で不適切な動作が発生しないように、完了後に必ず従来のコンテキストを復元してください。
多くの Windows API には DPI コンテキストがありません
多くのレガシ Windows API には、DPI または HWND コンテキストがインターフェイスの一部として含まれていません。 その結果、開発者は多くの場合、サイズ、ポイント、アイコンなど、DPI に依存する情報のスケーリングを処理するために追加の作業を行う必要があります。 たとえば、LoadIcon を使用する開発者は、読み込まれたアイコンをビットマップ ストレッチするか、LoadImage のような適切な DPI 用に適切なサイズのアイコンを読み込むための代替 API を使用する必要があります。
プロセス全体の DPI 認識の強制リセット
一般に、プロセスの初期化後にプロセスの DPI 認識モードを変更することはできません。 ただし、ウィンドウ ツリー内のすべての HWND が同じ DPI 認識モードであるという要件を破ろうとすると、Windows はプロセスの DPI 認識モードを強制的に変更できます。 Windows のすべてのバージョンで、Windows 10 1703 時点では、HWND ツリー内の異なる HWND を異なる DPI 認識モードで実行することはできません。 このルールを破って子と親の関係を作成しようとすると、プロセス全体の DPI 認識をリセットできます。 これは、次の方法でトリガーできます。
- 渡された親ウィンドウの DPI 認識モードが呼び出し元のスレッドと異なる場合の CreateWindow 呼び出し。
- 2 つのウィンドウが異なる DPI 認識モードに関連付けられている場合の SetParent 呼び出し。
次の表は、この規則に違反しようとした場合の動作を示しています。
操作 | Windows 8.1 | Windows 10 (1607 以前) | Windows 10 (1703 以降) |
---|---|---|---|
CreateWindow (In-Proc) | 該当なし | 子継承 (混合モード) | 子継承 (混合モード) |
CreateWindow (Cross-Proc) | (呼び出し元プロセスの) 強制リセット | 子継承 (混合モード) | (呼び出し元プロセスの) 強制リセット |
SetParent (In-Proc) | 該当なし | (現在のプロセスの) 強制リセット | 失敗 (ERROR_INVALID_STATE) |
SetParent (Cross-Proc) | (子ウィンドウ プロセスの) 強制リセット | (子ウィンドウ プロセスの) 強制リセット | (子ウィンドウ プロセスの) 強制リセット |