33: Windows のウィンドウを理解する
概要
DirectXというAPIを使う事で、ゲーム画面上に自分のオリジナルのグラフィックを加える事ができるのですが、その前にWindowsのウィンドウについて知っておく必要があります。今回は、Windowsのウィンドウを理解し、簡単なウィンドウを作成してみる事を目標にやっていきます。
Visual Studio 2019を使用し
C++
で記述する。
windowsでのウィンドウを作る手順
windowsでウィンドウを作るのは地味にめんどくさいです (故にチートクライアントでも.NETを使っていました)。windowsでウィンドウを作る際の大まかな手順は以下です。専門用語の意味は順に説明していきます。
- ウィンドウプロシージャを作成しておく
- ウィンドウクラスを設定し登録する
CreateWindowEx
でウィンドウを作成するShowWindow
で作成したウィンドウを表示させる- メッセージループ内で各種メッセージを処理する
メッセージ
windowsでは、ウィンドウが出てくるようなGUIアプリは全てイベントドリブンで動いています。つまり、マウスが動いたり、何かクリックされたり、キー入力されたり というイベントが起きるたびに、それに対応するイベントハンドラーの関数を呼び出すという仕組みで動きます。
このマウスが動いた みたいなイベントを、windowsのウィンドウ関連ではメッセージと呼びます。
メッセージは、メッセージキューというキューに逐次格納されていきます。メッセージキューは各メインのウィンドウ毎に一つずつあります。
よって、ウィンドウを扱うプログラムでは、逐次メッセージキューからメッセージを取得し、そのメッセージに応じて何らかの処理を行うという事をしなければなりません。 この処理の事をメッセージループと呼び、以下のプログラムで書くのが普通です。
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg); // キー入力のメッセージの形式を変換する(キー入力に対しての処理が必要なければ要らないが、一応これもセットで入れるのが普通)
DispatchMessage(&msg); // 各メッセージを処理するウィンドウプロシージャに処理を渡す(dispatchする)
}
ウィンドウプロシージャ
上記のメッセージを処理する関数です。 主に以下みたいな感じで書かれます。LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam) {
switch (mes) {
case WM_PAINT:
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, mes, wParam, lParam);
}
}
メッセージの内容によりswitch文で処理を分岐し、特に追加の処理が必要無いメッセージはdefault
内で
DefWindowProc
というデフォルトの挙動をさせる関数を呼び出します。WM_PAINT
や WM_DESTROY
はメッセージの種類で、その一覧は
ここ にあります。ウィンドウが消される時のメッセージである
WM_DESTROY
はいつも上記のように書く必要があり、PostQuitMessage(0)
を中で呼ばないと行けません。
PostQuitMessage(0)
により WM_QUIT
メッセージがメッセージキューに追加され、メッセージループの GetMessage
は WM_QUIT
の時だけ0
を返す仕様なので、メッセージループを抜ける事ができます。一つ注意なのは、オリジナルの描画を加えたいときは、
WM_PAINT
メッセージ は上記のように DefWindowProc
で対応するのではなく、何もせずに
break
しないと自分の描画が無視されてしまいます。
ウィンドウクラス
複数のウィンドウでウィンドウプロシージャを共有したりなどをできるようにするために、ウィンドウクラスという物があります。ウィンドウクラスは、ウィンドウの動作や外観などの全体的な設定を格納するための構造体です。
ウィンドウを作成する前に、ウィンドウクラスを登録しないといけないという決まりがあります。
こんな感じで
WNDCLASSEX
構造体のメンバにウィンドウプロシージャも含めて色んな設定を格納し、最後に RegisterClassEx
で登録します。
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = (HINSTANCE)params;
wcex.hIcon = 0;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = CreateSolidBrush(RGB(0, 0, 0));
wcex.lpszMenuName = windowName;
wcex.lpszClassName = windowName;
wcex.hIconSm = 0;
RegisterClassEx(&wcex);
めっちゃ設定項目が多いですが、以下がその意味になります。ぶっちゃけ大事なのは太字の4つだけな気がします。
- cbSize: この構造体のサイズ。ほとんどは
sizeof(WNDCLASSEX)
- style: クラススタイル(このウィンドウクラスの特徴みたいな)を指定する
CS_HREDRAW
: ウィンドウの 幅 を動かしたりサイズ変更したらウィンドウ全部を再描画するCS_VREDRAW
: ウィンドウの 高さ を動かしたりサイズ変更したらウィンドウ全部を再描画する- lpfnWndProc: ウィンドウプロシージャを指定する
- cbClsExtra: ウィンドウクラスの構造体に任意のメンバを追加でき、そのメンバのサイズを入れる
- cbWndExtra: 各ウィンドウにも任意のメンバを追加する事ができ、そのメンバのサイズを入れる
- hInstance: このウィンドウクラスのウィンドウプロシージャの内容が書かれてるインスタンスへのハンドルを渡す
- hIcon: ウィンドウのアイコンをここで指定する
- hCurosor: ウィンドウ上でのカーソルを指定
- hbrBackground: 背景の色を指定
- lpszMenuName: クラスメニューの名前
- lpszClassName: このクラスの名前
- hIconSm: ウィンドウクラスのアイコン
簡単なウィンドウを作ってみる
一番シンプルなウィンドウを作ってみます。チートで使う場合は大体いつもDLLなので、いつも通りDLLプロジェクトでやります。 プロジェクトを開いた後、ソリューションのプロパティの所でマルチバイト文字を使用するように変更しましょう。 また、リンカ > システム
の所でサブシステムがちゃんと Windows (/SUBSYSTEM:WINDOWS)
になってる事も一応確認しましょう。簡単なウィンドウを作成するコードは全体で以下になります。これをdllmain.cppに書きましょう。
#include "pch.h"
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam) {
switch (mes) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hWnd, mes, wParam, lParam);
}
}
DWORD WINAPI ThreadMain(LPVOID params) {
HWND hWnd;
LPCSTR windowName = "simple window";
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = NULL;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = (HINSTANCE)params;
wcex.hIcon = NULL;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = windowName;
wcex.hIconSm = NULL;
RegisterClassEx(&wcex);
hWnd = CreateWindowEx(0, windowName, windowName, WS_OVERLAPPEDWINDOW, 1, 1, 300, 200, NULL, NULL, (HINSTANCE)params, NULL);
ShowWindow(hWnd, SW_SHOW);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(0, 0, ThreadMain, hModule, 0, 0);
}
return TRUE;
}
(上記の説明内のコードと若干値変えてる所もあるので注意です)。これでビルドし、
rundll32.exe
を使ってこのDLLを適当に実行すれば真っ黒なウィンドウが出てくるはずです。
ほとんどは上記で説明した内容ですが、
CreateWindowEx
と ShowWindow
はまだ説明していません。
関数の意味はウィンドウを作成して表示するという文字通りの意味ですが、CreateWindowEx
の引数の意味は以下になります。
大事なのだけ太字にしてます。
- dwExStyle (0): ウィンドウの追加のウィンドウスタイルを入れる
- lpClassName (windowName): 使用するウィンドウクラスの名前で、ウィンドウクラスで指定した奴と同じにしないといけない
- lpWindowName (windowName): このウィンドウの名前
- dwStyle (WS_OVERLAPPEDWINDOW): ウィンドウスタイルを指定する
- X (1): ウィンドウを生成する座標(ウィンドウの左上の角の座標)の内のX座標
- Y (1): ウィンドウを生成する座標(ウィンドウの左上の角の座標)の内のY座標
- nWidth (300): ウィンドウの幅
- nHeight (200): ウィンドウの高さ
- hWndParent (NULL): 親ウィンドウへのハンドル
- hMenu (NULL): ウィンドウでよくAltキーとか押すと出てくるメニューへのハンドル
- hInstance ((HINSTANCE)params): このウィンドウのコードが書いてあるDLLへのハンドル(大体はこのDLLの)
- lpParam: ウィンドウに渡す引数的な