DirectX12を勉強してみる 第1回目 ウインドウの作成
はじめに
私はゲーム開発をするにあたって、ゲームエンジンを使って制作してきました。
そのため、ゲームエンジンがどのように作成されているのかは全くわかりません。
そこで有名なものとして、DirectXというものがあると聞き、勉強してみることにしました。
開発環境
エディタ: Visual Studio 2022 Pro
言語: C++
クラスの作成
まず、メイン処理をするmain.cpp、ウインドウ表示の制御を行うWindow.cpp/Window.hを作成しました。
ウインドウクラスの作成
#pragma once
#include <Windows.h>
class Window
{
public:
// 実行用
static void Run(HINSTANCE hInstance, int nCmdShow);
private:
// ウインドウプロシージャ
static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);
};
#include "Window.h"
LRESULT CALLBACK Window::WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, msg, wp, lp);
}
}
void Window::Run(HINSTANCE hInstance, int nCmdShow)
{
// ウインドウクラス
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.hInstance = hInstance;
wc.lpfnWndProc = WndProc;
wc.lpszClassName = L"DX12Sample";
RegisterClassEx(&wc);
// ウインドウ調整
RECT rect = { 0,0, 1280, 720 };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
// ウインドウ表示
HWND hWnd = CreateWindow(wc.lpszClassName, L"DX12Sample", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, hInstance, nullptr);
ShowWindow(hWnd, nCmdShow);
// メインループ
MSG msg = {};
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
// ウインドウ登録解除
UnregisterClass(wc.lpszClassName, wc.hInstance);
}
WndProcとは?
まず以下のコードから見て、わかったことをまとめてみました。
LRESULT CALLBACK Window::WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
・LRESULT -> ウィンドウプロシージャ(WndProc)が返す型。
Windows がウィンドウに対して送るメッセージ(例:マウスクリック、キー入力、ウィンドウサイズ変更など)に対する「処理結果」を返すために使う。通常は 0 や DefWindowProc の戻り値をす。
・CALLBACK -> Windows API が要求する呼び出し規約。
・HWND hWnd -> このメッセージを受け取ったウィンドウのハンドル。
・UINT msg -> メッセージの種類(例:WM_DESTROY, WM_PAINT, WM_KEYDOWN)。
・WPARAM wp, LPARAM lp -> メッセージに付随する追加情報。
例:マウス座標、キーコード、フラグなど。
switch (msg)
{
・メッセージの種類ごとに処理を分岐するために switch を使う。
・Windows はアプリに 大量のメッセージ を送ってくるので、必要なものだけ処理し、残りはデフォルト処理に渡すのが基本みたいです。
case WM_DESTROY:
PostQuitMessage(0);
return 0;
・WM_DESTROY
→ ウィンドウが破棄される直前に送られるメッセージ。
→ 例えばユーザーが「×ボタン」を押してウィンドウを閉じると、このメッセージが送られます。
・PostQuitMessage(0)
→ メッセージループに「終了せよ」という命令を送る。
→ 0 は終了コード(WinMain の戻り値)として使われる。
→ これを呼ぶことで、MSG msg; GetMessage(&msg, ...) のループが終了する。
・return 0
→ このメッセージを処理したことを OS に伝えます。
→ 0 を返すのが Windows の慣習。
default:
return DefWindowProc(hWnd, msg, wp, lp);
・これが 重要な安全装置 です。
・上で処理していないメッセージ(キーボード、マウス、サイズ変更など)はすべて DefWindowProc に渡して OS に処理させます。
・これを忘れると、ウィンドウがクリックや移動に反応しなくなったり、最悪クラッシュします。
WndProcのまとめ
・WndProc はウィンドウへの全メッセージの入り口
・処理したいメッセージだけ処理し、残りは DefWindowProc に投げる
・WM_DESTROY はアプリ終了処理のトリガー
Run関数
void Window::Run(HINSTANCE hInstance, int nCmdShow)
・HINSTANCE hInstance
→ アプリケーション自身のハンドル。Windows が WinMain から渡してくれます。
→ ウィンドウ作成やリソース読み込みの際に必要。
・int nCmdShow
→ ウィンドウ表示方法を指定(最大化、最小化、通常など)。
→ これも WinMain から渡されます。
// ウインドウクラス
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.hInstance = hInstance;
wc.lpfnWndProc = WndProc;
wc.lpszClassName = L"DX12Sample";
RegisterClassEx(&wc);
・WNDCLASSEX
→ ウィンドウの「型」を定義する構造体です。色々なプロパティがありますが、最低限必要なものだけ設定しています。
・cbSize
→ 構造体のサイズ。必須。
・hInstance
→ このウィンドウクラスが属するアプリケーションのハンドル。
・lpfnWndProc = WndProc
→ このウィンドウクラスで作られるウィンドウに対して、メッセージ処理を担当する関数(WndProc)を設定。このWndProc関数は staticでないといけない。
・lpszClassName
→ クラス名。ウィンドウを作成するときに指定します。
・RegisterClassEx
→ Windows にこのクラス情報を登録。登録しないと CreateWindow が使えません。
// ウインドウ調整
RECT rect = { 0,0, 1280, 720 };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
・RECT rect
→ ウィンドウの「クライアント領域(中身の描画部分)」のサイズを指定。ここでは 1280x720。
・AdjustWindowRect
→ タイトルバーや枠のサイズを考慮して、ウィンドウ全体のサイズに補正します。
→ 例えば、1280x720 のクライアント領域にするには、ウィンドウ枠を含めると少し大きくする必要があります。
・WS_OVERLAPPEDWINDOW
→ 標準的なウィンドウスタイル(タイトルバー+閉じるボタン+サイズ変更可)を指定。
// ウインドウ作成
HWND hWnd = CreateWindow(wc.lpszClassName, L"DX12Sample", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, hInstance, nullptr);
・wc.lpszClassName
→ 登録したウィンドウクラス名を指定。
・L"DX12Sample"
→ ウィンドウタイトル。
・WS_OVERLAPPEDWINDOW
→ スタイル(枠付きウィンドウ)。
・CW_USEDEFAULT
→ ウィンドウの初期位置を OS に任せる。
・幅・高さ
→ AdjustWindowRect で補正済みのサイズを指定。
・nullptr(親ウィンドウ・メニュー・追加パラメータ)
→ 今回は親ウィンドウなし、メニューなし。
・戻り値 HWND hWnd
→ 作成したウィンドウのハンドル。以降、描画やメッセージ処理で使います。
// ウインドウ表示
ShowWindow(hWnd, nCmdShow);
・実際に画面にウィンドウを出す。
・nCmdShow は「最大化で表示するか、通常表示か」を決めます。
// メインループ
MSG msg = {};
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
・MSG msg
→ Windows から届くメッセージを格納する構造体。
・PeekMessage
→ メッセージキューにメッセージがあるか確認。
→ PM_REMOVE で確認したメッセージを削除します。
・TranslateMessage
→ キーボード入力を文字メッセージに変換。
→ ほとんどの Windows アプリで必須。
・DispatchMessage
→ メッセージを WndProc に渡して処理させる。
・ループの終了条件
→ WM_QUIT が来たらループ終了。
→ WM_DESTROY で PostQuitMessage(0) を呼ぶと、msg.message が WM_QUIT になります。
// ウインドウ登録解除
UnregisterClass(wc.lpszClassName, wc.hInstance);
・アプリ終了時にクラス情報を Windows から削除。
・メモリリークやリソース管理のために、必ず最後に行うのが望ましいです。
Run関数まとめ
・Run は ウィンドウを作り、画面に出して、終了まで待つ関数
・大きな流れ:
1, ウィンドウクラス登録
2, サイズ補正
3, ウィンドウ作成
4, 表示
5, メッセージループ(アプリ動作中)
6, 終了処理(クラス登録解除)
メイン処理
#include "Window.h"
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
Window::Run(hInstance, nCmdShow);
return 0;
}
ここの部分での説明はそこまで必要ないかと思います。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
引数
・HINSTANCE hInstance
→ 自分自身のアプリケーションインスタンスのハンドル。
→ ウィンドウを作成するときに必要。
・HINSTANCE (2つ目)
→ 古い Windows では「前回実行中のインスタンス」を示したけど、今は常に nullptr が入ってくるので使わない。
→ よって名前すら付けずに捨ててる。
・LPSTR
→ コマンドライン引数(文字列)。
→ これも使わないから無名で捨てている。
・int nCmdShow
→ ウィンドウの初期表示状態を指定(最大化、最小化、通常表示など)。
→ ShowWindow の引数としてそのまま使う。
完成だ!!
結構長いコード最初から書いて大変でした。それでは実行!!

おお!完璧ですね!
最後に
初めてのWindowAPIを使用しましたが、かなり覚えるのが多いので大変です。
今の段階だと、ウインドウを表示しただけなのでDirectXはまだ使ってない状況です。
今回でこの難易度はやばいですね。投稿は不定期ですが、次回もよろしくお願いします。
Discussion