WPFの高DPI対応 ── 「DPIに強いはず」なのにぼやける・にじむ原因と対処

· · WPF, 高DPI, Windows, .NET, C#, .NET Framework, XAML, UI, 技術相談

「WPF なら DPI 対応は何もしなくていいはずですよね?」という質問を、高DPIがらみの相談の場でよく受けます。半分は正しくて、半分は違います。実際の症状として持ち込まれるのは、「ノート PC 単体ではきれいなのに、会議室の外部モニターにつなぐと画面全体がにじむ」「文字はくっきりしているのにツールバーのアイコンだけぼやける」「一覧の罫線が場所によって太くなったり消えたりする」「画面に埋め込んだ古い帳票コントロールの部分だけ小さい」──どれも「DPI に強いはず」の WPF アプリで現実に起きるものです。

前日の記事「WinForms の高DPI対応」では、Windows の DPI スケーリングの仕組みと DPI 認識モード(Unaware / System Aware / Per-Monitor V2)、そして 96 DPI 直書き時代の WinForms をどう救うかを整理しました。この記事はその WPF 版です。DPI 仮想化や DPI 認識モードの一般論は WinForms 記事に譲り、ここでは「最初から DPI 対応しているはずの WPF で、なぜ・どこに問題が残るのか」という WPF 固有の話に集中します。症状の切り分け、Per-Monitor DPI 対応の宣言方法(.NET Framework 4.6.2 / .NET それぞれ)、単線とビットマップのにじみ対策、WindowsFormsHost 混在の罠、そしてどこまでやるかの判断表まで、実務で使う順に並べます。

1. まず結論

  • WPF は「起動時の DPI」には最初から正しく追従します。 レイアウト単位が 1/96 インチのデバイス非依存単位(DIP)で、描画系がシステム DPI に合わせて自動スケールするため、既定で System DPI Aware です。WinForms のような AutoScaleMode の設定・点検は不要です。1
  • それでも残る問題は 4 系統に分かれます。(a) DPI の違うモニターへ移したときの全体のにじみ、(b) ビットマップ画像・アイコンのぼやけ、(c) 単線・罫線のにじみ、(d) WindowsFormsHost / WebBrowser などの混在コンテンツ。原因も直し方も別物なので、まず 3 章の表で切り分けてください。
  • (a) の根治は Per-Monitor DPI 対応です。.NET Framework 4.6.2 以降の WPF はこれをフレームワークとしてサポートしており、マニフェストで宣言すればウィンドウの再スケーリング自体は自動で行われます。23
  • .NET(Core 3.1〜.NET 8)の WPF も既定は System Aware のままです。WinForms の ApplicationHighDpiMode / SetHighDpiMode に当たる仕組みは WPF にはなく、宣言方法は .NET Framework と同じくマニフェストです。
  • (c) は DPI 以前のサブピクセル配置の問題です。ルート要素への UseLayoutRounding="True" が第一手で、SnapsToDevicePixels は描画時スナップという別の道具です。どちらも既定は無効です。45
  • (b) は Path / Geometry などのベクター資産を第一候補にします。ビットマップしかない資産は複数解像度を用意して DPI で切り替えます。WPF の既定の補間(Linear)は、非整数倍のスケールでアイコンのような小さい画像をいちばんぼやけさせます。6
  • (d) の混在コンテンツは Per-Monitor 対応の上限を決める要素です。特に ElementHost / HwndSource に「載せられた」WPF の Per-Monitor 動作は公式にサポート外と明言されています。3
  • DPI 認識モードの一般論(DPI 仮想化、System Aware と Per-Monitor V2 の違い、exe プロパティからの利用者側上書き)と混在 DPI テスト環境の作り方は、WinForms 記事の 2〜3 章・7 章と共通なので本記事では繰り返しません。

2. WPFはなぜ「DPIに強い」のか ── DIPとSystem DPI Aware

WinForms の高DPI問題の根っこは、座標もサイズも物理ピクセル直書きで、96 DPI 前提のレイアウトを実行時に換算し直す仕組み(AutoScale)を後付けで持っている点にありました。WPF は出発点が違います。XAML に書く Width="120" の 120 は物理ピクセルではなく、1/96 インチを 1 とするデバイス非依存単位(DIP)です。レイアウトは全部この単位で計算され、描画時にシステム DPI に応じた倍率(150% なら 1.5 倍)へ変換されます。文字もベクターフォントとして描画されるため、拡大してもビットマップ的な劣化がありません。この仕組みにより、WPF アプリは何も宣言しなくても System DPI Aware として動作します。1

WinForms との違いを一覧にしておきます。

  WinForms WPF
座標・サイズの単位 物理ピクセル DIP(1/96 インチ)
何も宣言しない場合 Unaware(OS のビットマップ拡大でぼやける) System Aware(主モニターではくっきり)
起動時 DPI への追従 AutoScaleMode によるフォーム単位の再計算。設定と点検が必要 描画系が自動スケール。設定不要
高DPIで崩れる典型 固定座標レイアウトの重なり・見切れ レイアウトは崩れず、にじみ・ぼやけとして現れる
デザイナー起因の事故 AutoScaleDimensions の書き換わり(定番) 原則なし(XAML は DIP のまま保存される)

この表のとおり、WinForms で工数の中心だった「レイアウトが崩れるのを直す」「デザイナー事故を防ぐ」という仕事が、WPF ではほぼ発生しません。「WPF は DPI に強い」は正しい認識です。

ただし、自動なのは System Aware の範囲までです。System Aware とは「サインイン時の主モニターの DPI に合わせる」というモードであって、モニターごとに DPI が違う環境への追従(Per-Monitor)は含みません。また、DIP で拡大されるのはあくまで WPF が自分で描くもの(テキスト、図形、コントロール)で、ビットマップ画像は拡大すればぼやけますし、HWND を持ち込む混在コンテンツは WPF のスケーリングの外側にいます。「DPI に強いはずなのに」という相談は、ほぼこの残りの部分で起きています。

3. それでも起きる問題 ── 症状から原因を切り分ける

相談を受けたとき、当社が最初に使う切り分け表です。WPF では症状と原因の対応が WinForms よりさらにきれいに分かれるので、この表だけで原因がほぼ特定できます。

症状 原因 対処
DPI の違うモニターへ動かすと全体が一様ににじむ。元のモニターへ戻すと直る System Aware のまま。OS がウィンドウ全体をビットマップ拡大している 4 章(Per-Monitor 化)
スケーリング設定を変えた直後や、高DPIクライアントからの RDP 接続でにじむ 同上(System DPI はサインイン時に固定されるため) 4 章
文字はくっきりなのにアイコン・画像だけぼやける、または小さい ビットマップ資産が補間拡大されている 5.3 節
1px の罫線・枠線が場所によって太さが違う、うっすらにじむ サブピクセル配置とアンチエイリアス 5.1 節
100%(96 DPI)のモニターで小さい文字がにじんで見える 既定のテキスト整形(Ideal)のアンチエイリアス 5.2 節
自前で作る画像(WriteableBitmap、RenderTargetBitmap 等)がぼやける ピクセルサイズを 96 DPI 前提で計算している 6 章
ウィンドウ位置・マウス座標・スクリーンショットの座標がずれる DIP と物理ピクセルの混同 6 章
WindowsFormsHost / WebBrowser / 一部サードパーティ製コントロールの中だけ小さい・粗い・崩れる 混在コンテンツ(WPF のスケーリング対象外) 7 章

1 行目の「全体が一様ににじむ」について補足します。これは WinForms 記事の 2 章で書いた DPI 仮想化(OS のビットマップ拡大)が System Aware アプリに対して働いている状態で、アプリが壊れているわけではありません。System Aware のアプリは「サインイン時の主モニターの DPI」で描画し、それ以外の DPI のモニターでは OS が拡大・縮小して帳尻を合わせます。レイアウトは崩れない代わりににじむ、という OS の救済措置です。1 直すとは、この救済を断って「モニターごとの DPI に自分で追従します」と宣言すること、つまり Per-Monitor 化です。

逆に 3 行目以降の症状は、System Aware のままでも直せます。「マルチモニター運用はないが、150% の画面でアイコンと罫線が汚い」という相談なら、4 章を飛ばして 5 章から着手して構いません。

4. マルチモニターのにじみ ── Per-Monitor DPI対応

4.1 System Awareの限界

System DPI はサインイン時に主モニターの DPI で固定されます。150% のノート内蔵ディスプレイが主モニターなら、WPF は全ウィンドウを 1.5 倍で描き、100% の外部モニターへ移すと OS が 2/3 に縮小表示します。この OS スケーリングは非整数倍のときに特にぼやけて見えます。1 200% と 100% の 2 倍差よりも、125% と 150% のような中途半端な組み合わせのほうが見た目の劣化が大きい、というのは検証してみると実感できます。

つまり System Aware の WPF で困るのは、DPI の違うモニターを行き来する使い方と、サインイン後にスケーリング設定が変わる状況(ドッキングステーションの抜き差しで主モニターが変わる、RDP でクライアント側の DPI が持ち込まれる、など)に限られます。全員が単一モニターの固定デスクトップで使う社内アプリなら、System Aware のままで実用上完結します。この判断は 8 章の表に戻ってきます。

4.2 .NET Framework 4.6.2以降での宣言方法

WPF の Per-Monitor DPI 対応は .NET Framework 4.6.2 で入りました。2 それ以前は、OS へ Per-Monitor を宣言しても WPF 側が DPI 変更に追従してくれず、ウィンドウの再スケーリングを全部自前で書く必要がありました(Windows 8.1 時代のサンプルがネイティブヘルパー DLL を使う大掛かりな作りだったのはこのためです)。4.6.2 以降は WPF が WM_DPICHANGED を処理し、ウィンドウサイズの調整・再レイアウト・再描画まで自動で行います。

要件は 2 つ、Windows 10 Anniversary Update(1607)以降の OS と、.NET Framework 4.6.2 以降をターゲットにしたビルドです。3 宣言はアプリケーションマニフェストに書きます。

<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:windowsSettings>
    <!-- .NET Framework 4.6.2〜4.7.2 は PerMonitor 単独で宣言する(開発者ガイドと同じ)。
         WPF の PerMonitorV2 サポートは .NET Framework 4.8 以降のため、
         4.8+ / .NET では「PerMonitorV2, PerMonitor」と V2 を先頭に置く -->
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
      >PerMonitor</dpiAwareness>
    <!-- dpiAwareness を認識しない古い OS 向け(System Aware にフォールバック) -->
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </asmv3:windowsSettings>
</asmv3:application>

この 2 段構えには意味があります。dpiAwareness 要素は Windows 10 1607 以降で認識され、カンマ区切りで並べた値のうち最初に認識できたものが使われます。7 dpiAwareness 要素自体を知らない古い OS では dpiAware の宣言(System Aware)にフォールバックする、という寸法です。

ここで PerMonitor と PerMonitorV2 をターゲットフレームワークで使い分ける点に注意してください。WPF が PerMonitorV2(と Mixed-Mode DPI)を正式にサポートするのは .NET Framework 4.8 以降8、4.6.2 の開発者ガイドのサンプルも PerMonitor 単独の宣言です。3 4.6.2〜4.7.2 ターゲットで V2 を先頭に書くと、Windows 10 1703 以降の OS はフレームワークが対応していない V2 モードを選んでしまい、特に WindowsFormsHost などの混在コンテンツ周りで想定外の動きになります。4.8 以降(および .NET の WPF)に上げてから PerMonitorV2, PerMonitor と V2 を先頭に置き、タイトルバー・スクロールバーなど非クライアント領域のスケーリングを OS に任せる(WinForms 記事の 3 章参照)のが当社のおすすめです。

1 点、ターゲットフレームワークの罠があります。実行環境の .NET Framework が 4.6.2 以降でも、プロジェクトのターゲットが 4.6.1 以前のままだと Per-Monitor 追従は既定で無効です。この場合は app.config の AppContext スイッチで明示的に有効化します。3

<configuration>
  <runtime>
    <!-- 二重否定に注意: 「DPI変更でスケールしない」を false にする=有効化 -->
    <AppContextSwitchOverrides value="Switch.System.Windows.DoNotScaleForDpiChanges=false"/>
  </runtime>
</configuration>

「マニフェストを書いたのに動かない」という問い合わせの原因は、経験上ほぼこのターゲットフレームワークか、マニフェストがビルドに含まれていない(プロジェクト設定で既定マニフェストのままになっている)かのどちらかです。

4.3 .NET (Core 3.1〜.NET 8) での宣言方法

.NET 上の WPF も、マニフェストなしの既定は System Aware のままです。WinForms がプロジェクトファイルの ApplicationHighDpiModeApplication.SetHighDpiMode という専用の入り口を持つのに対し、WPF にはコードやプロジェクト設定から DPI 認識モードを切り替える公式の仕組みがありません。宣言方法は .NET Framework と同じで、プロジェクトに app.manifest を追加し(Visual Studio の「新しい項目の追加」→「アプリケーション マニフェスト ファイル」)、4.2 節と同じ dpiAwareness 宣言を書き、プロジェクトファイルで参照します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

ランタイムが新しいので 4.2 節の AppContext スイッチは不要です。「.NET に移行すれば高DPIも自動で解決」とはならない点だけ注意してください。移行で楽になるのは WinForms 側の話で(WinForms 記事 4.1 節)、WPF は .NET Framework 4.6.2 の時点で今と同じ水準に達しています。

宣言後にやることが残っているのは WinForms と同じですが、中身が違います。WinForms ではレイアウト崩れの改修が主戦場でした。WPF ではレイアウトはフレームワークが追従してくれるので、残るのは全画面の表示点検と、ビットマップ資産の DPI 切替(5.3 節)ピクセルを直接扱うコードの追従(6 章)混在コンテンツの確認(7 章)です。画面数が同じなら、Per-Monitor 化の総工数は WinForms より一回り小さく済むのが通例です。

5. 描画のにじみ・ぼやけ ── 単線・文字・ビットマップ

この章の内容は Per-Monitor 化と独立です。System Aware のままでも効くので、マルチモニター運用のないアプリでも適用する価値があります。

5.1 単線・罫線のにじみ ── UseLayoutRoundingとSnapsToDevicePixels

DIP でレイアウトするということは、要素の境界が物理ピクセルの整数位置に乗るとは限らないということです。125% なら 1 DIP = 1.25px なので、幅 1 DIP の罫線は物理ピクセル 1.25 個分に相当し、ピクセルの中間に落ちたエッジはアンチエイリアスで半透明に描かれます。結果が「線がにじむ」「同じ 1px 指定なのに行によって太さが違って見える」です。96 DPI でも、Grid の星付きサイズ(*)の割り算や中央揃えの余白計算で 0.5px に乗れば同じことが起きます。これは WPF のサブピクセル描画の仕様であって、DPI 問題というより「DPI が上がると露見しやすくなる」問題です。4

第一手はレイアウト丸めです。ルート要素に 1 行足します。

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        UseLayoutRounding="True">

UseLayoutRounding はレイアウトパスで非整数のピクセル値を丸める仕組みで、既定は無効、ルートに設定すればビジュアルツリー全体へ伝播します。4 似た名前の SnapsToDevicePixels は役割が違い、レイアウトではなく描画時にエッジをピクセル境界へスナップします。こちらも既定は false で、設定はサブツリーへ継承されます。96 DPI を超える環境で単線周辺のアンチエイリアスによるにじみを抑える用途が公式ドキュメントにも挙げられています。5

  UseLayoutRounding SnapsToDevicePixels
働くタイミング レイアウトパス(Measure / Arrange の結果を整数ピクセルへ丸める) 描画時(エッジをピクセル境界へスナップ)
既定値 false false
適用範囲 ルートに設定すれば子孫へ伝播 同じく子孫へ継承
主な効き先 レイアウト全般。要素サイズ・位置の 0.5px ずれ、それに由来する線・境界のにじみ Border・単線など個別要素の残ったにじみ
使い方の目安 まずこれをルート Window へ。新規アプリは全ウィンドウ共通スタイルで UseLayoutRounding で残った箇所にピンポイントで

迷ったら「ルートに UseLayoutRounding="True"、それでも残る箇所に SnapsToDevicePixels="True"」の順です。丸めの副作用として、星付きサイズで等分していた列幅が 1px 単位で不均等になることがありますが、業務アプリの画面で問題になった経験はほとんどありません。なお DrawingContext で自前描画しているコードのにじみには GuidelineSet という低レベルの手段もありますが、まず描画座標を丸めるだけで大半は解決します。

5.2 文字のにじみ ── TextFormattingMode

「WPF は文字が薄い・にじむ」という昔からの不満は、DPI というよりテキスト整形モードの話です。WPF のテキスト整形には、フォント本来の理想メトリクスで配置する Ideal(既定)と、GDI 互換メトリクスで配置する Display の 2 モードがあります。9 Ideal は文字間隔が美しくスケーリングにも素直ですが、96 DPI(100%)で小さめの文字を描くとアンチエイリアスでにじんで見えることがあります。TextOptions.TextFormattingMode="Display" をルートに設定すると WinForms 的なくっきり感になります。

ただし高DPI環境では話が逆になります。Display は 96 DPI のピクセル格子に合わせる整形なので、スケーリング下ではかえって品質が落ちることがあります。100% 環境の利用者が多いなら Display、125% 以上が主流になった今の標準環境では既定の Ideal のまま、が実務的な割り切りです。100% のときだけ Display に切り替える実装も可能ですが、そこまでやる価値がある画面(テキストエディター的な画面)は限られます。

5.3 画像・アイコンのぼやけ ── ベクター優先と複数解像度

WPF は Image に指定したビットマップも DIP で配置し、DPI に応じて拡大します。16×16px のアイコンは 150% 環境では 24×24px に補間拡大され、既定の補間アルゴリズム(Linear)ではっきりぼやけます。6 「文字はきれいなのにアイコンだけ眠い」という WPF アプリの見た目は、ほぼ全部これです。

対策は優先順で 3 つあります。

  1. ベクター資産にする(第一候補)Path / Geometry / DrawingImage で持つアイコンはどの DPI でもくっきり描画され、切替コードも不要です。デザインツールや SVG からの XAML 変換ツールも充実しています。新規アプリのアイコンは最初からベクターで持つ、を規約にする価値があります。Segoe MDL2 Assets などのアイコンフォントも同じ効果です。
  2. 複数解像度のビットマップを DPI で切り替える。写真やスクリーンショットなどベクター化できない資産は、96 / 120 / 144 / 192 DPI 向け(16 / 20 / 24 / 32px など)を用意し、現在の DPI に応じて選びます。公式の開発者ガイドも、ぼやけ対策として DPI ごとの資産切替を推奨しています。1
  3. RenderOptions.BitmapScalingMode で補間を選ぶ。資産を増やせない場合の緩和策です。200% のような整数倍なら NearestNeighbor でドットのまま拡大するほうがくっきりし、大きい画像の縮小には HighQuality(Fant)が向きます。6

2 の切替は、Per-Monitor 対応済みなら OnDpiChanged(.NET Framework 4.6.2 以降で利用可)で行います。

public partial class MainWindow : Window
{
    protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
    {
        base.OnDpiChanged(oldDpi, newDpi);
        // モニター間移動やスケーリング変更のたびに呼ばれる
        AppIcon.Source = IconAssets.SelectFor(newDpi.DpiScaleX);
    }
}

public static class IconAssets
{
    // 16 / 24 / 32 px で用意した資産から拡大率に合うものを選ぶ
    public static BitmapImage SelectFor(double scale) => scale switch
    {
        <= 1.0 => Load("icon16.png"),
        <= 1.5 => Load("icon24.png"),
        _      => Load("icon32.png"),
    };

    private static BitmapImage Load(string name) =>
        new(new Uri($"pack://application:,,,/Assets/{name}"));
}

System Aware のままなら DPI は起動後変わらないので、起動時(Loaded)に VisualTreeHelper.GetDpi(this) で一度選ぶだけで済みます。切替コードを書く前に、「そもそもこの資産はベクターにできないか」を毎回問い直すのが、結局いちばん保守が楽です。

6. ピクセルを扱うコードとDPI変更への追従

WPF の自動スケーリングの傘の外に出るのが、物理ピクセルを自分で数えるコードです。典型は次の 3 つで、どれも「開発機(100%)では完璧、客先の 150% でだけぼやける・ずれる」という嫌な出方をします。

1 つめは WriteableBitmap / RenderTargetBitmap などピクセルバッファを自前で作るコードです。ピクセル数を DIP のサイズと同じ値で作ると、150% 環境では 1.5 倍に補間拡大されてにじみます。現在の DPI は VisualTreeHelper.GetDpi が返す DpiScale 構造体で取れるので、ピクセル数は物理ピクセルで、DPI はバッファに正しく焼き込みます。

// imageHost: 描画結果を表示する要素(ActualWidth/Height は DIP)
DpiScale dpi = VisualTreeHelper.GetDpi(imageHost);
int pixelWidth  = (int)Math.Ceiling(imageHost.ActualWidth  * dpi.DpiScaleX);
int pixelHeight = (int)Math.Ceiling(imageHost.ActualHeight * dpi.DpiScaleY);

// 96,96 固定で作らず、実際の DPI を渡す(1 物理ピクセル = 1 バッファピクセルになる)
var bitmap = new WriteableBitmap(
    pixelWidth, pixelHeight,
    dpi.PixelsPerInchX, dpi.PixelsPerInchY,
    PixelFormats.Bgra32, null);

2 つめはスクリーン座標や Win32 相互運用です。PointToScreen が返す座標、WM_MOUSEMOVE やフックで受け取る座標、MoveWindow に渡す座標はすべて物理ピクセルで、XAML 側の DIP と混ぜると 125% 環境で 1.25 倍ずれます。変換は CompositionTarget の行列を使います。

var source = PresentationSource.FromVisual(this);
if (source?.CompositionTarget is { } target)
{
    // DIP → 物理ピクセル
    Point device = target.TransformToDevice.Transform(new Point(x, y));
    // 物理ピクセル → DIP
    Point dip = target.TransformFromDevice.Transform(devicePoint);
}

3 つめはキャッシュです。DPI を織り込んで作ったビットマップ・レイアウト値・フォーマット済みテキストをキャッシュしている場合、Per-Monitor 化するとモニター間移動のたびに陳腐化します。5.3 節と同じく OnDpiChanged(またはウィンドウの DpiChanged イベント)で作り直してください。ここでも System Aware のままなら DPI は起動時に固定なので、追従コードは不要です。「Per-Monitor 化のコスト=ピクセルを扱っているコードの数」と考えて見積もると、実態とよく合います。

なお、こうした再生成処理を非同期でやる場合の UI スレッドの扱いは「WPF/WinForms の async と UI スレッド」で整理したとおりです。

7. 混在コンテンツの罠 ── WindowsFormsHost・WebBrowser・サードパーティ

WPF の自動スケーリングが及ぶのは、WPF が自分で描くものだけです。HWND を持ち込む要素は WPF の描画の「外」にあるため、DPI の扱いが一段ややこしくなります。Windows の公式資料でも、「WPF に載せた他フレームワーク、他フレームワークに載せた WPF は自動ではスケールしない」と明記されています。10

混在の形 何が起きるか 現実的な扱い
WindowsFormsHost(WPF に WinForms を載せる) DIP と物理ピクセルの 2 座標系をホストが変換するが、中身のスケーリングは WinForms コントロールが対応できる範囲まで11 中身の WinForms 側の DPI 対応(AutoScaleMode 等)を WinForms 記事の基準で点検。Per-Monitor 追従は期待しない
WebBrowser コントロール(IE エンジン) 別 HWND のネイティブコンテンツ。ズーム率がアプリのスケーリングと一致しないことがある レガシー扱い。WebView2 への移行を検討する際の材料に加える
ElementHost / HwndSource(WinForms や Win32 に WPF を載せる) Per-Monitor シナリオは公式サポート外3 ホスト側アプリの DPI 認識モードに合わせて System Aware までで設計する
サードパーティ製コントロール 製品ごとに対応水準がばらばら。Per-Monitor 未対応の製品はその画面の上限になる ベンダーの対応状況・対応バージョンを棚卸ししてから Per-Monitor 化を判断

実務でいちばん多いのは 1 行目です。「WPF に移行したが、帳票プレビューやグラフだけ古い WinForms / ActiveX コントロールを WindowsFormsHost で載せている」という構成のアプリを Per-Monitor 化すると、WPF 部分は完璧に追従するのに、ホストの中だけが移動後の DPI に追従せず、小さいまま・粗いままになります。Win32 レベルには混在 DPI をホストする仕組み(SetThreadDpiHostingBehavior)もありますが、WPF から気軽に使えるものではなく、当社では「混在部分が Per-Monitor 対応の上限を決める」と割り切って、先に混在コンテンツの一覧を作ることから始めます。混在をなくす方向の判断材料は「WinForms / WPF / WinUI をどう選ぶか」と「ActiveX/OCX を維持するか、ラップするか、置き換えるか」にまとめてあります。

8. どこまでやるか ── 判断表と段階的な進め方

WPF の場合、選択肢は実質 2 つです(WinForms のような「何もしない=Unaware」は、宣言しなくても System Aware なので存在しません)。

  System Aware のまま Per-Monitor 対応
見え方 主モニターではくっきり。DPI の違うモニター・スケーリング変更後・RDP ではにじむ 全モニターでくっきり
宣言の作業 不要(既定) マニフェスト追加のみ(4 章)
宣言後の残作業 ── 全画面の表示点検+ビットマップ資産の DPI 切替(5.3 節)+ピクセル系コードの DpiChanged 対応(6 章)+混在コンテンツの確認(7 章)
上限を握る要素 ── WindowsFormsHost・WebBrowser・サードパーティ製コントロール
向くケース 単一モニター・固定デスクトップ中心の社内アプリ。混在コンテンツが多いアプリ ノート+外部モニターの混在利用、長寿命の主力アプリ、顧客に配布する製品

判断軸は WinForms 記事 7 章と同じ(アプリの寿命・利用環境・改修予算)ですが、WPF は Per-Monitor 化の限界コストが小さい点が違います。宣言は 1 ファイル、レイアウトはフレームワークが追従、残作業はピクセル系コードと資産の数に比例。混在コンテンツが少ないアプリなら、Per-Monitor まで行く費用対効果は WinForms よりはっきり高いです。当社が実際の案件でおすすめしている進め方は次の 3 段階です。

  1. System Aware のままの品質改善: ルートへの UseLayoutRounding、アイコンのベクター化・複数解像度化、WriteableBitmap 系の DPI 修正。マルチモニターのにじみ以外はここで全部解消し、Per-Monitor 化しない判断をした場合もそのまま資産になります。
  2. Per-Monitor 宣言と点検: マニフェストを追加し、混在 DPI 環境で全画面を回して問題箇所を洗い出します。ここで見つかる問題は 5〜7 章のどれかに分類できるはずです。
  3. DPI 変更への追従実装: OnDpiChanged での資産切替・キャッシュ再生成。混在コンテンツはこの段階で「どこまで許容するか」を決めます。

テスト環境は WinForms 記事 7 章に書いた構成(スケーリングの違うモニター 2 枚、主モニター入替+再サインイン、実行中のスケーリング変更、高DPIクライアントからの RDP)がそのまま使えます。10 WPF で重点的に見るべきは、非整数倍(125% / 150%)での罫線・アイコン・自前描画と、モニター間をまたいでウィンドウをドラッグしたときの挙動です。利用環境の分布(何 % のユーザーがマルチモニターか)を先に把握しておくと投資判断がぶれません。この観点は「Windows アプリ UX 設計」でも書きました。

9. まとめ

WPF は DIP と自動スケーリングにより最初から System DPI Aware で、WinForms の主戦場だった「レイアウト崩れとの戦い」はほぼ存在しません。それでも残る問題は 4 系統──マルチモニターのにじみ(Per-Monitor 未対応)、ビットマップのぼやけ、単線のにじみ、混在コンテンツ──で、それぞれ対処が確立しています。

  • マルチモニターのにじみは、.NET Framework 4.6.2 以降 / .NET の WPF ならマニフェスト宣言だけで Per-Monitor 追従が自動で手に入る
  • 単線・罫線はルートの UseLayoutRounding="True" が第一手、残りに SnapsToDevicePixels
  • アイコンはベクター資産を第一候補、ビットマップは複数解像度+DPI 切替
  • WriteableBitmap やスクリーン座標などピクセルを扱うコードだけが本当の改修対象。その数が Per-Monitor 化の工数を決める
  • WindowsFormsHost / WebBrowser / サードパーティが対応の上限を握る。先に棚卸しする

「WPF だから大丈夫なはず」で止まっていたアプリも、この整理で見れば直す場所は数えるほどしかないことが多いです。手元のアプリがどこまで直せるか、混在コンテンツを含めた現状調査や対応レベルの判断に迷う場合はお手伝いできます。

関連記事

関連する相談領域

合同会社小村ソフトでは、WPF / WinForms アプリの高DPI対応(現状調査、Per-Monitor 化の可否判断、混在コンテンツの棚卸しと改修)、PC 入れ替え・4K モニター導入に伴う表示不具合の原因調査、UI 刷新の相談を扱っています。

参考リンク

  1. Microsoft Learn, Developing a Per-Monitor DPI-Aware WPF Application. WPF が既定で System DPI Aware であること、DIP による自動スケーリングの仕組み、DPI の異なるモニターへ移動すると OS がスケーリングし非整数倍で特にぼやけること、DPI ごとのビットマップ資産切替の考え方について。  2 3 4 5

  2. Microsoft Learn, What’s new in .NET Framework. .NET Framework 4.6.2 の WPF で Per-Monitor DPI awareness が有効になったこと、Switch.System.Windows.DoNotScaleForDpiChanges スイッチ、GitHub 上の開発者ガイドについて。  2

  3. GitHub (microsoft/WPF-Samples), Per Monitor DPI Developer Guide. Windows 10 Anniversary Update と .NET Framework 4.6.2 以降という要件、マニフェストの dpiAwareness / dpiAware の記述、4.6.2 より古いターゲット向けの AppContextSwitchOverrides、HwndSource / ElementHost に WPF を載せる構成が Per-Monitor でサポート外であることについて。  2 3 4 5 6

  4. Microsoft Learn, Layout - WPF. DIP によるレイアウトとサブピクセル描画でエッジがぼやける仕組み、レイアウト丸め(UseLayoutRounding)が既定で無効であること、ルート要素へ設定するとビジュアルツリーへ伝播することについて。  2 3

  5. Microsoft Learn, UIElement.SnapsToDevicePixels Property. 既定値が false であること、ルートに設定するとサブツリーへ継承されること、96 DPI 超の環境で単線付近のアンチエイリアスによる視覚的アーティファクトを軽減できることについて。  2

  6. Microsoft Learn, BitmapScalingMode Enum. 既定(Unspecified)が Linear であること、HighQuality(Fant)・NearestNeighbor 各補間アルゴリズムの特性について。  2 3

  7. Microsoft Learn, Setting the default DPI awareness for a process. dpiAwareness 要素(Windows 10 1607 以降)が dpiAware より優先されること、カンマ区切りで列挙した値のうち最初に認識できたものが使われるフォールバック動作について。 

  8. Microsoft Learn, What’s new in .NET Framework. .NET Framework 4.8 で WPF に Per-Monitor V2 DPI Awareness と Mixed-Mode DPI スケーリングのサポートが追加されたこと、ホストされた HWND / WinForms 相互運用の改善、有効化に必要な AppContext スイッチについて。 

  9. Microsoft Learn, TextFormattingMode Enum. テキスト整形の Ideal(理想メトリクス)と Display(GDI 互換メトリクス)の 2 モードについて。 

  10. Microsoft Learn, High DPI Desktop Application Development on Windows. UI フレームワーク別の Per-Monitor 対応表、WPF に載せた他フレームワーク・他フレームワークに載せた WPF が自動ではスケールしないこと、混在 DPI 環境でのテスト観点について。  2

  11. Microsoft Learn, Layout Considerations for the WindowsFormsHost Element. WindowsFormsHost が DIP と物理ピクセルの 2 つの座標系を変換すること、スケーリングは中の Windows Forms コントロールが対応できる範囲までであることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る