Windowsアプリをマルチモニターの可変DPI対応にした時のメモ

高DPIモニター環境で、古いWindowsアプリがボケて表示される問題、自作のWin32アプリ(Python & C++)でどのように対応したかを記録します。

複数モニター環境で、かつモニターによってDPIが異なる場合に特にややこしい処理が必要になります。

---

##### Step 1. DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2

まず、アプリの起動後の初期化処理の中で、SetProcessDpiAwarenessContext() を呼び出し、DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 を設定します。

``` c++
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
```

DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 では、非クライアント領域(Menuとかタイトルバーとか)のスケーリングは自動的に行われるので、EnableNonClientDpiScaling()を呼ぶ必要はありません。

Manifestの EnableDpiAwareness を PerMonitorHighDPIAware に設定する方法だと、V2が選択できないように見えたので、API呼び出しで設定。(プラットフォームバージョンなどによるかも)

ただし、DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 は Windows 10 の Creators Update 以降でないと使えないようです。

https://docs.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context:
> Per Monitor v2 was made available in the Creators Update of Windows 10, and is not available on earlier versions of the operating system.

私のアプリは、ほとんど自分専用みたいなものなので、Windows 10 の Creators Update以前のバージョンは考えないことにします。

##### Step 2. WM_DPICHANGED

WM_DPICHANGED メッセージを受け取り、DPIを取得。DPIの整数値を使っても良いし、私の場合は USER_DEFAULT_SCREEN_DPI との比を使ってレイアウト処理をしました。

``` c++
    case WM_DPICHANGED:
        {
            int dpi = HIWORD(wp);
            float scale = (float)dpi / USER_DEFAULT_SCREEN_DPI;
            
            // ここにDPI変更に反応する処理を書く予定
        }
        break;
```

#### Step 3. 再レイアウト処理

(都合上ここからいきなりPythonになります。)

WM_DPICHANGEDで受け取ったDPI値を使って、適切なフォントサイズを計算し、フォントオブジェクトを再作成します。

``` python
font_size = round( font_size * scale )
self.setFont( font_name, font_size )
```

また、ウインドウのサイズや位置を調整します。

``` python
window_rect = self.getWindowRect()
self.setPosSize( (window_rect[0] + window_rect[2]) // 2, window_rect[1], original_width, original_height, ORIGIN_X_CENTER | ORIGIN_Y_TOP )
```

ウインドウサイズの変更の時注意したいのは、ウインドウの上端の中央を基準にレイアウトするということです。左端を基準にすると、マルチモニター環境でDPIの切り替えが繰り返し発生してしまうケースがあります。垂直方向については上端で構いません。どうやらWindowsがモニターを跨ぐ境界をよろしく調整してくれるようです。


#### Step 4. 起動直後も忘れずにフォントサイズを調整する

WM_DPICHANGED は、DPIが変更されたときに来るメッセージですが、アプリ起動時は自分でDPIを取得して、最初に使うフォントサイズを決定する必要があります。

```c++
struct _FindMonitorFromPositionContext
{
    _FindMonitorFromPositionContext()
        :
        x(0),
        y(0),
        monitor(0)
    {
    }

    int x;
    int y;
    HMONITOR monitor;
};

static BOOL CALLBACK _FindMonitorFromPosition(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData)
{
    _FindMonitorFromPositionContext * context = (_FindMonitorFromPositionContext*)dwData;

    MONITORINFO mi;
    mi.cbSize = sizeof(MONITORINFO);
    GetMonitorInfo(hMonitor, &mi);

    if(context->monitor == NULL && mi.dwFlags & MONITORINFOF_PRIMARY)
    {
        context->monitor = hMonitor;
    }

    if (context->x >= mi.rcMonitor.left &&
        context->y >= mi.rcMonitor.top &&
        context->x < mi.rcMonitor.right &&
        context->y < mi.rcMonitor.bottom)
    {
        context->monitor = hMonitor;
        return FALSE;
    }

    return TRUE;
}

int Window::getDpiFromPosition(int x, int y)
{
    FUNC_TRACE;

    _FindMonitorFromPositionContext context;
    context.x = x;
    context.y = y;

    EnumDisplayMonitors(NULL, NULL, _FindMonitorFromPosition, (LPARAM)&context);

    UINT dpi_x, dpi_y;
    GetDpiForMonitor(context.monitor, MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y);

    return dpi_x;
}
```

ウインドウ作成予定の座標をもとに、EnumDisplayMonitors を使って所属予定のモニターを特定、そして GetDpiForMonitor を使ってそのモニターのDPIを取得します。

取得したDPIは、Step 2 と同様に、レイアウト計算に使用します。


#### テスト

手元で下記の手順で、アプリのフォントサイズやウインドウサイズなどが適切に切り替わるかを確認します。

1. 複数のモニターに別々のDPIを設定する (100% と 150% など)
2. 低解像度のモニター側でアプリを起動し、高解像度のモニター側にウインドウを移動させる。行ったり来たりさせる。
3. 高解像度のモニター側でアプリを起動し、低解像度のモニター側にウインドウを移動させる。行ったり来たりさせる。
4. 複数モニターの上下左右の位置関係を変更して、Step2,Step3をテスト。


#### まとめ

違ったDPIを持つ複数のモニターに対応した、Windowsアプリの可変DPI対応について紹介してみました。