EternalWindows
再描画 / 描画の一括処理

今回は前節のプログラムを効率的に書き換え、新しい機能を追加します。 次のコードは前節のWM_LBUTTONDOWNです。

case WM_LBUTTONDOWN: {
	int x, y;
	HDC hdc;

	if (nCount >= 10) {
		MessageBox(hwnd, TEXT("長方形の描画は10個までです。"), TEXT("OK"), MB_OK);
		return 0;
	}

	x = LOWORD(lParam);
	y = HIWORD(lParam);

	SetRect(&rc[nCount], x, y, x + 70, y + 30);

	hdc = GetDC(hwnd);
	
	Rectangle(hdc, rc[nCount].left, rc[nCount].top, rc[nCount].right, rc[nCount].bottom);

	ReleaseDC(hwnd, hdc);
	
	nCount++;

	return 0;
}

この処理ではWM_PAINTでの再描画に備えて、 長方形の位置や数を更新しているわけですが、 どうせなら長方形の描画もWM_PAINTに任せるのはどうでしょうか。 つまり、WM_LBUTTONDOWNでRectangleを呼び出すコードを書く代わりに、 WM_PAINTを呼び出すようなコードを書くのです。 長方形の位置や数は更新しているので、WM_PAINTでは正確に描画できるはずですし、 描画コードをWM_PAINTにまとめることもできます。 ウインドウにWM_PAINTを送るには、UpdateWindowを呼び出します。

BOOL UpdateWindow(
  HWND hWnd
);

hWndは、更新したいウインドウハンドルを指定します。 これにより、無効領域が有効となります。

メッセージにはポストとセンドという2種類がありますが、 UpdateWindowはメッセージをセンドします。 簡単に言えば、UpdateWindowが制御を返すときには、 既にWM_PAINTは処理されているということです。 しかし、必ずしもWM_PAINTがウインドウに送られるとは限りません。 WM_PAINTの目的は無効領域を有効にし、再描画することです。 無効になっていない領域は消されてないということなので、再描画の必要はありません。 実は、UpdateWindowは無効領域が存在しない場合は何もしないよう設計されているので、 この関数を呼ぶことによりWM_PAINTを処理させたいならば、 無効領域をプログラム自らが発生させる必要があるのです。 次に示すInvalidateRectは、第2引数で指定した範囲を無効領域とします。

BOOL InvalidateRect(
  HWND hWnd,
  CONST RECT* lpRect,
  BOOL bErase
);

hWndは、ウインドウハンドルを指定します。 lpRectは、無効領域としたい範囲を表すRECT構造体のアドレスを指定します。 NULLにした場合、クライアント領域全体が無効領域となります。 bEraseは、背景を消去するかどうかです。 TRUEにした場合、第2引数で指定した範囲が背景色で塗りつぶされます。 ここで述べている背景色とは、WinMain関数で宣言しているWNDCLASS構造体のhbrBackgroundメンバのことです。

InvalidateRectを呼ぶことにより無効領域が発生することは分かりました。 これで、やっとUpdateWindowを呼び出すことができるわけですが、 実はもうその必要はなくなっているのです。 無効領域が発生するということは、その領域を有効にしなくてはならないため、 InvalidateRectを呼び出した時点でWM_PAINTが生成されているのです。 ですから、UpdateWindowを呼び出さなくてもWM_PAINTが処理されることになるのです。 しかし、UpdateWindowには面白い使い方があるので後で再び取り上げます。

今回のプログラムは、WM_LBUTTONDOWNでInvalidateRectを呼び出すように書き換え、 さらにWM_RBUTTONDOWNで長方形をクリアするコードが追加されています。

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static int  nCount = 0;
	static RECT rc[10] = {0};

	switch (uMsg) {

	case WM_LBUTTONDOWN: {
		int x, y;

		if (nCount >= 10) {
			MessageBox(hwnd, TEXT("長方形の描画は10個までです。"), TEXT("OK"), MB_OK);
			return 0;
		}

		x = LOWORD(lParam);
		y = HIWORD(lParam);

		SetRect(&rc[nCount], x, y, x + 70, y + 30);

		InvalidateRect(hwnd, &rc[nCount], FALSE);
		
		nCount++;

		return 0;
	}

	case WM_RBUTTONDOWN:
		nCount = 0;
		InvalidateRect(hwnd, NULL, TRUE);
		return 0;

	case WM_PAINT: {
		int         i;
		HDC         hdc;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);

		for (i = 0; i < nCount; i++)
			Rectangle(hdc, rc[i].left, rc[i].top, rc[i].right, rc[i].bottom);

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

まずは長方形をクリアするコードを見てみましょう。 WM_RBUTTONDOWNは、マウスの右ボタンを押すことによって送られます。

case WM_RBUTTONDOWN:
	nCount = 0;
	InvalidateRect(hwnd, NULL, TRUE);
	return 0;

InvalidateRectの第3引数がTRUEであるため、ウインドウは真っ白に塗りつぶされます (正確には、WM_ERASEBKGNDというメッセージが送られ、 それをDefWindowProcに任せることにより背景が塗りつぶされることになります)。 無効領域の発生のためWM_PAINTが処理されることになりますが、 nCountを0にしているので長方形が描画されることはありません。

次にWM_LBUTTONDOWNを見てみます。

case WM_LBUTTONDOWN: {
	int x, y;

	if (nCount >= 10) {
		MessageBox(hwnd, TEXT("長方形の描画は10個までです。"), TEXT("OK"), MB_OK);
		return 0;
	}

	x = LOWORD(lParam);
	y = HIWORD(lParam);

	SetRect(&rc[nCount], x, y, x + 70, y + 30);

	InvalidateRect(hwnd, &rc[nCount], FALSE);
	
	nCount++;

	return 0;
}

InvalidateRectで無効領域を発生させることによりWM_PAINTに描画を任せたため、 WM_LBUTTONDOWNで描画処理を行う必要はなくなりました。 描画処理をWM_PAINTに集中させることが実現できたので、大きな進歩といえるでしょう。 InvalidateRectの第2引数に描画すべき範囲を指定することにより、 この範囲だけが無効領域となり、WM_PAINTでの処理の負担は軽減されます。 これは、無効領域がクリッピングリージョンとなるからなのですが、 詳細は次節で説明します。

では、ここでUpdateWindowの面白い使い方を説明します。 WM_LBUTTONDOWNのnCount++の後に、以下のコードを追加してみてください。

UpdateWindow(hwnd);
Sleep(1000);

Sleepという関数は引数で指定した時間だけ処理の進行を停止させます。 この場合、1秒間停止することになります。 UpdateWindowはWM_PAINTをウインドウに送信するので、 関数から制御が返した時点でWM_PAINTの処理は終了してします。 つまり、長方形は描画されています。 しかし、UpdateWindowを呼び出さなかった場合、長方形の描画は1秒後になります。 何故なら、InvalidateRectはWM_PAINTを生成させるものの、 それはメッセージキューにポストされることになるからです。 そのため、WM_PAINTを即座に処理させたい場合はInvalidateRectとUpdateWindowを 続けて呼ぶ必要があります。 トランプやリバーシなどのゲームではコンピュータの思考時間のために 一時的に処理に時間がかかることがあるため、このような手法を用いることがよくあります。

WM_ERASEBKGNDについて

ウインドウのクライアント領域が無効になった場合は、その無効になった領域が背景色のブラシで塗りつぶされることになっています。 この塗りつぶしのタイミングはWM_ERASEBKGNDとして通知され、 DefWindowProcに処理を任せた場合は次のようなコードが実行されるのではないかと思われます。

case WM_ERASEBKGND: {
	RECT rc;
	GetClientRect(hwnd, &rc);
	FillRect((HDC)wParam, &rc, (HBRUSH)GetClassLongPtr(hwnd, GCLP_HBRBACKGROUND));
	return 1;
}

WM_ERASEBKGNDでは無効領域を塗りつぶさなければならないため、 FillRectでrcの領域を塗りつぶしています。 rcはGetClientRectで初期化されているため、クライアント領域全体が塗りつぶしの対象になるように思えますが、 wParamのHDCにはクリッピングリージョンが設定されているため、 無効領域の部分のみが塗りつぶされることになります。 第3引数のブラシは背景色のブラシでなければならず、 これはWNDCLASSEX構造体のhbrBackgroundメンバに相当します。 よって、GetClassLongPtrにGCLP_HBRBACKGROUNDを指定することで、 hbrBackgroundメンバに指定されたブラシを取得するようにしています。 背景色を塗りつぶした場合は0以外の値を返さなければならないため、1を返すようにしています。



戻る