iPhone/iPad用ドット絵エディタ「EDGE touch」

最近のエントリー

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、
お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_07.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_07.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、
test0がフロントバッファに読み込まれると思います。

というわけで、今回は256色のビットマップ形式(*.bmp)をサーフェイスを読み込みたいと思います。

パレット設定

さて、256色のビットマップを正確に表示するためには、まずビットマップファイルが持つパレットを読み込む必要があります。
これをサボると、ビットマップの色がバケてしまいますからね。

ちなみに、256色の場合は、パレットが256個あり、サーフェイスの1ドット1ドットは表示しているパレット番号(色)を記憶しているだけです。これはビットマップ画像(*.bmp)も同じ構造です。

であ、よく解らない解説いってみよう!

//----------[ ビットマップファイルからパレットを読み込む ]--------------------------------------
void LoadPalette(char *filename){
	int i;
	FILE *fp;
	if((fp=fopen(filename,"rb"))==NULL){
		sprintf(tmp,"%s ファイルが見つかりません",filename);
		Quit(tmp);
	}
	fseek(fp,0x36,0);
	for(i=0;i<256;i++){
		peEntry[i].peBlue=fgetc(fp);
		peEntry[i].peGreen=fgetc(fp);
		peEntry[i].peRed=fgetc(fp);
		peEntry[i].peFlags=1;
		fgetc(fp);
	}
	fclose(fp);
	lpPalette->SetEntries(0,0,256,peEntry);
}

	略

	//呼び出し
	//パレット設定
	LoadPalette("test.bmp");

では、解説・・・って言っても見てわかりませんかねぇ?(爆)。私が解析したトコロ、BMP形式はパレット情報が0x36番地から格納されているので
fseekで読み込む位置をずらし、順番に青、緑、赤と読み込んでいます。256個読み込んだらlpPalette->SetEntries(0,0,256,peEntry);を実行して
DirectDrawのパレットを全て変更します(と言っても0、255番は変更できませんが・・・)。
ちなみにlpPalette->SetEntries(0,1,254,&peEntry[1]);でも良いでしょう(未確認)。

それと言っておきたい事は、これらの方法は1例にしか過ぎないという事です。
私もGDIはよく解らないので(というかよく解らない関数が多いので)適当にプログラムを組んでいます。
GDIに詳しい方ならもっと良い方法を知っているでしょう。
そういう場合は私にメールください(爆。

ビットマップ読み込み

ちうわけで、パレットの設定が終わったら、画像データ(?)を読み込みます。
パレットと同じようにデータを解析して適当に表示しても良いのですが、便利そうなシステムサービス関数を見つけたので今回はこれを使用してみました。

//----------[ ビットマップをサーフェイス上へ読み込む ]------------------------------------------
void LoadBitmap(LPDIRECTDRAWSURFACE sf,char *filename,int x,int y,int x_size,int y_size){
	HDC hdcs, hdcd;
	HANDLE hbmp,hbmpold;

	//APIを使用した読み込み
	if((hbmp=LoadImage(NULL,filename,IMAGE_BITMAP,
		x_size,y_size,LR_CREATEDIBSECTION|LR_LOADFROMFILE))==NULL){
		sprintf(tmp,"%s ファイルが見つかりません",filename);
		Quit(tmp);
	}
	
	//サーフェイスへ転送
	hdcs=CreateCompatibleDC(NULL);
	hbmpold=SelectObject(hdcs,hbmp);
	sf->GetDC(&hdcd);
	BitBlt(hdcd,x,y,x_size,y_size,hdcs,0,0,SRCCOPY);
	sf->ReleaseDC(hdcd);
	SelectObject(hdcs,hbmpold);
	DeleteDC(hdcs);
	DeleteObject(hbmp);
}

	略

	//ビットマップをフロントバッファへ読み込む
	LoadBitmap(lpFront,"test.bmp",0,32,160,32);

んー、面倒ですが仕方ない(謎)のでちゃちゃっと説明しちゃいますね。

	//APIを使用した読み込み
	if((hbmp=LoadImage(NULL,filename,IMAGE_BITMAP,
		x_size,y_size,LR_CREATEDIBSECTION|LR_LOADFROMFILE))==NULL){
		sprintf(tmp,"%s ファイルが見つかりません",filename);
		Quit(tmp);
	}

まず、最初のLoadImage関数、ここではビットマップファイルをメモリ内に読み込むといった処理をしています。

書式 HANDLE LoadImage( HINSTANCE hinst, LPCTSTR lpszName , UINT uType , int cxDesired , int cyDesired , UINT fuLoad );
hinst イメージが格納されているアプリケーションのインスタンスを認識するハンドル。

そもそもこの関数はリソースと呼ばれる実行ファイル(*.EXE)に含まれるデータから読み込むものなので、関係ないからNULLぢゃ。

lpszName ファイル名
uType ロードするイメージのタイプを指定するのですが、ビットマップを読み込むのでIMAGE_BITMAPを指定。
cxDesired 横幅
cyDesired 縦幅
fuLoad ロード用のフラグ。今回は外部ファイルからビットマップを読み込むという事でLR_CREATEDIBSECTION | LR_LOADFROMFILEを指定。
戻り値 ロードされたイメージのハンドル
	hdcs=CreateCompatibleDC(NULL);
	hbmpold=SelectObject(hdcs,hbmp);

そうしたら、次にhdcs=CreateCompatibleDC(NULL);を実行し、適当なデバイスコンテキストを作成します(戻り値:デバイスコンテキストのハンドル)。
次に、hbmpold=SelectObject(hdcs,hbmp);を実行し、デバイスコンテキストに先ほど読み込んだビットマップを選択させます(戻り値:選択前のオブジェクト)。
これでビットマップが読み込まれているデバイスコンテキストへのハンドルが取得できた事になります。よくわかりませんが、GDIはこういうものです(爆)。
まぁ、細かい事は気にせず、こうすれば良いという事を覚えてしまえばいいでしょう。

	sf->GetDC(&hdcd);
	BitBlt(hdcd,x,y,x_size,y_size,hdcs,0,0,SRCCOPY);
	sf->ReleaseDC(hdcd);

で、第4回で記述したように、サーフェイスへのデバイスコンテキストハンドルを取得し、BitBltというGDI関数を使用してビットマップをサーフェイスに転送します。

書式 BOOL BitBlt( HDC hdcDest, int nXDest , int nYDest , int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop );
hdcDest 転送先のデバイスコンテキストのハンドル
nXDest 転送先の左上X座標
nYDest 転送先の左上Y座標
nWidth 画像の横幅
nHeight 画像の縦幅
hdcSrc 転送元のデバイスコンテキストのハンドル
nXSrc 転送元の左上X座標
nYSrc 転送元の左上Y座標
dwRop 転送時のオプションのようなものだが、ここは「そっくりそのまま転送」を意味するSRCCOPYを指定。
戻り値 成功するとTRUEが返ってくるらしい。
	SelectObject(hdcs,hbmpold);
	DeleteDC(hdcs);
	DeleteObject(hbmp);

最後に、後始末です。最初にSelectObject(hdcs,hbmpold);としていますが、これはhdcsに変更前のオブジェクトを選択していると言う事です(謎)。そうしたら
DeleteDCで適当に作成したデバイスコンテキストを消去し、DeleteObjectで、メモリ内に読み込んであるビットマップを消去しています。

だー、もうGDIはよーわからん(^^;こんなんだからMS-DOSからWindowsになかなか移行できんプログラマが多いのだよ・・・。
今回は(も?)カット&ペーストでそのまま使用してください(逝)。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、
お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_06.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_06.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、「フロントバッファ」と「バックバッファ」がカラフルに交互に表示されると思います(謎。
今回は、パレットを作成したいと思います。

パレット作成

まず初めに、StartDirectDraw自作関数内へパレットオブジェクト作成部分を追加します
(ディスプレイの色数が256色以下になっている時にだけ、パレットを使用する事が出来ます)。

	//パレットの作成
	if(lpDD->CreatePalette(DDPCAPS_8BIT,peEntry,&lpPalette,NULL)!=DD_OK)
		return FALSE;
	lpFront->SetPalette(lpPalette);

まず、DirectDrawオブジェクト(lpDD)のメンバである「CreatePalette」関数を呼び出します。構文は以下の通りです。

書式 HRESULT CreatePalette( DWORD dwFlags, LPPALETTEENTRY lpColorTable, LPDIRECTDRAWPALETTE FAR *lplpDDPalette, IUnknown FAR *pUnkOuter );

dwFlags このフラグでは、作成したいパレットの種類(2,4,16,256色)をマクロで指定するらしいのですが、私は256しか使用したことがないので(というかこれが最適)DDPCAPS_8BITを指定します。
lpColorTable このDirectDrawPaletteオブジェクトを初期化するための2、4、16、256色分のPALETTEENTRY型配列へのポインタ。今回はあらかじめ定義しておいたPALETTEENTRY peEntry[256];のポインタを引き渡します。
lplpDDPalette 作成するDirectDrawPaletteオブジェクトの格納先をアドレスで指定するらしい。今回は、あらかじめ定義しておいたLPDIRECTDRAWPALETTE lpPaletteのアドレスを引き渡します。
pUnkOuter 将来拡張した時使用するらしいので、NULLを指定。
戻り値 成功した場合はDD_OKが返ってくるらしい。

そうしたら、次にサーフェイスのメンバ「SetPalette」関数を呼び出し、フロントバッファに作成したパレットを関連付けます。

色の設定

さて、パレットを256個作成したら、その中に色を設定してやります。考え方としては、美術で256個の皿(部屋?)のあるパレットの中に、絵の具で色を作成するが、何故か絵の具が赤、緑、青の3つしか無く、その3つの絵の具の量を調整して様々な色を作成するというもの(わけわからん)。

まぁ、とりあえずコードを見てみますか。

PALETTEENTRY peEntry[256];			//パレット情報

略

//----------[ パレット情報設定 ]----------------------------------------------------------------
void SetPaletteColor(unsigned char no,unsigned char r,unsigned char g,unsigned char b){
	if(no<0 || no>255)
		return;
	peEntry[no].peRed=r;
	peEntry[no].peGreen=g;
	peEntry[no].peBlue=b;
	peEntry[no].peFlags=1;

	lpPalette->SetEntries(0,no,1,&peEntry[no]);
}

略

	//パレット設定
	SetPaletteColor(1,26,70,255);
	SetPaletteColor(2,255,255,0);

自作関数SetPaletteColorでは、256個のパレットの内、no番目の色を「赤」、「緑」、「青」によって設定します。

ここではPALETTEENTRY構造体を使用します。この構造体のメンバ「BYTE peRed」(BYTE = unsigned char)、「BYTE peGreen」、「BYTE peBlue」
にそれぞれ「赤」、「緑」、「青」の量を0~255の間で指定します(peFlagsメンバは1固定)。例えば、「赤」、「緑」、「青」を255,0,255にすると、赤と青が混ざった紫、127,127,127にすると、灰色というような感じです。

そうしたら、前に作成したDirectDrawPaletteオブジェクト「lpPalette」のメンバ
であるSetEntriesという関数を呼び出して、パレットの色を変更します。SetEntriesの構文は以下の通りです。

書式 HRESULT SetEntries( DWORD dwFlags, DWORD dwStartingEntry, DWORD dwCount, LPPALETTEENTRY lpEntries );
dwFlags このフラグは現在使用していないらしいので0を指定しなければいけないらしい。
dwStartingEntry 何番のパレットから設定するか(変更開始パレット番号)。
dwCount 変更したいパレット数
lpEntries パレットエントリ(PALETTEENTRY構造体)へのポインタ
戻り値 成功した場合はDD_OKが返ってくるらしい。

この関数は、今回作成した自作関数のように1個1個パレットを設定するのでは無く、複数一気に変更出来るように
設計されています。ですから、peEntry[0]~peEntry[255]を全て変更し、lpPalette->SetEntries(0,0,256,peEntry);とでもすれば、全部一括してパレットを変更できます。

ただ、DirectDrawの特性上(?)0番(黒固定)と255番(白固定)のパレットは変更不可能になっています。
まぁ、白と黒は99%ゲームで使うと思うので変更出来ないからと言って気にはならないでしょう。

設定した色をGDI(テキスト)で使用してみる

今回は、自作関数DdTextOutを改造して、色を指定できるようにします。

//----------[ GDI関数を使用してテキストの出力 ]----------------------------------------------
void DdTextOut(LPDIRECTDRAWSURFACE sf,int x,int y,char *str,unsigned char col){
	HDC hdc;
	sf->GetDC(&hdc);
	SetBkMode(hdc,TRANSPARENT);
	SetTextColor(hdc,RGB(peEntry[col].peRed,peEntry[col].peGreen,peEntry[col].peBlue));
	TextOut(hdc,x,y,str,strlen(str));
	sf->ReleaseDC(hdc);
}

略

	DdTextOut(lpFront,0,0,"フロントバッファ",1);
	DdTextOut(lpBack ,0,0,"バックバッファ"  ,2);

まず、最初に付け加えたのがSetBkMode(hdc,TRANSPARENT);です。これは文字の背景を透明にするというもの。第5回までは、字の色が黒、背景が白という状態だったので
変な風に表示されていましたが、今回は背景を表示しないようにします。

次にSetTextColorという関数を呼び出しています。ここで色を設定しています。GDIで色を指定する場合はRGB値(COLORREF型)という1つの数値を用いるのですが
、なんやよーわからんのでマクロ「RGB(赤,緑,青)」という便利なものを使用してRGB値を指定します。RGB(255,0,255)とすれば紫のRGB値という感じです。で、GDIでDirectDrawPaletteの色をそのまま色番号で指定する事は出来ないので
前に設定したpeEntry構造体のメンバ値を使用してRGB値を設定しています。

パレット後始末

アプリケーション終了時には作成したDirectDrawPaletteオブジェクト(lpPalette)を解放しなければなりません。ですから自作関数EndDirectDrawの中に
「lpPalette->Release();」を追加する必要があります。まぁ、今まで通りマクロを使用する場合は「RELEASE(lpPalette);」ですが。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、
お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_05.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_05.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、「フロントバッファ」と「バックバッファ」が交互に表示されると思います。
今回は、バックバッファ(複合サーフェイス)を作成し、ページフリップを行いたいと思います。

バックバッファ作成

バックバッファを作成するには、プライマリサーフェイスを複合プライマリサーフェイスとして作成します。複合プライマリサーフェイスとして作成すると
、プライマリサーフェイス以外に、指定した数のサーフェイス(バックバッファ)が自動作成されます。
その後、バックバッファ(サーフェイス)の情報を取得すれば、
バックバッファへ描画する事ができます。

では、まず複合プライマリサーフェイスを作成します。

	//複合プライマリサーフェイスの作成
	ZeroMemory(&ddsd,sizeof(ddsd));
	ddsd.dwSize=sizeof(ddsd);
	ddsd.dwFlags=DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
	ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
	ddsd.dwBackBufferCount=1;
	
	if(lpDD->CreateSurface(&ddsd,&lpFront,NULL)!=DD_OK)
		return FALSE;

第3回でも記述した通り、サーフェイスを作成するにはDDSURFACEDESC構造体のdwFlagに使用するメンバを指定します。
プライマリサーフェイスだけなら、DDSD_CAPSだけでも良いのですが、今回は複合プライマリサーフェイスとしてバックバッファも作成するので
DDSD_BACKBUFFERCOUNTも同時に指定します。そうしたらdwBackBufferCountに1を指定します。
ここは作成するバックバッファの数を指定するのですが、今回は1つだけ作成します。
バックバッファは複数あると、管理しにくくなるという私の独断です(゜゜)☆O(–;)oばこ

さて、ddsCaps.dwCapsは3つ指定しています。DDSCAPS_PRIMARYSURFACEは、作成するサーフェイスがプライマリサーフェイスである事、DDSCAPS_FLIPは、フリッピングを行えるサーフェイスである事、DDSCAPS_COMPLEXは複合サーフェイスである事をそれぞれ表します。
というか、ここらは応用のしようが無いので丸暗記で結構です。

この後、lpDD->CreateSurface(&ddsd,&lpFront,NULL);を実行すると、同時にフロントバッファ、バックバッファを作成し、lpFrontにはフロントバッファ(兼プライマリサーフェイス)が格納されます。
で、これだけではバックバッファへ描画できないので、次にバックバッファを取得します。

	ddscaps.dwCaps=DDSCAPS_BACKBUFFER;           
	if(lpFront->GetAttachedSurface(&ddscaps,&lpBack)!=DD_OK)
		return FALSE;

GetAttachedSurfaceを使用すると、DDSCAPS構造体のメンバdwCapsへ指定した能力を持つサーフェイスを取得する事が出来ます。
ですから、この関数を使用して、さきほど作成したバックバッファを取得します。ちなみに構文は以下の通りです。

書式 HRESULT GetAttachedSurface( LPDDSCAPS lpDDSCaps, LPDIRECTDRAWSURFACE FAR *lplpDDAttachedSurface );
lpDDSCaps 取得したいサーフェイス情報を詰め込んだDDSCAPS構造体へのポインタ。
今回はddscapsのアドレスを引き渡しています。
lplpDDAttachedSurface 取得したサーフェイスを格納する変数へのポインタ。
今回はlpBackのアドレスを引き渡す。
戻り値 成功した場合はDD_OKが返ってくるらしい。

これで、lpBackにサーフェイスを取得出来ました。また、このサーフェイスは、プライマリサーフェイス等と同じような扱いが出来ます。
ですから、GDIを使用して描画すると言った事も、なんなく出来ます。

フリッピング

では、コードを見てみましょう。

void Flip(void){
	lpFront->Flip(NULL,DDFLIP_WAIT);
}

フリップを行うと、フロントバッファとバックバッファに描かれている内容がそっくりそのまま入れ替わります。
ですから、バックバッファにいろいろ書き込んでおき、フリップを行うと、フロントバッファとバックバッファの内容が入れ替わるので書き込んだ内容が画面に瞬時に表示されます。
んで、もう一度フリップすると、また入れ替わり、元の状態に戻ります。では、Flipの構文を見てみましょう。

書式 HRESULT Flip( LPDIRECTDRAWSURFACE3 lpDDSurfaceTargetOverride, DWORD dwFlags );
lpDDSurfaceTargetOverride サーフェイスを示すポインタです。ここは次の順番のバックバッファ以外のバッファにフリップする時に使用するらしいのですが、普段はNULLを指定して順番通りにフリップさせます。
dwFlags フリップオプションを指定するフラグです。ここはDDFLIP_WAIT固定と考えて良いでしょう。これは、何らかの原因によって直ぐにフリップ出来ない場合に、フリップ出来るまで待つという指定です。
戻り値 成功した場合はDD_OKが返ってくるらしい。

このFlipは、複合サーフェイス(フロントバッファ)のメンバとしてのみ呼び出すことが出来ます。
ですからlpBack->Flip・・・と呼び出すことは出来ません。

フリップは、垂直帰線中(走査線が右下から左上に戻る間)に瞬時に行われるため、ちらつく事がありません。
ちなみに、垂直帰線を待ってからフリップを行うという事なので、ディスプレイの構造上、フリップは秒間60回程度行うのが限界です。

後始末について

後始末は、今まで通りです。なんとなく「lpBack->Release();」とやりたくなる人もいるかもしれませんが、必要ありません。
というかコレ実行するとフリーズするので良い子はマネしないでね(謎。
バックバッファは複合サーフェイスとしてフロントバッファと同時に作成されたため、フロントバッファさえ解放してしまえば同時にバックバッファも解放される事になります(たぶん)。

疑似タイマー処理

さて、今回のサンプルプログラムはどうでしょう?1秒間隔でフリップを実行していると思います。
普段、このようなタイマー処理は、APIを使用して一定時間毎にある関数を呼び出すように設定します。
ですが、これが結構面倒なので、今回は擬似的な処理をしてます。
まず、ビルド → 設定 → リンク → オブジェクト/ライブラリモジュールに「winmm.lib」を書き込む → OK をします。
そして、プログラムの最初のほうで「mmsystem.h」をインクルードします。これでマルチメディア系のAPIが使用できるようになります。
その中にはtimeGetTime();という関数が用意されており、これを呼び出した時点での時間がミリ秒単位で取得出来ます(1000ミリ秒=1秒)。
ですから、メッセージループ周辺を改造して、

	DWORD tim;
	while(1){
		//疑似タイマー処理
		tim=timeGetTime();			//①
		//フリッピング
		Flip();
		do{
			//メッセージループ
			while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
				if(!GetMessage(&msg,NULL,0,0)) 
					Quit();
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}

		}while(timeGetTime()<tim+1000);	//②
	}

とします。こうすると、1番で取得した時間から1000ミリ秒経つまで、ひたすらメッセージループを回す事になります。
ちなみに、こういう一定時間で処理する場合にメッセージループを忘れてしまうと、キー処理がなされなくなり、終了出来ない→電源強制切断という最悪の事態を招くことになります(逝。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、
お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_04.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_04.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、『GDIによるテキストの表示』という文字列が表示されると思います。
今回は、サーフェイスにGDI関数を使用して描画をします。

GDI概要

GDIとはGraphic Device Interfaceの略であり、ウィンドウプログラミングには欠かせないグラフィックライブラリです。
第1回でも記述しましたが、DirectDrawの関数には文字や線、円などの描画関数が用意されていません。その場合、GDIを使用してサーフェイスに描画する事になります。
ただ、GDIの描画速度はあまり速くないので、速度重視ゲームの場合、多用は控えた方が良いでしょう。

デバイスコンテキストハンドルの作成、解放

今回は、テキストを出力する部分を『DdTextOut』関数にまとめました。さぁコードを見てみましょう。

//----------[ GDI関数を使用してテキストの出力 ]----------------------------------------------
void DdTextOut(LPDIRECTDRAWSURFACE sf,int x,int y,char *str){
	HDC hdc;
	sf->GetDC(&hdc);
	TextOut(hdc,x,y,str,strlen(str));
	sf->ReleaseDC(hdc);
}

略

//関数の呼び出し
DdTextOut(lpFront,0,0,"GDIによるテキストの表示");

GDIで描画するためには、デバイスコンテキストを認識するハンドルというものを取得しなければなりません。
デバイスコンテキストを認識するハンドルとは、描画可能なメモリのパスを収納しておくような感じのもので、GDI関数呼び出し時によく引数として使用します。
では、ハンドルの取得方法を説明します。

書式 HRESULT GetDC( HDC FAR *lphDC );
lphDC デバイス コンテキストが返されるハンドルへのポインタ。
戻り値 DirectDrawオブジェクトの作成に成功するとDD_OKが返ってくるらしい。

「lpFront->GetDC(&hdc);」のように、途中「->」を記述しています。これらはC++で使われるクラスというものですが、ここでは解説省略。クラスについては他のHPを参照してください。

こうして、GetDCを実行すると、そのサーフェイス(今回はプライマリサーフェイス)のデバイスコンテキストを認識するハンドルを取得出来ます。

では、次にGDI関数の『TextOut』について説明します。

書式 BOOL TextOut( HDC hdc, int nXStart, int nYStart, LPCTSTR lpString, int cbString );
hdc デバイスコンテキストを認識するハンドル
今回は「hdc」を引き渡す。
nXStart 描画先のX座標
nYStart 描画先のY座標
lpString 描画したい文字列
hdc 文字列のバイト数(文字数とも言うかも)
戻り値 成功した場合はTRUEが返ってくるらしい。

こうして、一通りGDI関数を使用したら、今度はハンドルを解放しなければなりません。
解放には、次の関数(サーフェイスのメンバ関数)を呼び出します。

書式 HRESULT ReleaseDC( HDC hDC );
hDC GetDCを使用して取得したデバイスコンテキストのハンドル。

今回はhdcを引き渡す。GetDCとは違い、ポインタでは無いことに注意しましょう。

戻り値 DirectDrawオブジェクトの作成に成功するとDD_OKが返ってくるらしい。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしていますので、
お持ちの方Visual C++でプロジェクトファイルを開いてください
(「ddraw_03.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_03.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います
(何かキーを押すと終了します)。
今回は、DirectDrawを使用した画面のフルスクリーン化について解説したいと思います。

Windowsプログラミング基礎

いくらDirectDrawを使うとは言え、結局はWindows上で動作しています。
ですから多少Windowsプログラミングの知識が必要になります。
ここでは、「ウィンドウ作成」と「ウィンドウメッセージ」について適当に説明します。

int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
	WNDCLASS wc;
	MSG msg;
	if(!hPrevInstance){
		wc.lpszClassName="dd";
		wc.lpfnWndProc=WndProc;
		wc.style=CS_HREDRAW | CS_VREDRAW;
		wc.hInstance=hInstance;
		wc.hIcon=NULL;
		wc.hCursor=NULL;
		wc.hbrBackground=(HBRUSH)GetStockObject(BLACK_BRUSH);
		wc.lpszMenuName=NULL;
		wc.cbClsExtra=0;
		wc.cbWndExtra=0;
		if(!RegisterClass(&wc)) return FALSE;
	}

	//ウィンドウ作成
	hw=CreateWindowEx(WS_EX_APPWINDOW,"dd","DD簡易講座第3回", WS_POPUPWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, NULL, NULL, hInstance, NULL);
	
	//ウィンドウの表示
	ShowWindow(hw,nCmdShow);		
	UpdateWindow(hw);

	続く

UNIX(Linux)やMS-DOSのC言語では、一番始めに実行されるのは「main」関数でした。
ですが、Windowsでは「WinMain」から始まります。
ウィンドウ作成については、あまり考えずに、こうすればとりあえずウィンドウを作成、
表示出来ると覚えてしまった方が良いです。
と言うより、私自身ウィンドウには興味が無いのでよく解りません(゜゜)☆O(–;)oばこ

Windowsには「ウィンドウメッセージ」というものが有り、OSとアプリケーションがコミュニケーションをとるために使用します。
例えば、ユーザーがアプリケーションのウィンドウを動かそうとした場合、OSはアプリケーションに対し、「ウィンドウが動くぞ」みたいな感じのメッセージを送信します。
そしてアプリケーションは、そのメッセージを受信し、なんらかの処理を行うことになります。
ここで、もしもアプリケーションがこのメッセージを無視してしまうと、ウィンドウが動かせないという状況が生まれます。

では、どうやってメッセージを処理するのでしょうか。コードを見てみましょう。

	while(1){
		while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
			if(!GetMessage(&msg,NULL,0,0)) 
				Quit();
			TranslateMessage(&msg); 
			DispatchMessage(&msg);
		}
	}

この処理は、一般的にメッセージループと呼ばれ、OSが送ってくるメッセージをひたすら待ち、
メッセージを受信したら、「WndProc」(コールバック)関数が呼ばれるようになっています。

//----------[ ウィンドウプロシージャ ]----------------------------------------------------------
HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){
	switch(iMessage){

	//キー押された
	case WM_KEYDOWN:
		Quit();
		break;

	省略

WndProc関数では、このようにメッセージiMessageをswitchによって分岐させ、
それぞれ処理しています。
例えば、ユーザーが、アプリケーションに対し、
何かキーを押した場合、OSが「キーが押されたぞ」みたいな感じのメッセージを送信し、
アプリケーションは、メッセージループ処理中、このメッセージを受信し、
WndProcを呼び出します。
WndProcでは、「キーが押された」というメッセージは「WM_KEYDOWN」
というように表されます(マクロ)。
この辺はWindows関連のHPに行けば、丁寧に解説されているのでそちらを参照してください。
手抜きすぎかも(゜゜)☆O(–;)oばこ

DirectDraw初期化

では、DirectDrawの初期化をします。

サンプルコードを見てみましょう。

#include <ddraw.h>

略

LPDIRECTDRAW lpDD=NULL; 	//DirectDrawオブジェクト
LPDIRECTDRAWSURFACE lpFront=NULL;	//サーフェイス

略

BOOL StartDirectDraw(HWND hw){
	DDSURFACEDESC ddsd;
   
	//DirectDrawオブジェクト作成
	if(DirectDrawCreate(NULL,&lpDD,NULL)!=DD_OK) 
		return FALSE;

	//協調レベル設定
	if(lpDD->SetCooperativeLevel(hw,DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE)!=DD_OK)
		return FALSE;
	
	//解像度設定
	if(lpDD->SetDisplayMode(640,480,8)!=DD_OK)
		return FALSE;

	//プライマリサーフェイスの作成
	ZeroMemory(&ddsd,sizeof(ddsd));
	ddsd.dwSize=sizeof(ddsd);
	ddsd.dwFlags=DDSD_CAPS;
	ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE;
	
	if(lpDD->CreateSurface(&ddsd,&lpFront,NULL)!=DD_OK)
		return FALSE;

	return TRUE;
}

第2回でも記述しましたが、メニューの「ビルド」 → 「設定」 → 「リンク」タブ → オブジェクト/ライブラリモジュールに「ddraw.lib」を書き込む → 「OK」 、ソースの始めの方で「ddraw.h」をインクルードしてください。
これでDirectDrawの関数が使用可能となります(サンプルのプロジェクトでは、既に設定済みです)。

では、順番に解説していきたいと思います。

	if(DirectDrawCreate(NULL,&lpDD,NULL)!=DD_OK) 
		return FALSE;

ここでは、DirectDrawオブジェクトを作成します。
とか言ってるけど、DirectDrawオブジェクト言われてもよーわからんて。
とりあえず、DirectDrawを使うために最初に実行しなきゃいけないらしいです。

では、構文を見てみましょう。

書式 HRESULT DirectDrawCreate( GUID FAR* lpGUID,LPDIRECTDRAW FAR* lplpDD,IUnknown FAR* pUnkOuter );
lpGuid DirectDrawオブジェクトを作成するディスプレイデバイスのGUIDを指定します。

とか言ってるけど、よくわからん(爆)。どうやら、表示したいディスプレイを変更できるらしいのですが、変更する必要が無いのでNULLにしてまう。

lplpDD DirectDrawオブジェクトを指すポインタのアドレスを指定します。

ここでは、グローバル変数として定義した「LPDIRECTDRAW lpDD;」のアドレスを引き渡します。

pInkOuter 将来拡張される時に使うらしいので、とりあえずNULL。DirectXの関数にはこういう物が多いが、使う時が来るのか謎。
戻り値 DirectDrawオブジェクトの作成に成功するとDD_OKが返ってくるらしい。
	if(lpDD->SetCooperativeLevel(hw,DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE)!=DD_OK)
		return FALSE;

ここでは、さきほど作成したDirectDrawオブジェクト(lpDD)とやらを使用して
協調レベルを設定しています。
またしてもよくわからん(爆)<協調。
というか私に言語能力が無いだけかも(死。

どうやら、ウィンドウに対する設定をいろいろと変更できるらしい。

というわけで構文いってみよぅ。

書式 HRESULT SetCooperativeLevel( HWND hWnd , DWORD dwFlags );
hWnd アプリケーションのメインウィンドウへのHWNDハンドルを指定します。これはDirectDrawオブジェクトと関連付ける
ウィンドウです。ここでは、ウィンドウ作成時に使用した「hw」を引き渡します。
dwFlags 設定する協調レベルを表すDWORD値を指定します。下に指定できる値の一覧を記述します。これらはビット毎のOR演算子(|)を使って組み合わせて使用します。
ここでは、フルスクリーンに必要な2つの値「DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE」を指定します。
戻り値 成功するとDD_OKが返ってくるらしい。

SetCooperativeLevelに指定できるDWORD値

DDSCL_ALLOWREBOOT 排他的(フルスクリーン)モード時に、CTRL+ALT+DELの機能の許可。
DDSCL_EXCLUSIVE 排他的レベルの要求。このフラグは、DDSCL_FULLSCREENフラグと共に用いる必要がある。

どうやら、このフラグを使用すると、他のアプリケーションでこのフラグを使用した時に、エラーが発生するようになるらしい。同時に2つ以上のアプリケーションがフルスクリーン化出来なくするためとか。

DDSCL_FULLSCREEN フルスクリーン化出来ます。必ずDDSCL_EXCLUSIVEと組み合わせて使用しなければならない。
DDSCL_NORMAL アプリケーションが、通常のWindowsアプリケーションとして機能することを表す。このフラグは、DDSCL_ALLOWMODEX、 DDSCL_EXCLUSIVE、またはDDSCL_FULLSCREENフラグと共に用いることはできない。
DDSCL_NOWINDOWCHANGES アクティベート時、DirectDrawがアプリケーションウィンドウを最小化したりリストアしたりさせない。
	if(lpDD->SetDisplayMode(640,480,8)!=DD_OK)
		return FALSE;

ここでは、DirectDrawオブジェクト(lpDD)を使用して、ディスプレイの解像度、色数を設定します。

書式 HRESULT SetDisplayMode( DWORD dwWidth, DWORD dwHeight, DWORD dwBPP );
dwWidth 画面解像度の横幅を指定します。
dwHeight 画面解像度の縦幅を指定します。
dwBpp 使用したい色数をビット数で指定します。例えば、8を指定すると、256色、16を指定するとHigh Colorとなります。
戻り値 成功するとDD_OKが返ってくるらしい。

画面解像度は、適当に指定できるわけではありません。指定できる解像度は、主に640×480、800×600、1024×768と言った一般的な解像度です(謎。色数も8(256色)、16(High Color)、24(True Color)、32(True Color)
です。ゲームでは、主に640×480×8(256色)を使用します。これよりも広かったり色数が多いと、
キャラクターデータを置くビデオメモリが足りなくなったり、画像転送速度に影響したりします。
また、256色モードの時だけは、パレットの変更が出来ます。パレットについては後で説明します。
それから、320×240(MODE X)と言った特別な解像度も指定出来ますが、これはいろいろと面倒なので後で説明しますパート2(ぉ。

	ZeroMemory(&ddsd,sizeof(ddsd));
	ddsd.dwSize=sizeof(ddsd);
	ddsd.dwFlags=DDSD_CAPS;
	ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE;
	
	if(lpDD->CreateSurface(&ddsd,&lpFront,NULL)!=DD_OK)
		return FALSE;

ここでは、プライマリサーフェイスを作成します。サーフェイスとは、ビデオメモリを意味し、実際に描画を行う場所となります。
プライマリサーフェイスは、画面に表示されているビデオメモリを意味します。ですから、ここへなんらかの描画を行った瞬間、画面に
表示される事になります。一般的には、複数作業用のサーフェイス(オフスクリーンサーフェイスと呼ぶ)を作成し、そこへキャラクターイメージを配置し、それらのデータを一度バックバッファと呼ばれるサーフェイスへ転送し、フリッピングという作業をすると、バックバッファとフロントバッファ(この場合プライマリサーフェイスの事)の内容がそっくりそのまま入れ替わり、画面へ表示される事になります。
オフスクリンサーフェイスや、バックバッファを作成する方法は、また後で説明します。ここではとりあえずプライマリサーフェイスだけを作成します。

まず、DDSURFACEDESCという構造体を使用して、作成するサーフェイスの情報をここへ詰め込みます。

DDSURFACEDESC構造体の主な内容は以下の通りです。

dwSize この構造体のサイズ(バイト数)
dwFlags 有効なメンバをDWORD値で指定します。どうやら、ここで使用したいメンバを指定し、そのメンバへ値を代入するようです。
指定できるメンバは下の方に記述してあります。
dwHeight サーフェイスの縦幅
dwWidth サーフェイスの横幅
lPitch メモリの水平行間のピクセル数
dwBackBufferCount 作成するバックバッファの数
ddsCaps サーフェイスの機能を指定します。

dwFlagsで指定できる主なメンバは以下の通りです。これらはビット毎のOR演算子(|)を使って組み合わせて使用します。

DWORD値 対応するメンバ
DDSD_DDSCAPS ddsCaps
DDSD_HEIGHT dwHeight
DDSD_WIDTH dwWidth
DDSD_BACKBUFFERCOUNT dwBackBufferCount

ここでは、プライマリサーフェイスを作成するために、DDSURFACEDESC ddsd構造体を使用し、ddsd.dwFlagsにDDSD_CAPSを指定し、ddsd.ddsCaps.dwCaps(何故かメンバの中にまたメンバ)にDDSCAPS_PRIMARYSURFACEを指定します。ここは、丸暗記ですかね(^^;
そうしたら、次にサーフェイスを実際に作成するためにCreateSurfaceを呼び出します。この構文は以下の通りです。

書式 HRESULT CreateSurface( LPDDSURFACEDESC lpDDSurfaceDesc, LPDIRECTDRAWSURFACE FAR *lplpDDSurface, IUnknown FAR *pUnkOuter );
lpDDSurfaceDesc 要求されたサーフェスを記述するDDSURFACEDESC構造体のアドレス。

今回は、先ほど設定したddsdのアドレスを引き渡しています。

lplpDDSurface 呼び出しが成功した場合、有効なDirectDrawSurfaceポインタによって初期化されるサーフェスへのポインタへのポインタ。

ややこしいですが、今回はあらかじめ宣言しておいた「LPDIRECTDRAWSURFACE lpFront;」のアドレスを引き渡しています。

pUnkOuter 将来拡張される時に使うらしいので、とりあえずNULL。
戻り値 成功するとDD_OKが返ってくるらしい。

DirectDraw後始末

コードを見てみましょう。

#define RELEASE(i) if(i){i->Release();i=NULL;}
BOOL EndDirectDraw(void){
	RELEASE(lpFront);
	RELEASE(lpDD);
	return TRUE;
}

サーフェイスや、DirectDrawオブジェクトはRelease();を実行すると終了する事が出来ます。
例えば、プライマリサーフェイスなら「lpFront->Release();」となります。
ここは、マクロ(define)を使用すると、簡潔に記述する事が出来ます。
プログラムを終了したい場合は、必ずこれらを実行しなければなりません。これを忘れてしまうと、Windowsに戻ったとき、パソコンが正常に動作しない可能性があります。