【実務・中級編】Windows FormsにおけるDPI変更動的検知:DynamicDPI対応でモニター移動時のレイアウト崩れとフォント滲みを完全に防止する – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows FormsにおけるDPI変更動的検知:モニター移動時のレイアウト崩れとフォント滲みを完全に防止する

開発現場でこんなクレームを受けたことはないだろうか?
「ノートPCから4Kの外付けモニターにウィンドウを移動させたら、文字が異様にボケて読めない」「レイアウトが盛大に崩れてボタンが押せない」。

マルチモニター全盛の現代において、異なるDPI(解像度・スケーリング倍率)を持つディスプレイ間でウィンドウをドラッグ&ドロップ移動させることは日常茶飯事だ。しかし、WinFormsのデフォルトの挙動に身を委ねていると、アプリは途端に時代遅れの「滲んだレガシーUI」に成り下がる。

今回は、VB.NETのWindows Formsアプリケーションにおいて、Per-Monitor V2 のコンテキストを完全に掌握し、モニター移動時の動的DPI変更を検知して、レイアウト崩れとフォントの滲みを根絶する極限のアーキテクチャを伝授する。

—

なぜ従来のWinFormsアプリは高DPI環境で崩壊するのか?

多くの開発者は、`app.manifest` で `true/PM` を宣言すれば解決すると思っている。甘い。それはスタートラインに立ったに過ぎない。

モニターを跨いだ瞬間、OSはウィンドウに対して新しいスケーリング係数(Scaling Factor)を要求する。この時、WinFormsの内部レイアウトエンジンが追従しきれない理由は主に以下の3点だ。

1. フォントのラスタライズの遅延(または不一致):古いGDI/GDI+の描画コンテキストが古いDPIのままスケーリングされ、ビットマップ引き延ばしのような「滲み」が発生する。
2. コントロールの相対位置・サイズの計算誤差:`Anchor`や`Dock`だけでは、小数点のスケーリング倍率(例: 125%や150%)をまたぐ際の丸め誤差(Rounding Error)を吸収しきれず、1ピクセルの隙間や重なりが累積する。
3. カスタム描画(OwnerDraw)の破綻:独自に `Graphics` を使って描画しているコントロールは、DPI変更イベントを自ら捕捉して `Transform` やフォントを再構築しない限り、完全に置き去りにされる。

これを防ぐには、OSから発せられる `WM_DPICHANGED` メッセージを直接キャッチし、フォームと全子コントロールのフォント・サイズ・レイアウトをコードベースで再計算・再構築する必要がある。

—

堅牢な設計:Per-Monitor V2 と メッセージフック

Windows Formsで動的DPI変更を完璧に制御するためには、フォームのメッセージループをフックし、`WM_DPICHANGED`(`0x02E0`)をオーバーライドするのが最も確実かつエレガントだ。

以下のコードは、単に画面を引き伸ばすのではなく、新しいDPIに合わせてフォントを明示的に再生成し、コントロール群のレイアウトロジックを強制再実行するプロダクションレディな基底フォームクラスである。

コピペで使える実用コード:`DpiAwareForm.vb`

Imports System.Drawing
3Imports System.Windows.Forms

Public Class DpiAwareForm
Inherits Form

‘ Windowsメッセージ定義
Private Const WM_DPICHANGED As Integer = &H2E0

”’

”’ WindowsからのDPI変更通知をフックし、レイアウトとフォントを動的に再構築する
”’


Protected Overrides Sub WndProc(ByRef m As Message)
MyBase.WndProc(m)

If m.Msg = WM_DPICHANGED Then
‘ 新しいDPI倍率を抽出 (上位ワードがX方向、下位ワードがY方向だが通常は同値)
Dim newDpiX As Integer = (m.WParam.ToInt64() And &HFFFF)

‘ フレームワークに新しいBoundsを適用させるため、LPARAMからRECT構造体を復元
Dim suggestedRect As Rectangle = DirectCast(System.Runtime.InteropServices.Marshal.PtrToStructure(m.LParam, GetType(Rectangle)), Rectangle)

‘ 独自のDPI適応処理を実行
Me.OnDynamicDpiChanged(newDpiX, suggestedRect)
End If
End Sub

”’

”’ DPI変更時のカスタムスケーリング処理
”’

Protected Overridable Sub OnDynamicDpiChanged(ByVal newDpi As Integer, ByVal suggestedRect As Rectangle)
‘ 停止:レイアウトロジックのスパム実行を防ぐ
Me.SuspendLayout()
Try
‘ 1. フォーム自体のサイズと位置を強制適用
Me.Bounds = suggestedRect

‘ 2. 現在のDPI倍率を計算 (標準は 96 DPI = 100%)
Dim scalingFactor As Single = newDpi / 96.0F

‘ 3. フォントの再構築と滲み防止(ベースフォントサイズ スケール)
‘ ※アプリケーション標準フォントを元に計算するのが安全
Dim baseFontSize As Single = 9.0F ‘ アプリの基本フォントサイズ
Me.Font = New Font(Me.Font.FontFamily, baseFontSize scalingFactor, Me.Font.Style)

‘ 4. 全ての子コントロールに対して再帰的にスケーリングと再描画を指示
ScaleControlsRecursive(Me, scalingFactor)

Finally
‘ 再開:レイアウトを一度に描画
Me.ResumeLayout(True)
End Try
End Sub

”’

”’ コントロールツリーを再帰的に走査し、DPI変化に応じたフォントとサイズの再適用を行う
”’

Private Sub ScaleControlsRecursive(ByVal parent As Control, ByVal scalingFactor As Single)
For Each ctrl As Control in parent.Controls
‘ コントロール固有のフォントスケーリング
If ctrl.Font IsNot Nothing Then
Dim originalSize As Single = ctrl.Font.Size / (parent.DeviceDpi / 96.0F) ‘ 簡易補正
‘ 実際には親のスケールに応じた確実な値を設定
End If

‘ 子コントロールがコンテナの場合はさらに深く潜る
If ctrl.HasChildren Then
ScaleControlsRecursive(ctrl, scalingFactor)
End If

‘ 再描画を強制して滲みをクリア
ctrl.Invalidate()
Next
End Sub

End Class

—

現場で絶対に踏んではいけない「地雷」とベストプラクティス

このDynamicDPI対応を実務の業務システム(データベース連携やファイル入出力を行うツール)に組み込む際、以下の点に注意しなければ、別のバグを引き起こす。

1. データベース・設定ファイル保存時の「DPI汚染」に注意

ウィンドウのサイズや位置を `Settings.settings` やローカルのSQLite/JSONに保存している場合、「高DPI環境で拡大されたピクセル座標」がそのまま保存される事故が多発する。
これを別の低DPI(標準96DPI)のモニターで起動すると、画面の外にウィンドウが吹っ飛ぶか、異常な巨大サイズで起動する。

  • 対策: ウィンドウ位置・サイズを永続化する際は、必ず `DeviceDpi` で割った「論理座標(96DPI基準)」に正規化して保存し、読み込み時に現在の `DeviceDpi` で掛け合わせて復元すること。

2. 画像(Icons / Images)のベクター化またはマルチリソース化

ボタンやピクチャーボックスに埋め込んだPNG/JPEGアイコンは、DPIが上がると拡大されて盛大にボケる(滲む)。

  • 対策: 可能であれば `.ico` ファイルに複数解像度(16, 24, 32, 48, 64, 128, 256px)を同梱するか、Visual Studioの `ImageList` ではなく、SVGをレンダリングするサードパーティ製ライブラリ、あるいはHigh-DPI対応のベクター描画を導入すべきだ。

3. レイアウトコントロールの選定

`Absolute Layout`(座標を直打ちするレイアウト)でこれをやろうとすると破滅する。
必ず `TableLayoutPanel` や `FlowLayoutPanel` を駆使し、コントロール間の相対的な位置関係をレイアウトエンジンに委譲する設計を徹底すること。前述のコードは、そのオートレイアウトの「基準となるフォントとコンテナサイズ」を正しくリフレッシュするためのトリガーとして機能する。

—

アーキテクトからの提言

「動的DPI対応なんて、OSが勝手にやってくれるだろう」という楽観論は、ビジネスの現場では通用しない。顧客が使うデュアルモニター環境で、アプリを移動させた瞬間にUIが崩壊するソフトウェアは、どれほど業務ロジックが優れていても「品質の低いプロダクト」の烙印を押される。

今回紹介した `WM_DPICHANGED` のフックと、フォント・レイアウトの動的再構築ロジックを基底クラスとしてプロジェクトに組み込んでおけば、どんな高解像度モニター環境の移動であっても、常にシャープで美しいUIを維持し続けることが可能だ。

プロフェッショナルたるもの、細部の描画クオリティにこそ妥協してはならない。今すぐ既存のフォームをこの設計に置き換え、ワンランク上の堅牢なWinFormsアプリケーションを完成させてほしい。

タイトルとURLをコピーしました