[level4] [ウィンドウ] [ HWND ] [ HINSTANCE ] [ MSG構造体] [Win32 データ型] [コールバック関数]


コールバック関数
Callback Funciton と Callback の考え方

 

 コールバック関数は、(単なる)普通の関数です。

 コールバックルーチン、コールバックプロシージャなどと言っても同じです。

 スケルトンコードの WndProc はコールバック関数です。WndProc の手前の "CALLBACK" というキーワードが目に付きますが、"CALLBACK" 修飾子をつけるとコールバック関数になる、というのとも少し違います。

 

 Windows API を利用するプログラミングでは、プロトタイプと戻り値は 「こうこうこのように※」 して、そのような関数の「中身だけ」をかいてくれ、というケースがあります。その関数(ただし関数アドレスを)は結局あとで自分で利用することにはなります。そういう関数は、ある他の "主たる関数" の「中」で、あるいは主たる関数「から」、「繰り返し呼ばれる」 という形で利用され、「コールバック関数 : Callback Funciton」 といいます。Windows API では、WndProc (ウィンドウプロシージャ) や、ダイアログボックスのためのダイアログボックスプロシージャ もそうで、メッセージのた・び・に 「繰り返し呼ばれ」 ます。また、何かを列挙するような関数では、なにか一つ一つの要素が見つかるた・び・に 「繰り返し呼ばれる」 コールバック関数が使われる場合もあります。コールバック関数に関することは、主体の "主たる関数" が(仕様として)定めます (WndProc だけは少々例外。RegisterClass()関数 〜 WNDCLASS構造体 を参照)。

 

 ※ たとえばウィンドウプロシージャは、

LRESULT CALLBACK WindowProc( HWND, UINT, WPARAM, LPARAM );

というプロトタイプが決められています。"WindowProc" というのはいちおう固有な名前ですが、実際のプログラミングではネーミングは任意です。これは、Win32 API 関数の一つなのです。一時的に "WindowProc" という名前で述べられている Win32 API の "ウィンドウプロシージャ" の説明では、「"WindowProc"という名前は place-holder※ である」 といっています。名前は自由だからスケルトンコードでは名前を変えて、"WndProc" としています。ダイアログプロシージャの place-holder は、"DialogProc" です。"DialogProc" のプロトタイプに関しては、ウィンドウプロシージャのプロトタイプとまったく同じです。

※ 日本語では、身代わり、とでも言うのか、この place-holder という単語は(わたくしの説明に限らず)コールバック関数の登場する説明にはよく現れます。仮に名前をつけないと説明が付かないのでいちおう固有な名前を付けて説明しているのです。

 

 CALLBACK キーワード自体は、 __stdcall と同じです。<windef.h>。

 


 

[Win32 API でのコールバックの方法]

 コールバックの考え方は、C 言語の関数アドレスの活用(応用)です。Win32 API に限ったことでもありません。そして、とくになにか、例えばテキストをはじめから終わりまで文字列ポインタ :〜 char *lpstr ; のような : でスキャンして、あらかじめ用意したキーワードのセットに一致するものを、一つ一つ 「列挙」 したりするような、「列挙」 タイプの関数をつくるときには大いに活用できます。

 もちろん列挙して、それらをリスト表示していくのか、更になにかの処理を加えるのかはそれぞれの場合によります。そして、その処理を、主たる関数に直接記述してしまうのか、コールバック関数に任せるようにするかの違いです。

 Windows API (Win32 API)関数の中では、EnumWindows() とか、EnumFontFamilies() 関数も、コールバック関数を利用する関数(主たる関数)のうちです。これらの関数はどの道、システムの中のデータテーブルを一つ一つあたっているのです。そして、一つ一つなにかアイテムが発見されるたびに、「コールバック関数」 にパラメータを渡して、コールバック関数を毎回呼ぶ(が毎回呼ばれる)しくみになっています。私たちは、一つ一つのアイテムごとにこの関数が呼ばれることを 「期待」 して、このコールバック関数を記述すればいいのです。

 

 EnumWindows() 関数が使用するコールバック関数の place-holder は EnumWindowsProc()、EnumFontFamilies() のは、 EnumFontFamProc() です。

 例えば上の EnumWindows() で、この関数を使うために「コールバック関数」、たとえば MyEnumWindowsProc( 引数の並び略 ) { ...略 ; } というものを書いたとしたら、"MyEnumWindowsProc" 自体は、int i ; の "i" のように識別名として単独で使用でき、これは "MyEnumWindowsProc() 関数のアドレス" を表しています。"主たる関数" EnumWindows() のパラメータの一つに、"MyEnumWindowsProc" というこの、関数(の)アドレスを表すために使える識別子、を指定することによって、"コールバック関数としてつかう関数" を指定するような 関数の仕様 になっています。関数アドレスの応用とはこのことを言ったのです。

 ※ 関数アドレスは「値」、関数"ポインタ" は変数です。すなわち、BOOL CALLBACK (*pfn) (HWND, LPARAM) ; の pfn は、関数ポインタ(変数)です。関数ポインタはここでは出てきません。単なる参考です。関数アドレスの理解には、[インスタンス](実体) のことを少しかいつまんでおくと楽でしょう。

 

 [コールバックの方法を自分のためにつかう]

 なにかを列挙するプログラムを組む場合では、一つの関数内に switch 文などを用いて、すべての case に関するすべての処理を記述してしまうことも可能です。例えば switch 文の case : のところに 「その case(場合) の処理を書き下してしまう」 かわりに、「コールバック関数を呼ぶ」 という形にしておいて、キーワード一つ一つが検出された場合の具体的な処理は、コールバック関数の中に書くようにすると、役割分担の意味で整理が付きます。

 実際、たとえばキーワード検索の場合など、かりに後で、「検索すべきキーワード(のリスト)が増えてしまった!」 場合などは、 "主たる" 検索関数にはちょっとだけ追加するだけで、あとはコールバック関数の中を変えればいいので、比較的混乱が避けられる利点があります。ただでさえ、検索アルゴリズムだけでも複雑になってしまうのに、追加されたキーワードの個別処理も加えるとなると、2つは別の仕事(処理)にしたくなるものなのです。ごり押しで続けたって、もうそんなごちゃごちゃしたプログラムは見たくない、くらいにやりきれなくなってしまうことでしょう。

 Windows API のメッセージ処理は、コールバックのやり方を利用していますが、「メッセージ毎」 ということであって 「列挙」とは一味違います。それでも コールバックのやりかたというものが、Windows API をデザインした側とプログラマの橋渡し役としてとてもうまく働いています。

 列挙したら(列挙したものの数が)いくつになるかわからない、というときなど、C++ を使えば多少簡単にできますが、数がわからないからあらかじめどれだけのメモリを確保したらいいか決められなくて面倒くさいことがあります。そういうときは コールバックのやり方を思い出すと、「メモリなんか確保しなくてもダイレクトにやれば済むじゃないか」 なんていう場合がまま、あります。

 

[最後に]

 このあたりの概念は、実際の作業を通さず抽象的に述べるのでは説明としてかなり限界に近いです。EnumWindows() を使ってみたり、なにかテキストを上から順に走査していくようなプログラムを組むと、それだけで身につくはずの、技術です。ところが、考えてみるとウィンドウプロシージャもコールバック関数なのに、なぜか、そのメッセージ処理の部分をいろいろ書き加えて自分の時間を費やしてみてもコールバック関数にうなずけない、つまり意味がよく分かってこない、というのはおもしろいことです。

 

[トップ] [戻る]