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

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

画面がボケる、レイアウトが崩れる……その原因は「DPI」にある。WinFormsを極めるための動的スケーリング完全攻略ガイド

こんにちは。技術の深淵へようこそ。

あなたが一生懸命に作り上げたWindows Formsアプリケーション。自分のノートPCでは完璧に動いていた。しかし、いざクライアントの環境や、高解像度(4K)の外部モニターに繋いだ瞬間、「なんだか文字が滲んでボケている」「ボタンが重なって表示されている」「コントロールが画面外に吹き飛んでしまった」……そんな経験はありませんか?

もし心当たりがあるなら、おめでとうございます。あなたは今、「単なるプログラミング」から「真のソフトウェアエンジニアリング」へと足を踏み入れようとしています。

今日扱うテーマは、現代のマルチモニター環境において避けては通れない「DPI(Dots Per Inch)変更の動的検知」です。Windows 10や11の「スケーリング(拡大率)」の変化に、アプリケーションがどう立ち向かうべきか。その極限の知見を、基礎から丁寧に伝授しましょう。

1. なぜあなたのアプリは「ボケる」のか?

まず、敵の正体を知ることから始めましょう。

現代のディスプレイは、密度が非常に高いもの(高DPI)が増えています。Windowsは、小さな画面でも文字を読みやすくするために、「スケーリング(拡大率)」という機能を使って、画面上の要素を擬似的に大きく表示します。

ここで、アプリケーションには3つの「振る舞い(DPI Awareness)」があります。

1. DPI Unaware (無意識): Windowsがアプリの画面を「無理やり引き伸ばす」モード。これが「ボケる」最大の原因です。
2. System Aware (システム認識): Windowsの設定(例:150%)に従うが、モニターを移動しても設定が変わらない。
3. Per-Monitor V2 (モニターごと認識): 最強のモード。モニターごとに異なる拡大率を検知し、アプリが自ら「自分自身のサイズとフォント」を再計算します。

私たちが目指すのは、この「Per-Monitor V2」への完全対応です。

2. 第一の関門:アプリケーションの「宣言」

コードを書く前に、絶対に忘れてはならない儀式があります。それは、Windowsに対して「私は高解像度を扱う準備ができています!」と宣言することです。これを怠ると、どんなに素晴らしいコードを書いても、Windowsによって無理やり引き伸ばされ、ボケた表示になってしまいます。

実装方法:app.manifest の活用

プロジェクトに `app.manifest` ファイルを追加し、以下の記述を有効にします。

<!-- app.manifest の中身(抜粋) -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <!-- PerMonitorV2 を有効にする設定 -->
    <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </windowsSettings>
</application>

> [!IMPORTANT]
> アーキテクトの助言:
> プログラムのコード以前に、この「マニフェスト」の設定が全ての土台です。ここが抜けると、OSレベルで「低解像度アプリ」として扱われ、努力が水の泡となります。

3. 第二の関門:動的な再配置のロジック

マニフェストを設定すると、今度は新しい問題が発生します。モニターを移動した瞬間に、アプリのサイズやフォントが、新しいモニターの拡大率に合わなくなるのです。

ここで登場するのが、WinFormsの `OnDpiChanged` イベント(または `DpiChanged` イベント)です。

実用的な実装テンプレート

以下に、モニターの移動を検知して、コントロールのサイズやフォントを動的に再計算するための「黄金のテンプレート」を示します。

Imports System.Drawing

''' <summary>
''' DPI変更に完全対応した高度なフォームの基底クラス
''' </summary>
Public Class DpiAwareForm
    Inherits Form

    ' オリジナルの解像度(設計時の解像度)を保持しておく
    Private _designDpi As Single
    ' オリジナルのコントロールサイズを保持するための辞書(高度な設計用)
    ' 今回はシンプルにするため、Scaleメソッドの特性を利用した手法を解説します

    Public Sub New()
        MyBase.New()
        ' 設計時のDPIを取得(通常は96 DPI)
        _designDpi = Me.CreateGraphics().DpiX
    End Sub

    ''' <summary>
    ''' DPIが変更されたときにWindowsから呼び出されるメソッドをオーバーライド
    ''' </summary>
    Protected Overrides Sub OnDpiChanged(e As DpiChangedEventArgs)
        ' 1. まずは基本のスケール処理を実行
        ' Me.Scale() は現在のコントロールを現在のDPIに合わせて拡大縮小します
        MyBase.OnDpiChanged(e)

        ' 2. ログ出力(デバッグ用)
        Console.WriteLine($"DPIが変更されました: 旧={_designDpi}, 新={e.NewDpi}")

        ' 3. レイアウトの再計算
        ' ここで、特定のコントロールのフォントサイズや、
        ' 絶対座標で配置している要素の再計算を行います。
        ReLayoutControls(e.NewDpi)

        ' 4. デザイン時のDPIへリセットして計算し直す手法(累積誤差防止)
        ' ※重要:Scaleを何度も繰り返すと、計算誤差が蓄積して表示がズレます。
        ' 本来は「設計時のサイズ」を基準に計算し直すのがプロの技です。
    End Sub

    ''' <summary>
    ''' コントロールの再配置ロジック
    ''' </summary>
    ''' <param name="newDpi">新しいDPI値</param>
    Private Sub ReLayoutControls(newDpi As Single)
        ' ここに、独自の再配置ロジックを記述します。
        ' 例: 特定のカスタムコントロールの再描画や、フォントの微調整
        
        ' 例: 全てのコントロールに対して、新しいDPIに基づいたフォント適用
        ' (WinFormsの標準機能で多くはカバーされますが、カスタム描画を行う場合は必須)
        For Each ctrl As Control In Me.Controls
            ' カスタム描画を行っているコントロールがあれば、ここで通知を送る
            ' ctrl.Invalidate() などを呼び出して再描画を促す
        Next
    End Sub
End Class

4. 陥りやすい「3つの罠」と回避術

ここで、多くの初学者が(そしてベテランですら)陥ってしまう罠を解説しておきましょう。これを知っているだけで、あなたのコードの品質は劇的に上がります。

罠①:絶対座標による配置 (`Location` の固定)

`Button1.Location = New Point(100, 100)` のように、数値を直接指定していませんか?
高DPI環境では、100ピクセルが「非常に小さく」見えることがあります。

  • 解決策: `Anchor` プロパティや `Dock` プロパティを使いましょう。コントロールが親コンテナに対して「相対的」に配置されるように設計するのが、現代の鉄則です。

罠②:累積誤差の恐怖(Scaling Accumulation)

`Me.Scale()` を何度も呼び出すと、`100px -> 150px -> 225px…` と、計算結果がどんどんズレていきます。これは浮動小数点演算の誤差が蓄積するためです。

  • 解決策: 理想的には、「設計時のサイズ(基準値)」をメモリに保持しておき、常に `(設計時のサイズ) * (現在のDPI / 設計時のDPI)` で計算するようにしてください。これが「プロの計算」です。

罠③:フォントの「滲み」

テキストボックスやラベルのフォントが、拡大時にぼやけて見えることがあります。

  • 解決策: `PerMonitorV2` を有効にしていれば、Windowsがフォントのヒンティング(滑らかにする処理)を適切に行ってくれますが、自分で `Graphics.DrawString` を使って描画している場合は、必ず `e.NewDpi` に基づいたフォントオブジェクトを再生成してください。

まとめ:ここをクリアすれば、基本はバッチリですよ

いかがでしたか?
DPI対応は、単に「画面を大きくする」ことではありません。「ユーザーがどの環境にいても、常に意図した通りの美しさと使いやすさを提供する」という、エンジニアとしての誠実さの現れなのです。

1. `app.manifest` で `PerMonitorV2` を宣言する。
2. `OnDpiChanged` を使い、変化をキャッチする。
3. 絶対座標を避け、相対的なレイアウト(Anchor/Dock)を心がける。

この3点をマスターすれば、あなたはもう「マクロの記録」から脱却し、プロフェッショナルなデスクトップアプリケーション開発の入り口に立っています。

一歩ずつ、確実に。コードの向こう側にいるユーザーの快適な視界を想像しながら、実装を進めてみてください。応援していますよ!

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