4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Win32は死なズ #3 高DPIに対応させる

4
Last updated at Posted at 2026-10-08

最近のWindowsでWin32アプリを作るなら、高DPI対応はほぼ避けて通れません。

「Win32は死なズ」 第三回は高DPI対応をWin32で行う場合について書いてみようと思います。

この記事は厳密には、固定的な高DPI対応というより、モニターごとにDPIが変わる環境への対応(可変DPI対応)を扱います。

昔のWindowsでは、

CreateWindow(
    ...
    100, 100,
    200, 30,
    ...
);

のようにピクセル単位で座標やサイズを決めていても、大きな問題はありませんでした。

しかし現在は、Windowsの表示倍率として、

  • 100%
  • 125%
  • 150%
  • 200%

などが普通に使われます。

さらに、例えばノートPCと外部モニターで表示倍率が異なるといった環境も珍しくありません。

今回は、Win32アプリを高DPIに対応させる基本的な考え方と、前回作成した自作ToggleSwitchへの適用方法をまとめます。

高DPI対応しないとどうなるか

例えば、100%表示を前提に、

50 x 30

の自作コントロールを作ったとします。

150%表示の環境でもそのまま50×30pxで作成すると、物理的には小さく表示されます。

Windows側に拡大を任せる方法もありますが、その場合はアプリ全体がビットマップとして拡大され、文字や自前描画がぼやけることがあります。

そこで、

このアプリは自分でDPIに対応する

とWindowsに宣言し、現在のDPIに応じてサイズを計算します。

Per-Monitor DPI Aware V2にする

まずはアプリをDPI Awareにします。

今回は、ウィンドウを作成する前に、

SetProcessDpiAwarenessContext(
    DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
);

を呼び出します。

例えば、

int APIENTRY _tWinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR    lpCmdLine,
    int       nCmdShow)
{
    SetProcessDpiAwarenessContext(
        DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
    );
    
    // DialogBox()など
}

という形です。

重要なのは、ウィンドウを作成する前に設定することです。

PER_MONITOR_AWARE_V2 を指定すると、異なるDPIのモニター間でウィンドウを移動した場合にも対応しやすくなります。

なお、実運用ではマニフェストでDPI Awarenessを指定する方法もあります。

現在のDPIを取得する

ウィンドウの現在DPIは、

GetDpiForWindow()

で取得できます。

UINT dpi = GetDpiForWindow(hWnd);

Windowsでは 96 DPIが基準です。

Windowsでは、モニター上の96ピクセル(px)で1インチを表しています

表示倍率 DPI
100% 96
125% 120
150% 144
200% 192

そこで、96 DPIを基準に値を変換する関数を用意します。

static int DpiScale(HWND hWnd, int value)
{
    UINT dpi = GetDpiForWindow(hWnd);
    return MulDiv(value, dpi, 96);
}

MulDiv() によって、

value × dpi ÷ 96

を整数で計算できます。

例えば、

DpiScale(hWnd, 50)

なら、おおよそ、

100% -> 50
150% -> 75
200% -> 100

となります。

自作コントロールをDPI対応する

前回作成したToggleSwitchを例にします。

以前は、

CToggleSwitch::Create(
    hDlg,
    IDC_TOGGLE,
    100,
    100,
    50,
    30
);

のように作成していました。

この座標とサイズはピクセル単位なので、そのままでは表示倍率が変わってもサイズは変わりません。

そこで、

CToggleSwitch::Create(
    hDlg,
    IDC_TOGGLE,
    DpiScale(hDlg, 100),
    DpiScale(hDlg, 100),
    DpiScale(hDlg, 50),
    DpiScale(hDlg, 30)
);

とします。

150%表示なら、

x      100 -> 150
y      100 -> 150
width   50 -> 75
height  30 -> 45

となります。

これだけでも初期表示はかなり自然になります。

ToggleSwitch内部ではDPIを意識しなくてもよい

今回のToggleSwitchでは、描画時に、

RECT rc;
GetClientRect(hWnd, &rc);
int width  = rc.right - rc.left;
int height = rc.bottom - rc.top;

のように現在のクライアントサイズを取得しています。

さらに、

int diameter = height - margin * 2;

のように、コントロール自身の大きさを基準に図形を描画しています。

そのため、外側のコントロールサイズが、

50 x 30

から、

75 x 45

へ変われば、内部の描画も自然に大きくなります。

つまり、ToggleSwitch内部でさらに、

GetDpiForWindow()

を呼び出して各座標を拡大する必要はありません。

むしろ両方で拡大すると、二重スケーリングになる可能性があります。

今回の構成では、

親ウィンドウ
    ↓
DPIに応じて位置とサイズを決定
    ↓
ToggleSwitch
    ↓
与えられた領域の中で描画

という役割分担にしています。

この方が、自作コントロール側をシンプルに保てます。

DPIの異なるモニターへ移動した場合

Per-Monitor DPI対応で重要なのが、モニター移動です。

例えば、

100%のモニター
↓
150%のモニター

へウィンドウを移動すると、ウィンドウのDPIが変わります。

このとき送られてくるのが、

WM_DPICHANGED

です。

基本的な処理は次のようになります。

case WM_DPICHANGED:
{
    RECT* prcNewWindow =
        reinterpret_cast<RECT*>(lParam);
    SetWindowPos(
        hWnd,
        nullptr,
        prcNewWindow->left,
        prcNewWindow->top,
        prcNewWindow->right - prcNewWindow->left,
        prcNewWindow->bottom - prcNewWindow->top,
        SWP_NOZORDER | SWP_NOACTIVATE
    );
    return 0;
}

lParam には、新しいDPIに適した推奨ウィンドウ矩形が渡されます。

この矩形を使ってウィンドウの位置とサイズを変更します。

子コントロールも再配置する

親ウィンドウだけ変更しても、自作コントロールの位置やサイズまでは自動的に変わりません。

そこで、レイアウト処理を関数化しておきます。

static void LayoutControls(HWND hDlg)
{
    HWND hToggle = GetDlgItem(hDlg, IDC_TOGGLE);
    
    MoveWindow(hToggle,
        DpiScale(hDlg, 100),
        DpiScale(hDlg, 100),
        DpiScale(hDlg, 50),
        DpiScale(hDlg, 30),
        TRUE
    );
}

WM_DPICHANGED では、

case WM_DPICHANGED:
{
    RECT* prcNewWindow =
        reinterpret_cast<RECT*>(lParam);
    SetWindowPos(
        hDlg,
        nullptr,
        prcNewWindow->left,
        prcNewWindow->top,
        prcNewWindow->right - prcNewWindow->left,
        prcNewWindow->bottom - prcNewWindow->top,
        SWP_NOZORDER | SWP_NOACTIVATE
    );
    LayoutControls(hDlg);
    return 0;
}

とします。

これで、

DPI変更
  ↓
WM_DPICHANGED
  ↓
親ウィンドウを更新
  ↓
LayoutControls()
  ↓
新しいDPIで子コントロールを再配置

という流れになります。

ダイアログリソースは少し事情が違う

今回のサンプルではリソースダイアログを使用しています。

.rc には、

IDD_SAMPLEBOX DIALOGEX 0, 0, 317, 217

のように記述されています。

この、

317 x 217

はピクセルではありません。

これは DLU(Dialog Unit) です。

リソースダイアログでは、Windowsがフォント情報などを使ってDLUから実際のピクセルサイズへ変換します。

そのため、

DpiScale(hDlg, 317)

のようにダイアログサイズまで自分で再スケーリングする必要はありません。

むしろ、二重に拡大してしまう可能性があります。

標準コントロールと自作コントロールの違い

例えばリソースに、

LTEXT       "DPI Test",IDC_STATIC,20,20,80,12
PUSHBUTTON  "Button",IDC_BUTTON1,20,40,60,18
EDITTEXT    IDC_EDIT1,20,65,100,14,ES_AUTOHSCROLL

と配置した場合、これらもDLU基準で扱われます。

DPI Awareなアプリであれば、標準コントロールについてはWindows側がかなり処理してくれます。

一方、

CreateWindowEx(
    ...
    x,
    y,
    width,
    height,
    ...
);

で自分で作成したコントロールは、ピクセル指定です。

そのため、

DpiScale()

のような処理が必要になります。

整理すると、

リソースで配置した標準コントロール
        ↓
Windows側がある程度DPI対応
自分でCreateWindowしたコントロール
        ↓
自分でDPI対応

となります。

自前描画は基本的に自分で対応する

GDIやGDI+などで自分で描画する場合は、DPI対応も基本的に自分の責任になります。

例えば、

TextOut()
Rectangle()
Ellipse()
Gdiplus::Pen
Gdiplus::Font

などです。

特に、

int margin = 1;

や、

Gdiplus::Pen pen(color, 1.5f);

のような固定値は注意が必要です。

ただし、固定値だからといって何でも単純にDPI倍率を掛ければよいわけではありません。

線幅などは200%だから必ず2倍にした方がよいとは限らず、見た目とのバランスも重要です。

今回のToggleSwitchのように、

GetClientRect()

で取得した現在サイズを基準に描画していれば、外側のサイズ変更だけで多くの部分は自然に追従します。

文字描画も要注意

自前でフォントを作成している場合もDPI対応が必要です。

例えば、

CreateFont()

で固定ピクセルの高さを指定している場合、そのままではDPI変更に追従しません。

一例として、

int fontHeight =
    -MulDiv(
        11,
        GetDpiForWindow(hWnd),
        72
    );

のように、ポイントサイズと現在DPIからフォント高さを計算できます。

フォントは72pxで1ポイント(pt)を表します

標準コントロールではWindows側がある程度処理してくれますが、自前描画ではフォントサイズも自分で管理する必要があります。

まとめ

今回やったことは、次の通りです。

1. PER_MONITOR_AWARE_V2 にする
2. GetDpiForWindow() で現在DPIを取得する
3. MulDiv(value, dpi, 96) で座標やサイズを変換する
4. 自作コントロールをDPIに応じたサイズで作る
5. WM_DPICHANGED に対応する
6. DPI変更時に子コントロールを再配置する
7. 自前描画の固定値やフォントにも注意する

高DPI対応というと、すべての描画処理を大きく書き直す印象がありました。

しかし実際には、

96 DPI時の設計値
        ↓
現在DPIに変換

というルールを決めておくと、かなり整理しやすくなります。

特に今回のToggleSwitchでは、

  • 親側でDPIに応じたサイズを決める
  • ToggleSwitch側は与えられたサイズの中で描画する

という形にしたことで、コントロール内部を大きく変更せずに対応できました。

こうすると、Win32でも高DPI対応の考え方自体はそれほど複雑ではないと思います。

重要なのは、

Windowsに任せる部分と、自分で対応する部分を分けて考えること

だと思います。

Win32は死なズ

高DPI対応もうまく行くと楽しい

4
0
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?