先日、Albus Boxのベータ版を公開した直後に「高DPIだとウィンドウが一部しか描画されない」というご報告をいただきました。

SnapCrab_NoName_2022-1-8_17-23-44_No-00
再現してみると、なるほど確かに表示がおかしい。ウィンドウの一部が途切れて表示されてしまいます。

原因

Albus BoxはOpenSiv3D製で、OpenSiv3D自体はdpiの変化に対応しているので全く問題はないのですが、Windows版のAlbus Boxではウィンドウの形をWin32APIで弄っており、Win32APIに渡すウィンドウサイズを示す引数がdpiの変化に対応していないことが原因でした。

// x1 = 0   : ウィンドウ左上のx座標
// y1 = 0   : ウィンドウ左上のy座標
// x2 = 400 : ウィンドウ右下のx座標
// y2 = 640 : ウィンドウ右下のy座標

#include <Siv3D/Windows/Windows.hpp>	// Siv3Dでない場合はWindows.h

void setWindowStyle(int x1, int y1, int x2, int y2, int w, int h) {
  // ウィンドウハンドルを取得(Siv3D)
  auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());
  // 角丸長方形の生成
  auto hRegion = CreateRoundRectRgn(x1, y1, x2, y2, w, h); // ここが原因
  // 角丸長方形をウィンドウの形に適用
  SetWindowRgn(hWnd, hRegion, 1);
}

setWindowStyleがウィンドウの設定を弄るメソッドで、変数x2, y2はウィンドウの右下の座標、すなわちウィンドウサイズを示す値です。これらの値は96dpiの場合のウィンドウサイズを基準として設定しています。
CreateRoundRectRgn()メソッドは角丸長方形を生成するWin32API関数で、この角丸長方形とウィンドウハンドルhWndSetWindowRgn()に渡してやることで角丸長方形ウィンドウを実現しています。つまり、角丸長方形を生成する際に渡したウィンドウサイズが96dpi前提の値で渡しており、dpiの変化に対応していないことが原因です。

dpiとは

dpiとはDots Per Inch、すなわち1インチ(2.54cm)あたりのドット数のことで、1インチ内にどれだけのドットを表示できるかを示すものです。

dpiの違いは気付きにくいが気をつけるべき

Windowsでは96dpiが標準設定で、その標準設定のままで開発したため96dpiでしか動作テストをしていませんでした。家には1920×1080のフルHDモニタしかないので高DPI環境の存在に全く気付かなかったのですが、4Kモニタの場合はWindowsでは拡大率を150%(144dpi)に設定することが推奨されているようです。
近頃は4Kモニタが普及したことでdpi設定がモニタによってバラバラになりやすくなりつつあり、画面サイズによってもdpiの推奨値は異なります。デスクトップアプリ開発でもdpi設定の違いを気にする必要性が以前よりも高まったと言えると思います。

解決策

Win32APIでdpi値を取得する

GetDpiForWindow(hWnd)

Win32APIのGetDpiForWindow(hWnd)メソッドでシステムの現在のdpi値を取得できます。例えば、標準設定(拡大率100%)のままなら標準のdpi値96が取得できます。hWndにはウィンドウのウィンドウハンドルを引数として渡します。GetDpiForWindow(hWnd)を使うにはWinUser.hをincludeする必要があります。

dpi値の反映

前述の通り96dpiを標準として開発していたので、96dpiと同様に表示したい場合は画面サイズ * GetDpiForWindow(hWnd) / 96の値をCreateRoundRectRgn()メソッドに渡してやればOK。

#include <Siv3D/Windows/Windows.hpp>	// Siv3Dでない場合はWindows.h
#include <WinUser.h>

#define DPI_STANDARD	96

double getDpiDist(HWND hWnd) {
  return (double)GetDpiForWindow(hWnd) / DPI_STANDARD;
}

void setWindowStyle(int x1, int y1, int x2, int y2, int w, int h) {
  // ウィンドウハンドルを取得(Siv3D)
  auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());
  // dpi値/96を取得
  double dpi_dist = getDpiDist(hWnd);
  // 角丸長方形の生成
  auto hRegion = CreateRoundRectRgn(x1, y1, x2 * dpi_dist, y2 * dpi_dist, w, h);
  // 角丸長方形をウィンドウの形に適用
  SetWindowRgn(hWnd, hRegion, 1);
}

DPI_STANDARDは96dpiを示す定数で、GetDpiForWindow(hWnd)で取得した現在のdpi値をDPI_STANDARDで割り、ウィンドウサイズを示す値に掛けています。これにより、96dpiのときのウィンドウサイズがdpiが変化した場合でも反映されるようになります。

動作確認

同じモニタでも、Windowsの設定から拡大率を変化させればdpiが変化します。
SnapCrab_設定_2022-1-8_17-56-45_No-00
例えば150%に設定すると、96dpiのときが100%ですから、 96×1.5=144dpiになります。
この状態で実行させると、
SnapCrab_NoName_2022-1-8_17-57-32_No-00
このように、dpiが変化しても正常に表示できるようになったことが確認できます。