EternalWindows
ビットマップ / ビットイメージ

これまで呼び出してきたBitBltやStretchBltは、ビットマップを1つの長方形として扱い、 これを指定した座標にコピーする機能を提供していました。 この機能の最大の利点は、アプリケーションがビットマップの1ピクセルずつを明示的にコピーしなくても済むというものですが、 場合によってはこれが不都合になることもあります。 たとえば、キャラクターのようなビットマップには背景色というコピーされてはならない色がありますから、 このような場合はビットマップを長方形としてコピーするわけにはいきません。 ビットマップのピクセルへアクセスし、背景色でないピクセルのみを明示的にコピーする必要があります。

ビットマップを構成する各ピクセルの色は、ビットイメージ(またはピクセルビット)と呼ばれる配列に格納されています。 よって、この配列にアクセスすれば、ビットマップの任意の位置の色を取得することができます。 ビットイメージは、BITMAP構造体のbmBitsメンバから参照することができます。

LPBYTE lp;
BITMAP bm;

GetObject(hbmp, sizeof(BITMAP), &bm);
lp = (LPBYTE)bm.bmBits; // lpはビットイメージの先頭を指すことになる

lp[0] = 255;
lp[1] = 0;
lp[2] = 0;

ビットマップにはビット数というものがあり、今日のビットマップの多くは、 8ビットか24ビット(フルカラー)を採用しています。 24ビットのビットマップの場合、1つのピクセルを表すのに3バイト(24ビット)必要で、 ビットイメージ上では、青、緑、赤の順に並んでいます(RGBの順ではないことに注意)。 lp[0]に255を代入するということは、青の成分を最高にするということです。 lp[1]の緑とlp[2]の赤がそれぞれ0であることから、 上記コードがピクセルの色の青にしていることが分かりますが、 一体、どの座標のピクセルを青にしたのでしょうか。 lpがビットイメージの先頭を指しているとなると、 ビットマップの(0, 0)のピクセルを青にしたのでしょうか。 実はそうではありません。

ビットマップは、ビットイメージの格納としてボトムアップ形式を採用しています。 ボトムアップとは、単純に解釈すれば、下から上ということになりますが、 左下隅のピクセルから格納される形式というふうに、考えたほうがよいでしょう。 ビットマップのサイズが(96, 96)ならば、lpは(0, 95)のピクセルを指しています。 つまり、先に示したコードは(0, 95)のピクセルを青にするという処理だったのです。 隣のピクセル(1, 95)を青にしたい場合は、以下のようになるでしょうか。

lp[4] = 255;
lp[5] = 0;
lp[6] = 0;

個々のピクセルは、3バイト境界であるため、 lp[4]は(1, 95)の青の成分を表すことになります。 しかし、上記コードよりも以下のようなコードの方が分かりやすいと思われます。

lp += 3;

lp[0] = 255;
lp[1] = 0;
lp[2] = 0;

個々のピクセルは、3バイト境界であるため、 3バイト進めば、次のピクセルを参照することになります。

それでは、1つ上の行(0, 94)にアクセスするにはどうするのでしょか。 上記のビットマップだと幅が96であるため、一行に要しているバイトは 96 × 3 となります。 1つの色を表すのに3バイト要するわけですから、3を掛けるのです。 この 96 × 3 の分だけポインタを進めれば、1つ上の行(0, 94)を指すことができます。

lp = (LPBYTE)bm.bmBits + (y * (bm.bmWidth * 3));

yを掛けているのが巧妙なところで、このyを変更することにより、 任意の行の先頭を指すことが可能となります。 実は上記の式には、幅が4の倍数であるビットマップしか正しく動作しないという 問題があるのですが、それに関しては次節で考慮します。

今回のプログラムは、ビットマップのビットイメージを取得し、 実際に個々のピクセルの色を変更します。

#include <windows.h>

void ChangeBits(HBITMAP hbmp);
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_CREATEDIBSECTION | 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);

		ChangeBits(hbmpMem);

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

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

	for (y = 0; y < bm.bmHeight / 2; y++) {
		lp = (LPBYTE)bm.bmBits + (y * (bm.bmWidth * 3));
		for (x = 0; x < bm.bmWidth; x++) {
			lp[0] = lp[0] / 2;
			lp[1] = lp[1] / 2;
			lp[2] = lp[2] / 2;

			lp += 3;
		}
	}
}

ビットイメージにアクセスするには、LoadImageの第6引数に、 LR_CREATEDIBSECTIONを指定しなければなりません。

hbmpMem = (HBITMAP)LoadImage(..., LR_CREATEDIBSECTION | LR_LOADFROMFILE);

これにより、BITMAP構造体のbm.bmBitsメンバには、 ビットイメージへのアドレスが格納されることになります。 ChangeBitsという自作関数で、ビットイメージにアクセスしています。

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

	for (y = 0; y < bm.bmHeight / 2; y++) {
		lp = (LPBYTE)bm.bmBits + (y * (bm.bmWidth * 3));
		for (x = 0; x < bm.bmWidth; x++) {
			lp[0] = lp[0] / 2;
			lp[1] = lp[1] / 2;
			lp[2] = lp[2] / 2;

			lp += 3;
		}
	}
}

lpのアドレスを調整する方法は、先に述べた通りです。 yはビットマップの高さの半分だけ増加し、xはビットマップの幅だけ増加するため、 ビットマップの下半分のピクセルを変更することになります。 下半分となるのは、ビットイメージがボトムアップ形式で表現されているからです。

プログラムを実行してみると分かりますが、ビットマップの下半分のピクセルは、 特定の色となったわけではなく、本来の色が薄暗くなっているだけです。 これは、新しいピクセルの色の算出に、既存のピクセルを基にしているからです。

lp[0] = lp[0] / 2;
lp[1] = lp[1] / 2;
lp[2] = lp[2] / 2;

既存のピクセルの色を減らして代入するとは、 その色の成分が減っていく、つまり0に近づくということです。 全ての色が0に近づくということは、ピクセルの色が黒になるということなので、 薄暗く見えるわけです。

さて、ビットイメージにアクセスする方法は分かったところで、 ビットマップをどのように透過描画することになるのかを考えてみましょう。 個々のピクセルの色はlp[0]、lp[1]、lp[2]を見れば分かるので、 そのピクセルが背景色であるかどうかの判別は可能です。 となると、背景色でないピクセルのみをバックバッファにコピーすれば、 結果的に、ビットマップを透過描画したことになるはずです。 バックバッファにコピーするとは、バックバッファのビットイメージを取得し、 そのビットイメージに、ビットマップの一部のビットイメージをコピーするということです。 問題は、バックバッファのビットイメージをどう取得するかですが、 ここにDIBなるものが大きく関わってくるのです。


戻る