ウィンドウプロシージャの呼びだし

 少しずつ佳境に入ってきました。ウィンドウプロシージャはDOS形式のプログラムには見られない、ウィンドウズプログラミングらしい面白い部分です。この辺がよく分かると、かなり見通しが良くなるような気がします。

////////////////////////////////////////////////////////////////////
// メッセージループです。

int Run()
{
	MSG	stMsg;

	// メッセージを送ります。
	while( GetMessage( &stMsg, NULL, 0, 0 ) )
	{
		TranslateMessage( &stMsg );
		DispatchMessage( &stMsg );
	}

	return stMsg.wParam;
}
	

メッセージの取得
 WinMain()で最後に呼ばれる関数、それがこのRun()です。この関数では、メッセージの転送と取得を行います。

 メッセージとは、ウィンドウズから送られてくる正の整数値です。例えば「最小化しろ」とか「左クリックされたぞ」など、こういったイベントが起きた時に送られてきます。これらのメッセージがどういう値なのかはWINUSER.Hに書かれています(q:\devstudio\vc\INCLUDE\の方)。
 このメッセージはキューと呼ばれる領域に、送られた順に並べられています。「キュー」とは「列」という意味なので、バスに乗る順番待ちの列みたいな感じでしょうか。
 ちなみにこのキューに蓄えられるのは、PostMessage()で送られてきた時のみです。SendMessage()で送られてきた時には、あとで説明するウィンドウプロシージャに直接送られます。

 GetMessage()というAPIは、このメッセージキューからメッセージを取り出します。そして、WM_QUIT、つまりアプリケーション終了の知らせが送られてきた時に、戻り値が0になります。そうすれば、この関数が終了し、WinMain()も終了し、アプリケーションが終了するというわけです。

キーコードの変換
 次に、メッセージをTranslateMessage()というAPIに掛けます。この関数は、キー入力関係のメッセージの中で、仮想キーコードを使用するWM_KEYDOWNWM_KEYUPが送られてきた時、通常のキャラクターコードを使用するWM_CHARに変換してくれます。
 まぁ、そんなに気にすることはないでしょう。

ウィンドウプロシージャへの送信
 この関数の詰め、DispatchMessage()です。このAPIはウィンドウプロシージャと呼ばれる関数へと、メッセージを送ります。
 では、ウィンドウプロシージャとは何なのでしょうか。

 ウィンドウプロシージャとは、メッセージを処理するための関数のことです。すべてのウィンドウはウィンドウプロシージャを持ち、送られてくるメッセージを処理します。
 この関数はアプリケーション側で用意し、そしてウィンドウズから呼び出されます。ちゃんと、今回のソースコードの中にも存在します。CALLBACKは、ウィンドウズが呼び出す関数ということを示しています。

LRESULT CALLBACK WndProc( HWND p_hWnd, UINT p_uiMsg, WPARAM wParam, LPARAM lParam )
{
	// 略。次回を待て。
}
	

 こちらで用意すると言っても、好き勝手な関数を使えるわけではありません。関数の形はWindowProc()という関数のページを読んでください。引数と戻り値の型さえ合っていれば、関数や引数の名前は違っていても構いません。

 ウィンドウプロシージャは、ウィンドウクラスを登録するときに指定します。ウィンドウクラスを登録するときにWNDCLASS型の構造体へのポインタを渡しますが、このメンバlpfnWndProcに、作製したウィンドウプロシージャ関数へのポインタを渡します。コードでは次のようになっています。

BOOL InitApplication( HINSTANCE p_hInstance )
{
	WNDCLASS	stWndClass;	//ウィンドウクラスです。

	stWndClass.lpfnWndProc = WndProc;
	// 他のメンバは略。

	// ウィンドウクラスを登録します。
	if( !RegisterClass( &stWndClass ) )
		return FALSE;

	return TRUE;
}
	

 このようにして、「どのウィンドウがどのウィンドウプロシージャで処理されるのか」ということを指定しておくわけです。

「ウィンドウ」の意味
 ウィンドウズの特徴のひとつは、オブジェクトのほとんどがウィンドウであり、これらの様々なウィンドウが集まってアプリケーションを作っているということでしょう。OSのシステムそのものとして、「ウィンドウ」という存在があるわけです。
 これらウィンドウには、ウィンドウプロシージャが存在し、送られてくるメッセージを逐一処理します。この部分は、目に見えません。普通にアプリケーションを使っていると、ウィンドウズの奥の方でまとめてやっているような気がしますが、実際にはアプリケーションが持つウィンドウプロシージャで処理しているわけです。

 本当のところ、ウィンドウには「表示される矩形の単位」というよりは、「メッセージを処理する単位」という意味の方が強いでしょう。
 例えば、常駐型アプリケーションを作製する場合にも、ウィンドウを作製する必要があります。本当の、ただの関数ひとつでもない限り、常駐型といえどもメッセージを受け取り、処理する必要があります。タスクトレイのアイコンがクリックされたり、終了させられたり、色々送られてきます。
 こういったメッセージを処理するため、わざわざウィンドウを作製し、見えなくさせるわけです。

 ウィンドウズはイベントドリブン(Event driven)なOSと言われます。つまり、「キーが押された」だの「マウスが動かされた」だのといった様々なイベントが起き、そこで初めてアプリケーションが動くという形を取っているわけです。
 こういった機能の根幹とも言えるのがウィンドウと、それに結びつけられたウィンドウプロシージャです。このふたつが処理単位として存在するおかげで、イベントドリブンなOSが成り立つのです。

 主要アプリケーションのほとんどは「何らかの動作を実行している時間よりも、ユーザーからの入力を待っている時間の方が長い」と言われます。これに当てはまらないのは、非常に美麗な3Dアニメーションを秒間60コマで再生するようなものくらいでしょう(これが格闘ゲームとかなら当てはまるかも)。
 こういった、特に最近のアプリケーション事情に合わせたシステムがイベントドリブンであり、そのために必要なのがウィンドウとウィンドウプロシージャと言えるのではないでしょうか。

MFCの場合
 さて、MFCの場合ですが、なんかかなりやっかいなことをしているみたいです。

 まずRun()ですが、この関数はCWinThreadのメンバ関数Run()をまねして作製しました。この関数は、ちゃんとAfxWinMain()の最後の方で呼び出されています。
 で、実際にはというと、APIコードのようなことはCWinThread::PumpMessage()で行われています。この関数はCWinThread::Run()で呼び出されています。中でしていることは、ホント同じことです。この辺は結構普通ですね。

 で、問題はウィンドウプロシージャの指定なんです。先に説明したとおり、ウィンドウプロシージャの指定はウィンドウクラスを登録するときに行います。ところが、MFCの中で登録される場合にはDefWindowProc()という関数へのポインタを指定しているのです。
 この関数は普通のAPIで、アプリケーションで特に色々することのないメッセージを自動的に処理してくれる関数です。詳細は次回説明します。APIのリファレンスも見てみてください。
 で、この関数へのポインタを指定することなんて、普通はないんです。だって、そんなアプリケーション作る意味がないんですから。
 で、MFCはどのようなことをしているのかというと、サブクラス化とフックいうシステムを使用しているみたいです。

 サブクラス化とは、ウィンドウプロシージャを入れ替えてしまうことです。この機能を使うと、送られてきたメッセージを横取りすることができます。実際にはSetWindowLong()というAPIを使用します。この関数で、ウィンドウプロシージャを入れ替えることができるのです。

 実際にSetWindowLong()が呼び出されているのは_AfxCbtFilterHook()という関数です。この関数はフックプロシージャと呼ばれるもので、まぁウィンドウプロシージャとは兄弟姉妹のような関係でしょうか。
 フックとは、特定の種類のメッセージを先取りしてしまうシステムで、「サブクラス化」に似ていますが、多くのウィンドウのメッセージを取得できるという利点があります。フックについてはシステムフックの基礎を参考にしてください。

 このフックプロシージャはスレッドローカルなフック(ローカルフック)としてインストールされます。これで、このアプリケーションに送られてくるメッセージが、まずこのフックプロシージャに送られてきます。といっても、フックのタイプがCBTフックというもので、これはウィンドウが構築されたり削除されたり移動したりフォーカスが移ったりしたときに送られるメッセージのみ、フックプロシージャに送ってもらうというものです。
 フックプロシージャの_AfxCbtFilterHook()の中では、ウィンドウが構築される直前のみ処理を行うようにしています。で、このときSetWindowLong()を使用して、ウィンドウプロシージャを置き換えているわけです。

 では、このフックがどこでインストールされているかというと、AfxHookWindowCreate()という関数です。で、この関数をたどってみるとAfxHookWindowCreate()<CWnd::CreateEx()<CWnd::Create()<CFrameWnd::LoadFrame()となります。
 この講座のフリークならもう分かったでしょう。前回前々回で、MFC任せでウィンドウを作製するときにはCFrameWnd::LoadFrame()が呼び出されると説明しましたね。実際、本当にウィンドウが構築される直前(つまりCreateWindowEx()が呼び出される直前)にフックがインストールされています。その後、ウィンドウが構築されたときにインストールされているフックプロシージャ_AfxCbtFilterHook()が呼び出され、この中でウィンドウプロシージャが置き換えられているというわけです。

 では、その置き換えられたウィンドウプロシージャがなんなのかというと、AfxGetAfxWndProc()という関数となっています。が、この関数はAfxWndProc()という関数へのポインタを呼び出すだけです。
 そう、AfxWndProc()こそが実質的なMFCにおけるウィンドウプロシージャということになります。やっとたどり着けましたね。戻り値の型や引数がWindowProc()と同じだということを確認しておいてください。

次回予告
 MFCにおけるウィンドウプロシージャは、サブクラス化とフックコンビネーションブローという見事なオチが着きました。もう笑うしかないですね。どーしてこんなことしてるのかは私には分かりません。誰か教えてください……。

 次回はウィンドウプロシージャの中身を見てみることにします。これがまた、MFCではかなり色々やっているみたいですが、まぁクラスウィザードを使って、メッセージを関数に関連づけてくれているわけですから、目頭立てるものじゃぁないのかもしれませんね。

(C)KAB-studio 1997 ALL RIGHTS RESERVED.