ウィンドウの位置を取りたいのに GetClientRect を使っていたり、描画範囲を取りたいのに GetWindowRect を使っていたりすると、座標がずれたように見えます。この記事では、GetWindowRect と GetClientRect が何を返すのかを、移動・サイズ変更・座標変換の実測結果と一緒に確認します。
後半では ScreenToClient、ClientToScreen、MapWindowPoints まで含めて、どの座標をどこへ渡すときに何を使うかを切り分けます。
使い分けの早見表
| やりたいこと | 使う関数 | 追加で使うもの | 注意点 |
|---|---|---|---|
| 画面上のどこにあるか知りたい | GetWindowRect | 必要なら ScreenToClient / MapWindowPoints | スクリーン座標で返る。枠やタイトルバーも含む |
| 描画できる中身の幅と高さを知りたい | GetClientRect | なし | 左上は常に (0,0)。right / bottom は幅と高さになる |
| スクリーン座標の点を自分のウィンドウ基準へ直したい | ScreenToClient | 対象ウィンドウの HWND | 非クライアント部分を変換すると負値になり得る |
| クライアント座標の点を画面上へ出したい | ClientToScreen | 対象ウィンドウの HWND | ポップアップ表示位置やドラッグ開始位置で使う |
| 親子・兄弟ウィンドウ間で矩形を渡したい | MapWindowPoints | RECT なら 2 点として渡す | 複数の座標系をまたぐときは手計算しない |
迷ったときは、「画面全体を基準にしたいのか」「自分のクライアント領域を基準にしたいのか」で分けると判断しやすくなります。
検証環境
| OS | Windows 11 |
| IDE | Visual Studio 2026 |
| ワークロード | C++ によるデスクトップ開発 |
| MFC / ATL | 有効 |
| ビルド構成 | x64 / Debug |
| 確認対象 | MFC ダイアログ自身の GetWindowRect / GetClientRect / ScreenToClient / ClientToScreen |
数値そのものはテーマ、DPI、Windows の見た目設定で変わります。ただし、どの値が動いて、どの値が動かないかという関係は同じです。
GetWindowRect と GetClientRect の返り値の違い
まず押さえるべき点は、2 つの関数は同じ RECT を返していても、原点が違うことです。
| 観点 | GetWindowRect | GetClientRect |
|---|---|---|
| 座標系 | スクリーン座標 | クライアント座標 |
| 左上の意味 | 画面上のウィンドウ外形の左上 | 常にクライアント左上の (0,0) |
| 右下の意味 | 画面上のウィンドウ外形の右下 | クライアント幅と高さ |
| 含まれる範囲 | 枠、タイトルバー、スクロールバーを含む | 描画・配置できる中身だけ |
| 主な用途 | 位置合わせ、他ウィンドウとの比較、画面上の配置 | 描画、内部レイアウト、ヒットテスト |
RECT の right / 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 回だけかける、という順番にしておくと、変換を何度も重ねて間違えることが減ります。
移動だけ変えたときに何が変わるか
最初の比較では、ウィンドウの外形サイズを変えずに位置だけを動かします。GetWindowRect の left / top は移動に合わせて変わりますが、GetClientRect の原点は常に (0,0) のままです。

左右の比較ビューでは、移動前と移動後で 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 を使うと、すべてのウィンドウが原点付近にあるように見えてしまいます。
サイズ変更でどこまで差が広がるか
次に、位置だけでなくサイズも変更してみます。外形のサイズとクライアント領域のサイズは一致せず、その差は枠やタイトルバーの厚みのぶんです。

比較ビューでは、変更後の Outer size と Client 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 へ渡すと、しばしば負の値になります。これは失敗ではなく、ウィンドウ外形の左上がクライアント領域の左上より外側にあるからです。

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 の使い分け
ScreenToClient と ClientToScreen は点の向きを反転しただけの関数ですが、使う場所ははっきり分かれます。
| 場面 | 使う関数 | 理由 |
|---|---|---|
| マウスカーソルや外部 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 を使う場面
ScreenToClient と ClientToScreen は 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 - 1、bottom - 1 を意識しないと 1 ピクセルはみ出します。
GetWindowRect の注意点
GetWindowRect は便利ですが、現行 Windows では 2 つ注意点があります。DPI 仮想化と、見えないリサイズ枠を含む場合があることです。
DPI 仮想化される
Microsoft Learn では、GetWindowRect は DPI 仮想化されると説明されています。高 DPI 環境や混在 DPI 環境で値を扱うときは、見た目のピクセル数と完全一致しないことがあります。
見えないリサイズ枠を含むことがある
Windows Vista 以降では、GetWindowRect の外形に見えないリサイズ枠が含まれる場合があります。スクリーンショット上の見た目とぴったり一致する可視境界が必要なら、DwmGetWindowAttribute と DWMWA_EXTENDED_FRAME_BOUNDS を使う選択肢があります。
ただし、通常の MFC ダイアログ配置や、相対位置の比較であれば、まずは GetWindowRect / GetClientRect の違いを正しく使い分けるほうが先です。
動かないときの確認表
| 症状 | まず見る場所 | 原因になりやすいこと | 対処 |
|---|---|---|---|
ScreenToClient 後に負値が出る | 変換前の点の意味 | 非クライアント部分の点を変換している | 失敗ではない。必要な点がクライアント内かを確認する |
| 別ウィンドウへ渡す矩形が少しずれる | 座標変換処理 | screen 座標のまま MoveWindow へ渡している | MapWindowPoints か ScreenToClient で親基準へそろえる |
見た目の境界と 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 仮想化され、見えないリサイズ枠を含む場合があります。
