メッセージの取得と送出
終了処理の改善
前ページまでのプログラムは、コードの最後にメッセージボックスを表示するコードを書いていました。
これはメッセージボックスが閉じられるまでは処理が一時停止することを利用して、プログラムが終了しないようにするためのものです。
もちろん実際のプログラムではこのような処理は行いません。
今度はここを改善してみましょう。
イベント駆動型
コンソールアプリは、コードを上から順に実行していき、コードの終端に達するとプログラムが終了します。
ウィンドウアプリでもコードの終端に達するとプログラムが終了する点は同じですが、ウィンドウに対してマウスやキーボードから様々な操作を行うことができます。
コンソールアプリではユーザー入力のタイミングはプログラムが指定しますが、ウィンドウアプリではユーザーの好きなタイミングで入力ができます。
(コンソールアプリでも任意のタイミングで入力させることは可能ですが)
例えばユーザーは、ウィンドウ上にあるボタンをマウスでクリックすることができます。
ボタンをクリックすれば何らかの処理が実行されます。
つまりボタンをクリックしたタイミングで任意のコードを実行する、ということを行っているのですが、そのためには「ボタンをクリックした」ことを何らかの方法で検知する必要があります。
「ボタンをクリックした」「ウィンドウサイズを変更した」などの情報をイベントと言います。
イベントはユーザーによる操作以外にもシステム(OS)や他のアプリが発生させるものもあります。
イベントが発生すると、システムはウィンドウに対して「こういうイベントが起こったよ」という通知を出します。
この通知をメッセージと言います。
つまりメッセージが送られてきたタイミングで処理を実行するコードを書けば良いわけです。
イベントの発生に応じて処理を実行するプログラムをイベント駆動型プログラミング(イベントドリブン)、またはメッセージ駆動型プログラミングといいます。
「アプリケーションを終了する」というのもイベントのひとつで、ウィンドウに送られてくる終了メッセージに対して適切に終了処理を行うことで、実際にアプリケーションを終了することができます。
GetMessage関数
ウィンドウに送信されたメッセージを取得するにはGetMessage
関数を使用します。
この関数は多くのアプリケーションである程度決まった形で使用されます。
#include <windows.h>
//グローバル変数
HINSTANCE hInst; //インスタンス
WCHAR szTitle[] = L"テストアプリ"; //タイトル
WCHAR szWindowClass[] = L"MyTestApp"; //ウィンドウクラス名
//関数プロトタイプ宣言
ATOM MyRegisterClass(HINSTANCE);
BOOL InitInstance(HINSTANCE, int);
int APIENTRY wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPWSTR lpCmdLine,
int nCmdShow)
{
MyRegisterClass(hInstance);
if (!InitInstance(hInstance, nCmdShow))
{
return 0;
}
//必要ないので削除
//MessageBox(NULL, L"ウィンドウアプリのテスト", L"タイトル", MB_OK);
//MSG構造体の変数を宣言
MSG msg;
BOOL ret;
//メッセージループ
while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
{
if (ret == -1)
{
//GetMessage関数の実行失敗
return -1;
}
if (msg.message == WM_LBUTTONUP)
break;
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
//ウィンドウクラスの登録
ATOM MyRegisterClass(HINSTANCE hInstance)
{
//省略
}
//ウィンドウの作成
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
//省略
}
このコードは前ページのサンプルコードとほぼ同じ動作をしますが、メッセージボックスは表示されません。
ウィンドウ上のタイトルバー以外の場所をマウス左クリックすることでアプリケーションは終了します。
- BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
); - メッセージキューからメッセージを取得し、lpMsgに格納する。
WM_QUITを取得した場合は0を返す。
WM_QUIT以外を取得した場合は0以外を返す。
エラーが発生した場合は-1を返す。
第一引数lpMsg
はMSG構造体というもので、メッセージを格納するための専用の構造体へのポインタです。
ポインタなのでアドレス演算子が必要です。
第二引数hWnd
はメッセージを取得するウィンドウのハンドルです。
NULLを指定するとスレッド内の全てのウィンドウからメッセージを取得します。
スレッドとは処理を並列(同時進行)で行うときの単位です。
今までのコードは全て並列処理は行っておらず、このようなプログラムはシングルスレッドと言います。
なので、ここにNULLを指定すれば複数のウィンドウを同時に表示していても全てのウィンドウに対するメッセージを取得することができます。
ちなみに並列処理を行うプログラムはマルチスレッドと言います。
第三、第四引数はメッセージのフィルタリングを行います。
両方に0を指定するとフィルタリングを行わず、全ての種類のメッセージを取得します。
通常は0で良いです。
戻り値はBOOL型です。
BOOL型は通常はTRUE
かFALSE
(0か0以外か)の二種類の状態のみを示す場合に使用する型ですが、GetMessage関数は「-1」という、TRUEとは別の意味を持つ値を返すこともあり得ます。
これは過去のシステムとの互換性のためだそうです。
(ちなみにBOOL型はint型のtypedef、TRUEは1、FALSEは0と定義されています)
メッセージループ
例えばウィンドウ上にあるボタンをクリックした場合でも、ウィンドウに直接メッセージが送られるわけではなく、メッセージキューと呼ばれる場所にメッセージは一時的に保存されます。
プログラムはGetMessage関数を使用してメッセージキューからメッセージを取り出し、ウィンドウに送信します。
メッセージにはたくさんの種類があり、割と頻繁に送られてくるので、ループ文を使用してGetMessage関数を何度も実行します。
GetMessage関数はWM_QUIT
というメッセージを受け取ると0を返すので、これをループ文の終了条件にします。
WM_QUIT
メッセージはアプリケーションを終了する際に送られてくるメッセージです。
ループ文を抜けるとWinMain関数の終端に達し、プログラムは終了します。
メッセージを取得(&送出)するためのこのループ文のことをメッセージループといいます。
通常のループ分はループを抜けるまで休むことなく処理を続けますが、GetMessage関数はメッセージキューが空の時(正確には、フィルターの条件を満たすメッセージが無い場合)は待機状態になります。
ウィンドウアプリの起動中は常にメッセージループを処理することになりますが、待機状態はシステムへの負荷はかかりません。
GetMessage関数は実行に失敗すると-1を返します。
この状態では正常に処理を続行できないため、サンプルコードではreturn文を直接実行してアプリケーションを終了させています。
Visual Studioのテンプレートが自動生成するコードのメッセージループは以下のようになっています。
(「nullptr」はNULLと同じものと考えてください)
while (GetMessage(&msg, nullptr, 0, 0))
{
//省略
}
このコードは、GetMessage関数が-1を返した場合でも条件は真となるため、アプリケーションは異常な動作となる可能性があります。
マイクロソフトのドキュメントでもこのようなコードは避けるべきと書かれています。
GetMessage 関数 (winuser.h) - Win32 apps | Microsoft Learn
(このサイト掲載のサンプルコードはマイクロソフト推奨の書き方です)
同じマイクロソフト同士でちぐはぐですが、GetMessage関数が失敗することはほぼないそうなので好きなほうを使用してください。
MSG構造体
MSG構造体は、メッセージを格納するための構造体です。
- typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, *NPMSG, *LPMSG; - メッセージを格納する構造体。
hwnd
メンバはウィンドウハンドルです。
message
メンバはメッセージを表す整数値です。
(値の比較には定数を使用します)
wParam
メンバとlParam
メンバはメッセージの追加情報で、メッセージの種類によって意味が異なる値です。
time
メンバは送信された時刻です。
pt
メンバは送信された時点でのマウスカーソル位置です。
サンプルコードでは、GetMessage関数の実行後にmessageメンバの値をチェックしています。
この値がWM_LBUTTONUP
という定数と一致する場合にbreak文を実行し、プログラムを終了するようにしています。
WM_LBUTTONUPはウィンドウ上でマウス左カーソルを離した場合に送信されるメッセージです。
//メッセージループ
while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0)
{
if (ret == -1)
{
//GetMessage関数の実行失敗
return -1;
}
if (msg.message == WM_LBUTTONUP)
break;
DispatchMessage(&msg);
}
ただし、このような終了の仕方は通常は行いません。
プログラムの終了は前述したWM_QUIT
メッセージの取得によって行うのが普通です。
その方法は次ページで説明します。
DispatchMessage関数
メッセージループ内では大まかに、メッセージの取得とメッセージの送出を行います。
メッセージの送出はDispatchMessage
という関数を使用します。
- LRESULT DispatchMessage(
const MSG *lpMsg
); - ウィンドウプロシージャにメッセージをディスパッチする。
ディスパッチというのはプログラミングでよく使用される言葉で、送出とか配送と訳されます。
単純に送るというよりは、適切な送信先に適切なタイミングで送るように(システムに)指示する、といった意味です。
DispatchMessage関数はMSG構造体変数のポインタを引数に取ります。
このMSG構造体の情報を「送る」わけですが、送信先はここでは明示されていません。
送り先は、MSG構造体のhwnd
メンバに関連付けられているウィンドウプロシージャです。
ウィンドウプロシージャというのはWNDCLASS構造体のlpfnWndProc
メンバに登録した関数です。
ここには今のところDefWindowProc
という関数を指定しており、ここにメッセージが送られることになります。
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = DefWindowProc;
//↑これ
戻り値はウィンドウプロシージャから返された値ですが、通常は使用しません。
ウィンドウプロシージャについては次ページで解説します。