古の技術でゲームプログラミング 第4回
今回はプログラムのエントリーポイントとウインドウの生成あたりまでの話。
テンプレートから作成したプロジェクトで実装されてるのはおおむね以下の内容になっています。
・プログラムのエントリーポイント
・ウインドウクラスの登録処理
・ウインドウ生成処理
・ウインドウプロシージャ
・メッセージループ
※カタカナでウインドウって書くのがなんか変な感じ
プログラムのエントリーポイント
エントリーポイントとなる関数
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow)
通常のC言語のプログラムはmain()が入口なんだけど、Windowsアプリの場合はこのWinMainが入口になります。
あたまにwがついてるのはマルチバイト文字コード用とワイド文字を自動で切り替えてくれるやつ。
文字列を扱うものはだいたいw付きになってて自動で切り替えてくれる。
テンプレプロジェクトはワイド文字仕様になってるので文字列を扱うときは要注意。
パラメータについても書いておこう。
・hInstance
アプリケーションインスタンスハンドル。
ウインドウクラスの登録とかウインドウ生成で使う。
ほかにも何かと要求されることがあるのでどこかに保存しておいてアクセスできるようにしておくと便利。
・hPrevInstance
使わない。
・lpCmdLine
アプリ起動時のコマンドライン文字列が入ってるらしい。
ゲームだと使わないかな。
・nCmdShow
ウインドウ表示するときにこの値を使わないとダメらしい。
ウインドウクラスの登録処理
コード例(っていうかテンプレそのまんま)
WNDCLASSEXW 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, MAKEINTRESOURCE(IDI_WINDOWSPROJECT2)); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT2); wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon( wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); RegisterClassExW(&wcex);
ウインドウクラスの登録に使う関数はRegisterClassExWってやつ。
パラメータとしてWNDCLASSEXWを渡す。
WNDCLASSEXのメンバにいろいろ設定が必要。
そもそもウインドウクラスってなんぞやって話なんだけど、ウインドウの属性をあらかじめ設定しておくもの、みたいな感じかな。
あとでウインドウを生成するときにどのクラスのウインドウを生成するかとか指定できる。
基本はテンプレ通りでいいんだけど気にしておきたいヤツを書いておこう。
・lpfnWndProc
超重要。
ウインドウメッセージを処理する関数を設定する。
ふつうのクラスのメンバ関数は登録できない。staticなヤツならイケる。
・hInstance
アプリケーションインスタンスハンドルを設定する。
WinMainのパラメータでもらえるヤツ。
・hbrBackground
背景塗りつぶし用のブラシオブジェクトを設定する。
GetStockObjectで取得できる規定のブラシを設定してもいい。
ゲーム向けでは使わないからNULLを突っ込んどいても問題ないはず。
・lpszMenuName
超重要その2。
ウインドウメニューを指定するところ。
ゲーム用のウインドウにメニューなぞいらん。
必要なら自前で描画せよって感じなのでNULLでイイ。
・lpszClassName
超重要その3。
ウインドウクラスを特定するための識別子となる文字列。
プロセス内でユニークな名前なら良い。
ウインドウ生成時にも使う。
ウインドウ生成処理
コード例
hInst = hInstance; // グローバル変数にインスタンス ハンドルを格納する HWND hWnd = CreateWindowW( szWindowClass, // lpClassName szTitle, // lpWindowName WS_OVERLAPPEDWINDOW, // dwStyle CW_USEDEFAULT, // x 0, // y CW_USEDEFAULT, // nWidth 0, // nHeight nullptr, // hWndParent nullptr, // hMenu hInstance, // hInstance nullptr); // lpParam if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd);
テンプレコードだとInitInstanceって関数でウインドウの生成をしてる。
なんでInitInstanceでやってんだろ。謎。
まあそれはいいとして、ウインドウ生成の関数はCreateWindowWってやつ。
これのリターンでウインドウハンドルというウインドウを識別するモノが返ってくるのでどこかに保存しておいてアクセスできるようにしておくと良い。
(テンプレコードでは一時変数に入れて使い捨ててるけど)
で、ウインドウが生成できたらShowWindow, UpdateWindowを呼ぶ。
お決まりパターンなのでテンプレ通りで良い。
ShowWindowの2個目のパラメータはWinMainでもらったやつを渡さないといけないみたい。
理由はよくわかんない。
CreateWindowはパラメータがけっこういっぱいなので簡単に解説しとこう。
・lpClassName
ウインドウクラス登録時に指定した文字列を設定する
・lpWindowName
ウインドウのタイトルバーに表示する文字列。
アプリ名でも表示しとけばいいんじゃないかな。
・dwStyle
ウインドウのスタイルを指定できる。
最前面に出っぱなしにしたり、ウインドウの枠をなくしたりできる。
ただめんどくさいからそのままでいいかも。
・x, y
ウインドウを表示する座標。
xにCW_USEDEFAULTを設定すると良い感じの位置に表示してくれる。
・nWidth, nHeight
ウインドウの大きさ。
WidthにCW_USEDEFAULTを設定すると良い感じの大きさになる。
ちなみにここで設定する大きさはタイトルバーとかウインドウの枠とかを含めた大きさなので、ゲーム用途だとちょっとひとひねり必要になる。
ウインドウのサイズについてはまたこんど解説したいと思います。
・hWndParent
親ウインドウのハンドルだけどNULLでいい。
・hMenu
メニューはいらんのでNULLでいい。
・hInstance
WinMainでもらったアプリケーションインスタンスハンドルを設定する。
・lpParam
ウインドウハンドラにパラメータを渡したいときに設定する。
直接データを入れてもいいし、データアドレスを渡しても良い。
ウインドウプロシージャ
いわゆるウインドウメッセージを処理する関数。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
:
:
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, ps);
// TODO: HDC を使用する描画コードをここに追加してください...
EndPaint(hWnd, ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
Windowsアプリはウインドウに対して発行されたいろんなメッセージに対して処理を行うことで動いている。
このウインドウメッセージを処理するための関数がウインドウプロシージャと呼ばれている。
パラメータについて簡単に解説。
・hWnd
メッセージの宛先となるウインドウのハンドル。
・message
ウインドウメッセージのID的なもの。
WM_XXXXXという名前で定義された値。
・wParam
メッセージ毎のパラメータが設定される。
メッセージ毎に内容が違うので都度調べないと中身が何なのかわからない。
ムズイ。
・lParam
CreateWindowで設定しておいたパラメータが設定される。
自由に使える。
テンプレコード中のSwich文はメッセージの振り分けをしている。
テンプレコードにあるWM_COMMANDはメニュー関係のメッセージなので抹殺していい。
WM_PAINTはウインドウの描画用なので超重要。
WM_DESTROYはウインドウを閉じたときのメッセージでアプリを終了するのに必要なので残しておかないといけない。
例えばウインドウの[X]を押してウインドウを閉じるとき、確認メッセージを出したいなどあればここで処理するメッセージを追加してアレコレする必要がある。
テンプレのままだと[X]を押したら即死。
メッセージループ
ウインドウ生成後に実行するのがこのメッセージループ処理。
// メイン メッセージ ループ: while (GetMessage(&msg, nullptr, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
ウインドウメッセージってのはスレッド(プログラムの実行単位)毎に持っているメッセージキューってやつに貯まっていく。
↑のGetMessageはこのメッセージキューからメッセージを取得してくる関数。
取得したメッセージがWM_QUITだったらループを抜けてアプリを終了する。
WM_QUITってのはウインドウプロシージャのところでWM_DESTROY受信時に呼んでるPostQuitMessageってやつでキューに積まれる。
つまり…
ウインドウを閉じる
↓
WM_DESTROYが発行される
↓
PostQuitMessageが呼ばれWM_QUITが発行される
↓
メッセージループを抜ける
↓
アプリ終了する
って流れ。
で、GetMessageくんがWM_QUIT以外のメッセージをとってきたときはテンプレコードではいろいろやってるけど、ぶっちゃけゲーム用途ならDispatchMessageをやるだけでイイと思う。
DispatchMessageを呼ぶと対象ウインドウのウインドウプロシージャが呼ばれる仕組み。
ちなみにGetMessageくんはメッセージがないときは関数から戻ってこないで待機状態になっちゃう。
ゲームプログラムって常に動き続けなきゃいけないのでこのGetMessageとは相性が悪い。
さらにウインドウメッセージの処理に時間を要してるとその間ゲームプログラムが動けないってのもいただけないのでこのあたりどうするか考えないといけない。
たとえばタイトルバーをドラッグしてウインドウを移動中とかはずっとウインドウメッセージ処理が動いちゃって他の処理ができなくなっちゃったりする。
ネット上でいろいろ調べてみると、GetMessageの代わりにPeekMessageを使うってのが多く見つかる。私が昔プログラミングしてたときもPeekMessageを使ってた。
けどそれだと上記の問題が回避できないんだよね。
なので今回はちょっと違うやり方をしてみたんだけど、それはまた後日書こうと思います。
おしまい。