デスクトップアプリの高DPI対応 #4 – WinFormsアプリ

前回の投稿でデスクトップアプリの中で.NET Frameworkを使ったWPFアプリの高DPI対応について説明しました。今回は .NET Frameworkを使ったWinFormsアプリの高DPI対応について考えます。

.NET Frameworkの高DPI対応

.NET Frameworkのアプリには

  • コンソールアプリ
  • WinFormsアプリ
  • WPFアプリ

の3種類あります。この中で高DPI対応が必要なアプリは、WinFormsアプリとWPFアプリです。

.NET FrameworkのUIフレームワーク

.NET Frameworkで標準で用意されているUIフレームワークは以下の二つがあります。

  • Window Forms (WinForms)
  • Windows Presentation Foundation (WPF)

Windows Forms (WinForms)は、.NET Frameworkの初期バージョンから用意されていたUIフレームワークです。すでに18年くらいの歴史があります。

Windows Presentation Foundation (WPF)は、.NET Framework 3.0 (2006年)で用意されたUIフレームワークです。リリース時期はWindows Vistaのリリースの時期と同じです。このUIフレームワークも14年くらいの歴史があります。

これらのUIフレームワークを利用したアプリを高DPI対応にするにはどうすればよいのでしょうか?

前回はWPFアプリの高DPI対応について説明しました。今回はWinFormsアプリの高DPI対応ついて説明します。

WinFormsのレイアウトの高DPI対応

WinFormsではレイアウトの座標指定は物理ピクセル(デバイス依存ピクセル: DDP, Device dependent pixels)を使用します。この物理ピクセルは、デバイスの物理ピクセルと1対1します。そのため、WinFormsは高DPI対応できていませんでした。

.NET Framework 1.0/1.1

この問題の対処として、.NET Framework 1.0/1.1では、システムのフォントサイズを基準とした自動スケーリング機能を使用できました。ただし、システムフォントサイズを基準としていることや、Form(ウィンドウ)クラスだけに実装されていることなどため、様々な問題を抱えていました。

.NET Framework 2.0

.NET Framework 2.0で、新しい自動スケーリング機能を導入しました。自動スケーリングをForm(ウィンドウ)クラスではなくCotainerControl(コントロール)クラスに実装することにより、すべてのコントロールで自動スケーリングのサポート利用できる。また、システムフォントサイズを基準とする従来の方法だけでなく、解像度(DPI)を基準とする新たな方法を追加しました。これにより、旧自動スケーリング機能にあった問題点を改善し、高DPI対応がされました。

DPI仮想化

WinFormsでは、自動スケーリング機能によりそれなりの高DPI対応ができており、Windows XPまでは、アプリ側の対応は特に必要ありませんでした。しかし、Windows Vistaでは、DPI仮想化が導入されたため、表示がにじむことがありました。そのため、Windows Vista以降での高DPI対応のためには、DPI仮想化が適用されないようにする必要があり、アプリ側での対応(高DPI対応であることの設定をすること)が必須となりました。

WinFormsの画像の高DPI対応

WinFormsでは高DPI対応において画像は指定された画像を拡大表示するのみです。UWPアプリのXAMLのように、ディスプレイスケール率に応じて使用する画像をUIフレームワーク側で自動的に切り替える処理はありません。

ディスプレイスケール率に合わせて使用する画像を切り替える場合は、アプリ起動(Loadイベント)と変更通知(DpiChanged / DpiChangedAfterParent / DpiChangedBeforeParentイベント)のタイミングで、アプリ側で適切なサイズの画像に切り替えます。

ただし、この変更通知は、.NET Framework 4.7で追加されたものです。もし、それ以前のバージョンで画像を切り替えを行いたい場合は、Windowsメッセージ(WM_DPICHANGED)で処理する必要があります。

WinFormsアプリの高DPI対応の具体的な方法

前述したように、WinFormsは、登場が古いので当初は高DPIに完全には対応していませんでした。後継バージョンでまずはSystem Monitor方式に対応しました。そして、2017年にリリースされた.NET Framework 4.7.0でついにPer Monitor V2方式の高DPIの対応が追加されました。

System Monitor/Per Monitor/Per Monitor V2のそれぞれの方式に対応する例を挙げます。しかし、これから実装・改変するWinFormsアプリは基本的にPer Monitor V2方式の設定をすればよいと思います。

System Monitor方式

.NET Framework 4.6以降であればWinFormsでSystem Monitor方式に対応済みです。 実際にはもっと前のバージョンから対応していましたが、高DPI対応に対するいろいろな調整が4.6ぐらいで完了した感じです。そのため、ここでは4.6以降としています。

.NET Framework 4.6.2以前を対象とする場合

System Monitor方式に対応していますが、標準では有効化されていません。有効化するためには、アプリケーションマニフェストに以下のような<dipAware>の宣言をします。<supportedOS>タグも忘れず設定します。

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True</dpiAware>
    </windowsSettings>
  </application>
  <compatibility>
    <application>
      <!-- Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
      <!-- Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
      <!-- Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
      <!-- Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
      <!-- Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
</assembly> 

また、App.configファイルに以下の設定を追加します。この設定によりNumericUpDownなど、コントロールにビットマップボタンを持つところなどの大きさの調整がされます。

<configuration>
  <appSettings>
    <add key="EnableWindowsFormsHighDpiAutoResizing" value="true" />
  </appSettings>
<configuration>

画像に関しては、ディスプレイスケール率に合わせて、アプリ側で最適なサイズの画像を設定します。具体的にはアプリ起動(Loadイベント)のタイミングで、最適なサイズの画像を設定します。 System Monitor方式の場合は、アプリの起動後にそのアプリに対するディスプレイスケール率が変更されることはありません。そのため、アプリ起動後のディスプレイスケール率の変更には対応する必要がありません。

.NET Framework 4.7.0以降のみを対象とする場合

System Monitor方式に対応していますが、標準では有効化されていません。有効化するためには、アプリケーションマニフェストに以下のように<supportedOS>タグを設定します。.NET Framework 4.7.0以降のWinFormsでは、<dpiAware><dpiAwareness>のタグは使用しません。

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
  <compatibility>
    <application>
      <!-- Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
      <!-- Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
      <!-- Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
      <!-- Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
      <!-- Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
</assembly> 

また、App.configファイルに以下の設定を追加します。

<configuration>
  <System.Windows.Forms.ApplicationConfigurationSection>
    <add key="DpiAwareness" value="System" />
  </System.Windows.Forms.ApplicationConfigurationSection>
<configuration>

画像に関しては、ディスプレイスケール率に合わせて、アプリ側で最適なサイズの画像を設定します。具体的にはアプリ起動(Loadイベント)のタイミングで、最適なサイズの画像を設定します。 System Monitor方式の場合は、アプリの起動後にそのアプリに対するディスプレイスケール率が変更されることはありません。そのため、アプリ起動後のディスプレイスケール率の変更には対応する必要がありません。

Per Monitor方式

WinFormsは、UIフレームワーク側の対応としては、この方式に正式には対応していません。強制的にPer Monitor方式の対応を宣言することができます。しかし、UIフレーム側のサポートはほとんどなく、必要に応じてアプリ側での対応が必要となります。

.NET Framework 4.6.2以前を対象とする場合

Per Monitor方式に対応していません。アプリケーションマニフェストファイルに設定して強制的にPer Monitor方式に設定することはできますが、UIフレームワーク側でのサポート機能はありません。

.NET Framework 4.7.0以降のみを対象とする場合

.NET Framework 4.7では、Per Monitor V2方式に対応しているため、Per Monitor方式でも動作します。しかし、標準では有効化されていません。有効化するためには、アプリケーションマニフェストに以下のように<supportedOS>タグを設定します。.NET Framework 4.7.0以降のWinFormsでは、<dpiAware><dpiAwareness>のタグは使用しません。

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
  <compatibility>
    <application>
      <!-- Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
      <!-- Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
      <!-- Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
      <!-- Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
      <!-- Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
</assembly> 

また、App.configファイルに以下の設定を追加します。

<configuration>
  <System.Windows.Forms.ApplicationConfigurationSection>
    <add key="DpiAwareness" value="PerMonitor" />
  </System.Windows.Forms.ApplicationConfigurationSection>
<configuration>

画像に関しては、ディスプレイスケール率に合わせて、アプリ側で最適なサイズの画像を設定します。具体的にはアプリ起動(Loadイベント)のタイミングとディスプレイスケール率の変更のタイミングで、最適なサイズの画像を設定します。

ディスプレイスケール率の変更を検出する方法はアプリを実行するWindows環境に依存します。アプリを実行するWindows環境がWindows 10 1703以降であれば、変更通知(DpiChanged / DpiChangedAfterParent / DpiChangedBeforeParentイベント)のタイミングを利用できます。アプリを実行するWindows環境がWindows 10 1607以前の場合はUIフレームワークによるサポートがないため、WM_DIPCHANGEDのメッセージを直接処理する必要があります。

Per Monitor V2方式

.NET Framework 4.6.2以前を対象とする場合

Per Monitor V2方式に対応していません。アプリケーションマニフェストファイルに設定して強制的にPer Monitor V2方式に設定することはできますが、UIフレームワーク側でのサポート機能はありません。

.NET Framework 4.7.0以降のみを対象とする場合

.NET Framework 4.7以降かつWindows 10 1703以降を利用する前提での対処方法です。実行環境がWindows 10 1607以前の場合は、Per Monitor方式もしくはSystem Monitor方式にフォールバックして動作します。

.NET Framework 4.7.0以降であればWinFormsでPer Monitor V2方式に対応済みです。しかし、標準では有効化されていません。有効化するためには、アプリケーションマニフェストに以下のように<supportedOS>タグを設定します。WinFormsの場合は、<dpiAware><dpiAwareness>のタグは使用しません。

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <compatibility>
    <application>
      <!-- Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
      <!-- Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
      <!-- Windows 8 -->
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
      <!-- Windows 8.1 -->
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
      <!-- Windows 10 -->
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility> 
</assembly> 

また、App.configファイルに以下の設定を追加します。

<configuration>
  <System.Windows.Forms.ApplicationConfigurationSection>
    <add key="DpiAwareness" value="PerMonitorV2" />
  </System.Windows.Forms.ApplicationConfigurationSection>
<configuration>

Program.csファイル(アプリのエントリーポイント)に以下のように、EnableVisualStyles()の呼び出しがあることを確認します。

static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form2());
}

画像に関してはディスプレイスケール率に合わせて、アプリ側で最適なサイズの画像を設定します。具体的にはアプリ起動(Loadイベント)とディスプレイスケール率の変更のタイミングで、最適なサイズの画像を設定します。

ディスプレイスケール率の変更を検出する方法はアプリを実行するWindows環境に依存します。アプリを実行するWindows環境がWindows 10 1703以降であれば、変更通知(DpiChanged / DpiChangedAfterParent / DpiChangedBeforeParentイベント)のタイミングを利用できます。アプリを実行するWindows環境がWindows 10 1607以前の場合はUIフレームワークによるサポートがないため、WM_DIPCHANGEDのメッセージを直接処理する必要があります。

WinFormsアプリの高DPI対応の動作確認

WinFormsアプリの高DPI対応の動作確認をするときには一つ注意点があります。アプリ側の高DPI対応が正しくできていても、動作確認のときに想定の動作をしないことがあります。アプリをVisual Studioから起動したときなど、デバッガでアタッチしているときに起きます。

この場合は、デバッガでアタッチせずに起動する(Visual Studioのメニューにある「デバッグ」→「デバッグなしで開始」)か、アプリを単体で起動することにより、本来の動作を確認することができます。Visual Studio上から作成したアプリを起動するときは、注意しましょう。

.NET Frameworkのソースコード

本投稿をするにあたって、マイクロソフトのドキュメントに明確に記載されていないことがいくつかありました。そのため、いくつかの情報は、.NET Frameworkのソースコードを参照して振る舞いを確認しました。ソースコードはこちらに公開されています。ソースコードが公開されているとこのような時に便利ですね。


今回はWinFormアプリの高DPI対応について説明しました。前回のWPFアプリの高DPI対応と合わせると、すべての.NET Frameworkアプリの高DPI対応の方法について説明できました。


2020年07月11日 投稿内容の更新 (XML名前空間の修正)

Visual Studioでアプリケーションマニフェストファイル(app.manifest)を追加したときに作成されるファイルがXML名前空間名(接頭辞:asmv3)を使わず、デフォルトXML名前空間のみを使った形式でした。

この投稿では、当初、以下のようなasmv3というXML名前空間名(接頭辞)を使用した記述でした。

xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"

しかし、Visual Studioで作成されるアプリケーションマニフェストファイルとの整合性を取るため投稿内でのManifestファイルの記述例をXML名前空間名(asmv3)を使用せず、デフォルト名前空間のみを使った形式に修正しました。

なお、XMLファイルの形式としては、どちらも間違っていないので、以前の記述でも問題なく動作します。

“デスクトップアプリの高DPI対応 #4 – WinFormsアプリ” への1件の返信

コメントを残す