2-1 窓メッセージについて

まず窓メッセージ(ウインドウメッセージ)についてお話をしとかないと、
Windowsプログラムは一切組めないのでここで簡単にやっときましょう。


■窓?→ウィンドウ?→Window?→複数の窓?→Windows

まずWindowsはイベント駆動型といわれるOSなんです。

イベントというのは例えばマウスをクリックしたとかキーボードを押したとか、
タイマーで割り込みをしたというようなメッセージを認識すると、
それに対応したプログラム関数(プロシージャと言う)が呼ばれます。
これらはウインドウごとに認識され、OSが自動的にそのメッセージを適切なプログラム関数に渡されます。

例えばメモ帳を使って文字を書くと、メモ帳のテキスト入力エリアに入力された文字が書かれます。
ペイントブラシではドラッグすると線が引けます。
そしてこれらは同時に起動していても、きちんと指定されたウインドウに渡されて処理されます。

さて、これらを入力するときにウインドウが後ろにあった場合、よくクリックをして手前に持ってくると思いますが、
実はマウスイベントというのは手前にあるアプリケーションに送信するようにOSで管理をしており、
このようにマウスの入力を受け付けるようになることをフォーカスの移動と言います。

また、手前に来て入力を受け付けるようになっている状態をアクティブ状態と言い、
逆に別ウインドウにフォーカスが移動した状態をと非アクティブ状態と言います。
※その別ウィンドウから見れば今度はそのウィンドウがアクティブ状態になったと言える

ちなみにこのアクティブ状態になったかどうかも、メッセージとして受け取ることが可能です。

Windowsプログラムは基本的に最低1つのウインドウを作らないといけません。
これをそのアプリケーション用のメッセージを受け取るウツワにします。
※ウィンドウの無いプログラムも作ることは出来ますが、
 このサイトで説明しているDirect3D、DirectSound、DirectInputは必ずウィンドウが必要です


余談ですが、よく見るボタンやテキストボックス、スクロールバーなども実は1つのウィンドウであり、
これらは親ウィンドウの中に子ウィンドウとして追加されています。

ちなみにこれらを一般的にコントロールウィンドウと言いますが、
この時、親ウィンドウのタイトルバーを持って動かすと、その親と同じ量だけコントロールウィンドウも移動するため、
結果的に親ウィンドウにくっついているように見えます。

さらに詳しく言うと、ウィンドウの中にはコントロールウィンドウしか追加出来ないわけではなく、
ウィンドウの子としてさらに別のウィンドウをポップアップ表示させることも出来ます。

一番分かりやすい例としてはエラーダイアログのようなメッセージウィンドウであり、
このウィンドウが出ている場合は親のウィンドウを操作させないように制限することも出来ます。
※裏技として1つのプログラムで親の無いウィンドウを複数作ることも出来ますが、
 この場合は下のタスクバーに作った分のタスクが全て表示されます

ちなみに、Direct3Dを使う場合はこれらのコントロールウィンドウを親ウィンドウに追加して使用することは出来ません。
厳密には追加すること自体は出来ますが、Direct3Dは指定したウィンドウいっぱいに描画を行うものなので、
コントロールウィンドウはその描画の下に隠れてしまう状態になるのですが、
描画のタイミングによっては一瞬だけコントロールウィンドウが表示されたりするため、
チラチラとおかしな表示になってしまったりします。


■mainとWinMainについて

Windows用のプログラムの通常のスタート関数はmain()ではなくWinMain()となります。

これはプログラムを起動するときにOSがまずWindows用のプログラムであることを登録し、
その時にインスタンスというプログラムの実行単位を割り当て、これを実際のプログラムに通知する必要があるためです。

プログラム側はこれを利用してウインドウを生成し、そこにメッセージを与えてもらえるようにする必要があるのです。


■ウインドウのサンプル

以下はウインドウ生成のサンプルです。

#include <Windows.h>    // 必ず必要なヘッダ

/////////////////////////////////////////////////////////////////
// プロトタイプ
/////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow);
LRESULT CALLBACK mainWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

/////////////////////////////////////////////////////////////////
// グローバル変数
/////////////////////////////////////////////////////////////////
HWND hWnd;              // ウインドウハンドル


/////////////////////////////////////////////////////////////////
// メイン関数だにょ~
/////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)
{
    // ウインドウクラスを設定レジストする
    WNDCLASSEXW wc;
    wc.cbSize                   = sizeof(wc);
    wc.style                    = CS_HREDRAW | CS_VREDRAW | CS_BYTEALIGNCLIENT;
    wc.lpfnWndProc              = mainWindowProc;
    wc.cbClsExtra               = 0;
    wc.cbWndExtra               = sizeof(DWORD);
    wc.hInstance                = hInstance;
    wc.hIcon                    = NULL;
    wc.hCursor                  = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground            = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName             = NULL;        
    wc.lpszClassName            = L"窓テスト";
    wc.hIconSm                  = NULL;
    if( !RegisterClassExW(&wc) )
        return -1;

    // ウインドウを生成する
    hWnd = CreateWindowExW(
                0,                  // 拡張ウインドウスタイルフラグ
                wc.lpszClassName,   // クラス名
                wc.lpszClassName,   // タイトル名
                WS_POPUP,           // ウインドウスタイルフラグ
                0,                  // 画面上の左上のX座標
                0,                  // 画面上の左上のY座標
                640,                // 生成するウインドウの幅
                480,                // 生成するウインドウの高さ
                NULL,               // 子ウインドウのハンドル
                NULL,               // メニューバーハンドル
                hInstance,          // プログラムハンドル
                NULL                // WM_CREATEメッセージのlParamとして渡されるCREATESTRUCT構造体へのポインタ
        );
    if( !hWnd )
        return -1;                  // 生成に失敗

    // ウインドウを表示
    ShowWindow( hWnd,SW_SHOW );

    // メッセージメインループ
    MSG msg;
    while(1){
        if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ) {   // プログラム内のメッセージ処理
            if( msg.message==WM_QUIT )
                break;                                     // ALT+F4が押されたらメインを抜ける
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        Sleep(15);                                         // CPUへのウエイト
    }
    return 0;
}

/////////////////////////////////////////////////////////////////
// ウインドウメッセージを受け取るコールバック関数
/////////////////////////////////////////////////////////////////
LRESULT CALLBACK mainWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch( uMsg )
    {
    case WM_DESTROY:                            // ALT+F4が押されたら
        PostQuitMessage( 0 );
        break;
    default:
        // その他のメッセージはデフォルトを返す
        return DefWindowProc( hWnd,uMsg,wParam,lParam );
    }
    return 0L;                                  // 関数のオーバーライド成功
}

なげ~~~~~~!!!!とか思わないで下さい。

このプログラムはどんなゲームにも必ず必要なルーチンであり、
DirectXもウインドウが必ず1つ必要なので省略することは出来ないのです!

ついでにこのサンプルは真っ白なウインドウしか作りませんが、ウインドウ生成時のウインドウスタイルのフラグを変えることにより、
閉じるボタンタイトルバーメニューバーなどが存在するウインドウが作成出来ます。
まぁここではDirectXのウインドウなんでここまで意識してませんが。

よくWindowsのプログラムでつまずくのはコンソールプログラム(DOS窓アプリ)と違い、
こういった処理になれていないためだと思うんです。



■CWindowクラスで楽をしよう

クラスとはC++でサポートされた構造体の一種で、基本的にライブラリとして定義された物です(自談)。これ参照。

そしてこのライブラリを使用してウインドウを生成するプログラムは以下のようになります。

#include "CWindow.h"    // クラス定義ヘッダ

int WINAPI WinMain( HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpszCmdParam,int nCmdShow )
{
    CWindow win;
    if( !win.Create(hInstance,L"窓テスト") )
        return -1;

    // メインループ
    MSG msg;
    while(1) {
        if( PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ) {   // プログラム内のメッセージ処理
            if( msg.message==WM_QUIT )
                 break;                                    // ALT+F4が押されたらメインを抜ける
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        Sleep(15);                                         // CPUへのウエイト
    }

    return 0;
}

たったこれだけで先ほどと同じプログラムの完成です。

ここではローカル変数としてCWindowというウインドウクラスの実体を作り、
そのクラスの関数(メンバと言う)のCreate()を呼び出しているだけですが、
これで上と同じプログラムが作れます。

ということで以下にサンプルプロジェクトを用意したので、
この中のCWindow.hCWindow.cppを自分のプロジェクトに追加して使用してください。

VisualStudio2010のプロジェクト
※VC2012、VC2013はこちら
TestWindow_vc2010.zip
VisualStudio2015のプロジェクト TestWindow_vc2015.zip

このサイトではウインドウの処理にこのクラスを使用しています。

このソースは完全フリーなので自由に改変して使用することが可能です。
ウインドウメッセージを調べたりメニューバー対応にするなどいろいろ使えると思います。
他の人もこういったものを作っているのが普通なので、
これはその雛型として使うと手間も省けるのではないかと思います。



■Sleepとは

上記のプログラムにはメインループの中にSleep()関数と言うのがあります。
これはその名の通りプログラムを指定時間停止させます。

正確には現在のスレッドを停止させます。
WinMainの中はOSが作ったメインスレッドで動作しているため、ここでは実質メインスレッドを停止したことになります。

また引数には止める時間を
ミリ秒単位で指定します。

この関数を入れないとCPU使用率が常時100%になってしまい、ほかのプログラムに処理が行かなくなる場合があります。
しかも、今回のような単純なプログラムにCPU100%なんてもったいないため、
その残りのCPUパワーをほかのプログラムに渡すために自分のループを停止させています。

なぜ今回15ミリ秒かというと、通常のゲームは毎秒60フレームで動くのが基本です。
ということは1フレームに必要な時間は0.0166666秒、つまり16.66666ミリ秒となります。
そこでプログラムの実行のためのCPUの使用率を考えて、ここでは処理に1msかかったと想定してウエイトを15ミリ秒に設定しています。

なお、この15ミリ秒と言うのはあくまでもこのプログラムでの数値であり、もっと処理の重い描画や計算をたくさん行う場合は、
このウエイトを減らしてあげないと逆に処理落ちが発生してしまいます。
このウエイトはアプリケーションによってとる値が違うので、重そうなプログラムではSleepを下げてやります。

ちなみにDirectXを使うゲームでは1~15の間で変更出来るようなっていれば、
CPU負荷を抑えつつも処理落ちしないゲームが作れます。


ただし実はSleepには精度というワナがあります。
これは次の章で詳しく説明しています。



■PeekMessage()とGetMessage()の違い

上記のプログラムではPeekMessage()を使用していますが、
一般的なWindowsプログラムではGetMessage()を使用します。

ちなみにこれらの違いについて簡単に説明すると、
PeekMessage()はメッセージの有無関係なく即座に関数から返ってきますが、
GetMessage()は新しいメッセージが追加されるまで関数がブロックされます。

そしてここが重要ですが、GetMessage()のブロック中はCPU使用率がほぼ0となります。
このため一般的なWindowsプログラムにてマウスを動かさなかったりすれば、
PCが待機状態となるため消費電力も少なくて済みます。

しかしDirectXでの描画は常に毎フレーム画面を書き直す必要があるため、
GetMessage()で処理がブロックされてしまうのはよろしくありません。
このためDirectXなどリアルタイムに処理が必要なプログラムでは、
メッセージが無くてもすぐに戻ってくるPeekMessage()を使います。

参考としてGetMessage()を使用したメッセージループは以下のようになります。

    MSG msg;
    while(1) {
        BOOL ret = GetMessage( &msg,NULL,0,0 );
        if( ret==0 || ret==-1 )
            break;
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

GetMessage()の戻り値はBOOLとなっており、仕様ではTRUEの時はWM_QUIT以外のメッセージを受け取ったときで、
FALSEの場合はWM_QUITメッセージを受け取ったときとなり、FALSE(=0)の時にループを終了させるのですが、
実はGetMessage()自体が失敗した時に-1を返すことがあるため、この時にも終了出来るように0か-1かをチェックしています。