Windows では OS がすべてを把握しているといってもいいでしょう。マウスの動き、キーボードの入力、ウインドウの変化、これらはすべて OS が管理しています。アプリケーションは、これらの変化を OS からメッセージとして教えてもらうのです。教えてもらうというよりは教えられるので必要な処理をする、といった感じです。
コンピュータ上で発生した変化を、アプリケーションはメッセージとして受け取ります。では、このメッセージにはどんなものがあるのか見てみます・・・が、すべてのメッセージを網羅すると膨大な量になるので、ここでは一般メッセージについてのみ見て・・・みようとしてもまだまだ大量なので、別ファイルにしました。その他の定義については、WinUser.hを参照してみてください。
では、そのメッセージを扱う構造体をみてみます。
typedef struct { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *PMSG;
hwndは、そのメッセージを受け取ったウインドウプロシージャが設定されているウインドウのハンドルが入ります。messageはメッセージの種類をあらわす番号で、先ほどのWinUser.hで定義されているものです。wParam, lParamはメッセージに付随する情報を渡すことができ、その内容はメッセージに依存します。timeはメッセージがポストされた時刻で、ptはその時のスクリーン上でのポインタの座標が入ります。
通常、発生したメッセージはメッセージキューに入れられます。システム用のキューがひとつと各スレッドごとのキューがあり、はじめにすべてのメッセージはシステムキューに入ります。その後、どのウインドウに行くべきメッセージかが判断されて、各ウインドウのスレッド毎のキューに振り分けられます。各スレッドではウインドウのプロシージャにメッセージを渡し、処理が行われます。ただし、例外なのがWM_PAINTメッセージで、時間のかかる再描画処理の回数をできるだけ少なくするために他のメッセージとは異なる処理が行われます。 また、いくつかのメッセージはキューに入らずに直接プロシージャに渡されます。
では、実際にメッセージを受け取り、消えた部分を再描画してみます。
0001: /* 0002: messageloop.c 0003: gcc messageloop.c -mwindows 0004: bcc32 -W messageloop.c 0005: */ 0006: 0007: #include <windows.h> 0008: 0009: LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ); 0010: 0011: int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, 0012: LPSTR lpCmdLine, int nCmdShow ) 0013: { 0014: HWND hWnd; 0015: MSG msg; 0016: WNDCLASS wndClass; 0017: static TCHAR szAppName[] = TEXT("MessageLoop"); 0018: int ret; 0019: 0020: /* ウインドウクラスの設定 */ 0021: wndClass.style = 0; 0022: wndClass.lpfnWndProc = WndProc; 0023: wndClass.cbClsExtra = 0; 0024: wndClass.cbWndExtra = 0; 0025: wndClass.hInstance = hInstance; 0026: wndClass.hIcon = NULL; 0027: wndClass.hCursor = NULL; 0028: wndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); 0029: wndClass.lpszMenuName = NULL; 0030: wndClass.lpszClassName = szAppName; 0031: 0032: RegisterClass( &wndClass ); /* ウインドウクラスの登録 */ 0033: 0034: hWnd = CreateWindow( /* ウインドウの作成 */ 0035: szAppName, /* 作成するウインドウのクラス名 */ 0036: TEXT("openwin-win32"), /* ウインドウタイトル */ 0037: WS_OVERLAPPEDWINDOW, /* ウインドウスタイル */ 0038: CW_USEDEFAULT, /* 左上頂点のX座標 */ 0039: CW_USEDEFAULT, /* 左上頂点のY座標 */ 0040: CW_USEDEFAULT, /* ウインドウの幅 */ 0041: CW_USEDEFAULT, /* ウインドウの高さ */ 0042: NULL, /* 親ウインドウのハンドル */ 0043: NULL, /* メニューのハンドル */ 0044: hInstance, /* 親モジュールのインスタンスハンドル */ 0045: NULL ); /* ウインドウへの引数 */ 0046: 0047: ShowWindow( hWnd, nCmdShow ); /* ウインドウの表示 */ 0048: 0049: UpdateWindow( hWnd ); /* 描画領域の更新 */ 0050: 0051: /* メッセージループ */ 0052: while( (ret = GetMessage( &msg, NULL, 0, 0)) != 0 ){ 0053: if( ret != -1 ){ 0054: DispatchMessage( &msg ); 0055: } 0056: } 0057: 0058: return msg.wParam; 0059: } 0060: 0061: /* ウインドウプロシージャ */ 0062: LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ){ 0063: HDC hdc; 0064: PAINTSTRUCT ps; 0065: HPEN hPen; 0066: RECT rect; 0067: 0068: switch( message ){ 0069: case WM_PAINT: 0070: hdc = BeginPaint( hWnd, &ps ); /* 描画開始 */ 0071: 0072: GetClientRect( hWnd, &rect ); /* クライアント領域の取得 */ 0073: 0074: hPen = GetStockObject( BLACK_PEN ); /* ペンの選択 */ 0075: SelectObject( hdc, hPen ); /* ペンの設定 */ 0076: 0077: MoveToEx( hdc, 0, 0, NULL ); /* ペン座標を原点へ */ 0078: LineTo( hdc, ps.rcPaint.right, ps.rcPaint.bottom ); /* 対角線を描画 */ 0079: 0080: EndPaint( hWnd, &ps ); /* 描画終了 */ 0081: return 0; 0082: case WM_DESTROY: 0083: PostQuitMessage( 0 ); 0084: return 0; 0085: } 0086: return DefWindowProc( hWnd, message, wParam, lParam ); 0087: }
0009: LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam );
まずは重要になるウインドウプロシージャです。こちらはプロトタイプ宣言ですが、ウインドウプロシージャの名前は決まっていませんが、返り値や引数については決まっています。
0021: wndClass.lpfnWndProc = WndProc; ... 0024: wndClass.hInstance = hInstance;
各ウインドウ毎に、そのウインドウに届いたメッセージを処理するためのウインドウプロシージャを設定します。ウインドウクラスを決める時にWNDCLASS.lpfnWndProcにプロシージャの名前を、WNDCLASS.hInstanceにそのプロシージャが含まれるモジュールのインスタンスハンドルを指定します。
0049: UpdateWindow( hWnd ); /* 描画領域の更新 */
BOOL UpdateWindow( HWND hWnd );
ShowWindowがウインドウを表示して、UpdateWindowがクライアント領域を更新します。この関数は実際にはhWndに対してWM_PAINTメッセージを送るだけです。この関数からはウインドウの再描画が必要な場合にのみWM_PAINTが送られます。この際、メッセージキューには入らずに、直接ウインドウプロシージャに届けられます。また、再描画が必要でない場合は、メッセージが送られません。
0051: /* メッセージループ */ 0052: while( (ret = GetMessage( &msg, NULL, 0, 0)) != 0 ){ 0053: if( ret != -1 ){ 0054: DispatchMessage( &msg ); 0055: } 0056: }
BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax );
GetMessageはスレッドのメッセージキューからhWndに関するものを探してそのメッセージを取り除き、lpMsgにコピーを作ります。wMsgFilterMinとwMsgFilterMaxには、探し出すメッセージの種類を指定します。これらが0の場合は、メッセージのフィルタリングは行われません。
GetMessageはWM_QUITを見つけると0を、それ以外の場合は0以外を返します。さらに、エラーが発生したときは-1を返します。返り値をwhileに直接渡すのは、エラー処理ができなくなってしまう誤った使い方なのですが、多くの解説書ではそういう形の説明が多いです。
BOOL DispatchMessage( const MSG lpMsg );
DispatchMessageはメッセージをウインドウプロシージャに送ります。ただし、WM_TIMERメッセージの場合かつMSG.lParamがNULLでない場合はMSG.lParamが指し示す関数を呼び出します。
0061: /* ウインドウプロシージャ */ 0062: LRESULT CALLBACK WndProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ){
ウインドウプロシージャにはどんな名前をつけてもいいですが、ウインドウクラスを作成する際にWNDCLASS.lpfnWndProcに正しく設定する必要があります。ウインドウプロシージャの引数は決まっていて変えることはできません。
hWndにはメッセージを処理するウインドウハンドル、messageには処理するメッセージが、wParam, lParamにはメッセージに関する引数が設定されます。
自分で処理しないメッセージについては、そのまま標準のウィンドウプロシージャDefWindowProcに渡します。
では、この例での具体的な処理をみてみます。
0068: switch( message ){ 0069: case WM_PAINT: ... 0081: return 0; 0082: case WM_DESTROY: 0083: PostQuitMessage( 0 ); 0084: return 0; 0085: }
このようにメッセージの種類によって処理を振り分けるのが通常です。メッセージを正しく処理したら0を返して関数を抜けます。
0069: case WM_PAINT: 0070: hdc = BeginPaint( hWnd, &ps ); /* 描画開始 */ 0071: 0072: GetClientRect( hWnd, &rect ); /* クライアント領域の取得 */ 0073: 0074: hPen = GetStockObject( BLACK_PEN ); /* ペンの選択 */ 0075: SelectObject( hdc, hPen ); /* ペンの設定 */ 0076: 0077: MoveToEx( hdc, 0, 0, NULL ); /* ペン座標を原点へ */ 0078: LineTo( hdc, rect.right, rect.bottom ); /* 対角線を描画 */ 0079: 0080: EndPaint( hWnd, &ps ); /* 描画終了 */ 0081: return 0;
描画に関する部分がここにきています。WM_PAINTが発生したらクライアント領域を再描画するための処理です。前回は描画が一回きりだったのでPAINTSTRUCTに返った値を使いましたがここではGetClientRectを呼んで、クライアント領域の範囲を取得しています。
BOOL GetClientRect( HWND hWnd, LPRECT lpRect );
GetClientRectはhWndで指定したウインドウのクライアント領域の座標をlpRectに設定します。左上隅の座標は常に(0,0)となります。
void PostQuitMessage( int nExitCode );
PostQuitMessageはWM_QUITメッセージを発行します。nExitCodeにはwParamに設定したい値を設定します。
さて、Windowsにおいてはクライアント領域と無効領域というのを把握しておく必要があります。
クライアント領域というのは、プログラムから自由に描画できる領域のことです。上のプログラムでいうと、対角線の描かれた白い矩形領域のことになります。この領域の大きさは GetClientRectで取得できるのですが、逆に、このクライアント領域のサイズを設定したいことがあります。SetWindowPos関数で設定できるのは、ウインドウの大きさです。クライアント領域の大きさからウインドウサイズを決めたい場合はAdjustWindowRectExを使います。また、ウインドウを構成する部品の大きさを知りたい場合は、GetSystemMetricsを使います。
無効領域というのは、クライアント領域のうち再描画が必要な領域です。Windows は描画効率を高めるために必要な領域しか再描画しないようになっています。実はPAINTSTRUCT.rectが持っている矩形領域は、すべての無効領域を含む最低限の大きさの矩形です。
無効領域が発生すると、WM_PAINTメッセージが生成されます。無効領域が有効化されるのはBeginPaintが呼ばれたときになります。
ためしに、上のプログラムを実行した後ウインドウをリサイズし、部分的に線を隠してからまた一番上に持ってきてみてください。リサイズに対する処理は行っていないので、線は元のままです。最前面に現れると新しいサイズで対角線を引こうとはしていますが、隠れていた部分だけが新しく描画され、もともと露出していた部分はそのままです。ウインドウが隠されたことによってその領域が無効領域となり、その範囲だけが描画されたのです。
ちなみに、リサイズ時に全体を再描画するには、WNDCLASS.styleにCS_HREDRAW|CS_VREDRAWを設定します。こうすることによってリサイズ時にはクライアント領域全体が無効化され、期待どおりに再描画されます。