😺

Windows ディスプレイ拡大率の反映と実DPIの取得

2022/07/23に公開

Windows で高解像度ディスプレイだと「設定」の「ディスプレイ」で 125% や 150% にすることも多く、特に何もしていないアプリは自動でその拡大がされますが、ピクセル単位で画像を扱いたいアプリでは困るので、拡大は自分で管理することになります。

ということでそのへんを調べた時の覚書。

manifest

vc++ でこの拡大を回避するには manifest を追加します。Windows10 1607 以降用ですが
※ 参考 プロセスの既定の DPI 認識を設定する

DpiAwarenessPerMonitorV2.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:application>
    <asmv3:windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

を .manifest としてセーブ(仮に DpiAwarenessPerMonitorV2.manifest)、
Visual Studio (2015-2022) の

「プロジェクト」 → 「xxxx プロパティページ」 → 「構成プロパティ」 → 「マニフェストツール」 → 「入出力」 → 「追加のマニフェストファイル」

にファイル名を追記します。

コマンドラインの場合は、

mt.exe -manifest DpiAwarenessPerMonitorV2.manifest -outputresource:(アプリ名).exe

のような感じでよいはず。

プログラム側で API を使っても設定できますが、OS 側が exe ファイルから知ることができないため、manifest で設定するのがよいようです。

「ディスプレイ」の拡大率の取得

上記 manifest を設定すると、OS 側が管理しているタイトルバー・メニューバー周辺以外の拡大がなくなるので、ディスプレイの拡大率を取得して描画のサイズ調整をすることになります。

「ディスプレイ」の拡大率を知るには、
※参考 DPI に関連する API およびレジストリ設定

    auto   hdc    = GetDC(nullptr);                 // カレントのスクリーン全体のデバイスコンテキスト取得.
    //auto x_dpi  = GetDeviceCaps(hdc, LOGPIXELSX);
    auto   dpi    = GetDeviceCaps(hdc, LOGPIXELSY);
    auto   rate   = dpi / 96.0;

のような感じに取得します。
縦横比はたいてい 1:1 だろうとして片方ですませています。
あるいは

| auto dpi = GetDpiForSystem();

でも出来るようです。(Windows10 1607以降)

これらの戻り値は DPI となっていますが本来の意味とは異り、「設定」→「ディスプレイ」で選んだ拡大率が

100%→96dpi 125%→120dpi 150%→144dpi 200%→192dpi

のように 96dpi を 100% として返って来るので、96 で割れば拡大率が求まります。
※ 一応 Windows ヘッダで USER_DEFAULT_SCREEN_DPI というラベルに 96 が設定されています。

拡大率変更の反映

「設定」→「ディスプレイ」の拡大率を変更すると、実行中のアプリには、WM_DPICHANGED が飛んできます。
このとき HIWORD(wParam) に新しい DPI が、lParam に (RECT*) ポインタで新しい Window サイズが入っているので、DPI 再描画&ウィンドウサイズを変更することになります。
※参考 メッセージのWM_DPICHANGED

case WM_DPICHANGED:
    {
        auto  dpi  = HIWORD(wParam);
        auto& r    = *(RECT const*)lParam;
        (…dpi に合わせて描画しなおす処理…)
        // 新たな Windows サイズに変更.
        SetWindowPos(hWnd, nullptr
                    , r.left, r.top, r.right - r.left, r.bottom - r.top
                    , SWP_NOZORDER | SWP_NOACTIVATE);
    }
    break;

※ DPI 再描画・SetWindowPos の順番やタイミングはアプリの作り次第。

実際の DPI の取得

ほぼ実寸で表示したい場合や、タップ操作(のアソビ)で指の物理的な移動量を知りたい場合など、実際の DPI や画面実寸がほしいことがあります。

ディスプレイのピクセル数およびディスプレイの物理的なサイズ(ミリメートル)は GetDeviceCaps で取得できるのでそれを用います。

    auto hdc      = GetDC(nullptr);                 // カレントのスクリーン全体のデバイスコンテキスト取得.
    auto actual_w = GetDeviceCaps(hdc, HORZSIZE);   // 横幅ミリメートル.
    auto actual_h = GetDeviceCaps(hdc, VERTSIZE);   // 縦幅ミリメートル.
    auto pixel_w  = GetDeviceCaps(hdc, HORZRES);    // GetSystemMetrics(SM_CXSCREEN);
    auto pixel_h  = GetDeviceCaps(hdc, VERTRES);    // GetSystemMetrics(SM_CYSCREEN);
    auto inch     = std::sqrt(actual_w * actual_w + actual_h * actual_h) / 25.4;
    auto dpi      = std::sqrt(pixel_w * pixel_w + pixel_h * pixel_h) / inch;

ここでは対角線で dpi を求めていますが、ディスプレイの縦横比をどう扱うかで対応は変わってくるでしょう。

※ 最初の manifest を設定していない場合は、拡大率が反映されたピクセル数が入ってきます。
例えば 3840 x 2160 ディスプレイで拡大率 150% の場合、1.5 で割った、2560 x 1440 が入っています。

ディスプレイ名が欲しかったりマルチディスプレイで複数の情報がほしかったりする場合は、

    struct DisplayInfo {   // 結果を入れる構造体の例.
        std::string    name;
        std::uint32_t  actual_w = 0;
        std::uint32_t  actual_h = 0;
        std::uint32_t  pixel_w  = 0;
        std::uint32_t  pixel_h  = 0;
        double         inch     = 0.0;
        double         dpi      = 0.0;
    };
    std::list<DisplayInfo>  displayInfoList;
    (--------)
    DISPLAY_DEVICEA device  = { sizeof(DISPLAY_DEVICEA) };
    DISPLAY_DEVICEA display = { sizeof(DISPLAY_DEVICEA) };
    for (int i = 0; EnumDisplayDevicesA(nullptr, i, &device, 0); ++i) {
        //if ((device.StateFlags & DISPLAY_DEVICE_ACTIVE) == 0) continue;   // カレント・ディスプレイだけでよいならコメント外す.
        EnumDisplayDevicesA(device.DeviceName, 0, &display, 0);
        if (display.DeviceString[0] == 0) continue;                         // ディスプレイ名の無いものは無視.
        auto hdc    = CreateDCA(device.DeviceName, device.DeviceName, nullptr, nullptr);
        displayInfoList.emplace_back();
        auto& r     = displayInfoList.back();
        r.name      = display.DeviceString;
        r.actual_w  = GetDeviceCaps(hdc, HORZSIZE);
        r.actual_h  = GetDeviceCaps(hdc, VERTSIZE);
        r.pixel_w   = GetDeviceCaps(hdc, HORZRES);
        r.pixel_h   = GetDeviceCaps(hdc, VERTRES);
        r.inch      = std::sqrt(r.actual_w * r.actual_w + r.actual_h * r.actual_h) / 25.4;
        r.dpi       = std::sqrt(r.pixel_w * r.pixel_w + r.pixel_h * r.pixel_h) / r.inch;
        DeleteDC(hdc);
    }

のように EnumDisplayDevices(A|W) を用いて取得することができます。

その他

Microsoft の高 DPI 向けの頁

等、Window10 1607 以前への対応とか、1アプリで Window別 に DPI スケーリング対応するとか、凝ったことやる場合は、ちゃんと調べたほうがよさそう。

Discussion