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

スポンサーリンク
スポンサーリンク

高解像度時代の生存戦略:Windows FormsにおけるDPI変更動的検知と「ボケない」UI設計の極意

業務アプリケーションの開発において、最もエンジニアのプライドを傷つける瞬間はいつか?
それは、完璧に作り込んだはずのUIが、クライアントのマルチモニター環境(例えば、4KノートPCからフルHDの外部モニターへウィンドウを移動させた瞬間)で、「文字が滲み、コントロールが重なり、レイアウトが崩壊する」という事態に直面した時だ。

「OSの仕様だから仕方ない」? 「スケーリング設定を変えてもらえ」?
そんな言い訳は、プロのアーキテクトの辞書にはない。

本記事では、Windows 10/11のモダンなディスプレイ環境、特にPerMonitorV2に対応し、動的なDPI変更(スケーリング変更)を完全に掌握するための、VB.NETにおける極限の設計手法を伝授する。

1. なぜ、あなたのアプリケーションは「ボケる」のか

多くの開発者は、Windows Formsの `AutoScaleMode = Font` 設定だけで、DPI問題は解決すると誤解している。しかし、これは致命的な間違いだ。

根本的な原因:スケーリングの「静的」な解釈

従来のWinFormsは、アプリケーション起動時のDPIを基準にレイアウトを決定する「静的」な設計思想に基づいている。
しかし、現代のワークスタイルでは、以下の事象が頻発する:
1. モニター間移動: 150%スケーリングのノートPCから、100%のスケーリングのモニターへウィンドウをドラッグ。
2. 動的スケーリング: Windowsの設定画面でスケーリング比率を変更。

これが発生した際、アプリケーションが「現在のモニターのDPI」を再認識し、コントロールのサイズやフォントを再計算(Recalculate)しなければ、OSによる強制的なビットマップスケーリングが発生する。これが、あの忌々しい「ボケ(Blurriness)」の正体だ。

2. 第一の防衛線:アプリケーション・マニフェストの強制

コードを書く前に、OSに対して「このアプリは最新のDPI認識能力を持っている」と宣言しなければならない。これを怠れば、どれほど高度なコードを書いてもOSによって「古いアプリ」として扱われ、強制的に低解像度で描画される。

`app.manifest` をプロジェクトに追加し、以下の設定を確実に有効にせよ。特に `PerMonitorV2` は必須だ。

<!-- app.manifest -->
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <!-- PerMonitorV2 を指定することで、モニター移動時のDPI変更通知を受け取れるようになる -->
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>
</assembly>

3. 第二の防衛線:`DpiChanged` イベントによる動的再配置

マニフェストを設定しただけでは、レイアウトが「壊れない」だけで、「最適化」はされない。モニターを跨いだ瞬間に、コントロールのサイズやフォントを現在のDPIに合わせて再定義するロジックが必要だ。

ここで、個別のFormにコードを散らすのは三流の仕事だ。我々アーキテクトは、すべてのFormの基底となる `BaseForm` を設計し、そこにロジックをカプセル化する。

プロダクション・クオリティの基底クラス実装例

以下に、DPI変更を検知し、安全にコントロールを再スケールさせるための堅牢な実装を示す。

Imports System.Windows.Forms
Imports System.Drawing

''' <summary>
''' DPI変更に完全対応した、業務アプリケーション用基底フォームクラス
''' </summary>
Public Class DpiAwareBaseForm
    Inherits Form

    ' 前回のDPIを保持(差分計算用)
    Private _lastLogicalDpi As Integer = 96

    Protected Overrides Sub OnLoad(e As EventArgs)
        MyBase.OnLoad(e)
        ' 初期起動時のDPIを取得
        _lastLogicalDpi = Me.DeviceDpi
    End Sub

    ''' <summary>
    ''' DPIが変更された際にOSから呼び出されるイベント
    ''' </summary>
    Protected Overrides Sub OnDpiChanged(e As DpiChangedEventArgs)
        ' OSの標準的な処理を先に呼び出す
        MyBase.OnDpiChanged(e)

        ' ログ出力やデバッグ用(実運用では適切なロギングへ)
        Console.WriteLine($"DPI Change Detected: {_lastLogicalDpi} -> {e.NewDpi}")

        ' レイアウト崩れを防ぐための再スケール処理
        ApplyDpiScaling(e.NewDpi)

        _lastLogicalDpi = e.NewDpi
    End Sub

    ''' <summary>
    ''' コントロール群を新しいDPIに合わせて動的に再配置する
    ''' </summary>
    ''' <param name="newDpi">新しいDPI値</param>
    Private Sub ApplyDpiScaling(newDpi As Integer)
        ' 1. 無限ループを防ぐため、描画停止
        Me.SuspendLayout()

        Try
            ' 2. スケール比率の計算
            Dim scaleFactor As Single = CSng(newDpi) / _lastLogicalDpi

            ' 3. フォーム自体のスケーリング
            ' 注意: Scale() メソッドは内部的に複雑な挙動をするため、
            ' 複雑なレイアウトの場合は、各コントロールのプロパティを直接制御する設計が望ましい。
            Me.Scale(New SizeF(scaleFactor, scaleFactor))

            ' 4. フォントの再適用(重要:フォントのボケを防ぐ鍵)
            ' WinFormsのScale()はフォントを完璧に扱えない場合があるため、
            ' 必要に応じてフォントサイズを明示的に再計算する。
            ReapplyFonts(Me, scaleFactor)

        Catch ex As Exception
            ' 業務ツールとして、UIの崩れが致命的なエラーにならないようハンドリング
            Debug.WriteLine($"DPI Scaling Error: {ex.Message}")
        Finally
            ' 5. 再描画の再開
            Me.ResumeLayout()
        End Try
    End Sub

    ''' <summary>
    ''' 再帰的にコントロールを走査し、フォントサイズを調整する
    ''' </summary>
    Private Sub ReapplyFonts(parent As Control, scaleFactor As Single)
        For Each ctrl As Control In parent.Controls
            ' フォントサイズを新しいスケールに合わせて微調整
            ' ※既存のフォントスタイルを維持しつつサイズのみ変更
            Dim newSize As Single = ctrl.Font.Size * scaleFactor
            ctrl.Font = New Font(ctrl.Font.FontFamily, newSize, ctrl.Font.Style)

            ' 子コントロールがある場合は再帰的に処理
            If ctrl.HasChildren Then
                ReapplyFonts(ctrl, scaleFactor)
            End If
        Next
    End Sub
End Class

4. 設計における「極限の注意点」:陥りやすい罠

この実装を導入する際、以下の3点に留意せよ。これを知らなければ、あなたのコードは「動くが、壊れやすい」ものになる。

① `Scale()` メソッドの限界と `TableLayoutPanel` の併用

`Me.Scale()` は非常に便利だが、コントロールが複雑に重なり合っている場合、計算誤差が累積してレイアウトが「ズレる」ことがある。
プロの設計指針:

  • 絶対座標(`Location`)による配置を極力避け、`TableLayoutPanel` や `FlowLayoutPanel` を活用せよ。
  • これらのコンテナは、DPI変更時の相対的なサイズ計算を内部でより堅牢に行う。

② ファイル・データベース連携との分離

DPI変更イベントの中で、データベースの再読み込みやファイルの再スキャンを行うような設計は厳禁だ。
DPI変更は「表示(View)」の問題であり、「データ(Model)」の問題ではない。UIの再描画ロジックと、ビジネスロジックを完全に分離せよ。UIの更新が重すぎて、モニター移動時にアプリがフリーズするような事態は、設計の敗北である。

③ 画像リソースの解像度問題

テキストはフォント変更で解決できるが、`PictureBox` に表示するアイコンや画像は、DPIが変わると一気にボケる。

  • 解決策: 高解像度用の画像(@2x, @3x)を用意し、DPIに応じて `Image.Clone` や適切な解像度のリソースに差し替えるロジックを `ApplyDpiScaling` 内に組み込むべきだ。

5. 結論:堅牢なUIは「変化」を受け入れることから始まる

Windows Formsは、もはや「静的な描画エンジン」ではない。マルチモニター、高解像度、可変スケーリングという、動的な環境がデフォルトとなった現代において、アプリケーションは「常に変化し続ける外界に対して、自らの姿を適応させる能力」を持たなければならない。

今回紹介した `DpiAwareBaseForm` によるアプローチは、単なるテクニックではない。それは、ユーザーの作業環境を尊重し、どのようなデバイスにおいても最高のパフォーマンスを提供しようとする、エンジニアとしての「設計思想」そのものである。

さあ、今すぐプロジェクトの基底クラスを見直し、その「ボケ」を、完璧な「鮮明さ」へと書き換えてほしい。

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