.NET 8 WinForms C# での高DPI/PerMonitorV2 対応(速報)

2023/12/16

正直なところこの方面には全く疎いので、大いに割り引いてご覧ください。
C# に初めて手を出し、わからないなりに試行錯誤して、とりあえずのところまでのレポートです。
「いまさら WinForms でやるの??」という議論は大ありなのですが、まあそれは横に置いておいて。
.NET 8 ではそのままでも高DPI/PerMonitorV2 対応は十分実用的なレベルになっているようですが、やはり細かいところで困る場合もあり、気づきを記録しておきたいと思います。ただし、あくまで、これを書いている現在(2023/12/16、Microsoft Visual Studio Community 2022 Version 17.8.3, Windows 11)の情報です。
なお、前回の .NET 7 での試行のレポートはこちら 。 WinForms の挙動は .NET 7 と .NET 8 とでほとんど変わっていないようで、基本事項は前回と同じものが使いまわせました。 その前の .NET 6 での試行のレポートはこちら 。.NET 6 ではいろいろと不具合があって困ったものでした。.NET 6 にあったあからさまな WinForms の不具合は、.NET 7 / .NET 8 ではほとんど解消されているようです。

参考文献は .NET 7 での試行のレポート の本文引用をご覧ください。


まずは結論

フォームの DpiChanged イベントで、フォーム上のすべてのコントロールに対して、フォントサイズを、単精度浮動小数点の丸めを考慮しつつ再計算して、そのフォントサイズ値を持つ新しいフォントを生成して適用する。(.NET 7 での試行のレポート と同じ)(フォーム自身に対しては、行わないのが無難らしい)
プライマリ ディスプレイの拡大率が 100% ではない場合に備えて、フォームの Shown イベントでも、上記 DpiChanged イベントと同じことを行う。

結論だけ知りたければ、この先は読む必要はありません。

参考:Windows フォームのイベントの順序(Microsoft Learn)


検証

準備作業:新規プロジェクトの生成

前回の .NET 7 での試行のレポート のページの最後のスクショ集も参照のこと。
「Windows フォームアプリ .NET Windows フォーム (WinForms) アプリを…」を選択。プラットフォームで .NET 8 を選ぶ。
最初に表示すべきフォームのプロパティで AutoScaleMode を DPI モードにする。
csproj ファイルにおいて PropertyGroup セクション内のすでにある内容のあと(PropertyGroup 終了タグの前)に <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>追記する 。 .NET 8 ではこれで PerMonitorV2 の動作になる。

Visual Studio 統合開発環境 18.3 において PerMonitorV2 指定をするとフォームデザイナーが頻繁にバグるのを回避するための設定が追加されたとのことです。 https://github.com/dotnet/winforms/blob/main/docs/designer/designer-high-dpi-mode.md

コントロール上のフォントサイズの問題(プライマリ ディスプレイの拡大率が 100% ではない場合を含む)

.NET 8 WinForms の ApplicationHighDpiMode PerMonitorV2 は、 .NET 7 での試行のレポート のそれとほとんど同じようで、同じ対策が有効でした。

さらに、プライマリ ディスプレイの拡大率が 100% ではない場合に、セカンダリ(または以降)の拡大率 100% のディスプレイから起動してその結果ウインドウがプライマリディスプレイに配置されるような条件下(=Visual Studio IDE からの実行)では、同じ計算ミス(7.2 が 7.2000003 になる)が起こりボタンの文字レンダリングサイズが大きめになってしまう不具合があるようです。 前回の .NET 7 での試行のレポート やその前の .NET 6 での試行のレポート ではこの部分の検証は端折っていました(.NET 6 での検証の時に「プライマリディスプレイが拡大表示(125% とか)になっている場合は、ここからさらに、ちょっとした意地悪問題になるようだ」と記した内容です)。

メインフォームの Shown イベントにおいて、そのフォーム上にあるすべてのコントロールのフォントを、正しいサイズのものに設定しなおします。DpiChanged イベントに関しては、.NET 7 での試行のレポート と同じです。

private void Form1_DpiChanged(object sender, DpiChangedEventArgs e)
{
    ListUpAllSubControls.fixAllFontSizeOfControls(this);
}
private void Form1_Shown(object sender, EventArgs e)
{
    ListUpAllSubControls.fixAllFontSizeOfControls(this);
}

この中で呼び出している ListUpAllSubControls.fixAllFontSizeOfControls() は基本的には .NET 7 での試行のレポート と同じなのですが、.NET 8 / C# 12 になって型検査がより厳格化されたようで、以下のように修正したものを使います。(修正点は OfType()OfType<Control>() に変更したことだけです。)

namespace WinFormsApp1
{
    internal class ListUpAllSubControls
    {
        // LINQ版
        // http://stackoverflow.com/questions/15186828/loop-through-all-controls-on-a-form-even-those-in-groupboxes
        public static List AllSubControls(Control control)
        {
            // var list = control.Controls.OfType().ToList(); // .NET 7
            var list = control.Controls.OfType<Control>().ToList(); // .NET 8
            var deep = list.Where(o => o.Controls.Count > 0).SelectMany(AllSubControls).ToList();
            list.AddRange(deep);
            return list;
        }

        public static void fixAllFontSizeOfControls(Control control)
        {
            // フォントサイズ修正
            // (デザインサイズが 9pt のとき PerMonitorV2 で 125% のディスプレイから起動したのち 100% に変更/移動すると
            // 7.2 となるべきものが 7.2000003 など微妙に大きな値になってそれが .NET 7 / .NET 8 WinForms ランタイムの
            // どこかで切り上げられてテキストがコントロールからはみ出すことがあるのを修正)
            var allctrls = AllSubControls(control);
            foreach (var ctrl in allctrls)
            {
                ctrl.Font = new Font(ctrl.Font.Name, (float)Math.Round(ctrl.Font.Size, 3, MidpointRounding.ToZero));
            }
        }
    }
}

これで、完全に、期待する結果が得られるようになりました。ここで述べたプライマリ ディスプレイの拡大率が 100% ではない場合の Shown イベントを用いた対策は .NET 7 でも有効なのかもしれないのですが、未検証です。

なお、メインフォーム自身に対しては、どうやらフォントサイズ再設定補正は行わないのが無難なようです。メインフォーム自身に対して補正を行おうとするとディスプレイをまたぐ瞬間の動作において副作用が生じるようで、メインフォーム自身のフォント再設定と、 ListUpAllSubControls.fixAllFontSizeOfControls() によるコントロールの再設定が二重に効いてしまい、ウインドウサイズまでおかしくなる場合があります。

また、Windows 11 でウインドウのスナップレイアウト機能が導入されましたが、このページで述べているフォントサイズ修正対策やその他のデバッグの都合上 DpiChanged やその他のイベントに MessageBox を入れると、関係するイベントの発動タイミングがスナップレイアウト機能と競合し、めちゃくちゃになって正しく機能しなくなる場合が頻繁にあるようです。したがって、この手のデバッグは Debug.Write() を使うか、フォーム上に適当なテキストボックスを用意して文字列を書き込むなどで行う必要があるようです。


メモ

以下、単なるメモ。私には全く理解できない内容なので、あしからず。
フォントやフォームサイズにおける .NET 7 との微妙な違いは、Mictosoft Learn / .NET / 破壊的変更 の .NET 8 / Windows フォーム に記載されている以下のようなことが影響しているのかもしれないと、勝手に思っています。


略歴


トップページ / 計算機関係の雑多なこと / .NET 8 WinForms C# での高DPI/PerMonitorV2 対応(速報)
このサイトについて・著者連絡先