EternalWindows
ビットマップ / タイマによるアニメーション

今回のプログラムは、ビットマップの輝度を表すための変数を用意し、 この変数を基にビットイメージのピクセルの成分を算出することで、 ビットマップの輝度が変更しているように見せかけています。 これら一連の処理がWM_TIMERで行われることにより、 ユーザーからは画像がフェードイン/フェードアウトしているように見えることになります。

#include <windows.h>

BYTE ArrangeBright(BYTE color, BYTE bright);
void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, BYTE bright);
LPBYTE GetBits(HBITMAP hbmp, int x, int y);
HBITMAP CreateBackbuffer(int nWidth, int nHeight);
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 int      nBright = 0;
	static UINT_PTR uTimerId = 0;
	static BOOL     bUp = TRUE;
	static HBITMAP  hbmpMem = NULL;
	static HDC      hdcBackbuffer = NULL;
	static HBITMAP  hbmpBackbuffer = NULL;
	static HBITMAP  hbmpBackbufferPrev = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		HDC hdc;

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

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

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

		uTimerId = SetTimer(hwnd, 1, 40, NULL);

		ReleaseDC(hwnd, hdc);

		return 0;
	}
	
	case WM_TIMER: {
		HDC    hdc;
		BITMAP bm;

		if (bUp) {
			nBright += 2;
			if (nBright >= 255) {
				bUp = FALSE;
				nBright = 255;
			}
		}
		else {
			nBright -= 2;
			if (nBright <= 0) {
				bUp = TRUE;
				nBright = 0;
			}
		}
		
		GetObject(hbmpMem, sizeof(BITMAP), &bm);

		CopyBits(hbmpBackbuffer, 0, 0, bm.bmWidth, bm.bmHeight, hbmpMem, (BYTE)nBright);

		hdc = GetDC(hwnd);

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

		ReleaseDC(hwnd, hdc);

		return 0;
	}

	case WM_PAINT: {
		HDC         hdc;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);

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

		EndPaint(hwnd, &ps);

		return 0;
	}

	case WM_DESTROY:
		if (hbmpMem != NULL)
			DeleteObject(hbmpMem);

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

		if (uTimerId != 0)
			KillTimer(hwnd, uTimerId);
		
		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

BYTE ArrangeBright(BYTE color, BYTE bright)
{
	if (bright > 128)
		color += ((255 - color) * (bright - 127)) / 128;
	else
		color = color * bright / 128;

	return color;
}

void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, BYTE bright)
{
	int    x, y;
	LPBYTE lpSrc, lpDest;

	for (y = 0; y < cy; y++) {
		lpSrc  = GetBits(hbmpSrc, 0, y);
		lpDest = GetBits(hbmpDest, xStart, yStart + y);
		for (x = 0; x < cx; x++) {
			lpDest[0] = ArrangeBright(lpSrc[0], bright);
			lpDest[1] = ArrangeBright(lpSrc[1], bright);
			lpDest[2] = ArrangeBright(lpSrc[2], bright);

			lpSrc  += 3;
			lpDest += 3;
		}
	}
}

LPBYTE GetBits(HBITMAP hbmp, int x, int y)
{
	BITMAP bm;
	LPBYTE lp;
	
	GetObject(hbmp, sizeof(BITMAP), &bm);

	lp = (LPBYTE)bm.bmBits;
	lp += (bm.bmHeight - y - 1) * ((3 * bm.bmWidth + 3) / 4) * 4;
	lp += 3 * x;

	return lp;
}

HBITMAP CreateBackbuffer(int nWidth, int nHeight)
{
	LPVOID           lp;
	BITMAPINFO       bmi;
	BITMAPINFOHEADER bmiHeader;

	ZeroMemory(&bmiHeader, sizeof(BITMAPINFOHEADER));
	bmiHeader.biSize      = sizeof(BITMAPINFOHEADER);
	bmiHeader.biWidth     = nWidth;
	bmiHeader.biHeight    = nHeight;
	bmiHeader.biPlanes    = 1;
	bmiHeader.biBitCount  = 24;

	bmi.bmiHeader = bmiHeader;

	return CreateDIBSection(NULL, (LPBITMAPINFO)&bmi, DIB_RGB_COLORS, &lp, NULL, 0);
}

プログラムを実行すると、ビットマップの輝度が常に変化しているのがわかります。 この輝度の変更の間隔、即ちアニメーションの処理を行う間隔は、 40ミリ秒として設定しています。

uTimerId = SetTimer(hwnd, 1, 40, NULL);

これにより、約40ミリ秒置きにWM_TIMER送られることになります。 この値を調整することは、アニメーションの速度を調整することと等価です。 uTimerIdでタイマ識別子を保存するのは、 タイマを作成したのかをWM_DESTROYで確かめるためです。

if (uTimerId != 0)
	KillTimer(hwnd, uTimerId);

タイマ識別子に0が割り当てられることはありません。 たとえ、SetTimerの第2引数に0を指定しても、戻り値は0以外の値となります。 よって、uTimerIdが0でないときはタイマが作成されていると判断できます。

WM_TIMERでは、ビットマップの輝度を表すnBrightという変数を常に更新します。 この変数の値が0のときはビットマップの輝度は真っ暗となり、 128のときは通常の輝度で、255のときは真っ白となります。

if (bUp) {
	nBright += 2;
	if (nBright >= 255) {
		bUp = FALSE;
		nBright = 255;
	}
}
else {
	nBright -= 2;
	if (nBright <= 0) {
		bUp     = TRUE;
		nBright = 0;
	}
}

bUpがTRUEのときはnBrightが増え続け、ビットマップは明るくなっていきます。 255を超えるときはbUpをFALSEとすることにより、 ビットマップの輝度を下がることになります。 この処理ではnBrightを2つずつ調整していますが、 1つずつ調整したほうが輝度の変更は滑らかに見えることでしょう。

CopyBitsでは、brightを基にピクセルをバックバッファにコピーします。 この関数が呼び出しているArrangeBrightという関数は、 brightとピクセルの成分を計算し、新しい成分の値を返します。 ArrangeBrightの内部は、次のようになっています。

BYTE ArrangeBright(BYTE color, BYTE bright)
{
	if (bright > 128)
		color += ((255 - color) * (bright - 127)) / 128;
	else
		color = color * bright / 128;

	return color;
}

この関数は、brightの値を元にcolor、即ち新しい色を決めるため、 colorの値は0から255に収束するよう設計されています。 else文の方は、単純に色と輝度を掛けることで、brightの値が128でない限り、 colorの値は減少することになりますから、ビットマップは段々と暗くなります。

WM_TIMERの最後の処理は、CopyBitsで更新したバックバッファを ウインドウに表示することです。

hdc = GetDC(hwnd);

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

ReleaseDC(hwnd, hdc);

人によってはここでの処理をInvalidateRectの呼び出しに置き換え、 WM_PAINTを新しいバックバッファの描画として働かそうと考えるかもしれませんが、 恐らくそれは好ましいことではないと思われます。 WM_TIMERは周期的に送られるメッセージですから、そこでのInvalidateRectの呼び出しは、 WM_PAINTの生成を周期的に行っているのと同じことです。 メッセージのポストは内部でメモリの確保が行われているため、 パフォーマンス上優れているとは言い難いでしょう。 また、メッセージには優先順位というものがあり、WM_PAINTはこの優先順位が低いため、 その他のメッセージがキューにある場合は、そちらが優先して取得されることになります。 結果として、バックバッファの描画が遅れることになりかねないのです。

メッセージの優先順位といえばWM_TIMERも関係のない話であり、 このメッセージは最も優先順位が低く設定されています。 そのため、先のWM_PAINTと似たような事が起こることになり、 希望していたタイムアウト値の間隔で、WM_TIMERを処理できる保障はありません。 もし、メッセージキューからWM_TIMERを取得する前に、 次のWM_TIMERが生成されたような場合は、既にあるキューのメッセージが 新しいメッセージによって置き換えられることになります。 つまり、複数のWM_TIMERがメッセージキューに存在することは有り得ません。 このメッセージの置き換えは、WM_PAINTのときでも行われています。 KillTimerはタイマを破棄する関数ですが、 それに伴ってメッセージキュー内のWM_TIMERが削除されることはありません。


戻る