EternalWindows
再描画 / 再描画の本質

今回は、前節や前々節のWM_PAINTの処理について考えていきたいと思います。

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;
}

このコードは長方形の数だけ描画を行っているわけですが、 たとえばnCountが5であったとして、本当に5個の長方形を描画する必要があるのでしょうか。 WM_PAINTは無効領域が発生したときに送られるので、 本当に再描画が必要なのは無効領域に含まれる長方形だけであるはずです。 消されてない長方形は再描画の必要はありません。 無効領域の範囲はGetUpdateRectという関数で取得できますが、 PAINTSTRUCT構造体のrcPaintメンバからでも参照できます。

BeginPaintはPAINTSTRUCT構造体を初期化するに加え、 rcPaintメンバが表す範囲をクリッピングリージョンに設定しています。 クリッピングリージョンとは描画許可範囲のことで、 デバイスコンテキストにクリッピングリージョンが設定されている場合、 クリッピングリージョン外への描画は、実際に表示されることはありません。 rcPaintは無効領域の範囲ですから、無効領域の範囲外に描画が行われることはないわけです。 これは、Rectangleのようなデバイスコンテキストを要求する関数が 内部でクリッピングリージョンの範囲を確認しているからだと思われますが、 アプリケーションからでもRectVisibleを呼び出すことで確認できます。

BOOL RectVisible(
  HDC hdc,
  CONST RECT* lprc
);

hdcは、デバイスコンテキストのハンドルを指定します。 lprcは、クリッピングリージョンの範囲に入っているかを確かめるためのRECT構造体のアドレスを指定します。 戻り値がTRUEの場合、lprcはクリッピングリージョンの一部に含まれています。 次に、RectVisibleを利用したコードを示します。

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

	hdc = BeginPaint(hwnd, &ps);

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

	EndPaint(hwnd, &ps);

	return 0;
}

このコードではRectangleの呼び出しの前に、 描画先となる位置がクリッピングリージョンに含まれているかどうかをRectVisibleで調べています。 含まれていない場合は、その描画先となる範囲は無効領域になっていない、 つまり描画内容は消去されていないということですから、 Rectangleを呼び出す必要はありません。

次にBeginPaintとGetDCの違いを考えていきます。 1つ目の違いは先ほど述べたクリッピングリージョンに関することです。 GetDCで返されるデバイスコンテキストはデフォルトでクライアント領域全体が クリッピングリージョンであるため、どこにでも描画することができます。 2つ目の違いは、BeginPaintが無効領域を有効にする点です。 無効領域を有効にしなければ、延々とWM_PAINTが送られてくるので、 WM_PAINTでは必ず無効領域を有効にしなければなりません。 となると、無効領域を有効にする機能を持っていないGetDCを WM_PAINTで呼び出すことはあってはならないのでしょうか。 実はそうでもないのです。

本質的な問題は無効領域を有効にするかどうかの話で、 デバイスコンテキストの取得方法は重要ではありません。 WM_PAINTで以下のような無効領域を有効にする関数を呼び出せば、 GetDCを呼び出しても問題はないのです。

BOOL ValidateRect(
  HWND hWnd,
  CONST RECT *lpRect
);

hWndは、ウインドウハンドルを指定します。 lpRectは、有効にしたい範囲を表すRECT構造体のアドレスを指定します。 NULLを指定するとクライアント領域全体が有効化されるので、 WM_PAINTで必ずすべきことは果たせたことになります。 この関数が使われない理由は、BeginPaintがValidateRectの役割を兼ね、 さらに再描画のためのデバイスコンテキストを返してくれるからでしょう。

今回のプログラムは、GetDCを利用してWM_PAINTで再描画を行っています。

#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_PAINT: {
		int i;
		HDC hdc;

		hdc = GetDC(hwnd);

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

		ReleaseDC(hwnd, hdc);

		ValidateRect(hwnd, NULL);

		return 0;
	}

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

この方法とBeginPaintを使った方法とどちらがよいかといったら、 恐らくBeginPaintの方であると思いますが、実のところ定かではありません。 BeginPaintはクリッピングリージョンを設定しているので、 描画関数は座標がクリッピングリージョン内かどうかの確認が必要になります。 これは、必要最低限の描画処理に済ませようとするのが目的なのでしょうが、 クリッピングリージョンの計算に時間がかかっているとするならば、 何処にでも描画できるGetDCのデバイスコンテキストの方が高速なのかもしれません。


戻る