ウィンドウプロシージャ

 今回の講座のメインイベント、ウィンドウプロシージャです。はっきり言って、今回は筆者にもよく分かってない部分があるので、もし「間違がっとる!」と思われる場所があったらどんどん指摘してください。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を呼び出します。
 この関数を呼び出すと、自動的にWM_QUITが送られます。前回少し触れましたが、このメッセージが送られたとき、GetMessage()は0を返します。このとき、whileループから抜け、アプリケーションが終了するというわけです。
 ちなみにPostQuitMessage()の第1引数に渡した値は、GetMessage()で取り出したメッセージのwParamに入っています。この値をWinMainで返すことで、アプリケーションがどのように終了したのかが分かるようにします。正常に終了した場合には0を返すようにします。

DefWindowProc()
 特別な処理をしないようなメッセージの場合には、DefWindowProc()というAPIにそのまま渡してしまいます。この関数は、基本的なウィンドウの操作を肩代わりしてくれます。例えば、最大化したり、最小化したりといったことです。前回ちょっと触れた、MFCがとりあえず登録する各ウィンドウのウィンドウプロシージャが、この関数です。

wParamとlParam
 最後にウィンドウプロシージャの引数について見てみましょう。これらの値はGetMessage()で受け取るMSG構造体のメンバで、それがウィンドウプロシージャに送られてきたときにひとつひとつ引数として渡されています。

 HWND p_hWndには、メッセージが送られてきたウィンドウのハンドルが入っています。ウィンドウプロシージャはウィンドウクラスごとに登録されるので、複数のウィンドウで使い回すことになります。当然、どのウィンドウに送られてきたのか知る必要があるわけですね。

 UINT p_uiMsgには、メッセージそのものが入っています。これを見ても、メッセージがUINT型、つまり32ビット整数値だということが分かると思います。

 WPARAM wParamとLPARAM lParamはちょっと特別です。このふたつの値は基本的には32ビット整数値(正確にはDWORD型)で、値の意味はメッセージによって変わります。ある時にはウィンドウハンドル、ある時には文字列へのポインタ、ある時にはマウスカーソルの位置と、とにかく様々な値が入っています。
 おそらく知っていると思われますが、一応説明すると、他の型の値を入れるときには型キャストを行います。変数の前に(LPCTSTR)wParamといった形で型を小カッコで囲ってくっつけると、その変数がその型としてみなされます。コンパイル時の指定によっては、これを行わないとエラーが発生します。
 また、これらの変数の中にはふたつの値が組み合わさって入っているときがあります。「組合わさっている」とは、32ビットを、上位16ビットと下位16ビット(0xffff0000で表せば、fが入っている部分が上位16ビット、0が入っている部分が下位16ビット)に分けて、ふたつの16ビットの値(WORD型)を入れているというものです。WPARAM型などをふたつに分けるときにはLOWORDHIWORDというふたつのマクロを使用します。また、ふたつのWORDをくっつけてWPARAMにするにはMAKEWPARAMマクロを、LPARAMにするにはMAKELPARAMマクロを使用します(第1引数が下、第2引数が上!)。

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()メンバ関数へのポインタが渡されています。この雰囲気を見ても、あとでこのポインタを使って関数が呼ばれそうな感じなのが見て取れます。
 この配列には、メッセージと関数の数だけの要素を持ち、最後にAfxSig_endの入った構造体が入る形になります。

 さて、これで呼び出される関数の準備ができました。今度はこの関数を呼び出す方、つまりウィンドウプロシージャについて見ていきましょう。
 前回、実質的なウィンドウプロシージャはAfxWndProc()だと書きました。ですが、メッセージの処理をするのはこの関数ではありません。送られてきたメッセージはAfxWndProc()>AfxCallWndProc()>CWnd::WindowProc()>CWnd::OnWndMsg()と呼ばれ、最後のCWnd::OnWndMsg()で、メッセージの振り分けを行っています。

 この関数は、まず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()を呼び出しているんです。mmfMessageMapFunctionsという構造体ですが、これは union(共用体)として定義されています。共用体とは特殊な構造体で、たったひとつのメンバ変数しか使用できないという機能を持っています。
 mmfにはあらかじめ、先ほどの構造体に入れておいたOnPaint()へのポインタを渡してあります。MessageMapFunctionsにはpfn_vvの他にも、「各メッセージに関連づけられた関数へのポインタ」型の変数が多数宣言されているのですが、どれを呼んでも最初に入れていた関数へのポインタが呼び出されるというわけです。

 なんでこんなことをしているのかというと、各関数によって引数が違うからです。OnPaint()は引数を持ちませんが、他の多くの関数は様々な形の引数を持ち、WPARAMとLPARAMの値を渡しているのです。ですが、関数へのポインタという点では変わりありません。この問題を解決しているのが共用体ということになります。共用体を使うことで、ひとつの関数ポインタを元に様々な引数を持つ関数を呼び出すことができるというわけです。

 なんかごちゃごちゃしちゃったので、まとめてみましょう。
 まず、あらかじめCWnd派生クラスに、メッセージと、それに関連づけられた関数へのポインタを入れた構造体の配列を作製しておきます。
 次に、ウィンドウプロシージャでその配列へのポインタを取得し、送られてきたメッセージを持つ構造体を検索します。
 構造体が見つかったら、中に入っている列挙型の値を使って、呼び出す関数を検索します。
 最後に、この構造体の中に入っている関数へのポインタを使って、OnPaint()を呼び出します。

 やっぱり、かなり難しい方法を使っていますね……。

山は越えた
 とりあえず、一番難しいところはこれでクリアしたと思います。と言っても、MFCの中身の解説はまったく読んでない方にしてみれば、山でもなんでもないですね……。
 次回は「ハンドル」について見ていきたいと思います。もし余裕があれば「文字列」についても見ていきます。ここからはかなり初歩的な部分になってくるので、読まなくて済む方にはごめんなさい……。

(C)KAB-studio 1997 ALL RIGHTS RESERVED.