ビットマップの表示2

前ページではウィンドウ上にビットマップを表示する方法を説明しました。
ここではこれをもう少し掘り下げて説明します。

ラスタオペレーション

BitBlt関数StretchBlt関数には、引数にラスタオペレーションというものがあります。
これは既に描画されているコピー先ピクセルと、これから描画するコピー元ピクセルとの色の合成方法の指定です。
前ページで使用した定数SRCCOPYは、合成は行わずにコピー元のピクセルで上書きするという意味です。
(何も描画していない状態でも、背景色が描画されています)

定数は以下のものがあります。

定数 説明
SRCCOPY コピー元のピクセルをコピー先のピクセルへそのまま描画する。
SRCPAINT コピー元とコピー先のピクセルを論理OR演算で合成する。
SRCAND コピー元とコピー先のピクセルを論理AND演算で合成する。
SRCINVERT コピー元とコピー先のピクセルを論理XOR演算で合成する。
SRCERASE コピー先の色を反転し、コピー元のピクセルと論理AND演算で合成する。
NOTSRCCOPY コピー元の色を反転し、コピー先に描画する。
NOTSRCERASE コピー元とコピー先のピクセルを論理OR演算で合成し、さらに反転する。
MERGECOPY コピー先のデバイスコンテキストで選択されているブラシの色と、コピー元のピクセルを論理AND演算で合成する。
MERGEPAINT コピー元の色を反転し、コピー先のピクセルと論理OR演算で合成する。
PATCOPY コピー先のデバイスコンテキストで選択されているブラシを出力先のビットマップにコピーする。
PATPAINT コピー先のデバイスコンテキストで選択されているブラシの色と、反転したコピー元のピクセルを論理OR演算で合成する。
それをさらにコピー先のピクセルと論理OR演算で合成する。
PATINVERT コピー先のデバイスコンテキストで選択されているブラシの色と、コピー先のピクセルを論理XOR演算で合成する。
DSTINVERT コピー先のピクセルを反転する。
BLACKNESS 物理パレットのインデックス0番の色(初期値は黒)でコピー先のピクセルを塗りつぶす。
WHITENESS 物理パレットのインデックス1番の色(初期値は白)でコピー先のピクセルを塗りつぶす。
NOMIRRORBITMAP ビットマップのミラーリングを防ぎます。
CAPTUREBLT 自身のウィンドウに重ねられたウィンドウを結果に含める。

説明文だけではわかりにくいと思いますので、色々と試してみると良いでしょう。
以下はSRCCOPYSRCPAINTSRCANDSRCINVERTSRCERASEを左上から順に描画した例です。
ラスタオペレーションの例

システム定義のビットマップ

ビットマップは自前で用意する以外に、システム(Windows)があらかじめ用意しているビットマップを使用することもできます。
これらはシステムで使用されているアイコンなどの画像です。

システムが定義済みのビットマップを使用するには、windows.hをインクルードする前にOEMRESOURCEという定数をdefineする必要があります。
また、LoadBitmap関数の第一引数(インスタンスの指定)はNULLを指定し、識別子に以下の定数を指定します。
(MAKEINTERSOURCEマクロを使用します)

OBM_BTNCORNERS OBM_OLD_RESTORE
OBM_BTSIZE OBM_OLD_RGARROW
OBM_CHECK OBM_OLD_UPARROW
OBM_CHECKBOXES OBM_OLD_ZOOM
OBM_CLOSE OBM_REDUCE
OBM_COMBO OBM_REDUCED
OBM_DNARROW OBM_RESTORE
OBM_DNARROWD OBM_RESTORED
OBM_DNARROWI OBM_RGARROW
OBM_LFARROW OBM_RGARROWD
OBM_LFARROWD OBM_RGARROWI
OBM_LFARROWI OBM_SIZE
OBM_MNARROW OBM_UPARROW
OBM_OLD_CLOSE OBM_UPARROWD
OBM_OLD_DNARROW OBM_UPARROWI
OBM_OLD_LFARROW OBM_ZOOM
OBM_OLD_REDUCE OBM_ZOOMD

OBM_OLD_から始まる定数はWindows3.0以前に使用されていたもので、現在のWindowsでは使用できません。


#define OEMRESOURCE
#include <windows.h>

//ウィンドウの生成等は省略

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	HDC hdc, hmdc;
	PAINTSTRUCT ps;
	RECT rect;

	HBITMAP	hBmp;
	BITMAP bmp;

	int x = 0, y = 0;
	int maxHeight = 0;

	switch (message)
	{
	case WM_PAINT: //ウィンドウの描画
		GetClientRect(hWnd, &rect);
		hdc = BeginPaint(hWnd, &ps);

		for (int i = OBM_LFARROWI; i < OBM_OLD_CLOSE; i++) {
			hBmp = LoadBitmap(NULL, MAKEINTRESOURCE(i));
			GetObject(hBmp, sizeof(BITMAP), &bmp);
			hmdc = CreateCompatibleDC(hdc);
			SelectObject(hmdc, hBmp);

			if (x > 0 && (x + bmp.bmWidth) > rect.right)
			{
				x = 0;
				y += maxHeight;
				maxHeight = bmp.bmHeight;
			}
			else
			{
				maxHeight = bmp.bmHeight > maxHeight
					? bmp.bmHeight : maxHeight;
			}

			BitBlt(
				hdc, x, y, bmp.bmWidth, bmp.bmHeight,
				hmdc, 0, 0, SRCCOPY);

			x += bmp.bmWidth;

			DeleteDC(hmdc);
			DeleteObject(hBmp);
		}

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

上記の定数をすべて描画した結果です。
(ただし順番はバラバラです)
システム定義済みのビットマップ

WM_CREATEメッセージ

今までのサンプルコードでは、ビットマップ表示のために必要な処理をすべてWM_PAINTメッセージ内で行っています。
WM_PAINTメッセージは割と頻繁に送られてくるので、処理は出来るだけ軽くしておくべきです。
ここであまり重い処理を行うと画面がちらつくことがあります。

メモリデバイスコンテキストへのビットマップの描画は、一度行ってしまえば(画像を差し替えないならば)以降はそれを使いまわすほうが効率が良いです。
そのためにはWM_PAINTメッセージではなくWM_CREATEメッセージを利用します。
このメッセージはCreateWindow関数などでウィンドウを生成する時に一度だけ送られてくるメッセージで、ウィンドウの描画に関する初期化のための処理はこのメッセージ内で行うことができます。

GetDC関数

メモリデバイスコンテキストを作成するためにはCreateCompatibleDC関数を使用しますが、引数にはウィンドウのデバイスコンテキストが必要です。
いままでデバイスコンテキストの取得に使用していたのはBeginPaint関数ですが、これはWM_PAINTメッセージ内でしか使用できません。
WM_PAINTメッセージ以外でデバイスコンテキストを取得するにはGetDC関数を使用します。

HDC GetDC(
 HWND hWnd
);
ウィンドウhWndのデバイスコンテキストを取得する。
hWndがNULLの場合は画面全体のデバイスコンテキストを取得する。
取得に失敗した場合はNULLを返す。

ReleaseDC関数

GetDC関数でデバイスコンテキストを取得した場合はReleaseDC関数で解放する必要があります。

int ReleaseDC(
 HWND hWnd,
 HDC hDC
);
ウィンドウhWndのデバイスコンテキストhDCを解放する。
解放に成功した場合は1を、失敗した場合は0を返す。

プログラムの実行中(ウィンドウの表示中)にメモリデバイスコンテキストを持ち続ける場合はWM_CREATEメッセージで取得し、WM_DESTROYメッセージで解放すると良いでしょう。
ただしウィンドウプロシージャ内のローカル変数は、ひとつメッセージを処理する毎に破棄される(関数が終了する)ので、グローバル変数に保存するかstatic変数にしておく必要があります。

サンプルコード

これらのメッセージと関数を実際に使用してみましょう。


#include <windows.h>
#include "resource.h"

//ウィンドウの生成等は省略

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//ウィンドウが存在している間保持したい変数は
	//staticにしておく
	static HDC hmdc;
	static BITMAP bmp;

	HDC hdc;	
	PAINTSTRUCT ps;
	HBITMAP hBmp;

	switch (message)
	{
	case WM_CREATE: //ウィンドウ作成
		hBmp = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));
		GetObject(hBmp, sizeof(BITMAP), &bmp);

		hdc = GetDC(hWnd); //デバイスコンテキストの取得
		hmdc = CreateCompatibleDC(hdc);
		SelectObject(hmdc, hBmp);

		DeleteObject(hBmp);
		ReleaseDC(hWnd, hdc); //デバイスコンテキストの解放
		break;

	case WM_PAINT: //ウィンドウの描画
		hdc = BeginPaint(hWnd, &ps);

		BitBlt(
			hdc, 0, 0, bmp.bmWidth, bmp.bmHeight,
			hmdc, 0, 0, SRCCOPY);

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		//ここでメモリデバイスコンテキストを破棄する
		DeleteDC(hmdc);
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

実行結果は同じなので省略します。
主要な処理をWM_CREATEメッセージに移動させ、WM_PAINTメッセージの処理はBitBlt関数だけになっています。
その分ウィンドウの再描画時の処理が軽くなっています。

PAINTSTRUCT構造体

上記コードでは、再描画が発生するたびにBitBlt関数でビットマップデータの全てをメモリデバイスコンテキストから転送しています。
ウィンドウの一部が欠けただけでもすべてのビットマップの転送を行っているので、処理に無駄があります。

これはPAINTSTRUCT構造体を上手く使うことで軽量化が可能です。
PAINTSTRUCT構造体のrcPaintメンバには無効領域がRECT構造体で格納されています。
つまりこの無効領域の箇所を転送するだけで問題なく表示ができます。


//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	//ウィンドウが存在している間保持したい変数は
	//staticにしておく
	static HDC hmdc;

	HDC hdc;	
	PAINTSTRUCT ps;
	HBITMAP hBmp;

	//ビットマップ描画の座標
	int x, y;

	switch (message)
	{
	case WM_CREATE: //ウィンドウ作成
		hBmp = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));

		hdc = GetDC(hWnd);	//デバイスコンテキストの取得
		hmdc = CreateCompatibleDC(hdc);
		SelectObject(hmdc, hBmp);

		DeleteObject(hBmp);
		ReleaseDC(hWnd, hdc);	//デバイスコンテキストの解放
		break;

	case WM_PAINT: //ウィンドウの描画
		BeginPaint(hWnd, &ps);

		x = y = 0;
		BitBlt(
			ps.hdc,
			ps.rcPaint.left,	//x座標
			ps.rcPaint.top,		//y座標
			ps.rcPaint.right - ps.rcPaint.left,	//幅
			ps.rcPaint.bottom - ps.rcPaint.top,	//高さ
			hmdc,
			ps.rcPaint.left - x, ps.rcPaint.top - y,
			SRCCOPY);

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		//ここでメモリデバイスコンテキストを破棄する
		DeleteDC(hmdc);
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

今回はどちらも0を指定していますが、変数xyに座標を指定することで任意の位置に表示することができます。
なお、BITMAP構造体は使用しなくなったので変数とGetObject関数は削除しています。

DeleteDC関数とReleaseDC関数は紛らわしいので注意してください。
基本的に、Create○○Delete○○Get○○Release○○という風に対応しています。
「生成」したものは「削除」、「取得」したものは「解放」です。

こういった「後処理」が必要は関数は、必要なデータをメモリにロードし、それを取り扱うためのハンドルを返します。
ハンドルは変数に格納しますが、適切に後処理をせずに変数の寿命が尽きてしまうとメモリ上のデータにアクセスする手段が無くなり、破棄することができないデータがメモリに残ったままになります。
(メモリリークという)

ちなみにGetObject関数はGDIオブジェクトの情報を単純に変数に格納する関数で、ハンドルを扱うわけではないのでReleaseObject関数というものはありません。
GetStockObject関数はストックオブジェクトを取得しますが、これには解放や削除の処理は必要はありません。

CreatePen関数やCreateBrush関数などはGDIオブジェクトを生成する関数で、これらはDeleteObject関数で削除する必要があります。
LoadBitmap関数で作成されるのもGDIオブジェクトですから、これもDeleteObject関数で削除します。

外部ビットマップファイルの読み込み

ビットマップの表示にはリソースを利用する方法と、外部のビットマップを読み込んで表示する方法があります。
外部のビットマップを表示するにはLoadBitmap関数ではなくLoadImage関数を使用します。
(もちろんリソースから取得することも可能)
(→LoadImage関数)

HANDLE LoadImageW(
 HINSTANCE hInst,
 LPCWSTR name,
 UINT type,
 int cx,
 int cy,
 UINT fuLoad
);
アイコン、カーソル、ビットマップをロードする。

第一引数hInstは、外部からビットマップを読み込む場合はNULLを指定します。
第二引数nameはファイルへのパスです。
第三引数typeにはビットマップを表す定数IMAGE_BITMAPを指定します。
第四引数cx、第五引数cyは0で良いです。
そして最後の引数fuLoadにはLR_LOADFROMFILEという定数を指定します。

これで引数nameで指定したパスのビットマップファイルを読み込むことができます。
パスの指定は絶対パスでも相対パスでも良いですが、Visual Studioからプログラムを起動している場合は相対パスの起点はプロジェクトフォルダになるので注意してください。
(→絶対パスと相対パス)


#include <windows.h>

//ウィンドウの生成等は省略

//ウィンドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HDC hmdc;

	HDC hdc;
	PAINTSTRUCT ps;
	HBITMAP hBmp;
	BITMAP bmp;

	switch (message)
	{
	case WM_CREATE:
		//外部ビットマップファイルの読み込み
		hBmp = LoadImage(
			NULL,
			L"C:/sample.bmp",
			IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);

		GetObject(hBmp, sizeof(BITMAP), &bmp);

		hdc = GetDC(hWnd);	//デバイスコンテキストの取得
		hmdc = CreateCompatibleDC(hdc);
		SelectObject(hmdc, hBmp);

		DeleteObject(hBmp);
		ReleaseDC(hWnd, hdc);	//デバイスコンテキストの解放
		break;

	case WM_PAINT: //ウィンドウの描画
		BeginPaint(hWnd, &ps);

		BitBlt(
			ps.hdc,
			ps.rcPaint.left, ps.rcPaint.top,
			ps.rcPaint.right - ps.rcPaint.left,
			ps.rcPaint.bottom - ps.rcPaint.top,
			hmdc,
			ps.rcPaint.left, ps.rcPaint.top,
			SRCCOPY);

		EndPaint(hWnd, &ps);
		break;

	case WM_DESTROY: //ウィンドウの破棄
		//ここでメモリデバイスコンテキストを破棄する
		DeleteDC(hmdc);
		PostQuitMessage(0);
		break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}