2023/04/01 (今日はエイプリルフールですが、ネタではありません。)
正直なところこの方面には全く疎いので、大いに割り引いてご覧ください。
C# に初めて手を出し、わからないなりに試行錯誤して、とりあえずのところまでのレポートです。
「いまさら WinForms でやるの??」という議論は大ありなのですが、まあそれは横に置いておいて。
.NET 7 ではそのままでも高DPI/PerMonitorV2 対応は十分実用的なレベルになっているようですが、やはり細かいところで困る場合もあり、気づきを記録しておきたいと思います。ただし、あくまで、これを書いている現在(2023/04/01、Microsoft Visual Studio Community 2022 Version 17.5.3)の情報です。
なお、過去の .NET 6 での試行のレポートはこちら 。.NET 6 ではいろいろと不具合があって困ったものでした。.NET 6 にあったあからさまな不具合は、.NET 7 ではほとんど解消されているようです。
[2023/12/16] 最新の .NET 8 での試行のレポートはこちら 。
参考文献は文中に明記しています。感謝いたします。
結論:フォームの DpiChanged イベントで、フォーム上のすべてのコントロール(フォーム自身を含む)に対して、フォントサイズを、単精度浮動小数点の丸めを考慮しつつ再計算して、そのフォントサイズ値を持つ新しいフォントを生成して適用する。
結論だけ知りたければ、この先は読む必要はありません。
このページの最後のスクショ集を参照。
「Windows フォームアプリ .NET Windows フォーム (WinForms) アプリを…」を選択。プラットフォームで .NET 7 を選ぶ。
最初に表示すべきフォームのプロパティで AutoScaleMode
を DPI モードにする。
csproj ファイルにおいて PropertyGroup セクション内のすでにある内容のあと(PropertyGroup 終了タグの前)に <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
を 追記する 。
.NET 7 ではこれで PerMonitorV2 の動作になる。
Visual Studio 統合開発環境では PerMonitorV2 指定をするとフォームデザイナーが頻繁にバグるので、フォームデザインするときは都度コメントアウト無効化するとよいようです。
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
をコメントアウトした状態でいったんビルドする。
.NET 7 WinForms の ApplicationHighDpiMode PerMonitorV2 で唯一目立つ問題点として、DPI 変更時のフォントサイズの再計算がうまくいっていないようです。DPI 変更のパターンによっては、ボタンなどコントロールの文字のサイズが期待する大きさよりも少し大きくなり、小さいボタンの文字が一部がはみ出して欠落することが、時々あります。
テストコードは以下のとおり。フォームデザイン関係はこのページの最後のスクショ集を参照してください。
namespace WinFormsApp1 { public partial class Form1 : Form { int dpi0; public Form1() { InitializeComponent(); dpi0 = this.DeviceDpi; } private void button1_Click(object sender, EventArgs e) { textBox1.Text = button1.Font.ToString() + "\r\n" + "dpi0=" + dpi0.ToString() + "\r\n" + "dpi_now=" + this.DeviceDpi.ToString(); } } } |
結果のスクリーンショットだけ貼っておきます。以下、Scale_P はプライマリディスプレイの表示スケール倍率とします。セカンダリディスプレイの表示スケール倍率は 100% 固定で、そこに Visual Studio 開発環境ウインドウを配置して作業しています。デバッグ実行ではウインドウはプライマリディスプレイで開始されます。
ボタンに表示されている「新規」の文字の大きさに注目してください。最もわかりやすいのは「規」の文字の旁「見」がレンダリングされたビットマップパターンが違っているところです。この例では辛うじて表示が保たれていますが、これはボタンが1個だけしかないためのようです。複数のボタンを設けていて、当該ボタンからフォーカスが外れると、表示されるのが「新」だけになってしまうなどレイアウトがダメになる場合があるようです(Yu Gothic UI, 9pt では button1.Size を 40, 22 にすると再現できます)。
次に、テキストボックスに表示させている情報に注目してください。7.2 が期待されるものが 7.2000003 になっています。
.NET WinForms のフォントサイズは単精度浮動小数点型です。浮動小数点数内部表現シミュレーター によると IEEE754 32bit float では 7.2 = 0x40e66666 = 7.199999809265137、7.2000003 = 0x40e66667 = 7.200000286102295 でこれは仮数部 1 違い、すなわち 1 ulp の違いです。
こんなことになってしまうのは、たぶん、コントロールのフォントをDPIに応じて何かしら計算する処理のうち単精度浮動小数点演算の途中のどこかで何らかの丸めが入っているか、そうでなければ部分的に1000倍した整数などでの疑似固定小数点演算が混ざっているか(フォームのドット数とかコントロールの Margin プロパティとかは「整数」しか有り得ないのでそういう残骸コードがある?)、あたりではないかと思います。また、タイミング的に、その途中過程で切り上げ丸めを入れた数値でフォントサイズを生成してしまっていて、その後の補正を忘れているとかも、有り得るかもしれません(7.2 と 7.2000003 とでレンダリング結果がここまで違うのは流石におかしい)。流石にそこまでの詳細は調べていません。
フォームの DpiChanged イベントにおいて、そのフォーム上にあるすべてのコントロール(フォーム自身を含む)のフォントを、正しいサイズのものに設定しなおします。とりあえず、その処理は独自に作成した ListUpAllSubControls.fixAllFontSizeOfControls(this)
というメソッドであると仮定し、その具体的な処理内容は次の段落で述べます。
private void Form1_DpiChanged(object sender, DpiChangedEventArgs e) { ListUpAllSubControls.fixAllFontSizeOfControls(this); } |
この処理はプログラム内のすべてのフォームで必要になるので、共通化のため、独立したソースファイルにしたものをプロジェクトに追加するのが良さそうです。それを ListUpAllSubControls
という静的メンバのみのクラスにしてみました。ただし C# に慣れていないので業界での標準的なコーディング流儀ではないかもしれませんがご了承ください(そもそも私は職業プログラマーではありません)。どなたかプロのプログラマーさんに、このページに代わるものを正しい流儀で広く公開していただけるとありがたいです。
「すべてのコントロールを列挙し、その一つ一つに対して何らかの処理を行う」ことについては、http://stackoverflow.com/questions/15186828/loop-through-all-controls-on-a-form-even-those-in-groupboxes が非常に参考になりました(感謝)。たくさんの実装例があるが正直理解しきれていません。最も簡単そうな実装をコピペしてしまいました。
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 |
ボタン文字列は少しくらい小さくしてでも全部表示してくれるほうが、GUI としてはありがたい、というスタンスです。Math.Round
で小数第二位~第三位くらいで丸めておくのがポイントのようです。
ちなみに Math.Round メソッドの「丸めと単精度浮動小数点値」 の解説に基づき、念のため Decimal 型へのキャストを行った例( MidpointRounding.ToZero
を併用せず)も試してみましたが、今回の 9pt を 7.2pt に縮小したい例の限りでは、キャストを行わなくても(暗黙の float → double 変換であっても)結果は変わらず問題ありませんでした。Decimal 型は 128bit でありそれへのキャストは結構重たい処理ではないかと思いますので、暗黙の float → double 変換で MidpointRounding.ToZero
を併用するのがいいのかなと思います。
MidpointRounding.ToZero
を省いてデフォルトの四捨五入で済ませても、問題は無いようですが、このあたりは、ありとあらゆるパターンを調べてはいませんので、限界はあると思います。
これで、ほぼ完全に、期待する結果が得られるようになりました。
WinFormsApp1.csproj ファイルは以下のとおり。前述のとおり <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
をフォームデザイン時はコメントアウト・いったんそれでビルドし、最終的に欲しい実行ファイルをビルドする時は有効にします。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net7.0-windows</TargetFramework> <Nullable>enable</Nullable> <UseWindowsForms>true</UseWindowsForms> <ImplicitUsings>enable</ImplicitUsings> <!-- --> <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> </PropertyGroup> </Project> |
トップページ /
計算機関係の雑多なこと /
.NET 7 WinForms C# での高DPI/PerMonitorV2 対応(仮)
このサイトについて・著者連絡先