ゆとりーなの日記

日記的な事を書いて行くと思はれる

第1回〜ウィンドウを表示する〜

これがDXライブラリとかなら一発なのですがwin32で作るとなるとなかなか複雑です。今回はこのウィンドウを作成する基盤となるアプリケーションクラスを作ります。
私の好みとして、メイン関数内にはアプリケーションの基盤となるクラスのインンスタンス生成とそのメインループメソッドしか置かないという嗜好があるので、完成するメイン関数は次のような物を想定しています。

// main.cc
#include "application.h"

namespace {
const LPCSTR kCaption("C++とwin32とDirectXとBoostとLuaで作る弾幕シューテイング講座");
const int kClientWidth(640);
const int kClientHeight(480);
const bool kWindowed(true);
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    Application application(kCaption, kClientWidth, kClientHeight, kWindowed);
    return appliccation.run();
}

基本戦略としては、コンストラクタでウィンドウ情報を保存して、runメソッドの頭でウィンドウクラスの登録、ウィンドウの生成、表示を行い、メッセージループに突入、デストラクタで後始末といったことを考えています。因みにWinMainの引数名が全て省略されているのは警告防止対策です。
というわけで早速実装。まずはインターフェイスの宣言から。

// application.h
#pragma once
#define _WIN32_WINNT 0x0501
#include <string>
#include <boost/noncopyable.hpp>
#include <windows.h>

class Application : private boost::noncopyable {
public:
    Application(const std::string &caption, int client_width, int client_height, bool windowed);
    virtual ~Application();
    int run();
    void quit();
private:
    static LRESULT CALLBACK subClassProcedure(HWND window_handle, UINT message, WPARAM wp, LPARAM lp, UINT_PTR this_ptr, DWORD_PTR);
    const HWND kWindowHandle_;
};

#define _WIN32_WINNT 0x0501は後々使うwinXP以降でのみ使える機能を使うために必要な定義です。のインクルード前に定義しておきます。
今回はコピーすることにあまり意義を感じられないので、コピー禁止クラスにするための便利ユーティリティであるboost::uncopyableをprivate継承しておきます。
また、ある程度色々なゲームで再利用できるようにするために実際に使うときはApplicationクラスを継承したクラスをメイン関数から呼び出す的なことをしたいので、デストラクタはvirtualにしておきます。
で、実装です。

// application.cc
#include "application.h"
#include <cassert>
#include <stdexcept>
#include <commctrl.h>

namespace {
HWND initWindow(const std::string &caption, const int kClientWidth, const int kClientHeight) {
    const LPCSTR kWindowClassName("c++win32directxboostlua");
    // ウィンドウクラスの初期化(割と定型句)
    const WNDCLASSEX kWindowClass = {
        sizeof(WNDCLASSEX),
        CS_HREDRAW | CS_VREDRAW,
        DefWindowProc,
        0,
        0,
        GetModuleHandle(nullptr),
        static_cast<HICON>(LoadImage(nullptr, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON, 32, 32, LR_DEFAULTSIZE | LR_SHARED)),
	static_cast<HCURSOR>(LoadImage(nullptr, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED)),
	static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)),
	nullptr,
	kWindowClassName,
	nullptr,
    };
    // ウィンドウクラスの登録
    if (!RegisterClassEx(&kWindowClass)) {
        throw std::runtime_error("ウィンドウクラスの登録に失敗");
    }
    // 解像度で短形情報構造体を初期化
    RECT rect = {
        0,
        0,
        kClientWidth,
        kClientHeight
    };
    const DWORD kWinsowStyle(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX);
    // 解像度からウィンドウサイズを計算
    if (!AdjustWindowRect(&rect, kWinsowStyle, FALSE)) {
        throw std::runtime_error("ウィンドウサイズの取得に失敗");
    }
    // ウィンドウ作成
    const HWND kWindowHandle(CreateWindow(kWindowClassName, caption_.c_str(), kWinsowStyle, CW_USEDEFAULT, CW_USEDEFAULT, rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, GetModuleHandle(nullptr), nullptr));
    if (!kWindowHandle) {
        throw std::runtime_error("ウィンドウの作成に失敗");
    }
    return kWindowHandle;
}
}

Application::Application(const std::string &caption, const int kClientWidth, const int kClientHeight, bool) : kWindowHandle_(initWindow(caption, kClientWidth, kClientHeight)) {
    // ウィンドウプロシージャの関連付けを設定
    if (!SetWindowSubclass(kWindowHandle_, subClassProcedure, reinterpret_cast<UINT_PTR>(this), 0)) {
        throw std::runtime_error("ウィンドウプロシージャの関連付けに失敗");
    }
    // ウィンドウの表示
    ShowWindow(kWindowHandle_, SW_RESTORE);
}

Application::~Application() {
}

int Application::run() {
    MSG message;
    for (;;) {
        // メッセージキューにメッセージがあるかを調べる
        if (PeekMessage(&message, nullptr, 0, 0, PM_NOREMOVE)) {
            // メッセージ取得
            const BOOL kResult(GetMessage(&message, nullptr, 0, 0));
            // 終了、あるいはエラーならループを抜ける
            if ((!kResult) || (!(~kResult))) {
                break;
            }
            // キー入力等のメッセージの変換
            TranslateMessage(&message);
            // メッセージの処理
            DispatchMessage(&message);
        } else {
            // ゲームの毎フレームごとの処理
        }
    }
    // メッセージループに入った後のWinMainの返り値はこれである必要がある
    return static_cast<int>(message.wParam);
}

void Application::quit() {
    // アプリケーション終了要請を出す
    PostQuitMessage(0);
}

LRESULT CALLBACK Application::subClassProcedure(const HWND kWindowHandle, const UINT kMessage, const WPARAM kWp, const LPARAM kLp, const UINT_PTR kThisPtr, DWORD_PTR) {
    Application * const kApplication(reinterpret_cast<Application *>(kThisPtr));
    assert(kApplication);
    switch (kMessage) {
    // ウィンドウ破棄要請が来た時
    case WM_DESTROY:
        // アプリケーション終了要請を出す
        PostQuitMessage(0);
        break;
    default:
        // 処理してないメッセージはデフォルトに任せる
        return DefSubclassProc(kWindowHandle, kMessage, kWp, kLp);
    }
    return 0;
}

コンストラクタでは、まずウィンドウを初期化作成するinitWindow関数を呼んで、ウィンドウハンドルを初期化します。
initWindow関数内ではウィンドウクラスの登録をまず行います。WNDCLASSEXを適当に初期化します。ゲームで使うのには、上の様な感じで初期化すればよいでしょう。初期化したらRegisterClassEx関数を呼んでウィンドウクラスの登録を行います。
続いて解像度から適切なウィンドウサイズを計算します。ウィンドウを作成するCreateWinsow関数に渡すウィンドウの大きさはウィンドウ全体のサイズになるので、解像度を直接入れると目指していたウィンドウよりも小さいウィンドウが出来てしまします。そこでまずAdjustWindowRect関数を使って解像度からウィンドウサイズを求めます。
これらの値を使ってCreateWindow関数を呼び出し、ウィンドウを作成します。ウィンドウ表示位置はCW_USEDEFAULTを指定してデフォルトに任せることにします。で、作ったウィンドウハンドルを返してやります。
ウィンドウクラスの初期化、CreateWindowでのウィンドウの生成には、インスタンスハンドルというものが必要となります。通常このインスタンスハンドルはWinMain関数の第一引数の値を利用するみたいですが、GetModuleHandle(nullptr);でも同じ値がとれるので、こちらで代用します。
ウィンドウの作成に成功したら、コンストラクタの本体部で、ウィンドウプロシージャをSetWindowSubclass関数を使って登録しなおします。ウィンドウクラスの登録の時にもウィンドウプロシージャを登録しましたが、あれはwin32側が用意してくれたプロシージャなので、最低限の動作しかしてくれません。自分で好きなように動作させるためには、自作のウィンドウプロシージャを登録しなおしてやる必要があります。ウィンドウプロシージャの指定には、関数ポインタを使うので、通常のクラスメソッドを指定することが出来ません。そこでメソッドをstaticにする必要があるのですが、そうすると通常のクラスメソッド、変数の参照ができません。そこで役に立つのがSetWindowSubclass関数なのです。これの第三引数にむりやりthisポインタをぶち込むことにより、プロシージャ呼び出しの際に必ずthisポインタがやってきてくれます。あと、こいつらの使用には"comctl32.lib"をリンクしとく必要があります。プロシージャ内で送られてきたthisポインタを元の型に戻して使うことで、通常のクラスメソッド、変数が参照できます。今回プロシージャでは、ウィンドウ破棄時の挙動のみ定義しておきます。これでウィンドウを閉じたときにゲームが終了します。処理しないメッセージはDefSubclassProc関数に渡してデフォルトに任せます。
また、application::quitメソッドを定義して、アプリケーションがいつでも終われるようにしておきます。
最後に残ったrun()メソッドのメッセージループの部分ですが、こいつも殆ど定型句なのでこの通り書いておけば取り敢えず問題はないです。通常のアプリケーションでは、PeekMessage関数などは使いませんが、ゲームの場合はこいつがミソになります。というのもGetMessage関数はメッセージキューにメッセージが来るまで待機するという挙動があるために、ループが途中で止まられると割と困るゲームには向いていません。そこで、PeekMessage関数で、メッセージキューにメッセージが来ているかを調べてからGetMessageを呼ぶという風に書く必要があります。そしてメッセージがなかった場合は、ゲームの処理を行います。
実行画面

以上でウィンドウの作成に関する部分は終わりです。