DirectX 12: ウィンドウの作成


~ DirectX 12: Create a window ~






■はじめに

DirectX 12の時代に入って10年ほど経っています。
時間を空けたので、少しずつ進めていきたいと思います。

ソースコードは、GitHubに上げています。


■プロジェクトの作成

Visual Studio の「新しいプロジェクトの作成」で、「Windowsデスクトップウィザード」から、プロジェクトを作成していきましょう。 デスクトップウィザードが表示されない場合には、「Visual Studio Installer」から「C++によるゲーム開発」等を選択してC++での開発ができるようにしましょう。 その際に「Windows SDK」がインストールされていなければ最新の物をインストールしましょう。

「Windowsデスクトップウィザード」ではプロジェクト名やインストール場所を決めてインストールします。 プロジェクト名は作成されるC++やそのヘッダーファイル名に使われますので、違う名前を設定した場合には、 後で読み替えてください。
プロジェクト名やインストールフォルダを決めたら「作成」で先に進みます。

次にアプリケーションの種類を指定します。ここでは標準的なexeファイルの作成を指定します。
まっさらな環境から作りたい方は「空のプロジェクト」をチェックすると良いでしょう。 使うヘッダーが決まっていれば、「プリコンバイル済みヘッダー」をチェックして使うと良いでしょう。

「OK」を押すとファイルが作られますので、Visual studio でソリューションを開きましょう。

作られるファイル群は、「こちらのGitHubのリポジトリ」のようになります。


■ Windowsデスクトッププロジェクト

Windowsアプリケーションの基本的な流れは、下記のようになります。

今回作られたプロジェクトでは、「wWinMain」がエントリーポイントとなっていますが、「main」や「WinMain」という関数を始まりの関数としてプログラムが始まります。
その後の初期化として、「RegisterClassEx」と「CreateWindow」を呼んだら、「ShowWindow」でアプリケーションを表示し始めます。 なお、今回の関数では、実際に呼ぶ関数名は、「RegisterClassExW」と「CreateWindowW」になります。 「w」はワイド文字列(UTF-8)に対応した関数を示しています。
Windowが表示された後は言わゆるメインループとして、マウスクリックのようなイベントを待って、何かあれば指定した関数「WindowProcedure」が呼ばれて処理を行います。

エントリーポイントのコード「wWinMain」は、こちらの形になります。

今回作られたプロジェクトでは、「wWinMain」がエントリーポイントとなっていますが、「main」や「WinMain」という関数を始まりの関数としてプログラムが始まります。

初期化関数「MyRegisterClass」では、「WNDCLASSEXW」構造体に必要な値を設定して、「RegisterClassExW」を呼び出します。 「WNDCLASSEXW」構造体では、OSがアプリケーションを認識するのに必要な情報が格納されます。

RegisterClassで大事な設定が、OSからアプリケーションに対して指示が出された場合に処理するコールバック関数の「WndProc」(ウィンドウプロシージャー)を登録することです。
アプリケーションウィザードでは、「About」ダイアログを呼び出すのと、メニューから終了を選択した場合にアプリケーションを終了させるコマンドと、 「再描画」及び「終了時」のためのメッセージの「WM_PAINT」や「WM_DESTROY」が自動的に作られます。

次の初期化関数「InitInstance」では、ウィンドウの表示内容を設定します。
「AdjustWindowRect」は表示するウィンドウの見た目に応じてアプリケーションのサイズを調べるための関数です。

「CreateWindowW」の返り値hWndは、HWND型になっています。HWNDはウィンドウごとに保持されていて、それぞれのウィンドウに対して指示を行う場合は、HWNDでウィンドウを区別して処理をおこないます (つまり、プリケーションでは複数のウィンドウを保持することが可能で、例えば、VRデバイスに表示する画面と、それを映しているPCで別々の表示を行うことが(処理は重くなりますが)可能です。)

その後、「ShowWindow」と「UpdateWindow」でウィンドウを表示するとともにWM_PAINTイベントを呼び出して、ウィンドウを初期化した状態で表示します。

メインループの流れが下になります。「PeekMessage」によって、OSがメッセージを飛ばしていないのか確認します。
メッセージが来ていたら、「TranslateAccelerator」で、キーボードでのショートカットキーだったら、それを呼び出し先の処理に変換します。 「TranslateMessage」は、キーモードのキーが押されていた場合に、「WM_CHAR」メッセージを生成するのに使います。
「DispatchMessage」は、ウィンドウプロシージャー「WndProc」を呼び出します。

以上の処理は、APIのバージョンによって関数名が変わったり、別の関数を使ったりしますが、Windowsアプリケーションの全体的な流れは似たような物になります。


■ 独自クラスの作成

DirectX 12は、サンプルといえども作るのに多くのコードが必要になってきます。 また、DirectX 12はゲームを作るAPIというよりもゲームエンジンを作るAPIとなっています。 そのため、DirectX 12を使ったプログラムでは、main関数をなるべく軽くして、ApplicationやEngineクラスに主な処理を持ってくるという方法が多く取られます。 単にクラスを作るというのは、ユーザー価値には結びつかないのですが、アプリケーション用のクラスを導入してみます。

cppファイルの追加

ソリューションエクスプローラーのプロジェクトの「ソースファイル」を右クリックして、出てきたコンテキストメニューから「追加」、「新しい項目」を選択してファイルを追加します。

ここでは、「Application.cpp」というファイル名のファイルを作ります。表示されたダイアログにファイル名を入力して、「追加」ボタンでファイルを追加します。

同様に、ソリューションエクスプローラーのプロジェクトの「ヘッダーファイル」を右クリックしてファイルを作成します。

ファイル名を「Application.h」として、先ほど作成した「Application.cpp」に対応するヘッダーファイルを作成します。

2つのファイルが、「ソースファイル」と「ヘッダーファイル」のフォルダに作られます。

エントリーポイントのファイル名の変更

ついでにエントリーポイントのファイル名をわかりやすく「main」に変更したいと思います。
ソリューションエクスプローラーの「ソースファイル」のプロジェクト名のファイル(ここでは、Dx12Project.cpp)を右クリックして、コンテキストメニューから「名前の変更」を呼び出して、名前を「main.cpp」に変更します。

同様に、ヘッダーファイル(Dx12Project.h)も「main.h」に変更すると、フォルダ構成は次のようになります。


■ 独自クラスのコード

■ アプリケーションクラス

コードの中身を実装します。
必要になるのは、初期化コードとメインループ(ここではUpdate)の処理ですが、初期化がある時には対応する片付け・解放する関数を用意するのが適切でしょう。 また、初期化や片付けはコンストラクタやデストラクタでできますが、コンストラクタはなるべく処理を軽くする方が良いと思うので、別の処理(InitializeとFinalize)とします。 失敗や終了が考えられる関数には返り値を設定します。返り値は0が正常状態でそれ以外が何らかのエラー(返り値がエラーコード)にしていきたいと思います。

Application.h に、今後膨らませていくプログラムのヘッダーを記載します。 作成するクラス宣言は下記のようになります。

Application.h

namespaceとして「tpot」を切りましたが、これは自分が好きな名前空間の名前をつけると良いでしょう。
また、初期化関数の引数をHWNDと画面の解像度を取りました。 これらはDirectXですぐに使いそうなので、あらかじめ宣言しておきました。必要に応じて引数は増えていくでしょうし、構造体になっていったりもするでしょう。

Applicationクラスが実装されるコードは、「Application.cpp」に記述します。 今回は、特に処理を移さずに空っぽにしたいとおもいます。返り値は正常値(0)を返すことにします。

Application.cpp

■ メイン関数

メイン関数のファイルも変更します。
ファイルの戦闘で、ヘッダーファイルの「Application.h」を読み込むと共に、「using namespace」を使って、ひとまず名前空間が表に出てこない形にします。 「using namespace」は使わない方が良いですが、自己主張が強くなってしまうので、ここでは表に出ない形にします。

main.cpp

メイン関数自体は、(メインループを除いて)次のように修正しました。
アプリケーションクラスの実体を用意して、ウィンドウを表示するShowWindowの前で初期化関数を呼び出し、成功したらメインループを呼び出すようにしました。 また、何らかの形でメインループが終わったら、Application::Finalizeを呼び出して、アプリケーションを終了するようにしました。
明示的に画面解像度(widthとheight)を指定しています。
また、WM_PAINTを使わないで描画するように変更するので、UpdateWindowを削除しています。

main.cpp

細かく言うと、「MyRegisterClass」、「InitInstance」、「WndProc」や「About」関数を宣言との二重管理が好きではないので、前の方に移動しています。
「InitInstance」は、解像度を指定する形式に変更しました。 解像度とウィンドウのスタイルに応じてウィンドウサイズを計算する「AdjustWindowRect」という関数があるので、それを使ってウィンドウサイズを計算しています。
また、ShowWindowはアプリケーションの初期化後に呼び出したいので、InitInstanceの外に移動しました。

main.cpp

ウインドウプロシージャも変更します。WM_PAINTを使わないので、WM_PAINTの処理を削除しました。

main.cpp

メインループは次の形にしました。PeekMessageでメッセージが来ているか確認し、来ていたら一連の処理をし、それが終わったらアプリケーションのUpdateを呼び出して、今後、強制的に画面を書き換える処理に対応します。 終了する際は、WM_QUITのメッセージが飛んできます。
処理をラムダ式にしていますが、これは趣味でしかありません。WM_QUITのメッセージとapp.Updateの返り値の両方で終了する可能性があるので、関数呼び出しのような形でなおかつ局所性を保つように今回の形にしました。 個人的には、MSGの変数のスコープが広くなるのが嫌なので、このような形にしました。

main.cpp

今回、WM_PAINTを使わないで描画するように変更しました。WM_PAINT内にApplication::Update()を記述しても良いでしょう(関数呼び出しが挟まるので、おじさんプログラマとしては避けたくなるのです)。 また、ショートカットキーを使わないのであればTranslateAcceleratorを、WM_CHARを直接使って処理しないのであればTranslateMessageを使わないほうが微妙に早くなるかもしれません。 それは、アプリケーションをゲームエンジンなどのUIのあるものとして発展させるか、全画面表示のゲームとして発展させるかによるでしょう。


■ さいごに

まだDirectXに入っていませんが、ぼちぼち進めていきたいと思います。





もどる

imagire@gmail.com