ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

DXライブラリとWindowsAPIの連携その1【準備】

書き手:肥田野

新年入ってから最初の更新ですね。明けましておめでとうございます。

波乱のデジゲー博を終えた後は、学期末ということでメンバー一同テストやレポートに邁進しておりました。

特に2年生は進級の可否がかかっていたことで、私を含む一部のメンバーなどはつい最近まで戦々恐々とした日々を過ごしていた次第となります。

そんな3学期は普段利用させていただいている部室が設備の入れ替えで出入りできなくなったため、各自実家やアパートなどで個人の開発を行っています。

そこで私はタイトルにもありますとおり、DXライブラリから少しはみ出てWindowsAPIを勉強し始めたので、そこで得た知識とDXライブラリを組み合わせることで、よりソフトウェアとして機能的にする試みを行ってみました。

(2/25追記:コードを一部修正しました。SettingAfterInit関数内で宣言していたhMainWnd変数をグローバル変数にしています)

このブログの閲覧者様にはプログラミング初心者が多いと思いますので、まずはDXライブラリとWindowsAPIの関係の説明から。

DXライブラリはもうお馴染みですね、Windows用ゲームを制作するのに特化した関数群です。

DXライブラリを配布しているページで説明されていますとおり、開発者がWindows用ソフトウェアの様式を意識することなくゲームを作るお膳立てをしてくれています。

この「Windows用ソフトウェアの様式」に従って開発をする為の関数群が「WindowsAPI」です。ですので、DXライブラリなどの開発支援ツールを使わずにWindowsのソフトウェアを開発する場合に、このWindowsAPIを頼ることになります。

もっと言えば、DXライブラリも裏でWindowsAPIを扱っています。WindowsAPIを更に扱いやすく単純化したものがDXライブラリであると考えても良いでしょう。

普通のC言語でプログラムを書くときは「int main()」と書くのがお約束ですが、DXライブラリでは「int WINAPI WinMain(HINSTANCE~~)」となっていました。ここにもWINAPIとありますね。つまりWinMainはDXライブラリの用語ではなくWindowsAPIの用語だったのです。

前置きが長くなりましたが、DXライブラリのプログラムにWindowsAPIの知識を持ち込むと、どんなことができるのでしょうか。

・メニューバーを設置できる

・メッセージボックスが表示できる

・ダイアログボックスを利用できる

・ファイルを開く、ファイルの保存などができる

・印刷をする

とまあ、普段見慣れているWindowsアプリにできることなら大体何でも出来てしまいます。

一見するとゲームに必要ない機能に思えますが、例えばメニューバーからゲームの新規開始をしたり、ウィンドウを閉じる前に確認メッセージを入れたり、ダイアログボックスで解像度や文字サイズの変更をしたり、プレイヤーが直接セーブデータやメディアファイルの読み書きをしたり、スコアボードやパスワードの印刷をすることも可能です。

ゲームの世界観を重視するのであれば、これらの機能はWindowsの標準機能を使うのではなく、何かしらオリジナルのUIで実行したいものですが、WindowsAPIを駆使すればそんなアレンジも自由自在です。

というわけで、Windowsの便利な機能を使いつつ、DXライブラリの恩恵に預かるための方法を、今後何回かに分けて解説したいと思います。

今回は準備編ということで、とりあえずプロジェクトを新規作成してcppファイルを作成したあと、プロジェクトのプロパティでDXライブラリが使える設定を行ってください(本家様解説ページ参照)。

設定が終わったら以下のコードを書き写してください。

#include<DxLib.h>

//従来のウィンドウプロシージャのアドレスを格納
WNDPROC dxWndProc;

//メインウィンドウのハンドル(2/25追記)
HWND hMainWnd;

//DxLib_Initを実行したあとに行う設定
int SettingAfterInit();

//自作ウィンドウプロシージャ
LRESULT CALLBACK MyProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    ChangeWindowMode(TRUE);
    DxLib_Init();
    SettingAfterInit();

    while (!ProcessMessage() && !ScreenFlip() && !ClearDrawScreen()) {
        
    }
    DxLib_End();
    return 0;
}

int SettingAfterInit() {
    SetDrawScreen(DX_SCREEN_BACK);
    //メインウィンドウのハンドルを取得
    /*HWND hMainWnd = GetMainWindowHandle();*/
    //2/25追記
    hMainWnd = GetMainWindowHandle();
    //メインウィンドウのウィンドウプロシージャのアドレスを格納
    dxWndProc = (WNDPROC)GetWindowLong(hMainWnd, GWL_WNDPROC);
    //自作ウィンドウプロシージャのアドレスを設定
    SetWindowLong(hMainWnd, GWL_WNDPROC, (LONG)MyProc);
    
    return 0;
}

LRESULT CALLBACK MyProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
    switch (msg) {
    case WM_LBUTTONDOWN:
        //メッセージボックス表示
        MessageBox(hWnd, "テスト", "test", MB_OK);
        break;
    }
    return CallWindowProc(dxWndProc, hWnd, msg, wp, lp);
}

ここで一番重要なのは「ウィンドウプロシージャ」という言葉です。

ウィンドウプロシージャとは関数の一種で、Windowsのプログラムにおいて頻繁に呼び出される物です。

WindowsAPIだけでプログラムを書く場合、このウィンドウプロシージャの中にソフトウェアの動きを色々書き込んでいくことになります。

ウィンドウプロシージャは定義だけすれば、プログラマーが一々呼び出してやる必要はありません。WindowsのOSが必要なタイミングで自動的に呼び出します。

関数なのにソースコードの中で呼び出されないという状態に違和感を覚えるかもしれませんが、どんなC/C++初心者でも一度はそんな例を目にしたことがあるはずです。main関数やWinMain関数ですね。他の自作関数とは異なり、これらだけは自分で呼び出すことはありません。プログラムが実行される時に最初に呼び出される関数、と覚えている人も多いのではないでしょうか。ウィンドウプロシージャもWinMainと同族で、定義だけしたら呼び出しはOS任せになります。

ウィンドウプロシージャには4つの引数があります。

一つ目はウィンドウハンドル。プロシージャが動作を行う対象のウィンドウのハンドルが送られてきます。

ウィンドウのハンドルというのもDXライブラリユーザーには馴染みがないと思います。これはWindowsAPIでは何をするにも付いて回る識別子なのですが、DXライブラリでは内部の動作に格納されてプログラマーが触れることはありません。基本的にDXライブラリの世界ではウィンドウが一つなので、ウィンドウハンドルも1つだけになり、わざわざ区別をする必要がないという事情もあるかと推測できます。

二つ目はメッセージ(MSG)構造体です。ウィンドウプロシージャではこれが一番重要になります。

これはOSがソフトウェアに、「今外部から○○のアクションがありました」と指示するための構造体です。この場合の外部とは、ユーザーの操作だったり、時間経過だったり様々です。

メッセージには「WM_LBUTTONDOWN(マウスの左ボタンが押された)」の他に、WM_CREATE(ウィンドウが作られたとき)やWM_PAINT(画像を描画する必要があるとき)などが代表的です。

このように、メッセージを用いてシステムとやり取りするプログラムを「メッセージ駆動型プログラミング」と言います。テストに出そうですね。

3つ目と4つ目のWPARAM、LPARAMは今回は使いませんが、メッセージの詳細な情報が格納されます。

具体的にはキーボードのキーが押された時に、msgはWM_CHAR(キーボードのキーが押された)、そしてどのキーが押されたかの情報がWPARAMに格納されます。

ここまでがウィンドウプロシージャの説明です。DXライブラリではウィンドウプロシージャはProsessMessage関数に格納されています。ウィンドウプロシージャを理解すれば、リファレンスでこれを定期的に呼び出す必要があると書かれている理由も納得できるのではないでしょうか。

そして、ウィンドウプロシージャは自分で新しく作ったものを利用することもできます。一般に「サブクラス化」と呼ばれる作業なのですが、この作業を自前のSettingAfterInit関数で行っています。

Dx_Init関数でウィンドウが作られるため、サブクラス化はそのあとに行う必要があります。手順としては、目当てのウィンドウのハンドルを取得し、そのハンドルを手がかりにSetWindowLong関数で新しいプロシージャを割り当てます。この時、以前のプロシージャも引き続き利用するので、事前にGetWindowLong関数で取得しておく必要があります。

このGetWindowLong/SetWindowLong関数は、ウィンドウの情報をやり取りするときに使います。と言ってもウィンドウは様々な情報を持っていますから、GWL_で始まる第二引数で扱う情報を指定してやります。今回はウィンドウプロシージャなのでGWL_WNDPROCですね。

自前のMyProcの最後で、CallWindowProc関数を呼び出しています。その名の通り指定されたウィンドウプロシージャを呼び出す関数です。これにより、MyProcで色々処理をさせたあと、DXライブラリが隠してくれている元のウィンドウプロシージャも実行することができるようになります。

最後にMyProcの中身ですが、メッセージ処理はswitch文で行われることが多いです。今回はマウスの左ボタンがクリックされたときにメッセージボックスを表示する処理をしています。

余談ですが、このMessageBox関数の第一引数でウィンドウハンドルが使われていますね。ウィンドウプロシージャ内であればシステムから送られてくる引数「hWnd」を使えば問題ないのですが、ここにGetMainWindowHandle関数を使うことで、WindowsAPIを意識しない普段のDXライブラリのゲームにも使用することができます。

さて、ここまで書いてビルドすれば、画面クリックでメッセージボックスが表示されるプログラムが完成しました。

WinMainのwhile文で、普段ゲームを作るときのようにDrawBox等で画像を表示、移動など色々試してみてください。DXライブラリの機能とWindowsAPIの機能が共存できていることが分かると思います(どちらも元々WindowsAPIなので共存という言い方は変ですね……)。

準備と言いつつかなり長くなってしまいましたが、次回はここにメニューバーを足していきたいと思います。