EternalWindows
ビットマップ / ビットマップフォーマット

前節は、ビットマップファイルからBITMAPINFOHEADER構造体までを読み取りました。 今回は、その次にあるビットマップファイル最後のデータ、 ビットイメージを読み取りたいと思います。

dwSizeImage = bmfHeader.bfSize - bmfHeader.bfOffBits;
lpBits = (LPVOID)HeapAlloc(GetProcessHeap(), 0, dwSizeImage);
ReadFile(hfile, lpBits, dwSizeImage, &dwResult, NULL);

ビットイメージのサイズは、ビットマップのサイズによって変化するため、 まずビットイメージを格納できるだけのメモリを確保することになります。 HeapAllocは、第3引数で指定したバイト分のメモリを返す関数で、 ここではbmfHeader.bfSize - bmfHeader.bfOffBitsとしています。 bfOffBitsは、いわばファイルの先頭からビットイメージまで要しているサイズであり、 それをファイル全体のサイズから減算すれば、ビットイメージのサイズを特定できます。 他にも、ビットマップの幅と高さを利用して次のようにサイズを求めることもできます。

dwSizeImage = bm.bmHeight * ((3 * bm.bmWidth + 3) / 4) * 4;
lpBits = (LPVOID)HeapAlloc(GetProcessHeap(), 0, dwSizeImage);
ReadFile(hfile, lpBits, dwSizeImage, &dwResult, NULL);

このコードはビットマップの幅と高さを掛け、さらに24ビットビットマップが1つの色を 3バイトで表すことを意識して3を掛けています。 このコードは全く問題ないものですが、256色ビットマップには 1バイトがカラーテーブルのインデックスという役割があるため、 そのようなときには3を掛けないように調整する必要があります。 なお、BITMAPINFOHEADER構造体には、 biSizeImageというビットイメージのサイズを格納するメンバがありますが、 このメンバは主に圧縮されてビットマップの場合に初期化されるため、 それ以外の場合は0が格納されている可能性があります。

さて、カラーテーブルという言葉が登場しましたが、 これはビットマップファイルのどこに存在するのでしょうか。 そもそも、カラーテーブルは24ビットビットマップには存在しないものですから、 ビットマップのフォーマット自体がビット数によって異なっているのではないでしょうか。 こう考えると、ビット数に応じて異なる処理をしなければならないように思えますが、 次に示すフォーマットの表からも分かるように、実際には1個の処理を追加するだけで済みます。

24ビットビットマップ
BITMAPFILEHEADER構造体
BITMAPINFOHEADER構造体
ビットイメージ
256色ビットマップ
BITMAPFILEHEADER構造体
BITMAPINFOHEADER構造体
カラーテーブル
ビットイメージ

各フォーマットの違いは、カラーテーブルがあるかどうかの話ですが、 それがビットイメージの前にあるというところが重要です。 よって、ビットイメージを読み取る前にビット数の確認を行い、 24ビットビットマップでない場合はカラーテーブルを読み取らなければなりません。

ReadFile(hFile, &bmiHeader, sizeof(BITMAPINFOHEADER), &dwResult, NULL);
if (bmiHeader.biBitCount < 24) {
	lpRgb = (LPRGBQUAD)HeapAlloc(GetProcessHeap(), 0, sizeof(RGBQUAD) * 256);
	ReadFile(hfile, lpRgb, sizeof(RGBQUAD) * 256, &dwResult, NULL);
}

このようにすれば、if文の後のビットイメージを読み取る処理の際では、 24ビットビットマップのときと同じように振舞うことができます。

ビットイメージを取得したら、いよいよそれをバックバッファにコピーすることになります。 今回のように、事前にBITMAPINFOHEADER構造体を取得しているよう場合、 StretchDIBitsという関数を呼び出せば簡単にコピーを実現できます。

int StretchDIBits(
  HDC hdc,
  int XDest,
  int YDest,
  int nDestWidth,
  int nDestHeight,
  int XSrc,
  int YSrc,
  int nSrcWidth,
  int nSrcHeight,
  CONST VOID *lpBits,
  CONST BITMAPINFO *lpBitsInfo,
  UINT iUsage,
  DWORD dwRop
);

hdcは、コピー先のデバイスコンテキストのハンドルを指定します。 XDestは、コピー先長方形の左上隅のx、YDestは左上隅のy座標を指定します。 nDestWidthは、コピー先長方形の幅、nDestHeightは高さを指定します。 XSrcは、DIB内のコピー元長方形の左上隅のx、YSrcはy座標を指定します。 nSrcWidthは、DIB内のコピー元長方形の幅、nSrcHeightは高さを指定します。 lpBitsは、ビットイメージを格納しているアドレスを指定します。 lpBitsInfoは、BITMAPINFO構造体のアドレスを指定します。 iUsageは、現在では多くの場合、DIB_RGB_COLORS定数を指定することになるでしょう。 dwRopは、SRCCOPYなどのラスタオペレーションコードを指定します。

StretchDIBitsに、コピー元のハンドルを指定する引数がないことを意識してください。 ファイル操作で明示的にデータを取得するようなデバイスを介さない設計では、 メモリデバイスコンテキストやビットマップハンドルの出るところではありません。 また、StretchDIBitsはカラーキー(透明にする色)を指定する引数がありませんから、 背景を透過して描画したいような場合は、以前扱ったCopyBitsのような関数が必要になるでしょう。 BITMAPINFO構造体は、次のように定義されています。

typedef struct tagBITMAPINFO { 
  BITMAPINFOHEADER bmiHeader; 
  RGBQUAD          bmiColors[1]; 
} BITMAPINFO, *PBITMAPINFO; 

StretchDIBitsがBITMAPINFOHEADERではなくこの構造体を受け取るのは、 カラーテーブルを持つビットマップの表示にも対応させるためです。 このような設計のおかがで、24ビットマップとそうでないビットマップの表示の違いは、 bmiColorsを初期化するだけであるという簡略化が可能になっているのです。 その肝心のbmiColorsですが、奇怪なことに配列の要素数が1となっています。 これは、BITMAPINFOHEADER構造体の後にカラーテーブルを用意せよという意味で、 1という数字自体には特に意味があるわけではありません。

lpbmInfo = (LPBITMAPINFO)HeapAlloc(GetProcessHeap(), 0, sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 256);

このようにすれば、BITMAPINFOHEADER構造体とカラーテーブルを格納できるだけのメモリを確保できるため、 後は各メンバに実際のデータをコピーすればよいことになります。

今回のプログラムは、先に述べたStretchDIBitsでビットマップを表示します。 読み取り対象となるビットマップは、24ビットでも256色でも問題ありません。

#include <windows.h>

BOOL ReadBitmap(LPTSTR lpszFileName, LPBITMAPINFO *lplpbmInfo, LPVOID *lplpBits);
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 HDC          hdcBackbuffer = NULL;
	static HBITMAP      hbmpBackbuffer = NULL;
	static HBITMAP      hbmpBackbufferPrev = NULL;
	static LPVOID       lpBits = NULL;
	static LPBITMAPINFO lpbmInfo = NULL;

	switch (uMsg) {

	case WM_CREATE: {
		HDC hdc;
		
		if (!ReadBitmap(TEXT("sample.bmp"), &lpbmInfo, &lpBits))
			return -1;

		hdc = GetDC(hwnd);

		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: {
		int         nWidth, nHeight;
		HDC         hdc;
		PAINTSTRUCT ps;

		hdc = BeginPaint(hwnd, &ps);

		nWidth = lpbmInfo->bmiHeader.biWidth;
		nHeight = lpbmInfo->bmiHeader.biHeight;
		StretchDIBits(hdcBackbuffer, 0, 0, nWidth, nHeight, 0, 0, nWidth, nHeight, lpBits, lpbmInfo, DIB_RGB_COLORS, SRCCOPY);

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

		EndPaint(hwnd, &ps);

		return 0;
	}

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

		if (lpBits != NULL)
			HeapFree(GetProcessHeap(), 0, lpBits);
		if (lpbmInfo != NULL)
			HeapFree(GetProcessHeap(), 0, lpbmInfo);
		
		PostQuitMessage(0);

		return 0;

	default:
		break;

	}

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

BOOL ReadBitmap(LPTSTR lpszFileName, LPBITMAPINFO *lplpbmInfo, LPVOID *lplpBits)
{
	int              nColor;
	HANDLE           hFile;
	DWORD            dwResult;
	LPVOID           lpBits;
	LPBITMAPINFO     lpbmInfo;
	BITMAPFILEHEADER bmfHeader;
	BITMAPINFOHEADER bmiHeader;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		MessageBox(NULL, TEXT("ファイルのオープンに失敗しました。"), NULL, MB_ICONEXCLAMATION | MB_OK);
		return FALSE;
	}
	
	ReadFile(hFile, &bmfHeader, sizeof(BITMAPFILEHEADER), &dwResult, NULL);
	if (bmfHeader.bfType != *(LPWORD)"BM") {
		MessageBox(NULL, TEXT("ビットマップファイルではありません。"), NULL, MB_ICONEXCLAMATION | MB_OK);
		CloseHandle(hFile);
		return FALSE;
	}
	
	ReadFile(hFile, &bmiHeader, sizeof(BITMAPINFOHEADER), &dwResult, NULL);
	if (bmiHeader.biBitCount < 24) {
		if (bmiHeader.biClrUsed == 0)
			nColor = 1 << bmiHeader.biBitCount;
		else
			nColor = bmiHeader.biClrUsed;
		
		lpbmInfo = (LPBITMAPINFO)HeapAlloc(GetProcessHeap(), 0, sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColor);
		ReadFile(hFile, &lpbmInfo->bmiColors, sizeof(RGBQUAD) * nColor, &dwResult, NULL);
	}
	else
		lpbmInfo = (LPBITMAPINFO)HeapAlloc(GetProcessHeap(), 0, sizeof(BITMAPINFOHEADER));

	lpbmInfo->bmiHeader = bmiHeader;

	lpBits = (LPVOID)HeapAlloc(GetProcessHeap(), 0, bmfHeader.bfSize - bmfHeader.bfOffBits);
	ReadFile(hFile, lpBits, bmfHeader.bfSize - bmfHeader.bfOffBits, &dwResult, NULL);
	
	*lplpBits = lpBits;
	*lplpbmInfo = lpbmInfo;

	CloseHandle(hFile);
	
	return TRUE;
}

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

まず、WindowProcで宣言されている静的変数を確認します。

static LPVOID lpBits   = NULL;
static LPBITMAPINFO lpbmInfo = NULL;

lpBitsは、ビットイメージの先頭を指すアドレスです。 これまでのプログラムは、GetObjectで必要に応じてビットイメージを取得していましたが、 今回はビットマップハンドルがないため明示的に宣言することになります。 この変数とlpbmInfoをReadBitmapで適切に初期化すれば、 StretchDIBitsにそのまま加工せず指定することができます。 次のコードは、ReadBitmapのカラーテーブルを取得する部分です。

if (bmiHeader.biBitCount < 24) {
	if (bmiHeader.biClrUsed == 0)
		nColor = 1 << bmiHeader.biBitCount;
	else
		nColor = bmiHeader.biClrUsed;
	
	lpbmInfo = (LPBITMAPINFO)HeapAlloc(GetProcessHeap(), 0, sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * nColor);
	ReadFile(hFile, &lpbmInfo->bmiColors, sizeof(RGBQUAD) * nColor, &dwResult, NULL);
}

まず、24ビットビットマップかの確認を行い、そうでなければカラーテーブルを読み取る処理に入ります。 biClrUsedメンバは、そのカラーテーブルのエントリ数を表すもので、 この値が0でない場合はbiClrUsedの値がカラーテーブルのエントリ数となります。 0である場合は、たとえばbiBitCountが8ならば最大の256ということになりますから、 1 << bmiHeader.biBitCountという式を書くことにより、 その最大値を算出します。

else
	lpbmInfo = (LPBITMAPINFO)HeapAlloc(GetProcessHeap(), 0, sizeof(BITMAPINFOHEADER));

lpbmInfo->bmiHeader = bmiHeader;

lpBits = (LPVOID)HeapAlloc(GetProcessHeap(), 0, bmfHeader.bfSize - bmfHeader.bfOffBits);
ReadFile(hfile, lpBits, bmfHeader.bfSize - bmfHeader.bfOffBits, &dwResult, NULL);

このelse分は、24ビットビットマップの処理です。 24ビットビットマップにはカラーテーブルが存在しないため、 単純にBITMAPINFOHEADER構造体を格納できるだけのメモリを確保するだけで十分です。 後はビット数を問わず同じ処理となり、BITMAPINFOHEADER構造体をメモリにコピーし、 ビットイメージを読み取るだけです。 当然ながら、StretchDIBitsの呼び出し方もビット数を問わず同じとなります。

nWidth = lpbmInfo->bmiHeader.biWidth;
nHeight = lpbmInfo->bmiHeader.biHeight;
StretchDIBits(hdcBackbuffer, 0, 0, nWidth, nHeight, 0, 0, nWidth, nHeight, lpBits, lpbmInfo, DIB_RGB_COLORS, SRCCOPY);

これまでのプログラムがビットイメージのコピーを自作関数で行っていたのに対して、 上記コードではStretchDIBitsで行っている点を意識してください。 StretchDIBitsには背景の透過という機能はないものの、 ビット数を問わず同じ方法でビットイメージをコピーできるという特徴があります。

さて、2節に渡ってビットマップのフォーマットを調べてきましたが、 このような長い手順を踏んでまで、データ取得することに何か意味があるのでしょうか。 たとえば、いつか256色ビットマップを表示したときには、 そのためだけにビットマップハンドルをメモリデバイスコンテキストに選択して GetDIBColorTableを呼び出していましたが、今回はそのようなことは全くしていません。 直接ファイルからデータを読む込む設計であるため、 デバイスが定義している抽象的な方法に捉われることはないわけです。 また、ビットマップ系の関数にはビットマップを保存する機能を持つものはないため、 このようなときはビットマップのフォーマットを基にファイルにデータを書き込むことになります。


戻る