11.Dx12版のウインドウ作成

今回は、Dx12版のウインドウを作成します。
いま作業しているのは、「シンプルバージョンDx12版」ですので、ソリューションは「SimplSampleTest」ディレクトリ内の「BaseCrossDx12.sln」ということになります。VS2015でこのソリューションを開いてください。

この記事は、
コミット「Dx11、Dx12両環境設定とDirectXTexをプロジェクトに追加。でもまだまだただのWindowsアプリです。」
から、
コミット「Dx12版のウインドウ作成」
の間の作業です。
GitHubサイト

https://github.com/WiZFramework/BaseCross

まず、最初に「BaseCrossDx12」プロジェクト「ヘッダー ファイル」フィルターに「Project.h」を追加します。ディレクトリは「WinMain.cpp」と同じ場所で結構です。
そしてその中に

#pragma once
#include "resource.h"

と記述します。

#pragma once

というのは、「すでにインクルードされているヘッダファイルはインクルードしない」ということをコンパイラに知らせる文です。

#ifndef  _Project_h
#define _Project_h

//この中にヘッダコードを記述

#endif

と書くのと同じです。
ここで作成した「Project.h」には、ゲームで使用するプレイヤーやキャラクターなどのゲームに直接使用するクラスのヘッダをまとめておきます。
このなかで、「resource.h」をインクルードします。「resource.h」はリソースID(Windowsアプリが持つリソースです。アイコンや、もし作成するならメニューやダイアログボックスのIDです)を管理します。直接編集することはあまりありませんし、ゲームプログラミングにおいては関係するのはアイコンくらいだと思います。

このように前準備したところで、「stdafx.h」を確認します。
これは前項でも紹介しました。STLやC言語のライブラリなど、コミット「Dx12版のウインドウ作成」時点では、結構な量のコードが記述されています。

そして最後に「WinMain.cpp」です。これがウインドウアプリケーションのエントリポイントであり、ウインドウを作成する関数が記述されています。

とにかく「ウインドウの初期化(作成)」のコードはどこにでもあります。インターネットで「ウインドウの作成 VC++」で検索すると、山ほどのコードが出てきます。
いずれこの記事もその山ほどの中に仲間入りするわけですが、僕はウインドウの作成には以下のように、最低限5つのポイントがあると思っています。

1、WinMain()関数の作成
2、ウインドウクラスの登録
3、ウインドウの作成
4、ウインドウプロシージャの作成
5、メッセージループ処理

このうち4以外は、WinMain()関数に書くか、そこから呼び出す関数に記述します。
検索で出てくるサンプルソースなどの中には、直接ソースを紹介して終わりにしたり、あるいは「こう記述しとけば問題ないからあまり考えるな」的な記述もみられます(多くはないですけどね)。
しかしここでは、ちょっと趣向を変えて、上記のポイントの役割や意味などについて書きたいと思います。(コードのコピペはどこでもできますからね。重要なのは、理解して書く、ってことはないでしょうか)

まず、「1、WinMain()関数の作成」ですが、コミット「Dx12版のウインドウ作成」時点では、以下のような記述になってます。

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
	_In_opt_ HINSTANCE hPrevInstance,
	_In_ LPTSTR    lpCmdLine,
	_In_ int       nCmdShow)
{
	//中略

WinMain()というのはWindowsアプリケーションの「エントリポイント」ですね。「エントリポイント」とはOSかOSに近い部分から呼び出される関数です。
Windowsアプリケーションは「WinMain()で始まってWinMain()で終わり」ます。つまりWinMain()の終了はアプリケーションの終了なのです。
_tWinMain()という関数名は「UNICODE対応のWinMain()関数」と考えることができます。
_tWinMain()に渡される引数のうち、「HINSTANCE hInstance」というのは、「インスタンスのハンドル」です。これは、このインスタンスの識別IDのようなもので、何かとWindowsプログラムには必要な変数です。
「HINSTANCE」の先頭の「H」は「ハンドル」を表します。「ハンドル」はインスタンスのハンドル、ばかりではなく、ウインドウのハンドル(HWND)、ペンのハンドル(HPEN)、ブラシのハンドル(HBRUSH)などなど、とにかく、Windowsプログラムで使用されるオブジェクトの多くは「ハンドル」という識別子を持っています。
ゲームプログラムでは、ゲーム画面内のグラフィック処理はテクスチャを自作したりインターフェイスを自作したりするで、このハンドルを使用する機会は少ないかもしれません。
でも多くのWindowsアプリケーションは、多くのグラフィック処理をWindowsのリソースを使って行います。「ボタン」とか「入力テキスト」とか、いちいちアプリケーションプログラマが記述するわけではありません。ですので「Hで始まればハンドルだな」くらいの知識でいいですので持っておくといいと思います。
2番目の引数の「HINSTANCE hPrevInstance」はよく「16ビット時代のなごり」と解説されてることが多いですが、たしかにそうはそうなんだけど、では「16ビット時代」は何が入っていたかというと、「1つ前のインスタンス」です。16ビット時代であっても、同じアプリケーションを複数起動できたわけで、そんなとき、このインスタンスのハンドルを参照すると(例えばウインドウメッセージを送ったりすると)、比較的簡単に「自分のクローンと」やり取りができたのです。これはこれで便利だったわけです。
完全32ビットの現在では「自分のクローン」とやり取りするのでも、「プロセス」「スレッド」などの知識が必要です。「なんちゃってマルチタスク」ではなく「ちゃんとしたマルチタスク」なので当たり前といえば当たり前なのですが、もし、ほかのインスタンスとのやり取りを考えているなら、ここでは述べるものではありませんが、「プロセス」「スレッド」「ほかのインスタンス」「通信」などをキーワードで検索するといろいろ出てくると思います。
「LPTSTR lpCmdLine」はコマンドライン引数が入ってます。BaseCrossでは「フルスクリーンかどうか」をコマンドラインに渡せるようになってます。ここのポイントは、型が「LPTSTR」になっているところで、UNICODE環境だとこれは「wchar_t」となります。
これはコマンドラインを「wstringで受けられる」ってことなので、便利ですよね。
「int nCmdShow」はこのインスタンスが起動したときの表示状態です「全画面表示」「通常表示」「最小化」が入ります。

これらの起動パラメータを受け取って、このインスタンスは起動するわけですが、まずやらなければいけないのは、「2、ウインドウクラスの登録」です。BaseCrossでは「MyRegisterClass関数」内で行ってます。
では「ウインドウクラス」とは何か。
例えばWindows共通で使えるリソースに「ボタン」がありますが、これはすべて「同じウインドウクラス」で作成されます。でも、ボタンの表示文字とか大きさとかはそれぞれのボタンで違いますよね。これらの違いは、「ウインドウの作成」で決めるわけです(ボタンもウインドウです)。
BaseCrossでいえば、作るウインドウはメインのウインドウ1つです。でもそのウインドウのために「ウインドウクラス」を作成し、登録しなければいけません。
このウインドウクラスはボタンのように他のプログラムで共有して使うウインドウクラスではないので、「アプリケーション特有のウインドウクラス」と考えられます。「アプリケーション特有のウインドウクラス」はそのアプリケーション内に同じ名前のものがなければ問題ありません。
「MyRegisterClass関数」では、WNDCLASSEX構造体にパラメータを設定して、RegisterClassEx()関数に渡しています。このパラメータには、ウインドウクラス名、ウインドウプロシージャ(後述)、アイコンやバックグラウンドカラーなどを指定します。

	WNDCLASSEX wcex;
	ZeroMemory(&wcex, sizeof(wcex));

	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style = CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc = WndProc;
	wcex.cbClsExtra = 0;
	wcex.cbWndExtra = 0;
	wcex.hInstance = hInstance;
	wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_ICON1);
	wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszMenuName = nullptr;
	wcex.lpszClassName = pClassName;
	wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_ICON1);
	return RegisterClassEx(&wcex);

こんな感じですね。ここではアイコンをリソースの「IDI_ICON1」を入れ、カーソルは通常の矢印を指定しています。

	wcex.lpfnWndProc = WndProc;

に注目してください。「WndProc」はWinMain.cppの冒頭にある

LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);

という関数宣言の「関数へのポインタ」を代入します。(C/C++では関数名は関数へのポインタになります)
WndProc関数の実体は、WinMain.cppの下のほうにあります。このWndProc関数こそ、「ウインドウプロシージャ」であり、「4、ウインドウプロシージャの作成」ポイントです。

ウインドウプロシージャの作成の前に「3、ウインドウの作成」を説明します。これは、WinMain.cppにある「InitInstance関数」で行ってます。
フルスクリーンの場合と通常ウインドウの場合を分岐して「CreateWindow関数」を呼んでます。「CreateWindow関数」には先ほど登録したウインドウクラスのクラス名を渡す必要あります。

//フルスクリーン
		hWnd = CreateWindow(
			pClassName,			// 登録されているクラス名
			pWndTitle,			// ウインドウ名
			WS_POPUP,			// ウインドウスタイル(ポップアップウインドウを作成)
			0,					// ウインドウの横方向の位置
			0,					// ウインドウの縦方向の位置
			iClientWidth,		// フルスクリーンウインドウの幅
			iClientHeight,		// フルスクリーンウインドウの高さ
			nullptr,				// 親ウインドウのハンドル(なし)
			nullptr,				// メニューや子ウインドウのハンドル
			hInstance,			// アプリケーションインスタンスのハンドル
			nullptr				// ウインドウの作成データ
		);

か、通常

//通常ウインドウ
		//ウインドウのサイズ調整
		RECT rc = { 0, 0, iClientWidth, iClientHeight };
		AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
		//ウインドウの作成
		hWnd = CreateWindow(
			pClassName,				// 登録されているクラス名
			pWndTitle,				// ウインドウ名
			WS_OVERLAPPEDWINDOW,	// ウインドウスタイル(オーバーラップウインドウを作成)
			CW_USEDEFAULT,			//位置はWindowsに任せる
			CW_USEDEFAULT,			//位置はWindowsに任せる
			rc.right - rc.left,		//幅指定
			rc.bottom - rc.top,		//高さ指定
			nullptr,					// 親ウインドウのハンドル(なし)
			nullptr,					// メニューや子ウインドウのハンドル
			hInstance,				// アプリケーションインスタンスのハンドル
			nullptr					// ウインドウの作成データ
		);

ですね。大きな違いは、CreateWindowの3番目の引数(WS_POPUPかWS_OVERLAPPEDWINDOW)と、通常ウインドウの場合

	AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);

で、クライアント領域のサイズから、ウインドウ全体の領域サイズを計算してるところです。
「ウインドウサイズ」というのは移動バーやシステムメニューの領域を含めた矩形の大きさです。CreateWindow関数にはこれを渡す必要があります。しかし、ここで持ってる情報は「ゲーム領域の大きさ」です。つまり、移動バーなどのサイズの分大きくしてあげないと、ゲームがはみ出してしまいます。これを計算するのがこの関数です。
手動で移動バーの大きさとか領海線の太さとかを計算することも可能ですが、結構な手間です。AdjustWindowRect関数はをそれを行ってくれます。
CreateWindowの戻り値はHWND型の値です。これはウインドウのハンドルと言って、インスタンスのハンドル同様「ハンドル」ですが、このハンドルも結構重要です。Windowsとののやり取りに頻繁に使用します。

ウインドウを作成したら、ShowWindow関数で「表示」させます。作っただけでは表示しません。ここにウインドウのハンドルhWndとWinMainに渡されたnCmdShowを渡します。

	//ウインドウの表示
	ShowWindow(
		hWnd,       //取得したウインドウのハンドル
		nCmdShow    //WinMainに渡されたパラメータ
	);

こんな感じです。ウインドウを表示したらクライアント領域を「更新」します。

	UpdateWindow(hWnd);

UpdateWindow関数は、領域を更新するようそのウインドウに「WM_PAINT」というメッセージを送ります。
各アプリケーションは、ウインドウプロシージャの「WM_PAINT」の処理ブロックで、その処理を行います。なんかややこしいですが、Windowアプリケーションというのは、「メッセージ」をWindowsから受け取り、それに呼応する形でアプリケーションを作成していきます。だから、UpdateWindow関数は、Windowsに「自分自身のウインドウにWM_PAINTを送ってね」という関数なのです。
ゲーム制作に慣れると、約60分の1秒に1度やってくる「ターン」時に、UpdateやDrawを行います。つまりWindowsとのやり取りはあまり気にせず、計算したり描画したりするわけですが、通常のWindowsアプリというのは、基本的にWindowsとのやり取りに(多くはWindowsからのメッセージに)多くの処理を記述します。
ここで注意点として、DxBase2015やDxBase2016でゲームを作成した時がある人は、フレームワークも「メッセージ」というのを持っているのを知っていると思います。フレームワークのメッセージは、ここでのWM_PAINTのような「ウインドウメッセージ」とは別のものですので注意しましょう。

続いて「4、ウインドウプロシージャの作成」ですが、これまでも少し出てきたように、WinMain.cppの下のほうにある関数

	LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

が「ウインドウプロシージャ」です。この関数は、Windowsからのメッセージを処理します。通常のWindowsアプリはこの関数こそ重要です。ゲームの場合は、ほとんど記述されません。
関数の冒頭についている「LRESULT CALLBACK」というのは「この関数はコールバック関数ですよ」という意味になります。コールバック関数はどこからか、関数のポインタを介して呼ばれることを前提とした関数、といった意味の関数です。つまり、この関数を呼び出すのはBaseCrossアプリケーションのどこかから、ではなく、Windowsから呼ばれる関数なのです。ですので、例えばゲームのコードなどからこの関数を呼んではいけません。あくまでこの関数を呼び出すのはWindowsです。
この関数内では、渡されたメッセージによって、switch文で分岐しています。

	switch (message)
	{
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	case WM_KEYDOWN:                // キーが押された
		if (wParam == VK_ESCAPE) {  // 押されたのはESCキーだ
			DestroyWindow(hWnd);	//ウインドウを破棄する
		}
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}

まず、defaultの分岐から説明します。これはこの関数でメッセージの処理をしない場合に分岐します。ここでは「DefWindowProc関数」を呼び出してます。つまり、Windowsに処理を任せるわけです(デフォルトの処理)。
ウインドウメッセージは、ここでは書き尽くせないほどたくさんあって、とても全部処理なんてできません。ですから、「必要なメッセージだけ分岐を作って(case文)」のこりは「DefWindowProc関数」に任せます。
ここで処理しているメッセージは3つです。
WM_PAINTは、先に出てきた「領域更新処理」です。通常のアプリであれば、何が画面に出したいものをここで記述します。このメッセージはウインドウが作られたときだけではなく、例えば裏に隠れていて前に出た時とか、ウインドウの大きさが変わったとか、そんな感じの時にメッセージが来ます。実はこのWM_PAINTはWindowsのバージョンによってメッセージが来るタイミングが違うようです。
ここでは

	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		EndPaint(hWnd, &ps);
		break;

と記述されています。これは「描画開始」と「描画終了」だけが記述されていて、他は何も行ってません。通常はここで描画用のWindowsAPIを呼び出します(ペンとかブラシとかそんなGDIオブジェクトを使って描画します。)
しかし、ゲームは自分で描画処理をするのでWindowsAPIに頼る必要がありません。だから何もしてないのです。

	case WM_DESTROY:
		PostQuitMessage(0);
		break;

は「ウインドウが破棄されようとするとき」に呼ばれます。このアプリケーションは「ウインドウの破棄」は「アプリケーションの終了」を意味します。ですので、Windowsに「アプリを終了しなさい」というメッセージを送ります。
「え、exit(0)じゃダメなの?」と思うかもしれません。これは良くないです。終了するかもしれませんが、行儀が悪い終了です。「PostQuitMessage(0)」を呼び出すことで、安全にアプリケーションを終了できます。
もう一つ

	case WM_KEYDOWN:                // キーが押された
		if (wParam == VK_ESCAPE) {  // 押されたのはESCキーだ
			DestroyWindow(hWnd);	//ウインドウを破棄する
		}
		break;

があります、これはDxBaseやBaseCross特有の処理です。ゲームはエスケープキーを押すことでいつでも終了できるように作成してます。ここでは「もしエスケープキーが押されたらDestroyWindow関数を呼ぶ」という処理です。
DestroyWindow関数は、WM_DESTROYメッセージを送ります(POSTします)。
ここでも、「え、PostQuitMessage(0)じゃダメなの?」と思うかもしれません。これも危険です。この段階ではウインドウが生きているからです。以下にエスケープキーが押された処理の流れを書きます。
1、エスケープキーが押されWM_KEYDOWNメッセージが渡される
2、その処理でDestroyWindowが呼ばれ、その中でWM_DESTROYがPOSTされる
3、WM_DESTROY処理の中で、PostQuitMessageが呼ばれる。
4、PostQuitMessageの中で、アプリケーションに終了するように告知される。
最後の「アプリケーションに終了するように告知」というのがよくわからないと思います。この説明は、次の「5、メッセージループ処理」で行います。

最後に「5、メッセージループ処理」はWinMain.cppでは、MainLoop関数で行ってます。
ポイントは以下の部分です。大幅に省略してあります

	//メッセージループ
	MSG msg = { 0 };
	while (WM_QUIT != msg.message) {
		if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}
	//msg.wParamには終了コードが入っている
	RetCode = (int)msg.wParam;

このメッセージループは、実際にゲームが実装されたときに変更しますので、ここではメッセージループの流れだけ説明します。
while文で無限ループを作り出してますが、これはmsg構造体のmsg.messageにWM_QUITEがあったときにループを抜けるようになっています。

そして、ウインドウプロシージャでPostQuiteMessage関数を呼ぶと、メッセージループのmsg.messageにWM_QUITEが入るのです。これが上記で「後ほど説明します」といった内容です。

PeekMessage関数は、このwhilwループの間実行されますが、これはこのアプリケーション向けのメッセージがあってもなくてもすぐに戻ってきます。メッセージがあれば、ウインドウプロシージャに配送します(DispatchMessage関数)。
これは、ゲームのように「アイドリングループ」をするときに使用します。
通常のWindowsアプリでは、PeekMessage関数の代わりにGetMessage関数を使う場合が多いです。GetMessage関数は内部で無限ループが走っていて、何かこのアプリケーションにメッセージがあったときだけ、リターンしてきます。
この2つの関数の違いを確認しましょう。
while文を抜けるということは、MainLoop関数からtWinMain関数に制御が戻りますので、そのままtWinMain関数が終了し、アプリケーションが終了します。

じつは、この「メッセージループ処理」はアプリケーションごと(ゲームごと)に違ってくる場合があります、このあと、このメッセージループに、シーンやアプリケーションクラスなど実際にゲームで使用するクラスの初期化が入ってきます。PeekMessageを呼ぶタイミングも、ゲームによっては変えたほうがいい場合もあるし、ゲームでキーボードを使用する場合のキーコード登録などもあります。
ですのでこのWinMain.cppは、ライブラリ内に入れることもできるのですが、あえてアプリケーション側に置いてあります。
今は、たんなるウインドウの初期化の関数類ですが、ゆくゆくはターンのタイミングを調整したり、あるいはフレームワークが用意したクラス群とは別のライブラリを実装する場合など、WinMain.cppを修正することも出てくるでしょう。
そういったことも念頭に入れながら「お決まりのコード群」とは考えないほうがいいと思います。

ソリューションをビルドして実行すると真っ白なウインドウが表示されますエスケープキーで終了します。

あと最後になりましたが、「SimplSampleTest」ディレクトリの配下(つまり「BaseCrossDx12」ディレクトリと同列)に「media」というディレクトリを作成しておきます。これはゆくゆくテクスチャとかコンパイル済みシェーダとかを入れておくディレクトリです。Dx11、Dx12共通で使用できるようここに作成しておきます。

これでこの項は終わりです。次回は、コミット「Dx11版ウインドウ作成と共有ファイルの追加」を説明します。

カテゴリー

ピックアップ記事

  1. 2016092201
    今回は前回のサンプルを少し機能を追加しまして、いろんなオブジェクトを追加しています。FullTuto…
  2. 2016092001
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル003をアップしました。内…
  3. eyecatch
    前回更新から時間がたってしまいましたが、今回はフルバージョンチュートリアル002で懸案となっていまし…
PAGE TOP