EternalWindows
ビットマップ / DIBセクション

DIB(device-independent bitmap)とは、デバイスに依存しないビットマップのことです。 デバイスに依存するというのは、そのビットマップが他のデバイスでは使えない、 つまり、特定のデバイスのみ有効になるということです。 デバイスに依存するビットマップは、DDB(device-dependent bitmap)と呼ばれ、 たとえ、DDBをファイルに保存しても、それは特定のデバイス上では ビットマップではありません。 デバイスが固有の方法でビットマップを表現しているため、 他のデバイスからは、その表現方法が解釈できないのです。 このように、ビットマップがデバイスに依存することを避けるために導入されたのが DIBであり、現在の拡張子bmpのビットマップはDIB形式であるため、 この形式に従ったビットマップの扱い方は、どのデバイスでも有効となります。

これまで、LoadImageやCreateCompatibleBitmapで扱ってきたビットマップは全てDDBです。 DDBからビットイメージを取得できないのは、 ビットマップのピクセルというものをデバイス固有の方法で定義しているからです。 前節でLoadImageの最後の引数にLR_CREATEDIBSECTIONフラグを指定したのは、 ビットマップをDIBとしてロードするためだったのです。 バックバッファのビットイメージにアクセスしたいのであれば、 CreateCompatibleBitmapを呼び出すわけにはいきません。 DIBを作成するCreateDIBSectionを呼び出すことになります。

HBITMAP CreateDIBSection(
  HDC hdc,
  CONST BITMAPINFO *pbmi,
  UINT iUsage,
  VOID **ppvBits,
  HANDLE hSection,
  DWORD dwOffset
);

hdcは、デバイスコンテキストのハンドルを指定できますが、通常はNULLを指定します。 pbmiは、BITMAPINFO構造体のアドレスを指定します。 この構造体に、DIBの幅や高さを指定することになります。 iUsageは、DIB_RGB_COLORS定数を指定します。 ppvBitsは、ビットイメージの先頭アドレスが返ります。 残りの2つの引数は、まず使用することがないのでNULLを指定することになるでしょう。

CreateDIBSectionはDIBを作成するわけですが、 それではDIBセクションとは一体何なのでしょうか。 これは恐らく、DDBとDIBを相互参照できるビットマップハンドルのことを 意味していると思われます。 RectangleのようなGDI関数は、デバイスコンテキストのハンドルを要求しますが、 ここにDIBセクションが関連付けたメモリデバイスコンテキストを指定した場合、 長方形は、実際にビットイメージに反映されます。

色々と難しいことを述べてきましたが、結論としては、 バックバッファのビットイメージにアクセスしたければ、 CreateCompatibleBitmapではなくCreateDIBSectionを呼び出すというだけのことです。 今回のプログラムは、バックバッファをCreateDIBSectionで作成し、 このビットイメージにビットマップの背景を除いた部分をコピーします。

#include <windows.h>

void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, COLORREF crTransparent);
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 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);

		ReleaseDC(hwnd, hdc);

		return 0;
	}

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

		hdc = BeginPaint(hwnd, &ps);

		CopyBits(hbmpBackbuffer, 0, 0, bm.bmWidth, bm.bmHeight, hbmpMem, RGB(0, 255, 0));

		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);
		}
		
		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, COLORREF crTransparent)
{
	int    x, y;
	BYTE   r, g, b;
	LPBYTE lpSrc, lpDest;
	
	b = (BYTE)(GetBValue(crTransparent));
	g = (BYTE)(GetGValue(crTransparent));
	r = (BYTE)(GetRValue(crTransparent));	

	for (y = 0; y < cy; y++) {
		lpSrc  = GetBits(hbmpSrc, 0, y);
		lpDest = GetBits(hbmpDest, xStart, yStart + y);
		for (x = 0; x < cx; x++) {
			if (lpSrc[0] != b || lpSrc[1] != g || lpSrc[2] != r) {
				lpDest[0] = lpSrc[0];
				lpDest[1] = lpSrc[1];
				lpDest[2] = lpSrc[2];
			}
			
			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);
}

BITMAPINFOHEADER構造体には、DIBの情報を設定します。 biPlanesは必ず1とし、biBitCountは24ビットにしておきます。 CreateDIBSectionが要求するのは、BITMAPINFOHEADER構造体ではなく BITMAPINFO構造体です。 この構造体はメンバとしてBITMAPINFOHEADER構造体を持つので、 単純にそれを代入するだけで構いません。 CreateDIBSectionの第4引数には、ビットイメージの先頭アドレスが返るのですが、 CreateBackbufferはこのアドレスを呼び出し側に返していません。 勿論、返してもよいのですが、このアドレスはGetObjectを呼び出すことにより いつでも取得可能なので、必要に応じて取得する方法を選びました。

WM_PAINTでは、ビットマップをバックバッファに描画して、 その後にバックバッファをデバイスコンテキストにコピーするという流れになります。 ビットマップの描画は、ビットイメージのコピーによって実装しています。

CopyBits(hbmpBackbuffer, 0, 0, bm.bmWidth, bm.bmHeight, hbmpMem, RGB(0, 255, 0));

この自作関数は、第6引数のビットマップのビットイメージを 第1引数のビットイメージにコピーします。 第2引数と第3引数はコピー位置のXとY、 第4引数と第5引数はコピーする幅と高さです。 第7引数は、透過したい色であり、この色のピクセルはコピーされることはありません。 基本的には、ビットマップの背景色を指定することになるでしょう。 この関数は、コピー元の位置を指定する引数はないので、 コピー元の開始位置を(0, 0)と定めています。

void CopyBits(HBITMAP hbmpDest, int xStart, int yStart, int cx, int cy, HBITMAP hbmpSrc, COLORREF crTransparent)
{
	int    x, y;
	BYTE   r, g, b;
	LPBYTE lpSrc, lpDest;
	
	b = (BYTE)(GetBValue(crTransparent));
	g = (BYTE)(GetGValue(crTransparent));
	r = (BYTE)(GetRValue(crTransparent));	

	for (y = 0; y < cy; y++) {
		lpSrc  = GetBits(hbmpSrc, 0, y);
		lpDest = GetBits(hbmpDest, xStart, yStart + y);
		for (x = 0; x < cx; x++) {
			if (lpSrc[0] != b || lpSrc[1] != g || lpSrc[2] != r) {
				lpDest[0] = lpSrc[0];
				lpDest[1] = lpSrc[1];
				lpDest[2] = lpSrc[2];
			}
			
			lpSrc  += 3;
			lpDest += 3;
		}
	}
}

Destはコピー先を表し、Srcはコピー元を表します。 GetBitsという自作関数は、引数で指定した位置を指すビットイメージのアドレスを返します。 この関数の便利なところは、呼び出し側がビットイメージのボトムアップ形式を 意識することのないよう、設計されているところです。 つまり、y = 0のときは、ビットマップの最下位の行を表すのではなく、 本当に先頭の行を表しているのです。 lpSrcのGetBitsはyの開始位置が0になっています。 これは、CopyBitsがコピー開始位置のXを0とする設計になっているからです。 lpDestは、コピー先を決めなければならないので、引数で指定された位置を基準とします。 透過描画を行っているのは、次の部分です。

if (lpSrc[0] != b || lpSrc[1] != g || lpSrc[2] != r) {
	lpDest[0] = lpSrc[0];
	lpDest[1] = lpSrc[1];
	lpDest[2] = lpSrc[2];
}

b、g、rは、crTransparentの各成分を格納しています。 crTransparentは透過すべき色を表しますから、 現在指しているピクセルがこの色と一致しない場合だけコピーを行います。 ちなみに、コピーは次のように行うこともできます。

CopyMemory(lpDest, lpSrc, 3);

CopyMemoryという関数は、第2引数のアドレスから第3引数に指定したバイトだけ、 データを第1引数にコピーする関数です。 ビットイメージが単純なピクセルの配列であるため、このようなことが可能なのです。

続いて、GetBitsの内部を見ていきます。 この関数の目的は、第1引数のビットマップからビットイメージを取得し、 そのビットイメージを引数で指定された分だけ移動します。 少し複雑なので順に見ていきましょう。

lp = (LPBYTE)bm.bmBits;

これは、単純にビットイメージの先頭アドレスをコピーしているだけです。 このlpを適切な位置まで移動することになります。

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

これは、lpをyで示される位置まで移動させるコードですが、 2つに分けて考えていきましょう。

(bm.bmHeight - y - 1)

既に述べてきたように、ビットイメージはボトムアップ形式で格納されており、 y = 0は、ビットマップの最下位の行を表すことになります。 しかし、このyをビットマップの高さを基準として計算することにより、 呼び出し側が希望する位置へと変換することができます。 bm.bmHeightが96だったとして、実際にアクセスできるのは0から95までであるため、 本来の高さから-1だけ減算することを忘れてはいけません。

((3 * bm.bmWidth + 3) / 4) * 4;

前節では、3 * bm.bmWidth を実行するだけでしたが、 今回は4を掛けて4で割るという奇怪な式を用いています。 実は、ビットイメージの一行は、必ず4の倍数でなければならないという決まりがあり、 4の倍数でない場合、たとえばbm.bmWidthが5である場合は、 強引に4の倍数にするために、値0の3バイトを加えて、一行を8バイトにしなければなりません。 つまり、単純に 3 * bm.bmWidth を実行しても、ビットマップの幅が4の倍数でなければ、 次の行まで移動するための値を導くことができないのです。 bm.bmWidthが5だとして上記の式に当てはめて計算すると次のようになります。

5  * 3 = 15 // 4の倍数ではない
15 + 3 = 18
18 / 4 = 4
4  * 4 = 16 // 4の倍数

上記の式では、bm.bmWidthを5として実行していますが、6であれ7であれ16に収束します。 この式を用いれば、ビットマップの幅が4の倍数でなくても、 ビットイメージの一行に要しているバイト数を算出することができます。

lp += 3 * x;

既に適切な行まで移動したので、後は指定された分だけ移動するだけです。 24ビットは、1つの色に3バイト要するので、3を掛けるのを忘れてはいけません。


戻る