パソコン活用研究C&C++であそぼ(C、C++、の活用研究)

正しくないWindowsプログラムその3
--メッセージループ--


今回は、いよいよメッセージループを追加してみます。あいかわらず、ウィンドウプロシジャがないので、ウィンドウプロシジャの関数(関数名=関数のポインタ)を指定すべきところ(wcex.lpfnWndProc)には、DefWindowProc関数を指定しておきます。


window1.cpp
#include<windows.h>

int WINAPI WinMain(
                HINSTANCE hInstance ,
                HINSTANCE hPrevInstance ,
                PSTR lpCmdLine ,
                int nCmdShow ) {
        HWND hwnd;
        WNDCLASS wcex;
        MSG msg;

        wcex.style              = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc        = DefWindowProc;
        wcex.cbClsExtra         = 0;
        wcex.cbWndExtra         = 0;
        wcex.hInstance          = hInstance;
        wcex.hIcon              = LoadIcon(NULL , IDI_APPLICATION);
        wcex.hCursor            = LoadCursor(NULL , IDC_ARROW);
        wcex.hbrBackground      = (HBRUSH)GetStockObject(WHITE_BRUSH);
        wcex.lpszMenuName       = NULL;
        wcex.lpszClassName      = "AppModel";

        if (!RegisterClass(&wcex)) return 0;

        hwnd = CreateWindow(
                        wcex.lpszClassName , "test" ,
                        WS_OVERLAPPEDWINDOW ,
                        100 , 100 , 200 , 200 , NULL , NULL ,
                        hInstance , NULL
        );

        if (hwnd == NULL) return 0;

        ShowWindow(hwnd , SW_SHOW);

// メッセージループ
while(GetMessage(&msg, NULL, 0, 0)) 
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}


        return 0;
}

以下のメッセージループが今回追加した部分です。
while(GetMessage(&msg, NULL, 0, 0)) {
      TranslateMessage(&msg);
DispatchMessage(&msg);
}


(メッセージループ説明)
Windowsではキーを押した、マウスをクリックしたというユーザの操作(=イベント)は、メッセージに変換されていったんメッセージキューと呼ばれる場所に格納されます。
メッセージループは、このメッセージキューからひたすらメッセージを取り出して処理する役割を果たします。

まずメッセージキューからメッセージを受け取るには GetMessage() 関数を使用します。この関数は4つの引数をとります。
GetMessage(LPMSG lpMsg , HWND hWnd , UINT wMsgFilterMin , UINT wMsgFilterMax);

第一引数の LPMSG 型 は、MSG構造体のポインタ型です。lpMsg に、MSG構造体変数のポインタを渡します。MSG構造体については下で説明します。

第二引数hWnd には、メッセージを受け取るウィンドウのハンドルを渡します。アプリケーションで表示している全てのウィンドウから受け取る場合はNULL を指定します。

第三引数wMsgFilterMin は、受け取るメッセージの最小値
第四引数wMsgFilterMax は、受け取るメッセージの最大値です。
この2つの引数を指定することで受け取るメッセージのフィルタリングを行います(特定のメッセージだけを得ることができる)。

全てのメッセージを受け取る(フィルタリングを行わない)場合は、第三、第四引数は 0 を指定します。

戻り値は、通常は 0 以外の値 TRUE を返します。
WM_QUIT というメッセージを受け取った時のみ FALSE(NULL) を返します。
WM_QUITについてはウィンドウプロシージャのところで説明したいと思いますが、これがプログラム終了のメッセージです。WM_QUITを受け取るとGetMessage関数はNULlを返し、whileのループ(メッセージループ)から抜け出すことになります。

メッセージの管理は MSG 構造体型変数で行います。
この構造体は、WINUSRE.H で次のように定義されています。
typedef struct tagMSG {
    HWND   hwnd;      
    UINT   message;
    WPARAM wParam;
    LPARAM lParam;
    DWORD  time;
    POINT  pt;
} MSG , * PMSG;
実際には少し違ったりするので、直接ヘッダファイルを調べても良いでしょう。
ここで、WPARAMLPARAM という型が出てきました。
これは、メッセージのやり取りに使う専用の 32ビット 型です。 (WPARAMは Win16 で16ビットです)

hwnd には、メッセージを受け取る関数を持つウィンドウのハンドルが入ります。
メッセージを受け取る関数とは、前回も触れた「ウィンドウプロシージャ」です。

message は、メッセージを識別する整数が入ります。
wParam と lParam は、メッセージの付加情報が入る場所で意味や内容はメッセージによって様々です。

time は、メッセージがポストされた時間
pt は、メッセージがポストされた時のカーソル位置を格納します。

POINT 型は構造体型で、WINDEF.H で定義されています
typedef struct tagPOINT { 
    LONG x; 
    LONG y; 
} POINT;
x と y には、二次元空間の x と y 座標を表す数値が格納されます。


次のTranslateMessage関数は一部のキー入力について前処理をする関数です。キー入力の処理を行わない場合は省略しても構いません。

そして、取得したメッセージを適切なウィンドウプロシージャに渡す役割を果たすのがDispatchMessage関数です。
LONG DispatchMessage(CONST MSG *lpmsg);

lpmsg には GetMessage() で受け取ったメッセージを格納したMSG 構造体変数のポインタを渡します。
DispatchMessageは、内部でMSG構造体のhwndメンバを用いてウインドウプロシージャのアドレスを取得する関数を呼び出し、その後、MSG構造体のメンバを引数としてそのウインドウプロシージャにメッセージを送出(ディスパッチ)します。ウィンドウプロシージャの処理が終了すると、DispatchMessage関数に制御が戻り、 戻り値はウィンドウプロシージャの戻り値が返ります。一般的には戻り値は無視して、whileループ(メッセージループ)がぐるぐる回ることになります。

このプログラムを実行してみると、普通のウィンドウが表示しています。余計なMessageBoxもついていないし、外見上はほぼ完全なWindowアプリケーションに見えますね。

(実行画面)



ただし、ウィンドウプロシージャがない分、何かが足りないはずです。
では×ボタンでアプリケーションを閉じてみましょう。ウィンドウは消えました。めでたしめでたし。完全なプログラムじゃん、と思いたいところですが、タスクマネージャでプロセスを見るとWindow1.exeというプロセスが残っていますね。表示は消えたけどまだプロセスは残っていますね。これは不完全なプログラムです。×ボタンで閉じたら、ホントはプロセスもなくなって、メモリから完全に退場してもらわなくてはなりませんよね。



プロセスが残ってしまうのは、getMessage関数がWM_QUITメッセージを受け取っていないため、メッセージループがループから抜けられていないからです。表示は消えたが、プログラムは終了していない(メッセージループがぐるぐる回り続けている)という状態です。

ちょっと説明すると、×ボタンをクリックすると、ウィンドウプロシージャでは通常以下の(A)〜(D)の処理が行われます。
(A)×ボタンがクリックされたというメッセージ(WM_SYSCOMMAND)が送られる →
(B)WM_CLOSE メッセージ(ウィンドウが閉じられようとしている)が送られる →
(C)WM_DESTROYメッセージ(ウィンドウが破棄されようとしている)が送られる →
(D)WM_DESTROYメッセージを受けたらPostQuitMesaage関数でWM_QUITメッセージを送る →
そして、メッセージループでGetMeesageがWM_QUITを受けると、メッセージループが終了→プログラム終了
となるわけです。
これがたぶん、通常のプログラムの終了の仕方です。

実は、このプログラムがちゃんと終了しないのは、ウィンドウプロシージャ関数をお手軽にDefWindowProcにまかせてしまったせいなんですね。
上記(A)〜(C)の処理は、賢いDefWindowProcが自動的にやってくれるのですが、(D)はやってくれないようなのです。ここにミソがありまして、(D)の処理がなされない、すなわちWM_QUITが送出されないため、このプログラムではメッセージループが終了しないということになるわけです。

ということで(D)の処理についてはウィンドウプロシジャのところで見てみたいと思います。

とりあえず、こんなあたり、OSの仕組みとともに、ルールを守らずにプログラムをすると、どんな不具合がおこっちゃうのか、といったことも少し見えてきました。一見正常終了しているようで、実はメモリにい続けるなんて、最悪なプログラムですね。


ではもう少し、メッセージループの中で遊んでみることにします。
通常、windowsからのメッセージを受け取り処理する工程はウィンドウプロシージャに記述するわけですが、メッセージループの中にメッセージを処理するコードを直接書いちゃうという反則技?もできなくはないわけです。

#include<windows.h>

int WINAPI WinMain(
                HINSTANCE hInstance ,
                HINSTANCE hPrevInstance ,
                PSTR lpCmdLine ,
                int nCmdShow ) {
        HWND hwnd;
        WNDCLASS wcex;
        MSG msg;

        wcex.style              = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc        = DefWindowProc;
        wcex.cbClsExtra         = 0;
        wcex.cbWndExtra         = 0;
        wcex.hInstance          = hInstance;
        wcex.hIcon              = LoadIcon(NULL , IDI_APPLICATION);
        wcex.hCursor            = LoadCursor(NULL , IDC_ARROW);
        wcex.hbrBackground      = (HBRUSH)GetStockObject(WHITE_BRUSH);
        wcex.lpszMenuName       = NULL;
        wcex.lpszClassName      = "AppModel";

        if (!RegisterClass(&wcex)) return 0;

        hwnd = CreateWindow(
                        wcex.lpszClassName , "test" ,
                        WS_OVERLAPPEDWINDOW ,
                        100 , 100 , 200 , 200 , NULL , NULL ,
                        hInstance , NULL
        );

        if (hwnd == NULL) return 0;

        ShowWindow(hwnd , SW_SHOW);

// メッセージループ
while(GetMessage(&msg, NULL, 0, 0)) 
{


TranslateMessage(&msg);
if (msg.message == WM_LBUTTONDOWN) break; /*ここ追加*/
DispatchMessage(&msg);
}


        return 0;
}

ではプログラムがちゃんと終了する仕組みも付けてみることにします。
if (msg.message == WM_LBUTTONDOWN) break;
この1行は、マウスの左ボタンが押されたらメッセージループから抜ける(break;)→アプリケーション終了というわけです。マウスの左ボタンを押下すると、プログラムは終了し、プロセスも消えてなくなります。

breakで強制的にwhileループを抜けるのがいやならば、
if (msg.message == WM_LBUTTONDOWN) PostQuitMessage(0);
と書く手もあります。
マウスの左ボタンが押されたら、PostQuitMessage関数を呼ぶ。
この関数はMW_QUITメッセージを送出しますので、次のループに入る時にGetMssage関数がMW_QUITを受け取ってプログラム終了というわけです。

ただし、このプログラムでは、×ボタンで閉じると、相変わらずプロセスが残りますね。
これは、WM_LBUTTONDOWNというメッセージはウィンドウのクライアント領域をクリックした時に発生するメッセージだからです。
×ボタンがあるのは非クライアント領域のため、×ボタンを左クリックするとWM_NCLBUTTONDOWNというメッセージが発生し、DefWindowProcの処理にまかされてしまうからですね。

まあ、擬似的ですが、×ボタンの領域をクリックしたら終了するという動きにしたい場合は、
if(msg.message==WM_NCLBUTTONDOWN) {
if (msg.wParam == HTCLOSE) {break;} }
と記述することで可能です。メッセージ構造体変数のところで、wParamにはメッセージの付加情報が入ると書きましたが、このパラメータを調べれば非クライアント領域のどこがクリックされたのかを調べることができます。HTCLOSEは×ボタンをあらわす定数。値は20です。
ちなみに、HTMINBUTTONがアイコン化ボタン(最小化ボタン)。定数の値は8。
HTMAXBUTTONが最大化ボタン。定数の値は9。

ちょっといたずらしてみて、
if(msg.message==WM_NCLBUTTONDOWN) {
if (msg.wParam == HTMAXBUTTON) {break;} }
と書くと、どうなるでしょうか。
最大化ボタンをクリックすると、アプリケーションが終了するいたずらプログラムになってしまいます。

TopPage