【.NET Framework】PresantationCore.dllを読み込むとウィンドウが縮小する問題

.NET FrameworkのWindowsフォームアプリケーションで、ウィンドウの大きさを変えるようなコードを書いていないのにもかかわらず、突然小さくなってしまった経験はないでしょうか？
それはもしかすると、<INS>PresentationCore.dllの読み込み</INS>が原因になっているかもしれません。その場合、<INS>dpiAwareの設定を追加する</INS>ことで解決できます。

この記事では、[発生条件](#発生条件)、[再現コード](#再現コード)、[修正方法](#修正方法)、[原因](#原因)を説明します。

直し方だけ知りたい方は、[**修正方法**](#修正方法)をお読みください。

# 縮小したときの見た目
以下のフォームは、ボタンを押すと不具合が発生するようになっています。
**縮小前**
![押下前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3986818/46fd367d-aa61-1f79-1a54-a2200617ad1b.png)

**縮小後**
![押下後.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3986818/bbc5523d-7230-71e5-ae29-b347f5ca28ec.png)

# 発生条件
![qiita1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3986818/fc886abf-ab20-984e-83fa-18dee807ed36.png)

（Windows 11の場合）
1. システム > ディスプレイの「拡大/縮小」を**100%以外**に設定する
2. PresentationCore.dllで定義されているクラスや構造体を使用する（例：`System.Windows.DataFormat`）

# 再現コード
再現コードでは、ボタンが配置されたフォームを生成します。ボタンを押すとウィンドウが縮小します。

```C#
using System;
using System.Windows.Forms;

namespace ShrinkFormExample
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 前提：PresentationCore.dllを参照に追加すること。
            new System.Windows.DataFormat("test", 0);
        }
    }
}
```

※コンパイルが面倒な場合は、PowerShellに以下のコードをペーストして実行してください。
```powershell
Add-Type -Assembly System.Windows.Forms

# フォーム生成
$form = New-Object System.Windows.Forms.Form
$form.Size = New-Object System.Drawing.Size(640, 480)

# ボタン配置
$button = New-Object System.Windows.Forms.Button
$button.Text = "クリックで縮小"
$button.Location = New-Object System.Drawing.Point(100, 100)
$button.size = New-Object System.Drawing.Size(200, 100)
$form.Controls.Add($button)
$button.Add_Click({
    # ボタンを押した場合の処理
    # 以下のコードが実行されると、ウィンドウが縮小する。
    Add-Type -AssemblyName PresentationCore
    [System.Windows.DataFormat]::new("test", 0)
})
$form.ShowDialog()
```

# 修正方法
「app.manifestを追加する」か「SetProcessDpiAwareness(0)を実行する」ことで修正できます。

## app.manifestの追加
「追加」>「新しい項目の追加」>「全般」にある「アプリケーションマニフェストファイル」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3986818/8ceb667d-2ecd-2edd-799d-0481f043d973.png)

その後、以下のコードを追加します。

```xml
<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
    </windowsSettings>
</application>
```


## SetProcessDpiAwarenessの実行
app.manifestが用意できない場合、Windows APIの`SetProcessDpiAwareness`関数をコンストラクタ等から呼び出すことで縮小の影響を無くせます。
```c#
[DllImport("Shcore.dll", EntryPoint = "SetProcessDpiAwareness")]
private static extern int SetProcessDpiAwareness(int value);

public Form()
{
    SetProcessDpiAwareness(0);
}
```

# 原因
PresentationCore.dllで定義されるクラスや構造体にアクセスするとModuleInitializerが実行されますが、その中の処理に問題があります。

GitHubにあがっている実装を読むと、[ModuleInitializer.csの49行目](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationCore/ModuleInitializer.cs#L49)に原因があります。
`SetProcessDPIAware_Internal`というメソッドが実行されるコードになっていますが、実体はWindows APIの`SetProcessDPIAware`をDllImportしたものになっており、この関数が実行されるとウィンドウの縮小が発生します。
この挙動は.NET Frameworkだけでなく、ネイティブコードでも発生します。以下は、C++で再現したコードです。
```c++
// ShrinkFormExampleInWin32.cpp : Defines the entry point for the application.
//

#include "framework.h"
#include "ShrinkFormExampleInWin32.h"

#define MAX_LOADSTRING 100
constexpr int Button_ID = 1;

// Global Variables:
HINSTANCE hInst;                                // current instance
WCHAR szTitle[MAX_LOADSTRING];                  // The title bar text
WCHAR szWindowClass[MAX_LOADSTRING];            // the main window class name

// Forward declarations of functions included in this code module:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                      _In_opt_ HINSTANCE hPrevInstance,
                      _In_ LPWSTR    lpCmdLine,
                      _In_ int       nCmdShow) {
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // TODO: Place code here.

    // Initialize global strings
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_SHRINKFORMEXAMPLEINWIN32, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    // Perform application initialization:
    if (!InitInstance(hInstance, nCmdShow)) {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_SHRINKFORMEXAMPLEINWIN32));

    MSG msg;

    // Main message loop:
    while (GetMessage(&msg, nullptr, 0, 0)) {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    return (int)msg.wParam;
}

//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
ATOM MyRegisterClass(HINSTANCE hInstance) {
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SHRINKFORMEXAMPLEINWIN32));
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_SHRINKFORMEXAMPLEINWIN32);
    wcex.lpszClassName = szWindowClass;
    wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

//
//   FUNCTION: InitInstance(HINSTANCE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) {
    hInst = hInstance; // Store instance handle in our global variable

    HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
                              CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

    if (!hWnd) {
        return FALSE;
    }

    CreateWindow(
        L"BUTTON",
        L"クリックで縮小",
        WS_CHILD | WS_VISIBLE,
        100, 50, 100, 30,
        hWnd,
        (HMENU)Button_ID,
        hInstance,
        NULL
    );

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

//
//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  PURPOSE: Processes messages for the main window.
//
//  WM_COMMAND  - process the application menu
//  WM_PAINT    - Paint the main window
//  WM_DESTROY  - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
        case WM_COMMAND:
        {
            if (LOWORD(wParam) == Button_ID) {
                // ボタンをクリックしたときの処理
                SetProcessDPIAware();
            } else {
                int wmId = LOWORD(wParam);
                // Parse the menu selections:
                switch (wmId) {
                    case IDM_ABOUT:
                        DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                        break;
                    case IDM_EXIT:
                        DestroyWindow(hWnd);
                        break;
                    default:
                        return DefWindowProc(hWnd, message, wParam, lParam);
                }
            }
        }
        break;
        case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &ps);
            // TODO: Add any drawing code that uses hdc here...
            EndPaint(hWnd, &ps);
        }
        break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
    UNREFERENCED_PARAMETER(lParam);
    switch (message) {
        case WM_INITDIALOG:
            return (INT_PTR)TRUE;
        case WM_COMMAND:
            if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
                EndDialog(hDlg, LOWORD(wParam));
                return (INT_PTR)TRUE;
            }
            break;
    }
    return (INT_PTR)FALSE;
}
```

## SetProcessDPIAwareとは
DPIスケールの設定を変更する関数です。DPIスケールとは、ディスプレイの解像度に応じてウィンドウやそのパーツを拡大縮小する機能です。これが行われると、文字がぼやけて見える場合があります。大雑把に書けば、この関数はその拡大・縮小を無効化するためのものです（つまり、今回のケースならぼやけないようにウィンドウの大きさを小さくします）。
こういった挙動をするため、アプリケーションが起動した直後に使われるべきですが、ユーザが操作している間に実行されてしまうことで変な挙動をしているように見えてしまいます。

# おまけ
## どういうケースで問題につながるのか？
本来、PresentationCore.dllはWPFアプリケーションで使われているライブラリで、新しくプロジェクトを作成すると必ず参照されています。（おそらく）WPF以外で使われることを想定して作られていないため、設定が不足すると意図しないタイミングでウィンドウが縮小します。

なので、そもそも「PresentationCore.dllをWindowsフォームアプリケーションで使わなければよい」と考えることができます。
ところが、場合によっては外部のライブラリがPresentationCore.dllを利用している場合があり、意図しないところで発生する可能性があります。

### ケース：CommonOpenFileDialogクラス（Microsoft.WindowsAPICodePack）
https://teratail.com/questions/8jfnzzh4zuc329

このteratailの質問では、WindowsAPICodePackに実装されている`CommonOpenFileDialog`クラスの`ShowDialog`メソッドを利用した結果、問題に遭遇しています。

このケースでは、`CommonOpenFileDialog.ShowDialog`メソッド内で実行される`CommonFileDialog.ApplyNativeSettings`メソッドに原因があります。[ApplyNativeSettingsメソッドの686行目](https://github.com/jlnewton87/Programming/blob/master/C%23/Windows%20API%20Code%20Pack%201.1/source/WindowsAPICodePack/Shell/CommonFileDialogs/CommonFileDialog.cs#L686)の処理が行われると、`System.Windows.Application.Current`に対してNullチェックが行われます。`Application`クラスはPresentationCore.dllで定義されているため、そのクラスが判定された時点でModuleInitializerが実行され、問題につながります。

この問題は、上記で挙げた解決策か、`System.Windows.Forms.OpenFileDialog`クラスを代わりに利用することで解決できます。

## 動作を再現したコードについて
自分のGitHubにプロジェクトファイルをアップロードしたので、良ければ参考にしてください。プロジェクトには以下の内容が含まれています。
* 縮小が発生するフォームアプリケーション
* 縮小の発生を修正したフォームアプリケーション
* Windows APIを使った縮小例アプリ
* WindowsAPICodePackを使って縮小させた例

https://github.com/Osakana16/ShrinkFormExample

# 参考ページ
https://stackoverflow.com/questions/42975285/commonopenfiledialog-cause-windows-form-to-shrink

https://teratail.com/questions/8jfnzzh4zuc329

私が今回の問題に当たった時、teratailとStack Overflowを参考に解決しました。ライブラリ内のどの部分が原因かはググっても見つからなかったので、独自に調査したうえで解決方法と一緒にこの記事にをまとめさせていただきました。