Windows Formsアプリケーションの高DPI対応

# 高DPI対応
4K,8K等の高解像度ディスプレイや、画面の小さいタブレットPC等の普及に伴い、アプリの高DPI対応が必須な時代となってきました。
.NETのWindows Formsは、一応、解像度の変更にも対応しているのですが、一部、注意が必要な場合があります。

## 実験
とりあえず、普通に作ったアプリを高DPI環境に持っていくとどうなるのか見てみましょう。LabelとTextBoxを適当に並べ、更に、PictureBoxを配置した、簡単なアプリを作ってみます。（わざと雑に作ってあります）

```C#:Form1
private void Form1_Load( object sender, EventArgs e ) {
    Bitmap bmp = new Bitmap( pictureBox1.Width, pictureBox1.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb );

    using ( Graphics grph = Graphics.FromImage( bmp ) ) {
        // 塗りつぶす
        grph.Clear( Color.Black );

        // 四角い枠を描く
        grph.DrawRectangle( Pens.Aqua, 90, 50, 160, 50 );

        // 枠の中に文字を書く
        using ( Font font = new Font( "Arial", 20 ) ) {
            grph.DrawString( "ABCDEFG", font, Brushes.White, 100, 60 );
        }
    }

    pictureBox1.Image = bmp;
}
```

標準解像度での実行結果

![作ってみたアプリ](https://qiita-image-store.s3.amazonaws.com/0/105792/374ed90d-6c6e-7c1e-41db-99c6c8bae512.png)

これを、高DPI環境で実行するとどうなるでしょうか？

![実行してみた１](https://qiita-image-store.s3.amazonaws.com/0/105792/cf695e0c-7c9d-4f8a-49c7-d389231b1002.png)

**ボケます。**

DPI仮想化機能が働いて、高DPIに対応していないアプリをそのまま拡大して表示してくれます。

## 解決してみる
まず、高DPIに対応するには、アプリがDPI aware（DPI許容？）を宣言する必要があります。プロジェクトに以下のようなマニフェストファイルを追加します。

```XML:app.manifest
<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>
```

あるいは、P/Invokeを利用して、SetProcessDPIAwareを呼び出す方法もあります。この場合、app.manifestファイルは必要ありません。また、DPI awareを実行時に切り替えることができるようになります。

```C#:Program.cs
static void Main() {
    SetProcessDPIAware();
    // 後略
}

[System.Runtime.InteropServices.DllImport( "user32.dll" )]
private static extern bool SetProcessDPIAware();
```

それでは、これを高DPI環境で実行するとどうなるか見てみましょう。

![実行してみた２](https://qiita-image-store.s3.amazonaws.com/0/105792/6e3f62de-3555-b62e-2522-96ce021e2755.png)

**ズレます。**

文字はすっきり見やすくなりました。また、コントロールの位置やサイズはフレームワークのほうで自動的に調整してくれるので、特に問題は起きません。が、PictureBoxの中に描画したグラフィックは悲惨です。

## 何が起きたのか？
何が起きたのか、判りやすいように、最初の画像と重ね合わせて見てみましょう。

![重ね合わせてみる](https://qiita-image-store.s3.amazonaws.com/0/105792/18237b79-649a-d696-20b8-349537911812.png)

水色の枠の位置と大きさはそのままで、文字だけ大きくなっていることが判ります。

DrawRectangleで指定した座標やサイズは、あくまでピクセル単位（ワールド変換が可能なので語弊がありますが）です。これは基本的に、表示デバイスのピクセルと１：１となっていて、**DPIの設定には影響を受けません。**そのため、水色の枠は最初と同じ場所に同じサイズで描画されます。

それに対して、フォントのコンストラクタは、
`` public Font( string familyName,  float emSize )``
MSDNのドキュメントにはこう書かれています。
> emSizeType: System.Single
> The em-size, in points, of the new font.

そうです。よく使われるこのFontのコンストラクタの第２パラメータの単位は「ポイント」なのです。

ポイント(Point)は1/72インチのことで、これが表示デバイス上に実際に何ピクセルで描画されるかは、**DPIの設定の影響を受けます。**


## グラフィック描画の高DPI対応

問題の原因がおおよそわかったところで、対策を考えてみます。
文字の大きさは、自動的にDPIの設定に合わせて大きくなりました。これはＯＫです。問題は水色の枠のほうです。

Graphicsの解像度は``` grph.DpiX ```で取得できます。（DpiYもありますが通常同じなので割愛します）Windowsの標準の解像度は96DPIなので、実際に取得したDPI値に応じて拡大して描画することで、DPIの設定に対応することができます。

```C#
// 四角い枠を描く
float scale = grph.DpiX / 96f;
grph.DrawRectangle( Pens.Aqua, 90 * scale, 50 * scale, 160 * scale, 50 * scale );
```

しかし、描画するものが多い時は、すべての箇所にこのような修正を加えるのは面倒なので、**ワールド変換**を利用してもいいでしょう。

```C#
// 四角い枠を描く
float scale = grph.DpiX / 96f;
Matrix morg = grph.Transform;
grph.ScaleTransform( scale, scale );
grph.DrawRectangle( Pens.Aqua, 90, 50, 160, 50 );
grph.Transform = morg;
```

ただしこの場合、Penも拡大されて、**アンチエイリアスで線がボケる**ことがあるので注意です。線の太さを変えないためには、別途、scaleに反比例した太さのPenを作ります。

```C#
Pen pen = new Pen( Color.Aqua, 1 / scale );
```

また、DrawStringの**文字も拡大されてしまう**ので、同じくフォントのサイズを調整します。

```C#
Font font = new Font( "Arial", 20 / scale );
```

最終的なコードは以下の通り。

```C#:Form1
private void Form1_Load( object sender, EventArgs e ) {
    Bitmap bmp = new Bitmap( pictureBox1.Width, pictureBox1.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb );

    using ( Graphics grph = Graphics.FromImage( bmp ) ) {
        // 塗りつぶす
        grph.Clear( Color.Black );

        // DPIに合わせてワールド変換を設定
        float scale = grph.DpiX / 96f;
        Matrix morg = grph.Transform;
        grph.ScaleTransform( scale, scale );

        // 四角い枠を描く
        using ( Pen pen = new Pen( Color.Aqua, 1 / scale ) ) {
            grph.DrawRectangle( pen, 90, 50, 160, 50 );
        }

        // 枠の中に文字を書く
        using ( Font font = new Font( "Arial", 20 / scale ) ) {
            grph.DrawString( "ABCDEFG", font, Brushes.White, 100, 60 );
        }

        // ワールド変換を元に戻しておく
        grph.Transform = morg;
    }

    pictureBox1.Image = bmp;
}
```

これで、正確に高DPIに対応できました！

![対応完了](https://qiita-image-store.s3.amazonaws.com/0/105792/42620b4b-2f2d-a2e4-117b-4a51373996b0.png)

## もう一つの方法

Fontのコンストラクタにはもう一つ有用な形式があります。
`` public Font( string familyName,  float emSize, GraphicsUnit unit )``

ここで、第３パラメータにGraphicsUnit.Pixelを指定することで、フォントをポイント単位ではなく、DPIの設定に影響を受けない、ピクセル単位で作ることができます。

なおこの場合、ポイント単位のサイズからピクセル単位のサイズに変換する必要がありますが、Windowsの標準解像度は96DPIで、1ポイントは1/72インチなので、以下の計算式となります。

`` px = ( pt * 96f / 72f ) * ( DPI / 96f );``

あとは、前述の方法と組み合わせて描画してみてください。