【MFC】GetWindowRect vs GetClientRect:スクリーン座標とクライアント座標

【MFC】GetWindowRect vs GetClientRect:スクリーン座標とクライアント座標

ウィンドウの位置を取りたいのに GetClientRect を使っていたり、描画範囲を取りたいのに GetWindowRect を使っていたりすると、座標がずれたように見えます。この記事では、GetWindowRect と GetClientRect が何を返すのかを、移動・サイズ変更・座標変換の実測結果と一緒に確認します。

後半では ScreenToClient、ClientToScreen、MapWindowPoints まで含めて、どの座標をどこへ渡すときに何を使うかを切り分けます。


目次

使い分けの早見表

やりたいこと使う関数追加で使うもの注意点
画面上のどこにあるか知りたいGetWindowRect必要なら ScreenToClient / MapWindowPointsスクリーン座標で返る。枠やタイトルバーも含む
描画できる中身の幅と高さを知りたいGetClientRectなし左上は常に (0,0)right / bottom は幅と高さになる
スクリーン座標の点を自分のウィンドウ基準へ直したいScreenToClient対象ウィンドウの HWND非クライアント部分を変換すると負値になり得る
クライアント座標の点を画面上へ出したいClientToScreen対象ウィンドウの HWNDポップアップ表示位置やドラッグ開始位置で使う
親子・兄弟ウィンドウ間で矩形を渡したいMapWindowPointsRECT なら 2 点として渡す複数の座標系をまたぐときは手計算しない

迷ったときは、「画面全体を基準にしたいのか」「自分のクライアント領域を基準にしたいのか」で分けると判断しやすくなります。


検証環境

OSWindows 11
IDEVisual Studio 2026
ワークロードC++ によるデスクトップ開発
MFC / ATL有効
ビルド構成x64 / Debug
確認対象MFC ダイアログ自身の GetWindowRect / GetClientRect / ScreenToClient / ClientToScreen

数値そのものはテーマ、DPI、Windows の見た目設定で変わります。ただし、どの値が動いて、どの値が動かないかという関係は同じです。


GetWindowRect と GetClientRect の返り値の違い

まず押さえるべき点は、2 つの関数は同じ RECT を返していても、原点が違うことです。

観点GetWindowRectGetClientRect
座標系スクリーン座標クライアント座標
左上の意味画面上のウィンドウ外形の左上常にクライアント左上の (0,0)
右下の意味画面上のウィンドウ外形の右下クライアント幅と高さ
含まれる範囲枠、タイトルバー、スクロールバーを含む描画・配置できる中身だけ
主な用途位置合わせ、他ウィンドウとの比較、画面上の配置描画、内部レイアウト、ヒットテスト

RECTright / bottom は、どちらも右端・下端の外側を指します。幅と高さは手で引き算せず、CRect::Width()CRect::Height() で取るのが安全です。

CRect rcWindow;
CRect rcClient;

GetWindowRect(&rcWindow);
GetClientRect(&rcClient);

const int outerWidth = rcWindow.Width();
const int outerHeight = rcWindow.Height();
const int clientWidth = rcClient.Width();
const int clientHeight = rcClient.Height();

子ウィンドウでも GetWindowRect はスクリーン座標

見落としやすい点ですが、GetWindowRect は子ウィンドウや子コントロールに対して呼んでも、親クライアント基準ではなく screen 座標を返します。親ダイアログの中にあるボタンやスタティックでも同じです。

そのため、子コントロールの外形を取って別の子コントロールに重ねたいときは、GetWindowRect の結果をそのまま MoveWindow へ渡してはいけません。親のクライアント座標へ直してから使います。

CRect rcButton;
m_wndTargetButton.GetWindowRect(&rcButton);

::MapWindowPoints(
    HWND_DESKTOP,
    GetSafeHwnd(),
    reinterpret_cast<LPPOINT>(&rcButton),
    2);

m_wndHighlight.MoveWindow(&rcButton);
そのまま使うとどうなるか正しい考え方
screen 座標の矩形を親クライアント基準だと思って渡し、右下へずれるGetWindowRect の結果を親基準へ変換してから使う
子コントロール同士の位置比較で GetClientRect を見てしまい、両方 (0,0) に見える位置比較は GetWindowRect、内部サイズ比較は GetClientRect と分ける

実務でのコツ

レイアウトや配置の計算をしている間は、なるべく 1 つの座標系の中だけで処理を進めます。GetClientRect の値で計算を終え、別の座標系(親のクライアント座標や画面座標)へ渡す直前に ScreenToClient / ClientToScreen / MapWindowPoints を 1 回だけかける、という順番にしておくと、変換を何度も重ねて間違えることが減ります。


移動だけ変えたときに何が変わるか

最初の比較では、ウィンドウの外形サイズを変えずに位置だけを動かします。GetWindowRectleft / top は移動に合わせて変わりますが、GetClientRect の原点は常に (0,0) のままです。

GetWindowRect と GetClientRect の移動前後の違いを左右比較した実行結果

左右の比較ビューでは、移動前と移動後で WindowRect の位置だけが変わり、ClientRect はどちらも (0,0) 起点のまま表示されます。GetClientRect は自分のクライアント領域の中しか見ていないためです。

CRect rcBeforeWindow;
CRect rcBeforeClient;
CRect rcAfterWindow;
CRect rcAfterClient;

GetWindowRect(&rcBeforeWindow);
GetClientRect(&rcBeforeClient);

MoveWindow(48, 48, rcBeforeWindow.Width(), rcBeforeWindow.Height());

GetWindowRect(&rcAfterWindow);
GetClientRect(&rcAfterClient);

この違いを知らないまま、別ウィンドウの横に並べる処理や、ドラッグ開始位置の記録に GetClientRect を使うと、すべてのウィンドウが原点付近にあるように見えてしまいます。


サイズ変更でどこまで差が広がるか

次に、位置だけでなくサイズも変更してみます。外形のサイズとクライアント領域のサイズは一致せず、その差は枠やタイトルバーの厚みのぶんです。

GetWindowRect と GetClientRect のサイズ変更前後を比較した実行結果

比較ビューでは、変更後の Outer sizeClient size が別の値で表示されます。差分は枠やタイトルバーの厚みで、Windows 11 のような現行環境ではこの差が見た目以上に効きます。

ダイアログ内のリストやプレビュー領域を追従させるなら、基準は GetWindowRect ではなく GetClientRect です。外形サイズをそのまま使うと、下端や右端がはみ出します。

void CChildLayoutDlg::OnSize(UINT nType, int cx, int cy)
{
    CDialogEx::OnSize(nType, cx, cy);

    if (!::IsWindow(m_wndList.m_hWnd))
        return;

    CRect rcClient;
    GetClientRect(&rcClient);

    const int margin = 12;
    m_wndList.MoveWindow(
        margin,
        margin,
        rcClient.Width() - margin * 2,
        rcClient.Height() - margin * 2);
}

OnSize の中で使う基準値を GetClientRect の戻り値だけに統一しておくと、後から座標変換まわりの不具合を切り分けやすくなります。


ScreenToClient(WindowRect.LeftTop) が負になる理由

GetWindowRect の左上をそのまま ScreenToClient へ渡すと、しばしば負の値になります。これは失敗ではなく、ウィンドウ外形の左上がクライアント領域の左上より外側にあるからです。

ScreenToClient と ClientToScreen の対応関係を図とカードで示した実行結果
CRect rcWindow;
GetWindowRect(&rcWindow);

CPoint pt = rcWindow.TopLeft();
ScreenToClient(&pt);

// pt.x / pt.y は負になることがある

変換後の値が負になるのは、タイトルバーや枠のぶんだけ、WindowRect.LeftTop がクライアント原点より上と左にあるからです。逆に、ClientToScreen(0,0) は「実際のクライアント左上が画面上のどこにあるか」を返します。

  • GetWindowRect().TopLeft() は外形の左上です。
  • ClientToScreen(CPoint(0, 0)) はクライアント原点の画面位置です。
  • 両者の差分が、非クライアント領域の厚みとして現れます。

この挙動を知らないと、負値を見て「変換に失敗した」と誤解しがちです。実際には、座標系を正しくまたいだ結果として自然な値です。


ClientToScreen と ScreenToClient の使い分け

ScreenToClientClientToScreen は点の向きを反転しただけの関数ですが、使う場所ははっきり分かれます。

場面使う関数理由
マウスカーソルや外部 API から受けた screen 座標を自分の中へ入れるScreenToClient自分のクライアント座標へ直してから描画やヒットテストを行うため
自分の中で決めた点をメニューやポップアップの表示位置へ出すClientToScreen画面上の絶対位置が必要になるため
複数点や RECT を別ウィンドウ基準へまとめて変換するMapWindowPoints点ごとの変換より安全で読みやすい

たとえば、クライアント内の計算結果を右クリックメニューの表示位置へ使うなら、最後に ClientToScreen を通します。

CPoint ptPopup(16, 16);
ClientToScreen(&ptPopup);

pPopupMenu->TrackPopupMenu(
    TPM_LEFTALIGN | TPM_TOPALIGN,
    ptPopup.x,
    ptPopup.y,
    this);

一方、別 API から screen 座標で受けた点を自分の描画処理へ渡すなら、先に ScreenToClient をかけます。描画や子コントロール配置へ渡す前に、どの座標系かをそろえるのが基本です。


親子ウィンドウ間では MapWindowPoints を使う場面

ScreenToClientClientToScreen は 1 つのウィンドウを基準に点を変換する関数です。親子や兄弟ウィンドウの間で矩形を受け渡すなら、MapWindowPoints で 2 点まとめて変換するほうが安全です。

CRect rcChildScreen;
m_wndPreview.GetWindowRect(&rcChildScreen);

::MapWindowPoints(
    HWND_DESKTOP,
    m_wndPanel.GetSafeHwnd(),
    reinterpret_cast<LPPOINT>(&rcChildScreen),
    2);

m_wndOverlay.MoveWindow(&rcChildScreen);

この例では、まず子ウィンドウの外形を screen 座標で取り、その後 m_wndPanel のクライアント座標へ変換しています。RECT は 2 点として扱うため、最後の引数は 2 です。

Microsoft Learn でも、レイアウトのミラーリングが絡む場合は ScreenToClient ではなく MapWindowPoints を使うよう案内されています。通常の左から右のレイアウトでも、複数ウィンドウをまたぐ矩形変換は MapWindowPoints と覚えておくと事故が減ります。


よくある誤用

描画範囲に GetWindowRect を使う

CDC の描画座標はクライアント基準です。GetWindowRect をそのまま描画へ渡すと、画面上の位置とクライアント内の位置が混ざります。

// NG
RECT rc;
GetWindowRect(&rc);
pDC->FillSolidRect(&rc, RGB(255, 0, 0));

// OK
GetClientRect(&rc);
pDC->FillSolidRect(&rc, RGB(255, 0, 0));

中身の幅を GetWindowRect で取る

GetWindowRect の幅と高さには枠やタイトルバーが含まれます。中身の幅を欲しいのにこれを使うと、余白計算がずれて、子コントロールが下にはみ出しやすくなります。

別ウィンドウとの位置比較に GetClientRect を使う

GetClientRect はどのウィンドウでも (0,0) 起点です。別ウィンドウとの相対位置を知りたいなら、比較材料は GetWindowRect です。

right / bottom を実座標の内側だと思い込む

RECT の右下は排他的です。右端のピクセルに線を引きたいときは、right - 1bottom - 1 を意識しないと 1 ピクセルはみ出します。


GetWindowRect の注意点

GetWindowRect は便利ですが、現行 Windows では 2 つ注意点があります。DPI 仮想化と、見えないリサイズ枠を含む場合があることです。

DPI 仮想化される

Microsoft Learn では、GetWindowRect は DPI 仮想化されると説明されています。高 DPI 環境や混在 DPI 環境で値を扱うときは、見た目のピクセル数と完全一致しないことがあります。

見えないリサイズ枠を含むことがある

Windows Vista 以降では、GetWindowRect の外形に見えないリサイズ枠が含まれる場合があります。スクリーンショット上の見た目とぴったり一致する可視境界が必要なら、DwmGetWindowAttributeDWMWA_EXTENDED_FRAME_BOUNDS を使う選択肢があります。

ただし、通常の MFC ダイアログ配置や、相対位置の比較であれば、まずは GetWindowRect / GetClientRect の違いを正しく使い分けるほうが先です。


動かないときの確認表

症状まず見る場所原因になりやすいこと対処
ScreenToClient 後に負値が出る変換前の点の意味非クライアント部分の点を変換している失敗ではない。必要な点がクライアント内かを確認する
別ウィンドウへ渡す矩形が少しずれる座標変換処理screen 座標のまま MoveWindow へ渡しているMapWindowPointsScreenToClient で親基準へそろえる
見た目の境界と GetWindowRect が一致しないDPI と見えない枠DPI 仮想化、不可視リサイズ枠必要なら DwmGetWindowAttribute を検討する
右端や下端で 1px ずれるRECT の使い方right / bottom を内側座標だと思っている排他的境界として扱い、幅高さは Width() / Height() を使う

トラブルの多くは API そのものより、screen 座標と client 座標を混ぜたまま次の API へ渡していることが原因です。変換前後の座標系をコメントで残すだけでも、かなり追いやすくなります。


まとめ

  • GetWindowRect は screen 座標で外形を返し、GetClientRect は client 座標で中身を返します。
  • 位置の比較は GetWindowRect、描画と内部レイアウトは GetClientRect が基本です。
  • ウィンドウ左上を ScreenToClient すると負値になることがありますが、非クライアント領域ぶんの自然な結果です。
  • client の点を screen へ出すときは ClientToScreen、screen の点を自分の中へ入れるときは ScreenToClient を使います。
  • 親子・兄弟ウィンドウ間で矩形を渡すなら、手計算より MapWindowPoints のほうが安全です。
  • GetWindowRect は DPI 仮想化され、見えないリサイズ枠を含む場合があります。
目次