クライアント領域のサイズからウィンドウ全体のサイズを設定する

ちょっとTwitterで流れてたので書いておく。


Win32 APIでウィンドウのサイズを操作する場合、クライアント領域非クライアント領域というものを常に意識しなければならない。
たとえば次のようなウィンドウがあったとする。

クライアント領域というのは我々がコントロールを配置したり図形描画したりするための領域で、下図の青く塗った部分がそうだ。
非クライアント領域というのはウィンドウの外枠部分で、我々が直接何か操作を行う部分というよりは、そのウィンドウの共通機能を与える部分だと言ってよい。下図の赤く塗った部分がそうだ。
(ただしこの説明には多少語弊があります。詳しくは他のサイト様で)

Win32 APIにはウィンドウのサイズを変更できる関数がいくつかある。
しかしこれらは大抵、非クライアント領域も含めたウィンドウ全体のサイズを調節するもので、「直接クライアント領域のサイズを指定してウィンドウをリサイズする」という操作はない。

クライアント領域のサイズが直接変更できないのは何か都合があるのだと思われるが、ゲームのウィンドウや、普通のアプリケーションでもトップレベルのウィンドウなどでは、非クライアント領域のサイズを無視できる方が大抵ありがたいことが多い。
このため、他のAPIを駆使して自分で実装してやる必要がある。

これにもいくつか方法があり、WebにはGetSystemMetricsでシステム設定を取得したり、AdjustWindowRectを使ったりする方法なども転がっている。
ここではウィンドウの属性に依存しにくく、ある程度汎用的と思われるGetWindowRect, GetClientRectを使う方法を紹介する。

まず必要なのは、

  • 現在のウィンドウ全体のサイズ
  • 現在のクライアント領域のサイズ
  • 希望するクライアント領域のサイズ

である。
希望するサイズは自分で設定するものだが、現在のサイズはAPIによってウィンドウハンドルから取得する必要がある。
ウィンドウ全体のサイズはGetWindowRect、クライアント領域のサイズはGetClientRectによってそれぞれ取得できる。

RECT rw, rc;
::GetWindowRect(hWnd, &rw); // ウィンドウ全体のサイズ
::GetClientRect(hWnd, &rc); // クライアント領域のサイズ

結果はRECT構造体で返されるので、それぞれ.rightから.leftを引いたものが幅、.bottomから.topを引いたものが高さとなる。
(ただし、クライアント領域の.leftと.topは常に0なので無視してもよい)

ここで、ウィンドウ全体のサイズをW、非クライアント領域のサイズをW_b、現在のクライアント領域のサイズW_c、希望するクライアント領域のサイズをW_{c'}とすると、設定するべきウィンドウのサイズW'は、
W' = W_b + W_{c'} = (W - W_c) + W_{c'}
である。

// 希望するクライアント領域のサイズを持つウィンドウサイズを計算
int new_width = (rw.right - rw.left) - (rc.right - rc.left) + width;
int new_height = (rw.bottom - rw.top) - (rc.bottom - rc.top) + height;

あとはこれをSetWindowPosなどのウィンドウサイズを変更できるAPIに投げてやればよい。
SetWindowPosはウィンドウのサイズや位置、前後関係などをまとめて変更できる万能系のAPIだが、今回はサイズのみ変更したいので、オプションで他の引数を無視させている。

// サイズの設定
// ウィンドウの位置や前後関係は変更しない
::SetWindowPos(hWnd, NULL, 0, 0, new_width, new_height, SWP_NOMOVE | SWP_NOZORDER);

これでクライアント領域のサイズを指定してウィンドウ全体のサイズを調整することができる。

ついでに、上記をまとめて関数にでもしておくと便利かもしれない。

BOOL SetClientSize(HWND hWnd, int width, int height)
{
	RECT rw, rc;
	::GetWindowRect(hWnd, &rw);
	::GetClientRect(hWnd, &rc);

	int new_width = (rw.right - rw.left) - (rc.right - rc.left) + width;
	int new_height = (rw.bottom - rw.top) - (rc.bottom - rc.top) + height;

	return ::SetWindowPos(hWnd, NULL, 0, 0, new_width, new_height, SWP_NOMOVE | SWP_NOZORDER);
}