DirectXを始める前にウィンドウを作ろう


概要

DirectXを使えばウィンドウに絵を表示することができます。
ただ、その絵を表示するための基盤となるウィンドウを作成する機能は
DirectXでは提供されていません。

directx_0101

ウィンドウの作成は別の方法で行い、そのウィンドウをDirectXで使用して絵を表示します。
このページでは、そのウィンドウの作成をWindowsAPIを使用して行う方法の説明をします。
※サンプルではメインループの実装も行っているので、メインループ、ゲームループの
 説明を読んでいない方、または分からない方はこちらで確認をお願いします。

サンプル

サンプルはここからダウンロードできます。
環境については以下の内容となっています。

開発環境
VSのバージョン VisualStudio 2019
内容 ウィンドウを作成するだけなので
実行しても何も起きない

作成方法

ウィンドウの作成は「CreateWindow関数」を使用して行います。
ただ、以下の表のように関数を使用する前後でいくつか実装するべき処理があるので、
そちらも合わせて説明します。

順番 説明
ウィンドウプロシージャの作成
ウィンドウ情報の登録
ウィンドウの作成
ウィンドウのリサイズ
ウィンドウを表示する

①.ウィンドウプロシージャの作成

ウィンドウプロシージャとはOS(Windows)から送られてきたメッセージを
処理するためのコールバック関数です。
この関数は次の「②ウィンドウ情報の登録」で登録をします。

directx_00102
// ウィンドウプロシージャ
LRESULT CALLBACK WindowProcedure(HWND window_handle, UINT message_id, WPARAM wparam, LPARAM lparam)
{
	return DefWindowProc(window_handle, message_id, wparam, lparam);
}

ウィンドウプロシージャは戻り値や引数の型や数は決まっており、
変更することができません。
変更可能な箇所は関数名と引数名、関数の処理部分です。

ウィンドウプロシージャの使い方

ウィンドウプロシージャには様々なメッセージが送信されます。
このメッセージ全てに対して対応する必要はなく、
必要なメッセージだけ、対応処理の実装を行います。

// ウィンドウプロシージャの使い方
LRESULT CALLBACK WindowProcedure(HWND window_handle, UINT message_id, WPARAM wparam, LPARAM lparam)
{
	// メッセージに対する対応する
	switch (message_id)
	{
	// ウィンドウの閉じるアイコンがクリックされた
	case WM_CLOSE:
		PostQuitMessage(0);
		break;
	// 対応しない
	default:
		return DefWindowProc(window_handle, message_id, wparam, lparam);
		break;
	}

	// 対応したから0を返す
	return 0;
}

上のコードでは「WM_CLOSE」に対する処理を実装しています。
WM_CLOSEはウィンドウの右上の「×」がクリックされた際に送信されるメッセージで、
PostQuitMessageでメッセージキューにWM_QUITを送信する処理を実装しています。
このようにメッセージに対する処理を実装したら戻り値を0にして
何も行わなかったら「DefWindowProc関数」の結果を返します。
DefWindowProc関数にはウィンドウプロシージャで渡されている引数をそのまま使用します。

②.ウィンドウ情報の登録

ウィンドウを作成するには数多くの情報が必要です。
ですが、それをCreateWindow関数で全て指定した場合、
引数の数が多大になってしまうので、いくつかの情報を事前に設定しておきます。

その情報は「WNDCLASS」または「WNDCLASSEX」構造体にまとめてあるので、
作成前にこの構造体のメンバに必要な情報を設定して、「RegisterClass関数」
または「RegisterClassEx関数」で作成時に使えるように登録します。

WNDCLASS構造体

WNDCLASS構造体で設定できる内容はウィンドウスタイル(閉じるボタン無効化、
ウィンドウサイズが変更された時に再描画を行う等)やタイトルのアイコン、
アプリのアイコン、カーソル、背景色などがあります。

directx_0089

WNDCLASSEXは「cbSize」「hIconSm」メンバが追加されており、
タイトルバーにアイコン設定をすることができます。

directx_0090
// WNDCLASSEX構造体に必要な情報を設定する
WNDCLASSEX window_class =
{
	sizeof(WNDCLASSEX),		// 構造体のサイズ
	CS_HREDRAW | CS_VREDRAW,	// クラスのスタイル
	WindowProcedure,		// ウィンドウプロシージャ
	0,				// 補助メモリ
	0,				// 補助メモリ
	hInstance,			// このアプリのインスタンスハンドル
	LoadIcon(NULL, IDI_APPLICATION),// アイコン画像
	LoadCursor(NULL, IDC_ARROW),	// カーソル画像
	NULL,				// 背景ブラシ(背景色)
	NULL,				// メニュー名
	TEXT("CreateWindow"),		// クラス名							
	NULL				// 小さいアイコン
};

ウィンドウ情報の登録

WNDCLASS構造体の設定が完了したら、次は「RegisterClass関数」
または「RegisterClassEx関数」で情報の登録を行います。
どちらの関数を使用するかは設定に使用した構造体がWNDCLASSと
WNDCLASSEXのどちらを使用しているかで決まります。

directx_0091
// WNDCLASSEX構造体の登録
if (RegisterClassEx(&window_class) == 0)
{
	// 失敗
	return 0;
}

戻り値のATOM型で値が返ってきますが、この値はシステムが管理している値です。
システム管理になるので、このアプリケーションのみの値ではなく、
PCで動作しているアプリ全体の値として扱われています。
※RegisterClassExは引数がWNDCLASSEX構造体になるだけなので省略します。

③.ウィンドウ作成

ウィンドウ情報の登録が完了したら、「CreateWindow関数」でウィンドウを作成します。

directx_0092
// ウィンドウ作成
HWND window_handle = CreateWindow(
	// 登録しているウィンドウクラス構造体の名前
	TEXT("CreateWindow"),
	// ウィンドウ名(タイトルバーに表示される)
	TEXT("ウィンドウ生成サンプル"),
	// ウィンドウスタイル
	(WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME),
	// 表示位置
	CW_USEDEFAULT,
	CW_USEDEFAULT,
	// サイズ
	width,
	height,
	// 親ハンドル
	NULL,
	// メニューハンドル
	NULL,
	// インスタンスハンドル
	instance,
	// WM_CREATEメッセージでlpparamに渡したい値
	NULL);

if (window_handle == NULL)
{
	return 0;
}

少しわかりづらいと思われる引数の解説をします。
第一引数で登録しているウィンドウ情報のどれを使用するかを決めるので、
RegisterClassで登録したウィンドウクラスの名前を指定してください。

第三引数はウィンドウのスタイル設定ができます。
この設定はビット演算で設定しており、複数設定する時は「|」を使用してください。
今回の設定について説明増します。
まず「WS_OVERLAPPEDWINDOW」ですが、これはは複数のスタイルが合わさった定数です。
この中には「WS_SYSMENU」「WS_OVERLAPPED」などのいくつものスタイルが
「|」で設定されています。

/*
 * Common Window Styles
 */
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED     | \
                             WS_CAPTION        | \
                             WS_SYSMENU        | \
                             WS_THICKFRAME     | \
                             WS_MINIMIZEBOX    | \
                             WS_MAXIMIZEBOX)

この中にある「WS_THICKFRAME」はウィンドウのサイズを変更できる機能を付与します。
ゲームのウィンドウサイズは基本的に固定しているモノがほとんどですので、
可変になってしまう「WS_THICKFRAME」が邪魔です。
なので、「^(排他的論理和)」でWS_OVERLAPPEDWINDOWからWS_THICKFRAMEを外しています。

第四、五引数の「CW_USEDEFAULT」は特に指定をしないということになるので、
システムが勝手に座標などを割り当ててくれます。

④.ウィンドウのリサイズ

次はウィンドウのリサイズです。
CreateWindowで作成したウィンドウのサイズはタイトル部分を含めたサイズになっているので、
ウィンドウの表示領域部分だけで指定したサイズにする必要があります。
※ウィンドウの表示領域部分を「クライアント領域」と呼びます。

directx_0002

リサイズの方法

リサイズの方法はWindowsAPI関数の「AdjustWindowRect関数」を使用する方法と
「開発側でリサイズ用のサイズを計算をする」方法があります。
今回は以下のようにリサイズの手順や計算の詳細を説明書できるので、
開発側で計算をする方法を選択しています。

手順 内容
ウィンドウ全体のサイズ取得
クライアント領域のサイズの取得
フレームサイズの算出
リサイズ用のウィンドウサイズの算出
ウィンドウサイズの更新

①.ウィンドウ全体のサイズ取得

最初はウィンドウ全体がどのくらいのサイズで作られているかを把握するために
「GetWindowRect関数」を使用します。

directx_0093
RECT window_rect;

// ウィンドウサイズを取得
if (GetWindowRect(window_handle, &window_rect) == false)
{
	return 0;
}

②.クライアント領域のサイズの取得

次はクライアント領域のサイズを「GetClientRect関数」で取得します。

directx_0094
RECT client_rect;

// ウィンドウサイズを取得
if (GetClientRect(window_handle, &client_rect) == false)
{
	return 0;
}

③.フレームサイズの算出

二つのサイズ領域が取得できたので、外枠のいわゆるフレームサイズの算出をします。
directx_0003
// フレームサイズ算出
int frame_size_x = (window_rect.right - window_rect.left) - (client_rect.right - client_rect.left);
int frame_size_y = (window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top);

④.リサイズ用のウィンドウサイズの算出

フレームサイズの算出が完了したら、この情報を使用して新規のウィンドウサイズを算出します。
計算式はフレームサイズと本来クライアント領域にしたかったサイズを足します。

directx_0004
// リサイズ用サイズの算出
int resize_width = frame_size_x + width;
int resize_height = frame_size_y + height;

⑤.ウィンドウサイズの更新

リサイズ用のサイズ算出が完了したので、最後に「SetWindowPos関数」を使って
ウィンドウサイズの更新を行います。
新規サイズが割り出せたらSetWindowPos関数を使用して
新規のサイズを登録します。

directx_0095
// ウィンドウサイズ更新
SetWindowPos(
	// ウィンドウハンドル
	window_handle,
	// 配置順序のハンドル(NULLでよし)
	NULL,
	// 表示座標X
	CW_USEDEFAULT,
	// 表示座標Y
	CW_USEDEFAULT,
	// リサイズ横幅		
	resize_width,
	// リサイズ縦幅
	resize_height,
	// SWP_NOMOVE => 位置変更なし
	SWP_NOMOVE);

これでウィンドウのリサイズは完了です。

⑤.ウィンドウを表示する

ウィンドウを作成、リサイズが完了したので、最後に「ShowWindow関数」を使用して
ウィンドウを表示します。
directx_0096
// ウィンドウを表示状態にする
ShowWindow(window_handle, SW_SHOW);
以上でウィンドウの作成は終了です。
「WindowsAPI」「メインループ」についても既に読了済みでしたら、
DirectXを使用する前に行う準備は完了です。