はじめに
今回はビットマップの構造とウィンドウへの描画について解説します。
前回までに生成したウィンドウに中身を詰め込んでいきましょう。
DIB(Device Independent Bitmap)の構造
サンプルのソースコードをまじえての説明する前に、予備知識としてDIBについて軽く説明します。
DIBとは、名前のとおりデバイスに依存しない画像データです。
今回のサンプルではディスプレイというデバイスに画像データを出力しますが、その他のデバイスでも同じ形式で扱うことができます。
Windowsアクセサリの「ペイント」で扱えるBMPファイルがDIB形式となります。
BMPのファイル構造を以下に示します。
- BITMAPFILEHEADER (BMPファイルの情報を持つ構造体)
- BITMAPINFO
- BITMAPINFOHEADER (画像イメージの情報を持つ構造体)
- カラーテーブル (RGBQUAD構造体の配列(8bitカラーのみ))
- ビットマスク (DWORD型の配列(16bit/32bitカラーのみ))
- イメージビット用バッファ
この構造を把握すれば、何もないところから画像を生成することができます。
また、独自の構造で画像ファイルを生成することもできます。
例えばゲームの場合、セーブした瞬間の画面ショットを保存したいこともあるでしょう。
その画像データの中にゲームのセーブデータを内包する、という応用も考えられます。
では、このファイル構造について、実際にBMPファイルを読み込む処理から詳細に触れていきます。
※ファイルを入力情報としているため、各構造体への値のセットは、ファイルを先頭から読み込んでいくだけですね。
解説としては、ゼロベースからBMPファイルを生成する方が適切だったかもしれません。
BITMAPFILEHEADER
BMPファイルの情報を持つ構造体です。
重要なメンバは下記になります。
bfType
: ファイルの種類を指し、BMPの場合"BM"がセットされています。
bfSize
: ファイルのサイズです(バイト単位)。
bfOffBits
: ファイルの先頭からイメージビットの開始までのオフセット(=ヘッダ部分の長さ)です(バイト単位)。
CDib.cpp - CDib24::ReadBMPFile
// BITMAPFILEHEADER構造体の読み込み
BITMAPFILEHEADER bmfh;
fin.read((char *)&bmfh, sizeof(BITMAPFILEHEADER));
if(fin.gcount() != sizeof(BITMAPFILEHEADER))
{
return DispErrorDialog(hwnd, L"ファイル読込", L"BITMAPFILEHEADER構造体の読み込みに失敗");
}
// ファイルがBMPであるか判定
if(bmfh.bfType != 0x4d42)
{
return DispErrorDialog(hwnd, L"ファイル読込", L"BMP形式ではないファイルを指定");
}
構造体のサイズだけファイルを読み込み、bfType
が"BM"(0x4d42)かチェックを行います。
BITMAPINFO
BITMAPINFOHEADER
画像イメージに関する情報を持つ構造体です。
処理中で特に参照される(と思われる)メンバは下記になります。
biWidth
: イメージの幅です(ピクセル数)。
biHeight
: イメージの高さです(ピクセル数)。
biBitCount
: ピクセルごとのビット数(bpp)です。
biCompression
: BMPの場合、bppに応じてBI_RGBまたはBI_BITFIELDSのいずれかを指定します。
bppが8bitもしくは24bitであればBI_RGBを、16bitもしくは32bitであればBI_BITFIELDSを指定します。
CDib.cpp - CDib24::ReadBMPFile
// BITMAPINFOHEADER構造体の読み込み
fin.read((char *)lpbmi, sizeof(BITMAPINFOHEADER));
if(fin.gcount() != sizeof(BITMAPINFOHEADER))
{
return DispErrorDialog(hwnd, L"ファイル読込", L"BITMAPINFOHEADER構造体の読み込みに失敗");
}
// イメージが24bitカラーであるか判定
if(lpbmi->bmiHeader.biBitCount != 24)
{
return DispErrorDialog(hwnd, L"ファイル読込", L"24bitカラーではないBMPファイルを指定");
}
サンプルでは、24bitカラーのビットマップを扱っています。
カラーテーブル
カラーパレット用の情報を持つ構造体で、8bitカラーの場合に使用します(今回のサンプルは24bitのため使用していません)。
8bit(0~255)分の要素数の配列で、各要素にはRGB(赤緑青)のカラー値を設定します。
ビットマスク
RGB各カラーのビットマスクを表す配列で、16bit/32bitカラーの場合に使用します。
DWORD型配列の先頭から赤、緑、青の順で各色に対応するビットマスクを設定します。
イメージビット用バッファ
画像そのもののデータです。
画像イメージの幅や高さ、bppにより必要なサイズが決まります。
CDib.cpp - CDib24::ReadBMPFile
// イメージビットのメモリ割り当て
UINT uImageSize = bmfh.bfSize - bmfh.bfOffBits;
if(AllocBitsMem(uImageSize) == FALSE)
{
return DispErrorDialog(hwnd, L"ファイル読込", L"イメージビットのメモリ割り当てに失敗");
}
// イメージビットの読み込み
fin.read((char *)lpBits, uImageSize);
if(fin.gcount() != uImageSize)
{
return DispErrorDialog(hwnd, L"ファイル読込", L"イメージビットの読み込みに失敗");
}
ファイル全体のサイズとファイル先頭からのオフセット値の差が、イメージビットのサイズとなります。
bppが24bitの場合、イメージビットの先頭から青、緑、赤の順で各ピクセルのカラー値が格納されます。
ウィンドウへの描画
BMPファイルから画像データの読み込みが正常に完了しましたら、次はウィンドウへ描画します。
CDib.cpp - CDib24::DrawBits
StretchDIBits(
hDC, // 描画先のデバイス
nCoordX, nCoordY, // 描画先の座標(X, Y)
lpbmi->bmiHeader.biWidth, // 描画サイズ(幅)
lpbmi->bmiHeader.biHeight, // 描画サイズ(高さ)
0, 0, // 描画元の座標(X, Y)
lpbmi->bmiHeader.biWidth, // 描画元サイズ(幅)
lpbmi->bmiHeader.biHeight, // 描画元サイズ(高さ)
lpBits, // イメージビット
lpbmi, // 当イメージのBIMAPINFO構造体
DIB_RGB_COLORS, // RGB値イメージビット
SRCCOPY // 描画元の矩形をコピー
);
描画はStretchDIBits
関数を呼び出すだけで実現できます。
第1引数のhDC
で指定するデバイスに対して描画を行います。
DIBはデバイスに依存しないため、ディスプレイ以外のデバイスも指定できます。例えば、DirectX使用時などは、グラフィックボードのビデオメモリへのハンドルを指定したりします。
第2引数~第5引数は描画する位置とサイズを指定します。
第6引数~第9引数は描画元の位置とサイズです。描画先と描画元のサイズが異なる場合、イメージは描画先のサイズに合わせて拡大/縮小されます。
第10引数は描画元のイメージビットです。
第11引数は前述のBITMAPINFOです。
第12引数はカラー値の取り扱いです。イメージがRGB値を指す場合はDIB_RGB_COLORSを指定します。
第13引数は描画方式を指定します。サンプルで使用したSRCCOPYは描画元から描画先への単純なコピーです。
描画に必要な値のほとんどは、BMPファイルを読み込むことで取得可能ですが、デバイスへのハンドルは異なります。
デバイスへのハンドルを取得している処理まで、関数の呼び出し元をさかのぼっていきます。
example001_003.cpp - WindowFunc
HDC hDC;
PAINTSTRUCT ps;
switch(message)
{
// 再描画
case WM_PAINT:
hDC = BeginPaint(hwnd, &ps);
myex.UpdateScreen(hDC);
EndPaint(hwnd, &ps);
break;
呼び出し元はウィンドウ関数で、新たなメッセージを処理をするコードを追加しています。
WM_PAINT
WM_PAINTは、アプリのウィンドウを復元する必要があるときに受信するメッセージです。
アプリのウィンドウを最小化したとき、あるいは、アプリのウィンドウが他のウィンドウによって隠されたとき、それでもウィンドウの中身は問題なく表示されます。
その理由は、ウィンドウ関数がWM_PAINTのメッセージを処理し、表示内容を再描画しているからです。
WM_PAINTの処理は、BeginPaint
で始まり、EndPaint
で終わります。
BeginPaint
の第1引数で対象ウィンドウを指定します。第2引数のPAINTSTRUCTには、再描画に関する情報が出力されます。
また、戻り値として再描画対象ウィンドウのデバイスコンテキストが取得できます。
今回のサンプルコードを実行すると、ウィンドウの表示と同時に画像が表示されます。
しかし、ウィンドウが生成されたばかりの状態で、最小化されたわけでも、他のウィンドウに隠されたわけでもありません。
つまり、ウィンドウ復元の必要がないので、WM_PAINTが送られてくる理由がありません。
それでもWM_PAINTをトリガーに描画を行っています。
CExample.cpp - InitExample
InvalidateRect(hwnd, NULL, FALSE);
InvalidateRect
関数を呼び出すことで、OSに対してWM_PAINTメッセージを送るよう催促することができます。
第1引数にはメッセージの送信対象となるウィンドウを、第2引数には再描画範囲の座標とサイズを(NULLは全体を指します)、第3引数は背景消去の要否を指定します。
これにより、アプリのタイミングで再描画の要求が可能となります。
Windowsアプリにおいて、プログラマーが意図したタイミングで描画を行いたいケースも出てくることでしょう。
そのとき、OSからのWM_PAINTを待ちぼうけるわけにはいきません。InvalidateRect
による明示的な要求が必要となります。
次回予告
次回もビットマップの描画について取り扱います。
今回の解説で、ビットマップのイメージ全体をそのままウィンドウへ描画できるようになりました。
しかし、イメージの一部分を表示したいこともあります。
また、背景画像の上に人物画像を重ねて表示したいということもあるでしょう。
そのあたりを解説していきます。