今回の講座のメインイベント、ウィンドウプロシージャです。はっきり言って、今回は筆者にもよく分かってない部分があるので、もし「間違がっとる!」と思われる場所があったらどんどん指摘してください。C++って奥が深いね……。
|
////////////////////////////////////////////////////////////////////
// ウィンドウプロシージャです。
LRESULT CALLBACK WndProc( HWND p_hWnd, UINT p_uiMsg, WPARAM wParam, LPARAM lParam )
{
// メッセージを処理します。
switch( p_uiMsg )
{
case WM_PAINT:
WriteHello( p_hWnd );
return 0;
case WM_DESTROY: //×ボタンが押されたとき。
PostQuitMessage( 0 ); //アプリケーションを終了させます。
return 0;
}
return DefWindowProc( p_hWnd, p_uiMsg, wParam, lParam );
}
|
呼び出されるウィンドウプロシージャ
さて、今回見るWndProc()という関数は、ウィンドウズから呼び出される関数です。前回見たように、ウィンドウプロシージャはウィンドウクラスの登録時に各ウィンドウに設定されます。その後、各アプリケーションがメッセージを取得したら、DispatchMessage()というAPIを使ってウィンドウプロシージャにメッセージを飛ばします。ウィンドウズはこの命令を受けて、ウィンドウプロシージャを呼び出してメッセージを送りつけるわけです。 この手の「ウィンドウズから呼び出される関数」は結構あります。例えばEnumWindows()というAPIは、トップレベルウィンドウ(つまり親ウィンドウを持たないウィンドウ)を検索して、見つかったものからどんどん、指定された関数へと送ります。その関数はEnumWindowsProc()という関数で、これもプログラマが自分で作製し、ウィンドウズから呼び出される関数です。 |
メッセージの振り分け
さて、では実際にどのようにメッセージを処理するのか、見てみましょう。 メッセージは、前回触れたように正の32ビット整数です。そこで、このメッセージをswitchを使って振り分けます。上の例では、WM_PAINTが送られてきた時とWM_DESTROYが送られてきた時に何らかの処理をするようにしています。
WM_PAINTは、ウィンドウを表示する時に送られてくるメッセージです。例えば最初にウィンドウが開いたときとか、最小化していたのが元に戻されたときとか、そういった「ウィンドウ」という形を描画したり再描画しなければならないときに送られてくるメッセージです。
WM_DESTROYは、ウィンドウが削除されたときに送られてきます。具体的には、右上にある「×」ボタンを押したときに送られます。「このウィンドウが閉じたらアプリケーションを終了させる」というウィンドウのウィンドウプロシージャでは、WM_DESTROYが送られてきたらPostQuitMessage()というAPIを呼び出します。 |
DefWindowProc()
特別な処理をしないようなメッセージの場合には、DefWindowProc()というAPIにそのまま渡してしまいます。この関数は、基本的なウィンドウの操作を肩代わりしてくれます。例えば、最大化したり、最小化したりといったことです。前回ちょっと触れた、MFCがとりあえず登録する各ウィンドウのウィンドウプロシージャが、この関数です。 |
wParamとlParam
最後にウィンドウプロシージャの引数について見てみましょう。これらの値はGetMessage()で受け取るMSG構造体のメンバで、それがウィンドウプロシージャに送られてきたときにひとつひとつ引数として渡されています。
HWND p_hWndには、メッセージが送られてきたウィンドウのハンドルが入っています。ウィンドウプロシージャはウィンドウクラスごとに登録されるので、複数のウィンドウで使い回すことになります。当然、どのウィンドウに送られてきたのか知る必要があるわけですね。
UINT p_uiMsgには、メッセージそのものが入っています。これを見ても、メッセージがUINT型、つまり32ビット整数値だということが分かると思います。
WPARAM wParamとLPARAM lParamはちょっと特別です。このふたつの値は基本的には32ビット整数値(正確にはDWORD型)で、値の意味はメッセージによって変わります。ある時にはウィンドウハンドル、ある時には文字列へのポインタ、ある時にはマウスカーソルの位置と、とにかく様々な値が入っています。 |
MFCの場合
例によってめんどくさい方法を使っていますが、個人的には、メッセージにメンバ関数を割り当てるというのは分かりやすくていいと思うので、まぁ目をつぶるとしましょう。ちなみにCWnd::WindowProc()という関数がそのままウィンドウプロシージャとして使えるので、上のような処理をしたい場合にはこの関数を利用しましょう。筆者もかなり利用しちゃっています。
まぁ、そんなことはともかく、MFCでどのようにメッセージを関連づけているのか、WM_PAINTを例に見てみましょう。このメッセージはCWnd::OnPaint()という関数を呼び出すようになっています。そして、普通にSDIアプリケーションを作製すると、初めからCView派生クラスの中に作製されています。例としてはうってつけですね。
さて、このクラスのヘッダーファイル内には、次のようなコードがあります(説明に不要な部分は省いています)。 |
// 生成されたメッセージ マップ関数
protected:
//{{AFX_MSG(CTestView)
afx_msg void OnPaint();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
AFX_MSGは単なるコメントで、クラスウィザードがどこからどこまでのコードがメッセージに関連づけられた関数を書き込む行なのかを知るためのものです。afx_msgが付いた関数は、メッセージに関連づけられているという意味を持っています。afx_msgはコンパイル時に削除されるので、プログラムとしての意味は持っていません。これも、クラスウィザードのための目印です。
面白いのはDECLARE_MESSAGE_MAP()です。このマクロは、次のコードと同じです。 |
// 生成されたメッセージ マップ関数
protected:
//{{AFX_MSG(CTestView)
afx_msg void OnPaint();
//}}AFX_MSG
private:
static const AFX_MSGMAP_ENTRY _messageEntries[];
protected:
static AFX_DATA const AFX_MSGMAP messageMap;
virtual const AFX_MSGMAP* GetMessageMap() const;
つまり、このマクロによって自動的にAFX_MSGMAP_ENTRY構造体の配列とAFX_MSGMAP構造体、そしてGetMessageMap()というメンバ関数を作製します。
で、これらの関数の定義や構造体の初期化は、CPPファイルの方で行われています。上の方に、次のようなコードが書かれているはずです。 |
BEGIN_MESSAGE_MAP(CTestView, CView)
//{{AFX_MSG_MAP(CTestView)
ON_WM_PAINT()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
これらのマクロを置き換えると、次のようになります。
|
const AFX_MSGMAP* CTestView::GetMessageMap() const
{
return &CTestView::messageMap;
}
AFX_DATADEF const AFX_MSGMAP CTestView::messageMap =
{
&CView::messageMap, &CTestView::_messageEntries[0]
};
const AFX_MSGMAP_ENTRY CTestView::_messageEntries[] =
{
{
WM_PAINT, 0, 0, 0, AfxSig_vv
, (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(void))&OnPaint
},
{
0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0
}
};
まずはGetMessageMap()というメンバ関数の定義です。この関数は、単純にすぐ下にあるmessageMapという構造体へのポインタを返すだけです。
そのmessageMap構造体には、派生元のクラスが持つ同じ名前のメンバ変数へのポインタと、すぐ下にある_messageEntriesという配列の先頭ポインタを入れています。
で、その_messageEntriesは、AFX_MSGMAP_ENTRY構造体の配列になっています。構造体の最初のメンバにはメッセージが入っています。4番目にはAfxSig_vvという値が入っていますが、これはAFXMSG_.hの46行目でenum(列挙型)として宣言されています。列挙型は上から宣言された順に整数値が割り当てられています。これが、あとで役立ってきます。最後のメンバには、OnPaint()メンバ関数へのポインタが渡されています。この雰囲気を見ても、あとでこのポインタを使って関数が呼ばれそうな感じなのが見て取れます。
さて、これで呼び出される関数の準備ができました。今度はこの関数を呼び出す方、つまりウィンドウプロシージャについて見ていきましょう。
この関数は、まずAfxFindMessageEntry()を呼び出します。この関数には、CWnd派生クラスのGetMessageMap()メンバ関数で取得したmessageMapのメンバから_messageEntries[]の先頭ポインタを取得します。そして、この配列の中の構造体をひとつひとつ取得します。構造体の中のメッセージとウィンドウズから送られてきたメッセージ(ここではWM_PAINT)が一致するかどうか確認し、一致しなかったらポインタをインクリメントして次の構造体を取得します。そうやって、AfxSig_endが出てくるまで、つまり最後の構造体に行き着くまでwhileループで検索します。
見つかったら、CWnd::OnWndMsg()に戻って、APIの方でコーディングしたようなswitchを使った振り分けを行います。と言っても、メッセージを使うわけではなく、構造体の中に入っていたAfxSig_なんたらという値を使います。WM_PAINTの場合にはAfxSig_vvで処理します。ちなみに、switchは整数しか扱えませんが、AfxSig_vvは列挙型なので、整数として扱えるんです。これは結構便利そうですね。 |
case AfxSig_vv:
(this->*mmf.pfn_vv)();
break;
実は、このコードがOnPaint()を呼び出しているんです。mmfはMessageMapFunctionsという構造体ですが、これは union(共用体)として定義されています。共用体とは特殊な構造体で、たったひとつのメンバ変数しか使用できないという機能を持っています。
mmfにはあらかじめ、先ほどの構造体に入れておいたOnPaint()へのポインタを渡してあります。MessageMapFunctionsにはpfn_vvの他にも、「各メッセージに関連づけられた関数へのポインタ」型の変数が多数宣言されているのですが、どれを呼んでも最初に入れていた関数へのポインタが呼び出されるというわけです。
なんでこんなことをしているのかというと、各関数によって引数が違うからです。OnPaint()は引数を持ちませんが、他の多くの関数は様々な形の引数を持ち、WPARAMとLPARAMの値を渡しているのです。ですが、関数へのポインタという点では変わりありません。この問題を解決しているのが共用体ということになります。共用体を使うことで、ひとつの関数ポインタを元に様々な引数を持つ関数を呼び出すことができるというわけです。
なんかごちゃごちゃしちゃったので、まとめてみましょう。
やっぱり、かなり難しい方法を使っていますね……。 |
山は越えた
とりあえず、一番難しいところはこれでクリアしたと思います。と言っても、MFCの中身の解説はまったく読んでない方にしてみれば、山でもなんでもないですね……。 次回は「ハンドル」について見ていきたいと思います。もし余裕があれば「文字列」についても見ていきます。ここからはかなり初歩的な部分になってくるので、読まなくて済む方にはごめんなさい……。 |
(C)KAB-studio 1997 ALL RIGHTS RESERVED. |