読者です 読者をやめる 読者になる 読者になる

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

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

DXライブラリとWindowsAPIの連携その4【コントロール】

書き手:肥田野

新学期に合わせてWebサイトを作ったりプレゼンの準備をしたりなどバタバタしておりましたが、こちらもあまり引き延ばせ過ぎずに片付けたいと思いますので、あと2回程度で終わらせられればなと思います。

前回はダイアログボックスを表示して、初期状態のOK・キャンセルボタンを動作させるところまで行いました。今度はここにコントロールを追加しましょう。

WindowsAPIではチェックボックスラジオボタンなど様々なコントロールが用意されています。これらをプログラムに組み込む方法には主に2種類あって、おそらく簡単なのはリソースエディタでダイアログボックスにペタペタと貼り付け、ソースコードではそれを探して取得するというものだと思います。

ですが、折角なのでここはもうひとつの手段、関数で動的に作り出すやり方を見ていきましょう。

こちらの方がリソースエディタの有無に影響なく利用できますし、ソースコードの文量としてはほぼ変わらないので、工数の少ない方を選んでみました。

それでは詳しい解説は続きからどうぞ。

コントロールを使用する例として、今回はゲーム中のFPSを変更してみたいと思います。

ゲームをよくプレイする人にはお馴染みの言葉だとは思いますが、FPSとはフレーム・パー・セカンドの略で、1秒間に何回画面を描画しなおすかの数値です。通常は30か60が多いと思います。

ほとんどのモニターは常に1/60秒に1回画面を書き換えているため、普通にScreenFrip()とClearDrawScreen()を実行していれば60FPSになるそうですが、これを30FPSにしたい場合はWaitTimer()で1/30秒経過するまで次のフレームに移行しないよう待機させる必要があります。

その待機時間は「(1000/FPS)-(現在のフレームでの経過時間)」という計算式で出すことができます。WaitTimer()の単位はミリ秒なので、1/60秒もしくは1/30秒から経過時間を引いているのが分かるかと思います。

そして、今回はこのFPSの値を、前回作ったダイアログボックスにチェックボックスを埋め込むことで変化させてみようというわけです。

チェックボックスラジオボタンといった要素は一般的にはコントロールと呼ばれていますが、実は種類としてはウィンドウに該当するんです。

何を言っているのかというと、実は各種コントロールを作り出す関数って「CreateWindow()」なんですよね。DXライブラリではおそらくDXLib_Init()の中に隠されていると思いますが、メインのウィンドウを表示するときにも必ずこの関数が使われています。

ここで引数の一部を"BUTTON"など特定のキーワードにすることで、CreateWindow関数では例外的に普通のウィンドウではなくボタンを生成するという仕組みになっています。

それではソースコードに移ります。ちょっと長くなっていますが、全体の構成としては前回までと変わりません。

#include<DxLib.h>
//コントロール制御のための関数やマクロの動作に必要
#include<windowsx.h>
#include"resource.h"

//チェックボックスに割り当てるID
#define ID_CHECK 100

WNDPROC dxWndProc;
HWND hMainWnd;
HINSTANCE hInst;

//FPSはグローバル変数で定義
int FPS = 60;

int SettingBeforeInit(bool);
int SettingAfterInit();
LRESULT CALLBACK MyProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp);

BOOL CALLBACK MyDlgProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) {
    hInst = hInstance;
    SettingBeforeInit(true);
    DxLib_Init();
    SettingAfterInit();

    int x = 320,vec = 5;

    while (!ProcessMessage() && !ScreenFlip() && !ClearDrawScreen()) {
        int startTime = GetNowCount();
        if (x + vec > 640) {
            x = 640;
            vec *= -1;
        }
        if (x + vec < 0) {
            x = 0;
            vec *= -1;
        }
        x += vec;
        DrawCircle(x, 240, 5, 0xffffff, TRUE);
        int endTime = GetNowCount();
        WaitTimer((1000 / FPS) - (endTime - startTime));
    }
    DxLib_End();
    return 0;
}

int SettingBeforeInit(bool wndMode)//変更ないため省略

int SettingAfterInit()//変更ないため省略

LRESULT CALLBACK MyProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) //こちらも省略

BOOL CALLBACK MyDlgProc(HWND hDlg, UINT msg, WPARAM wp, LPARAM lp) {
    static HWND hCheckBtn;
    static HFONT hFont;
    HDC hdc;
    switch (msg)
    {
    case WM_INITDIALOG:
        hFont = CreateFont(12, 6, 0, 0, FW_NORMAL, FALSE, FALSE, 0, SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, "MS Pゴシック");
        hCheckBtn = CreateWindow("BUTTON", "FPS", WS_VISIBLE | WS_CHILD | BS_AUTOCHECKBOX, 20, 20, 10, 10, hDlg,(HMENU)ID_CHECK, hInst, NULL);
        
        if (FPS == 60) {
            Button_SetCheck(hCheckBtn, BST_UNCHECKED);
        }
        else {
            Button_SetCheck(hCheckBtn, BST_CHECKED);
        }
        break;
    case WM_PAINT:
        hdc = GetDC(hDlg);
        SelectObject(hdc, hFont);
        SetBkMode(hdc, TRANSPARENT);
        TextOut(hdc, 40, 20, "30FPS", strlen("30FPS"));
        ReleaseDC(hDlg, hdc);
        break;
    case WM_COMMAND:
        switch(LOWORD(wp)) {
        case IDOK:
            EndDialog(hDlg, IDOK);
            return TRUE;
        case IDCANCEL:
            if (MessageBox(hDlg, "設定を変更せずにダイアログを閉じます", "確認", MB_OKCANCEL) == IDOK) {
                EndDialog(hDlg, IDOK);
            }
            return TRUE;
        case ID_CHECK:
            if (Button_GetCheck(hCheckBtn) == BST_CHECKED) {
                FPS = 30;
            }
            else {
                FPS = 60;
            }
            return TRUE;
        }
        return FALSE;
    }
    return FALSE;
}

まず各種コントロールを操作するためにwindowx.hを新たにインクルードしています。普通のwindow.hがDxLib.hに含まれているのはまあ当然ですが、こっちが入っていなかったのはちょっと意外でした。

今回リソースエディタを使わないため、チェックボックスのIDはグローバル領域で定義します。FPSもWinMain()とMyDlgProc()の両方から参照するため、グローバル変数にしました。

WinMain()ではFPSの変化が分かりやすいように、左右に一定速度で球を動かすプログラムを書いています。ここはFPSの変化が分かれば何でもいいので、好きなように書き込んでいただければと思います。

while文内の最初と最後でGetNowCount()を用いて、現在のWindowsの起動時間をミリ秒単位で取得しています。そしてWaitTimer()で前述の計算式を実行しているのが分かるかと思います。

そして、一番肝心なMyDlgProc()です。チェックボックスのハンドルであるhCheckBtnがHWND型になっていることからも、コントロールがウィンドウの一種なんだなということが見て取れるでしょうか。

ローカル変数では他にフォントを作るために必要なhFontと、文章を表示するためにデバイスコンテキストを取得する必要があるためhdcを用意しました。

デバイスコンテキストという言葉はDXライブラリを使う上でほぼ意識したことはないのではないかと思います(それを意識しなくて良いのがDXライブラリの最大の利点と言ってもいいかもしれません)。これはウィンドウに文章や画像を表示する際に使用する仮想的な画面と考えて良いでしょう。DXライブラリで用いられる裏画面の概念も、デバイスコンテキストと密接に関わっています。

まずswitch文中のcase WM_INITDIALOG:に注目してください。

これはダイアログボックスが立ち上がる時に1回だけ呼び出されるメッセージです。普通のウィンドウプロシージャではWM_CREATEが使われますが、ダイアログボックスではこちらを使う決まりになっています。

hFontにはCreateFont()でフォントを作成しています。引数が山ほどあり目を背けたくなりますが、重要なのは最初の2つ(高さ、幅)と使用する文字コード(SHIFTJIS_CHARSET)、用いるフォント(MS ゴシック)の部分くらいでしょう。

hCheckBoxでは、先述したとおりCreateWindow()でチェックボックスを作成しています。正確には、引数に"BUTTON"を指定して、そのあとにオプションでBS_AUTOCHECKBOXを指定することでチェックボックスを作るよう指示しています。

BS_AUTOCHECKBOXの次は設置する座標、ボタンのサイズです。その次は、このウィンドウ(=チェックボックス)の親ウィンドウを指定します。ここではダイアログボックスが親なのでhDlgになりますね。

その次で、このチェックボックスに割り当てられるIDと、プログラム全体のインスタンスハンドルを指定します。最後の引数はオプションデータ用ですが、今回は必要ありません。

次のif文では、現在のFPSからチェックボックスにチェックを入れておくかどうかを切り替えています。初回起動時は60FPSなのでチェックは外れますが、この操作をしないと30FPSに設定したあともう一度ダイアログボックスを開いた時にチェックが外れており、正しくON/OFFが切り替えられなくなります。

次のWM_PAINTは、このウィンドウにOSから描画しなおすよう指示が出た時に呼び出されます。

どういった状況で描画し直す必要があるのかというと、最初に表示するときはもちろん、ウィンドウをドラッグしてモニタの端に隠れた部分が現れたときなどですね。

Windowsのプログラムが応答なしになってしまった時、そのウィンドウをモニタ端に一度追いやってまた引っ張ってくると、一度隠れた部分が真っ黒になっている現象を見たことはありませんか? それは何らかの影響でWM_PAINTが送られなくなったために発生するんです。

というわけでcase WM_PAINT:では、チェックボックスの脇に添えるテキストを表示させます。

DXライブラリ上でテキストを書くときはDrawFormatString()を実行すればよいのですが、WinAPIではもうちょっと手間がかかります。

GetDC()で、そのウィンドウのデバイスコンテキストを取得し、こうして得たデバイスコンテキストハンドルを宛先に指定して、文章を書いたり画像を描画するのです。そして一連の処理が終わったらReleseDC()でデバイスコンテキストを手放してやります。面倒ですね。DXライブラリは偉大です。

ここでは追加でデバイスコンテキストにこれから書き込むテキストのフォントと背景色を、それぞれSelectObject()とSetBkMode()で指定しています。引数に関しては説明は必要なさそうですね。

そして最後に、WM_COMMAND中のswitch文にcase ID_CHECK:を追加し、チェックがされたらFPSを30に、外されたらFPSを60に設定しています(ID_CHECKはボックスにチェックが入った時だけでなく、外された時にも呼び出されます)。

コードの説明は以上です。上手くFPSを切り替えることができましたでしょうか。

今回追加したコントロールは1種のみですが、同様の方法でいくらでもコントロールを増やすことができますので、是非自分だけの「充実したオプション」を作っていただければと思います。

次回は外部ファイルの読み込みについてやってみたいと思います。次で最終回の予定です。もうひと踏ん張り、頑張っていきましょう。