と、言うワケで(どーゆーワケだい)、Windowsでビットマップイメージ描画するための知識をひけらかす事にする。

WindowsといってもWindows3.1とかの16bitなヤツは無視なのでDDBも無視。DIBしか説明しない。あと、自分はこの辺りに関して完璧な知識を持っているワケでもないので多少間違っているかもしれぬ。

あと基礎知識として、Windowsでは以下の基本データ型が存在する事を書いておく。

BYTE型…実体はunsigned charで8bitのデータ
WORD型…同じくunsigned shortで16bitのデータ
DWORD型…とにかく32bitの符号無しデータ


とにかく、Windowsでビットマップイメージを描画しようと思った場合、StretchDIBits()というAPIを使う以外無いと思っておくべし。って事でStretchDIBits()APIの書式なんぞ参照する。

int StretchDIBits( HDC hdc,				// handle to device context
           int XDest,				// x-coordinate of upper-left corner of dest. rectangle
           int YDest,				// y-coordinate of upper-left corner of dest. rectangle
           int nDestWidth,			// width of destination rectangle
           int nDestHeight,			// height of destination rectangle
           int XSrc,				// x-coordinate of upper-left corner of source rectangle
           int YSrc,				// y-coordinate of upper-left corner of source rectangle
           int nSrcWidth,			// width of source rectangle
           int nSrcHeight,			// height of source rectangle
           CONST VOID *lpBits,			// address of bitmap bits
           CONST BITMAPINFO *lpBitsInfo,	// address of bitmap data
           UINT iUsage,				// usage flags
           DWORD dwRop				// raster operation code
          );

hdcは、コメントに「デバイスコンテキストへのハンドル」とか英語で書かれているが、この「デバイス」ってのは正確には「GDIデバイス」という類のモノで、ディスプレイドライバやプリンダドライバに対してアクセスするためのナニだ。つまり、このhdcがディスプレイドライバのモノだとビットマップイメージはディスプレイに表示され、プリンタドライバだったらプリンタからビットマップイメージが印刷される(印刷は微妙に事情が異なるが無視する)。ディスプレイドライバへのhdcは、Windowsの画面更新メッセージ(WM_PAINTメッセージ)が発生した時にBeginPaint()APIで取得したり、GetDC()APIで取得したりする。ただし、これらのAPIで取得したhdcはWindows上の限られたリソースなので取得しっぱなしにはできない。使いたい時だけ取得して、スグに開放する必要があり、それぞれEndPaint()APIやReleaseDC()APIを使う。

StretchDIBits()はビットマップイメージの拡大縮小もサポートしているので、XDestからnSrcHeightまでの引数は「そーゆーもの」と知るべし。

次にlpBitsだ。コメントにもあるように、これはビットマップイメージデータそのものへのポインタだ。で、この中身のフォーマットがどうなっているのか等々の情報はlpBitsInfoの指すBITMAPINFO構造体である。

BITMAPINFO構造体の中身はBITMAPINFOHEADER構造体という長たらしい名前の構造体とRGBQUAD構造体という24bitのRGB値を保存する配列が1要素のみである。これは1個で用が足りるという意味ではなくて、可変長で使ってくれという意味だ。そもそも1bpp、4bpp、8bppビットマップイメージ時のパレット情報が格納されるメンバなので、最低でも2要素、最高で256要素必要だ。したがって、例えば256個パレット付きのBITMAPINFO構造体をアロケートするなら以下のようになる。

p = (BITMAPINFO *)malloc( sizeof(BITMAPINFOHEADER) + (sizeof(RGBQUAD) * 256) );

iUsageは後で説明する。

dwRopは、ビットマップイメージをhdc上のイメージとどのように合成するかを指定する。普通はSRCCOPYしか使わないが、やろうと思えばマスクしてから合成みたいな事もできる(マスクと合成の2回StretchDIBits()する必要があるが)。


という事でBITMAPINFOHEADER構造体を説明してみようと思う。以下は例によってヘルプからの引用だ。

typedef struct tagBITMAPINFOHEADER{ // bmih
	DWORD	biSize;
	LONG	biWidth;
	LONG	biHeight;
	WORD	biPlanes;
	WORD	biBitCount;
	DWORD	biCompression;
	DWORD	biSizeImage;
	LONG	biXPelsPerMeter;
	LONG	biYPelsPerMeter;
	DWORD	biClrUsed;
	DWORD	biClrImportant;
} BITMAPINFOHEADER; 

biSizeは、この構造体自体の大きさ(バイト数)を格納する。どうしてこーゆー事をするかと言うと、所謂バージョンチェックでWindowsのAPIはこの手法が大好きで各所で使用している。古くはOS/2のビットマップ情報との区別のため、最近では、BITMAPV4HEADER構造体(Windows95,WindowsNT4.0以降)とかBITMAPV5HEADER構造体(Windows98以降)等の新手との区別に使う。これら最新の構造体ではJPEGの表示なんかもサポートされているようだが使った事が無いのでよくわからない。という事で、biSizeにはsizeof(BITMAPINFOHEADER)を入れておくべし。

biWidthやbiHeightはビットマップイメージの幅と高さのドット数だ。biHeightについては後にまた話題に上る事になるだろう。

biPlanesは、Microsoftが将来レイヤー機能を持たせたいと考えていた名残で、いつまで経っても1プレーンしかサポートされないので「常に1をセット」だ。

biBitCountは、ビットマップイメージが何bppのイメージかを指定する。サポートされるフォーマットは1bpp,4bpp,8bpp,16bpp,24bpp,32bppだ。16bppと32bppは昔はサポートされていなかった。この中で、1,4,8bppはパレット情報(RGBQUAD構造体)を持つ。つまり、パレット付きフォーマットの場合ビットマップイメージ内のピクセルデータは、このRGBQUAD構造体の配列のインデックスという事だ。その他のフォーマットでは、ピクセルデータがそのままRGB値を指す。詳しくは後の説明を待て。

biCompressionはビットマップイメージの圧縮モードを指定する。が、説明はしない。そもそも今回の説明はWindowsで仮想VRAM的な事をするための説明なのに、圧縮されてたらVRAMみたいにいぢれないじゃん。ここは無圧縮を意味するBI_RGBか16bppや32bppならBI_BITFIELDSでも指定しておきやがれぃ。

biSizeImageは、ビットマップイメージデータのサイズを入れておくのだが、ヘルプファイルの説明によると、ほとんどの場合0で良いそうだ。ただ「ほとんどの場合」ってのが気に入らないので、自分は必ず設定するようにしている。と言う事で単純に(biWidth×1ドットのデータ量)×biHeightで良いかというと、そういうワケでもない。Windowsの制限で、ビットマップイメージの幅はDWORD境界でないとイケナイからだ。って事で幅のバイト数を4で割って余ったら、余らないようにテキトーにパディングする。

biXPelsPerMeterとbiYPelsPerMeterは、1メーターが何ドットかという「だれもマトモに値を設定しない」項目だ。PhotoShopとかマジメなツールなら設定しているだろう。自分は0か、何か別の目的で使う事が多い。

biClrUsedは「何色使っているか?」つまり、RGBQUAD構造体の要素数だ。ただし0の場合は「そのビットマップイメージがサポートする最大数」を意味する。つまり、1bppなら2、4bppなら16、8bppなら256だ。

biClrImportantはbiClrUsedのうち何色が重要な色であるかを指定する。ちなみにRGBQUAD構造体に設定するパレットは「先頭ほど重要な色を置く」という暗黙の了解があるのだ。肌色とかのパレットを先頭に置いておけば、色不足でパツキンギャルがデスラー総統になってしまう事も防げるってもんだ。ちなみに、0を設定しておけば「全部重要」なんていうワガママな状態になる。が、各種制限でそう上手くはいかぬのが世の定め。


次にビットマップイメージのフォーマットを書いておく。大きく分けて、1ドット1バイト以下とその他だ。

・1ドット1バイト以下

1bpp

1バイト目 2バイト目…
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16…

4bpp

1バイト目 2バイト目…
01ドット 02ドット 03ドット 04ドット

・その他

8bpp
 1バイトが1ドットだ。

24bpp
 3バイト単位にR値、G値、B値だった記憶がある。BGRな並びだったかもしれない。

16bppや32bpp(ただしbiCompressionがBI_BITFIELDSの場合)
 昔のWindowsでサポートされてなかっただけあって、やや特殊だ。16bppなら1ドット1WORD、32bppなら1ドット1DWORDなのだが、その中のどのビットがRGB成分のどれなのかを指定するDWORD型のマスクデータが別に必要だ。これらのデータは、本来ならRGBQUAD構造体のデータが置かれる場所にセットする。つまり、先に例示したBITMAPINFO構造体のアロケートは以下のようになる。

p = (BITMAPINFO *)malloc( sizeof(BITMAPINFOHEADER) + (sizeof(DWORD) * 3) );

そして、マスクの設定はこのように行う。

DWORD *pdwMask;

pdwMask = (DWORD *)p->bmiColors;
// A-5-5-5 format
pdwMask[0] = 0x7C00;	// red   mask is 0x7C00
pdwMask[1] = 0x03E0;	// green mask is 0x03E0
pdwMask[2] = 0x001F;	// blue  mask is 0x001F

上記は16bppの時にRGBそれぞれ5ビットずつ割り当てた場合のものだ。Windows95や98ではこのRGBが5-5-5のフォーマットと5-6-5のフォーマットしかサポートしないとヘルプに書いてある。NTなら何でも良いのか?


以上を踏まえて簡単なサンプルプログラムなぞ書いてみた。

----<sample1.c>----------------------------------------------------------------
#include <windows.h>

int main( void )
{
	BITMAPINFO *pBI;
	BYTE       *pBits;
	DWORD       cbBI;
	DWORD       cbBits;

	// 全部まとめてアロケート
	cbBI   = sizeof(BITMAPINFOHEADER) + (sizeof(RGBQUAD) * 256);
	cbBits = 256 * 256;
	pBI = (BITMAPINFO *)GlobalAlloc( GPTR, cbBI + cbBits );
	if ( pBI == NULL ) {
		return 1;
	}

	// BITMAPINFOHEADER設定
	pBI->bmiHeader.biSize          = sizeof( BITMAPINFOHEADER );
	pBI->bmiHeader.biWidth         = 256;
	pBI->bmiHeader.biHeight        = 256;
	pBI->bmiHeader.biPlanes        = 1;
	pBI->bmiHeader.biBitCount      = 8;
	pBI->bmiHeader.biCompression   = BI_RGB;
	pBI->bmiHeader.biSizeImage     = cbBits;
	pBI->bmiHeader.biXPelsPerMeter = 0;
	pBI->bmiHeader.biYPelsPerMeter = 0;
	pBI->bmiHeader.biClrUsed       = 0;
	pBI->bmiHeader.biClrImportant  = 0;

	// パレット設定
	{
		int nIdx;

		for ( nIdx = 0; nIdx < 256; nIdx++ ) {
			pBI->bmiColors[nIdx].rgbRed      = nIdx;
			pBI->bmiColors[nIdx].rgbGreen    = nIdx;
			pBI->bmiColors[nIdx].rgbBlue     = nIdx;
			pBI->bmiColors[nIdx].rgbReserved = 0;
		}
	}

	// ビットマップデータの位置
	pBits = &((BYTE *)pBI)[cbBI];

	// ビットマップデータ設定
	{
		int nIdx;

		for ( nIdx = 0; nIdx < 256; nIdx++ ) {
			BYTE *pTop = &pBits[256 * nIdx];
			FillMemory( pTop, 256, nIdx );
		}
	}

	// 表示
	{
		HDC hDC = GetDC( 0 );

		StretchDIBits( hDC
		             , 0, 0, 256, 256
			     , 0, 0, 256, 256
		             , pBits, pBI
		             , DIB_RGB_COLORS, SRCCOPY
		             );
		ReleaseDC( 0, hDC );
	}

	GlobalFree( pBI );

	return 0;
}
----<sample1.c>----------------------------------------------------------------

このプログラムで画面左上に綺麗なグレイスケールが表示されたら、あなたの表示環境はハイカラー以上だろう。256色モードにしてもう一度実行してみよう。悲しい結果が露見する。

それと、もう一つ。やや本題からズレる話題なのだが、表示用のビットマップイメージデータ(グレイスケール)を設定しているコードは、上から下に明るくなるようにしているのに実際はその逆だ。これはWindowsのビットマップの原点が左下にあるためで、ボトムアップタイプなどと呼ぶ。ってことでBITMAPINFOHEADER構造体のbiHeightを設定している所を以下のように変えてみて欲しい。

	pBI->bmiHeader.biHeight        = -256;

期待した表示になっただろうか? って事でbiHeightデータでbiSizeImageなんかを計算するときはabs()で囲むクセを付けておくと良いだろう。

そして本題に戻るが、そもそもサンプルプログラムの表示用のビットマップイメージデータは256色のグレイスケールであるワケだから、256色モードなら綺麗に表示できそうなモノである。なのにキタナーいのが表示されるにはワケががある。

表示先にパレットを設定していないからだ。


ってなワケで、表示先に対してパレットを設定するのだが、表示先、つまりHDCすなわちデバイスコンテキストのハンドルにパレットを設定スル場合もこれまたハンドル様が登場する。その名もHPALETTE型だ。

このHPALETTEを作るAPIも色々あるのだが、今回はCreatePalette()APIを使用する。例によってヘルプから。

HPALETTE CreatePalette( CONST LOGPALETTE *lplgpl // pointer to logical color palette);

LOGPALETTE型構造体はこんな感じ。

typedef struct tagLOGPALETTE { // lgpl
	WORD		palVersion;
	WORD		palNumEntries;
	PALETTEENTRY	palPalEntry[1];
} LOGPALETTE; 

これも可変長配列データだ。さらにPALETTEENTRY構造体はこんな感じ。

typedef struct tagPALETTEENTRY { // pe
	BYTE peRed;
	BYTE peGreen;
	BYTE peBlue;
	BYTE peFlags;
} PALETTEENTRY; 

ほとんどRGBQUAD構造体と同じだ(RGBQUAD構造体は引用してないが、ヘルプで検索すればスグだ)。

これだけ材料がそろったら、パレットハンドルの作り方解るよね? まぁ、LOGPALETTEのpalVersionメンバに入れる値はヘルプ見ないと不明だろうけど、これは0x0300(つまり3)を設定しておけばいい。

この辺りを考慮した表示部分のソースは以下の通りだ。

--------------------------------------------------------------------------------
	// 表示
	{
		HDC          hDC = GetDC( 0 );
		LPLOGPALETTE lpLogPal;
		int          nIdx;
		HPALETTE     hPal;
		HPALETTE     hPalOld;


		lpLogPal = GlobalAlloc( GPTR
		                      , sizeof(LOGPALETTE)
		                      + sizeof(PALETTEENTRY) * 255
							  );
		if ( lpLogPal == NULL ) {
			return 1;
		}

		lpLogPal->palVersion    = 0x300;
		lpLogPal->palNumEntries = 256;

		for ( nIdx = 0; nIdx < 256; nIdx++ ) {
			lpLogPal->palPalEntry[nIdx].peRed   = pBI->bmiColors[nIdx].rgbRed;
			lpLogPal->palPalEntry[nIdx].peGreen = pBI->bmiColors[nIdx].rgbGreen;
			lpLogPal->palPalEntry[nIdx].peBlue  = pBI->bmiColors[nIdx].rgbBlue;
			lpLogPal->palPalEntry[nIdx].peFlags = 0;
		}
		hPal = CreatePalette( lpLogPal );
		GlobalFree( lpLogPal );

		hPalOld = SelectPalette( hDC, hPal, TRUE );
		RealizePalette( hDC );
		StretchDIBits( hDC
		             , 0, 0, 256, 256
			     , 0, 0, 256, 256
		             , pBits, pBI
		             , DIB_RGB_COLORS, SRCCOPY
		             );
		if ( hPalOld ) {
			SelectPalette( hDC, hPalOld, FALSE );
		}
		DeleteObject( hPal );

		ReleaseDC( 0, hDC );
	}
--------------------------------------------------------------------------------

ちょっとマシになったハズである。そうならないなら、表示環境が16色モードとかかも知れぬ。

他には以下のようなアプローチもあるのだが、色の再現性は劣る。

--------------------------------------------------------------------------------
	// 表示
	{
		HDC          hDC = GetDC( 0 );
		HPALETTE     hPal = CreateHalftonePalette( hDC );
		HPALETTE     hPalOld;

		hPalOld = SelectPalette( hDC, hPal, TRUE );
		RealizePalette( hDC );
		StretchDIBits( hDC
		             , 0, 0, 256, 256
			     , 0, 0, 256, 256
		             , pBits, pBI
		             , DIB_RGB_COLORS, SRCCOPY
		             );
		if ( hPalOld ) {
			SelectPalette( hDC, hPalOld, FALSE );
		}
		DeleteObject( hPal );

		ReleaseDC( 0, hDC );
	}
--------------------------------------------------------------------------------

とりあえず、今回はここまで。書くのに時間がかかるから、明日続きが書けるかは不明。