33: Windows のウィンドウを理解する

概要

DirectXというAPIを使う事で、ゲーム画面上に自分のオリジナルのグラフィックを加える事ができるのですが、その前にWindowsのウィンドウについて知っておく必要があります。
今回は、Windowsのウィンドウを理解し、簡単なウィンドウを作成してみる事を目標にやっていきます。
Visual Studio 2019を使用しC++で記述する。

windowsでのウィンドウを作る手順

windowsでウィンドウを作るのは地味にめんどくさいです (故にチートクライアントでも.NETを使っていました)。
windowsでウィンドウを作る際の大まかな手順は以下です。専門用語の意味は順に説明していきます。
  1. ウィンドウプロシージャを作成しておく
  2. ウィンドウクラスを設定し登録する
  3. CreateWindowExでウィンドウを作成する
  4. ShowWindowで作成したウィンドウを表示させる
  5. メッセージループ内で各種メッセージを処理する

メッセージ

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_PAINTWM_DESTROY はメッセージの種類で、その一覧は ここ にあります。
ウィンドウが消される時のメッセージであるWM_DESTROYはいつも上記のように書く必要があり、PostQuitMessage(0)を中で呼ばないと行けません。 PostQuitMessage(0) により WM_QUIT メッセージがメッセージキューに追加され、メッセージループの GetMessageWM_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つだけな気がします。

簡単なウィンドウを作ってみる

一番シンプルなウィンドウを作ってみます。チートで使う場合は大体いつも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を適当に実行すれば真っ黒なウィンドウが出てくるはずです。
ほとんどは上記で説明した内容ですが、CreateWindowExShowWindow はまだ説明していません。 関数の意味はウィンドウを作成して表示するという文字通りの意味ですが、CreateWindowEx の引数の意味は以下になります。 大事なのだけ太字にしてます。 基本的には、ウィンドウの種類を指定するウィンドウスタイルがでかい設定項目で、後は座標とかhInstanceとか入れて終わりって感じで書いてます。