クラスまみれのゲームプログラミング入門

7:ウィンドウの作り方

 ウィンドウを作成するにはWin32APIの「CreateWindow」関数を呼び出せばOKです。しかし、その前にどのようなウィンドウを作りたいかをシステムに伝えなくてはいけません。そのシステムに伝えるための命令が「RegisterClass」です。登録するための情報は多くあるため、あらかじめ構造体に格納した後で、RegisterClassへと送り込みます。

wndclass.cpp
#include <tchar.h>
#include <windows.h>

int APIENTRY _tWinMain
(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpszCmdLine, int nCmdShow)
{
	WNDCLASS wc;	// 登録する情報を格納する構造体

	wc.cbClsExtra = 0;	// 構造体のサイズを拡張するバイト数(通常は0)
	wc.cbWndExtra = 0;	// ウィンドウのサイズを拡張するバイト数(通常は0)
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
		// ウィンドウの背景を塗りつぶすためのブラシ。
		// ここではシステムが用意している白一色で塗りつぶすブラシを割り当てている。
	wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
		// ウィンドウ上にマウスカーソルがきたときのマウスの形状
		// ここではシステム標準の矢印形カーソルを登録している。
	wc.hIcon = NULL;
		// ウィンドウの左上に表示されるアイコン。
		// 今回はNULL値を指定してアイコンを登録しない状態にしているが、
		// LoadImage関数を使ってオリジナルアイコンを呼び出してもよい。
	wc.hInstance = hInstance;
		// ウィンドウを作るためのインスタンス(≒プログラムの)ハンドルを指定する。
		// 通常はWinMainが呼び出されたときに格納されているhInstanceで問題ない。
	wc.lpfnWndProc = ---;
		// ウィンドウから通知されたイベントを処理するための関数を指定。
		// 後で解説していくので、ここではいったん省略。
	wc.lpszClassName = _T("Game");
		// 登録するウィンドウのクラス名。
		// この文字列を元に、Windowsがウィンドウの検索を行ったりできる。
	wc.lpszMenuName = NULL;
		// メニューのリソースID。
		// 今回はメニューは作成しないのでNULL値。
	wc.style = NULL;
		//ウィンドウクラスのスタイル。
		// 特に指定するものはないのでNULL値にしている。
	
	// ウィンドウクラスの登録
	if(RegisterClass(&wc) == NULL){
		// ウィンドウクラスの登録に失敗したら、
		// ウィンドウの作成が不可能なので、ここにて終了。
		return 0;
	}

	return 0;
}

 ウィンドウクラスの登録に成功したら、次はウィンドウを作成します。ウィンドウ作成は「CreateWindow」、作成したウィンドウの表示には「ShowWindow」関数を使います。

createwindow.cpp
#include <tchar.h>
#include <windows.h>

int APIENTRY _tWinMain
(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpszCmdLine, int nCmdShow)
{
	WNDCLASS wc;

	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
	wc.hIcon = NULL;
	wc.hInstance = hInstance;
	wc.lpfnWndProc = ---;	//後で解説
	wc.lpszClassName = _T("Game");
	wc.lpszMenuName = NULL;
	wc.style = NULL;
	
	// ウィンドウクラスの登録
	if(RegisterClass(&wc) == NULL){
		return 0;
	}

	HWND hWnd = CreateWindow(
		wc.lpszClassName,		// 登録したクラス名
		_T("クラスまみれのゲームプログラミング入門"),
			// ウィンドウに表示するタイトル
		WS_OVERLAPPEDWINDOW,
			// ウィンドウのスタイル
			// (タイトルバー、システムメニュー、最小化ボタンを使用)
		CW_USEDEFAULT,	// ウィンドウのx位置(システム標準値を使用)
		CW_USEDEFAULT,	// ウィンドウのy位置(システム標準値を使用)
		640,			// ウィンドウの幅
		480,			// ウィンドウの高さ
		NULL,
			// 親ウィンドウのハンドル
			// このプログラムに親ウィンドウは存在しないのでNULLを渡します
		NULL,
			// メニューハンドル
			// このプログラムにメニューは存在しないのでNULLを渡します
		hInstance,		// インスタンスハンドル
		NULL
			// ウィンドウ作成時のイベントへ渡すデータのポインタ
			// 渡すデータはないのでNULLを指定します
		);

	// ウィンドウを表示します
	// hWnd : 表示するウィンドウのハンドル
	// nCmdShow : 最初のウィンドウの状態
	//			  ここではショートカットなどから報告された
	//			  初期ウィンドウ状態の指定を利用しています
	ShowWindow(hWnd, nCmdShow);

	return 0;
}

 続いて、作成したウィンドウに対して発生する、作成された・描画されたなどのイベントを処理するための関数を作成して、WNDCLASSのlpfnWndProcに連結させます。この関数は、ウィンドウに対して何らかのアクションが発生するたびに呼び出されます。関数名は別に「WndProc」にする必要はないのですが、かといって他の名前にする理由もないので、プログラムの内容を明確にする意味でも、この名称を使っておくのが無難でしょう。

wndproc.cpp
#include <tchar.h>
#include <windows.h>

//ウィンドウイベントを処理する関数
// hWnd		: イベントが発生したウィンドウのハンドル
// msg		: 発生したイベントの内容
// wParam	: イベントから通知されたデータ(イベントにより内容は異なります)
// lParam	: イベントから通知されたデータ(イベントにより内容は異なります)
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg) {
		case WM_DESTROY:
			// ウィンドウが閉じられたときにこの部分が実行される
			PostQuitMessage(NULL);
			// プログラムの終了をシステムに通知する関数です
			// カッコ内の数値は終了イベント(WM_QUIT)に
			// 通知したいデータを格納します
			// (今回は通知するデータはないのでNULL)
			break;
		default:
			// 作成するプログラムに必要のないイベントであれば
			// システムにイベント処理を丸投げする命令で対処する
			return (DefWindowProc(hWnd, msg, wParam, lParam));
	}
	return (0);
}


int APIENTRY _tWinMain
(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpszCmdLine, int nCmdShow)
{
	WNDCLASS wc;

	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
	wc.hIcon = NULL;
	wc.hInstance = hInstance;
	wc.lpfnWndProc = WndProc;	//イベント処理関数を連結
	wc.lpszClassName = _T("Game");
	wc.lpszMenuName = NULL;
	wc.style = NULL;
	
	if(RegisterClass(&wc) == NULL){
		return 0;
	}

	HWND hWnd = CreateWindow(
		wc.lpszClassName,
		_T("クラスまみれのゲームプログラミング入門"),
		WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
		CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,	
		NULL, NULL, hInstance, NULL );

	ShowWindow(hWnd, nCmdShow);

	return 0;
}

 最後にウィンドウイベントを送受信する部分を記述して、ウィンドウ周りのプログラムは完成です。

message.cpp
#include <tchar.h>
#include <windows.h>

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg) {
		case WM_DESTROY:
			PostQuitMessage(NULL);
			break;
		default:
			return (DefWindowProc(hWnd, msg, wParam, lParam));
	}
	return (0);
}


int APIENTRY _tWinMain
(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpszCmdLine, int nCmdShow)
{
	WNDCLASS wc;

	wc.cbClsExtra = 0;
	wc.cbWndExtra = 0;
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
	wc.hIcon = NULL;
	wc.hInstance = hInstance;
	wc.lpfnWndProc = WndProc;	//イベント処理関数を連結
	wc.lpszClassName = _T("Game");
	wc.lpszMenuName = NULL;
	wc.style = NULL;
	
	if(RegisterClass(&wc) == NULL){
		return 0;
	}

	HWND hWnd = CreateWindow(
		wc.lpszClassName,
		_T("クラスまみれのゲームプログラミング入門"),
		WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
		CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,	
		NULL, NULL, hInstance, NULL );

	ShowWindow(hWnd, nCmdShow);

	// ウィンドウが閉じられるまで永久にループさせます
	MSG msg;
	while(TRUE){
		// ウィンドウに通達されようとしているイベントを取得
		if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){
			// 終了イベントだったならループを抜けます
			if(msg.message == WM_QUIT) break;

			// 仮想キーメッセージを文字メッセージへ変換します
			TranslateMessage(&msg);
			// 実際にウィンドウへイベントを通達
			DispatchMessage(&msg);
		}else{
			// イベントが発生していなかったのなら
			// ここで空き時間に行いたい処理内容を記述
			Sleep(10);
		}
	}

	return 0;
}

 プログラムの解説によっては、PeekMessageの箇所がGetMessageになっている場合があります。PeekMessageはイベント発生の有無にかかわらず、ノンストップで処理が進行するのに対し、GetMessageは何らかのイベントを取得するまで処理はストップしてしまいます。ちょくちょく処理が停止してしまっては、リアルタイム性が要求されるプログラムにとって不利になってしまいますので、ゲームプログラミングではPeekMessageを使うようにしましょう。


転職、求人情報ならリクルートの転職サイト【リクナビNEXT】

 このプログラムをコンパイルして実行してみると画面上に真っ白なウィンドウが登場するのですが、何か変。よくよくみると、ウィンドウの描画領域が少しばかり小さい気がします。それもそのはず、ウィンドウ作成時に指定したサイズは、最小化や最大化ボタンやタイトルなどを表示するためのタイトルバーと、ウィンドウの縁を含めたものになっているからです。これを解決するには、ウィンドウの初期サイズにこれらの幅も考慮したのを指定しなくてはいけません。

 最近のOSはウィンドウのデザインがバラエティに富んでいるため、ウィンドウの枠のサイズを取得するだけでは完全ではないこともあります。そのためここではウィンドウ領域とクライアント領域の差分よりウィンドウサイズを調整しています。

metrics.cpp
	HWND hWnd = CreateWindow(
		wc.lpszClassName,
		_T("クラスまみれのゲームプログラミング入門"),
		WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
		CW_USEDEFAULT, CW_USEDEFAULT,
		640, 480, NULL, NULL, hInstance, NULL );

	RECT r1, r2;
	GetClientRect(mainWnd, &r1);
	GetWindowRect(mainWnd, &r2);
	MoveWindow(mainWnd, r2.left, r2.top,
		640 + ((r2.right - r2.left) - (r1.right - r1.left)),
		480 + ((r2.bottom - r2.top) - (r1.bottom - r1.top)), FALSE);

 これで描画領域のサイズが640×480になりました。