EternalWindows
ビットマップ / バックバッファ

前節のように、ビットマップをウインドウへ直接描画する方法は、 描画するビットマップが複数個存在する場合に問題があります。 理由は、BitBltの呼び出しの度にウインドウが更新されるため、 描画している過程がユーザーに見られることになるからです。 この問題を解決するには、描画されるためのビットマップを作成しておき、 これに対して全てのビットマップの描画を集約させることです。 このようにすれば、そのビットマップをウインドウに描画するだけで、 全てのビットマップが描画されたように見えることになります。

ウインドウを画面と表現するならば、上記のようなビットマップは裏画面といえるでしょう。 なぜなら、ウインドウに表示される内容は、この裏画面をコピーしたものであるからです。 裏画面はバックバッファやオフスクリーンなどとも呼ばれ、 CreateCompatibleBitmapで作成することができます。

HBITMAP CreateCompatibleBitmap(
  HDC hdc,
  int nWidth,
  int nHeight
);

hdcは、将来このビットマップを描画することになるであろう デバイスコンテキストのハンドルを指定します。 作成されるビットマップは、このデバイスコンテキストと互換性が生じることになります。 nWidthは、ビットマップの幅を指定します。 nHeightは、ビットマップの高さを指定します。 戻り値は、作成されたビットマップのハンドルです。 このビットマップは、初期状態で黒色となっています。

今回のプログラムは、ビットマップをバックバッファに描画し、 その後バックバッファをデバイスコンテキストにコピーします。

#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;
	RECT       rc;
	DWORD      dwStyle;

	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;

	dwStyle = WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX;
	SetRect(&rc, 0, 0, 640, 480);
	AdjustWindowRect(&rc, dwStyle, FALSE);

	hwnd = CreateWindowEx(0, szAppName, szAppName, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, 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 HDC     hdcMem = NULL;
	static HBITMAP hbmpMem = NULL;
	static HBITMAP hbmpMemPrev = NULL;
	static HDC     hdcBackbuffer = NULL;
	static HBITMAP hbmpBackbuffer = NULL;
	static HBITMAP hbmpBackbufferPrev = NULL;
	
	switch (uMsg) {

	case WM_CREATE: {
		HDC hdc;

		hdc = GetDC(hwnd);
		
		hdcMem = CreateCompatibleDC(hdc);
		hbmpMem = (HBITMAP)LoadImage(NULL, TEXT("sample.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
		if (hbmpMem == NULL) {
			ReleaseDC(hwnd, hdc);
			return -1;
		}

		hbmpMemPrev = (HBITMAP)SelectObject(hdcMem, hbmpMem);

		hdcBackbuffer = CreateCompatibleDC(hdc);
		hbmpBackbuffer = CreateCompatibleBitmap(hdc, 640, 480);
		if (hbmpBackbuffer == NULL) {
			ReleaseDC(hwnd, hdc);
			return -1;
		}

		hbmpBackbufferPrev = (HBITMAP)SelectObject(hdcBackbuffer, hbmpBackbuffer);

		ReleaseDC(hwnd, hdc);

		return 0;
	}

	case WM_PAINT: {
		HDC         hdc;
		BITMAP      bm;
		PAINTSTRUCT ps;
		
		GetObject(hbmpMem, sizeof(BITMAP), &bm);

		hdc = BeginPaint(hwnd, &ps);
		
		BitBlt(hdcBackbuffer, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

		BitBlt(hdc, 0, 0, 640, 480, hdcBackbuffer, 0, 0, SRCCOPY);

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_DESTROY:
		if (hdcMem != NULL) {
			if (hbmpMem != NULL) {
				SelectObject(hdcMem, hbmpMemPrev);
				DeleteObject(hbmpMem);
			}
			DeleteDC(hdcMem);
		}

		if (hdcBackbuffer != NULL) {
			if (hbmpBackbuffer != NULL) {
				SelectObject(hdcBackbuffer, hbmpBackbufferPrev);
				DeleteObject(hbmpBackbuffer);
			}
			DeleteDC(hdcBackbuffer);
		}

		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

バックバッファは、ウインドウに対する裏画面ですから、 ウインドウと同じサイズを持っていなければなりません。 よって、今回のウインドウサイズは(640, 480)という固定になっています。

dwStyle = WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX;
SetRect(&rc, 0, 0, 640, 480);
AdjustWindowRect(&rc, dwStyle, FALSE);

hwnd = CreateWindowEx(0, szAppName, szAppName, dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hinst, NULL);

dwStyleは、ウインドウスタイルを格納します。 標準のスタイルから、WS_THICKFRAME(サイズ変更)とWS_MAXIMIZEBOX(最大化)を取り除くことによって、 動的にウインドウサイズを変更できないようにしています。 AdjustWindowRectは、第1引数に希望するクライアント領域の大きさを要求します。 関数は、指定したクライアント領域を持つウインドウの大きさを第1引数に格納します。 rc.right - rc.left は クライアント領域の幅ではなくウインドウの幅ですが、 この幅はクライアント領域の幅が640になるよう計算されているため、 これをCreateWindowExに指定することにより、クライアント領域の幅は640となります。 (640, 480)という値は解像度の面でも安定しており、一般のゲームでもよくあるサイズです。

バックバッファを作成するコードは、次のようになっています。

hdcBackbuffer = CreateCompatibleDC(hdc);
hbmpBackbuffer = CreateCompatibleBitmap(hdc, 640, 480);
if (hbmpBackbuffer == NULL) {
	ReleaseDC(hwnd, hdc);
	return -1;
}

hbmpBackbufferPrev = (HBITMAP)SelectObject(hdcBackbuffer, hbmpBackbuffer);

まず、バックバッファのメモリデバイスコンテキストを作成し、 次にバックバッファのビットマップを作成します。 ここに指定するサイズは、先のウインドウのサイズと同一にしています。 ビットマップが作成できれば、これをメモリデバイスコンテキストに選択します。

WM_PAINTでは、バックバッファへの描画と、ウインドウへのバックバッファのコピーが行われています。

BitBlt(hdcBackbuffer, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

BitBlt(hdc, 0, 0, 640, 480, hdcBackbuffer, 0, 0, SRCCOPY);

最初のコードは、バックバッファへビットマップを描画するコードです。 よって、この時点でウインドウの内容が変化することはありません。 この処理は、描画したいビットマップの数だけ行うことになります。 バックバッファに対して全ての描画を終えたならば、 最後にバックバッファをウインドウにコピーします。 これにより、ウインドウにはバックバッファの内容が表示されることになります。 この処理は、一度だけ行うようにします。


戻る