📚

DirectX12を勉強してみる 第1回目 ウインドウの作成

に公開

はじめに

私はゲーム開発をするにあたって、ゲームエンジンを使って制作してきました。
そのため、ゲームエンジンがどのように作成されているのかは全くわかりません。
そこで有名なものとして、DirectXというものがあると聞き、勉強してみることにしました。

開発環境

エディタ: Visual Studio 2022 Pro
言語: C++

クラスの作成

まず、メイン処理をするmain.cpp、ウインドウ表示の制御を行うWindow.cpp/Window.hを作成しました。

ウインドウクラスの作成

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);
};
Window.cpp
#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とは?

まず以下のコードから見て、わかったことをまとめてみました。

Window.cpp
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 -> メッセージに付随する追加情報。
例:マウス座標、キーコード、フラグなど。

Window.cpp
switch (msg)
{

・メッセージの種類ごとに処理を分岐するために switch を使う。
・Windows はアプリに 大量のメッセージ を送ってくるので、必要なものだけ処理し、残りはデフォルト処理に渡すのが基本みたいです。

Window.cpp
case WM_DESTROY:
    PostQuitMessage(0);
    return 0;

WM_DESTROY
→ ウィンドウが破棄される直前に送られるメッセージ。
→ 例えばユーザーが「×ボタン」を押してウィンドウを閉じると、このメッセージが送られます。
PostQuitMessage(0)
→ メッセージループに「終了せよ」という命令を送る。
→ 0 は終了コード(WinMain の戻り値)として使われる。
→ これを呼ぶことで、MSG msg; GetMessage(&msg, ...) のループが終了する。
return 0
→ このメッセージを処理したことを OS に伝えます。
→ 0 を返すのが Windows の慣習。

Window.cpp
default:
    return DefWindowProc(hWnd, msg, wp, lp);

・これが 重要な安全装置 です。
・上で処理していないメッセージ(キーボード、マウス、サイズ変更など)はすべて DefWindowProc に渡して OS に処理させます。
・これを忘れると、ウィンドウがクリックや移動に反応しなくなったり、最悪クラッシュします。

WndProcのまとめ

・WndProc はウィンドウへの全メッセージの入り口
・処理したいメッセージだけ処理し、残りは DefWindowProc に投げる
・WM_DESTROY はアプリ終了処理のトリガー

Run関数

Window.cpp
void Window::Run(HINSTANCE hInstance, int nCmdShow)

HINSTANCE hInstance
→ アプリケーション自身のハンドル。Windows が WinMain から渡してくれます。
→ ウィンドウ作成やリソース読み込みの際に必要。

int nCmdShow
→ ウィンドウ表示方法を指定(最大化、最小化、通常など)。
→ これも WinMain から渡されます。

Window.cpp
// ウインドウクラス
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 が使えません。

Window.cpp
// ウインドウ調整
RECT rect = { 0,0, 1280, 720 };
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);

RECT rect
→ ウィンドウの「クライアント領域(中身の描画部分)」のサイズを指定。ここでは 1280x720。

AdjustWindowRect
→ タイトルバーや枠のサイズを考慮して、ウィンドウ全体のサイズに補正します。
→ 例えば、1280x720 のクライアント領域にするには、ウィンドウ枠を含めると少し大きくする必要があります。

WS_OVERLAPPEDWINDOW
→ 標準的なウィンドウスタイル(タイトルバー+閉じるボタン+サイズ変更可)を指定。

Window.cpp
// ウインドウ作成
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
→ 作成したウィンドウのハンドル。以降、描画やメッセージ処理で使います。

Window.cpp
// ウインドウ表示
ShowWindow(hWnd, nCmdShow);

・実際に画面にウィンドウを出す。
nCmdShow は「最大化で表示するか、通常表示か」を決めます。

Window.cpp
// メインループ
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_DESTROYPostQuitMessage(0) を呼ぶと、msg.messageWM_QUIT になります。

Window.cpp
// ウインドウ登録解除
UnregisterClass(wc.lpszClassName, wc.hInstance);

・アプリ終了時にクラス情報を Windows から削除。
・メモリリークやリソース管理のために、必ず最後に行うのが望ましいです。

Run関数まとめ

Run は ウィンドウを作り、画面に出して、終了まで待つ関数
・大きな流れ:
1, ウィンドウクラス登録
2, サイズ補正
3, ウィンドウ作成
4, 表示
5, メッセージループ(アプリ動作中)
6, 終了処理(クラス登録解除)

メイン処理

main.cpp
#include "Window.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
    Window::Run(hInstance, nCmdShow);

    return 0;
}

ここの部分での説明はそこまで必要ないかと思います。

main.cpp
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)

引数
HINSTANCE hInstance
→ 自分自身のアプリケーションインスタンスのハンドル。
→ ウィンドウを作成するときに必要。

HINSTANCE (2つ目)
→ 古い Windows では「前回実行中のインスタンス」を示したけど、今は常に nullptr が入ってくるので使わない。
→ よって名前すら付けずに捨ててる。

LPSTR
→ コマンドライン引数(文字列)。
→ これも使わないから無名で捨てている。

int nCmdShow
→ ウィンドウの初期表示状態を指定(最大化、最小化、通常表示など)。
ShowWindow の引数としてそのまま使う。

完成だ!!

結構長いコード最初から書いて大変でした。それでは実行!!

おお!完璧ですね!

最後に

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

Discussion