コールバック関数を使う


ちょっと凝った事をするときにWindowsAPIを直接呼び出す事はよくある事だと思いますが、この時コールバック関数と言う言葉に出くわす事があります。一体なんぞや? こんな疑問にぶち当たるかと思います。コールバック関数とは、呼び出すと、相手が逆に自分の中の関数を(必要なだけ)呼び出してくれる関数です。そのままやんけ...(^^;。というわけで、今回はコールバック関数を使ってみます。Delphi2.0J以降対応です。

コールバック関数

fig.1 EnumWindowsProcを例にした概念図
calback1.gif

コールバック関数を使うときにはWindowsに呼び出してもらいたい関数(コールバック関数)のアドレスを指定して関数をコールします。そうする事によって、Windowsが指定されたこちらの中の関数を呼び出してくれます。しかも、こちらが要求する情報がなくなるまで(中断もしようと思えば出来ますが)ひたすら呼んでくれます。コールバック関数を通してこちらが受け取る情報をどう扱うかは、プログラマの勝手です(^^)。とにかく、「プログラムが」ではなく、「Windowsが」プログラムの中の関数を呼び出すのです。これがコールバック関数と言われる所以です。たとえてみれば、「ここの住所に情報を届けておいてね。届いたら(そこに居るやつが)処理をするからどんどん届けてね」とやるわけです。

では、具体的な例でやってみましょう。よく使うコールバック関数としてEnumxxxと言うのがあります。ヘルプで"コールバック"を引いてみると、例えばEnumWindows(画面上のトップレベルウィンドウを列挙)、とかEnumFontFamilies(指定されたデバイスで、利用可能なフォントを列挙)などを見つける事が出来ます。

fig1.の例ではEnumWindowsと言うWindowsAPIの呼び出しの時に、EnumWindowsProcと言うコールバック関数を指定しています。Windowsはトップレベルウィンドウのウィンドウハンドルを次々と返してきますので、この関数の中で、自分のやりたい事を記述します。

フォームにメモコンポーネント(Memo1)をおいて、ボタン(Button1)を押した時に、Memoにウィンドウのタイトル(があるものだけ)や、クラス名を表示したいとして、例えば List1. のような感じになります。実行してみればどうなるかは分かりますが、私の所(NT4.0)では、IME関係がどばーっと出てきて、うげーです(^^)。

List.1 EnumWindowsを使った例---------------------------------------

function EnumWindowsProc(WH: hWnd; ID:LongInt):Bool; stdcall; 
var
  bufWT : array[0..70] of char;                    //ウィンドウタイトル用のバッファ
  bufCN : array[0..31] of char;                    //クラス名用のバッファ
begin
  if GetWindowText(WH, BufWT, 71) <> 0 then begin  //ウィンドウタイトルがあれば
    GetClassName(WH, BufCN, 31);                   //クラス名も取得して
    Form1.Memo1.Lines.add(BufWT+' - '+BufCN);      //メモに追加表示
  end;
  result := true;
end;

procedure TForm1.Button1Click(Sender:TObject); 
begin 
  EnumWindows(@EnumWindowsProc, 0); 
end;

-------------------------------------------------------------------

EnumWindowsProcでは、トップレベルウィンドウのウィンドウハンドルが得られます。List1.ではウィンドウハンドルからタイトルバーのテキスト及びクラス名を得るためにそれぞれGetWindowText、GetClassNameを使っています。ここでウィンドウを選別してメッセージを送ったり、いろいろな処理をさせる事が出来ます。

また、このコールバック関数はWindowsのものなので、フォームなどのメソッドではなくて、単なる関数として記述するようにしないとだめです。

Delphiでコールバック関数を使う時の注意

コールバック関数の名前(ここではEnumWindowsProc)については何でもかまいません。但し、関数によって引数の型は決まっています。ヘルプなどでは"プレースホルダ"という単語がよく使われていますが、要は「こういう引数をとる関数の型(場所)を用意してあるから使う時は適当な名前と機能をつけて使ってね」という事です。実際どんな引数をとるかはコールバック関数をヘルプで調べる事になります。

そして、Windowsから呼び出されるという事はDLLを作ったり使ったりする時と同じで、stdcall指令を付け加えないといけません。stdcallについてはヘルプへGo!(^^)

また、呼び出す時にはあくまで、コールバック関数のアドレスを指定して呼び出します。ですから上記の例で言うと"@"がついているわけです。

コールバック関数の戻り値
List1.では、Result:=true;を最後に入れています。これはコールバック関数の戻り値がfalseになるまで、または列挙するものがなくなるまで、Windowsが列挙を続けるからですが、この行を消すとどうなるでしょうか? 実験結果でしか示せませんが、Win95ではうまく動作し、NT4.0J上ではうまく動きませんでした。安直に考えると、こういう場合Win95ではデフォルトでTrue、NT4.0Jではfalseになっているのかな?と思っちゃうのですが、 このからくりをご存知の方おられましたら教えて下さいm(__)m。何々? 戻り値を書かない(書き忘れた)関数なんて、単なるコーディングミスやんか? ごもっとも...(^^)

他のアプリを操作したり、メッセージを送ったりするツールを作り出すと、FindWindow[Ex]などのAPIを使う事になってきますが、目的のアプリが複数立ち上がっている事を考慮したりすると、いろいろ面倒になってきます。おまけにコーディング量も増えます(^^)。そういった時にはEnumWindowsでやってしまうと、すっきり書けます。

その他、比較的よく使うのはフォントを扱う場合などでしょうか。DelphiではScreenオブジェクトの中のFontsプロパティに画面に表示できるフォントが入っていますが、例えば「縦書きフォントだけ」とか「日本語のフォントだけ」を取り出したいという要望はすぐに出てくると思います。MicrografxのWindowsDraw5のようにフォント選択のコンボボックスの中で、"そのフォントの字体で"フォント名を表示したい、なんて場合にも使う事が出来ます。以下に、フォントのコールバック(変な言い方ですが...)を使った例を書いておきます。と思ったらそのままずばりがBORLANDJapanのQ&Aの所にありました(^^)。これを参照して下さい。...で終わったら面白くないので何か別のサンプルを考えて載せる事にします。

Delphi同士でコールバック97/9/11追加

fig.2 よく使ってる手続き型の例
例えばApplicationオブジェクトのイベントハンドラをいじって、ヒント表示を標準のポップアップではなくてステータスバーに表示したいような場合、以下のようにします。
procedure TForm1.ShowHint;
begin
  StatusBar.Text := Application.Hint;
end;

procedure TForm1.OnCreate;
begin
  Application.OnHint := ShowHint;
end;
フォームの生成時に、アプリケーションオブジェクトのOnHintイベントハンドラをShowHintと言う独自に作った関数に結び付けています。これは直接関数のアドレスを設定しているわけではないですが、手続き型を代入するという事はそのメソッドのポインタ(アドレス)を代入しているのとほぼ同じ事と言う事で、こんな例を出しました。

変なタイトルですが、サンプルを考えて載せると書いたものの、いろいろ既に出回っていますから、DSPなどを探してもらう事にして(^^)、Delphiで作ったモノ同士で、コールバック関数を使ってみようというのをやります。

何もコールバック関数はWindowsしか持てないわけじゃあないです。要するにAPP側から関数をエクスポートするような形になる(関数を外部に公開する)だけなので、Delphiで作ったDLLとAPP同士なんかでコールバックを使う事も出来ます。それにしても、この事って本に載ってないんですね。かなり有効な手段だと思っていたのですが、皆さんあんまり使ってないのかな?それとも、手続き型を知っていれば書くほどの事でもないって事かな...?ま、それは置いといて。

APP側のコールバック関数のエクスポート部分は上でやったのと同じです。コールバックを頼むのがWindowsではなくて、DLLなのです。で、DLL側での実装をどうするかなのですが、DLLの動的ロードの方法を思い出してみて下さい。LoadLibraryした後何をしますか?そう、GetProcAddressを使って手続き型の変数のアドレスを設定してやりますね。これと同じような事をDLL側でやるわけです。何、手続き型が分からない?→ヘルプへGO!ちゃんと載っています(^^)。普段何気なく使っていると思うのですが、いざ自分で定義しようとすると迷いますね。分かります(^^)。手続き型の良く使う例としてはFig.3を参照して下さい。これは、メソッドポインタとしての使い方の例ですが、コールバック関数で使うのはもう一つのグローバル手続きポインタと言うやつです。

うだうだ書く事でもないので、さっとやります(^^)。では、DLLの中に手続き型の変数を宣言してみましょう。

var SomeProc:function (Value:integer):integer; stdcall;

これでOKですね。変数宣言の前にType節で型を定義しておいても同じ。


Type
  TSomeProc = function (Value:integer):integer; stdcall;
...
var
  SomeProc : TSomeProc;
...
この手続き型変数と言うのは要は関数のアドレス(ポインタ)ですから、使う前にその関数のアドレスを設定してやります。その仕事をやるのがコールバック登録関数になります。APPからDLLを呼ぶ時には前述のようにGetProcAddress()を使いますが、コールバックインストール関数は呼び出す関数のアドレスを引数にもらって、それを設定すればいいだけですので、コールバック関数本体は上のように宣言したとして、

function InstCallback(addr:Pointer):boolean;
begin
  @SomeProc := addr;
end;
雛型としてはこれだけなんです。あっさり(^^)。これで、DLL内のどこでもSomeProc()を呼ぶ事が出来ます。チャンチャン\(^^)/。

さてサンプルを考えてみましたが、あまり面白いものではありません(^^;。でも、重要な要素を含んでいます。APP側からコールバック関数をインストールすると、コールバック関数が、計算結果を返してきます。リストを見て下さい。また、エラー処理など一切していませんから動かす時は注意です。関数の戻り値も使っていません。

List.2 Application側(抜粋)-----------------------------------------------

//DLLから呼ばれるコールバック関数本体
function CallbackFunc(Value:integer; pStr:PChar):integer; stdcall;
begin
  Form1.Memo1.Lines.Add(pStr+' : '+IntToStr(Value));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  hDLL:THandle;
  CallbackSomeFunc:function (Addr:Pointer; V:integer):boolean;stdcall;
begin
  //DLLの使用準備
  hDLL := LoadLibrary('CBDLL.DLL');
  @CallbackSomeFunc := GetProcAddress(hDLL,'CallbackSomeFunc');

  //コールバック関数登録
  CallbackSomeFunc(@CallbackFunc, StrToInt(Edit1.Text));

  //後始末
  FreeLibrary(hDLL);
end;
-------------------------------------------------------------------
Application側ではWindowsにコールバックをお願いする時と同じようにコールバック関数本体はフォームなどのオブジェクトのメソッドとしてではなく、素のままの単なる関数として宣言しておきます。DLLは動的ロードしていますが、要点はコールバック関数の登録部分(青太表示の部分)だけです。前述のように、EditBoxの数字を引数にして渡しています。

コールバック関数本体では受け取った引数をメモに表示しています(見りゃあ分かるって)。ここで、この関数はTForm1のメソッドではないので、Form1修飾子を付けてやってます。

List.3 DLL側(抜粋)-----------------------------------------------

interface

function CallbackSomeFunc(Addr:Pointer; V:integer):
  boolean; export; stdcall;

implementation

Type
  TCBFunc = function(Value:integer; pStr:PChar):integer;stdcall;
var
  cbFunc:TCBFunc;

function CallbackSomeFunc(Addr:Pointer; V:integer):boolean; export; stdcall;
var
  i:integer;
  S:String;
begin
  result := false;
  if Addr = NIL then exit;
  //コールバック関数のアドレスを設定
  @cbFunc := Addr;
  for i := 1 to 10 do begin
    S := IntToStr(i)+'倍';
    cbFunc(V*i,PChar(S));
  end;
  result := true;
end;
-------------------------------------------------------------------
DLL側では、コールバック登録関数だけをエクスポートしています。Application側はこれを使ってコールバックを(DLLに)お願いします。コールバック登録関数も中身の要点はアドレスを設定している部分(青太表示の部分)だけです。雛形そのまんま(^^)。アドレスを設定したら、引数を計算してコールバックしています。コールバック関数はType節で型を定義して、Var節で宣言しています。

これで、Application側でEditBoxに数字を入れてボタンを押すと、ずらずらと結果が表示されます。たいてい、コールバックルーチンにする使い方としては、「DLLに探し物を頼む、見つかれば(アクションが起こると)コールバック関数を呼び出してもらう」形だと思いますが、ここでは簡単にするためにコールバック関数を登録する時についでに引数を渡して、コールバック登録関数の中で勝手にアクションを起こしています。弱いなぁ〜(^^)。

以下続く...nokana...???

まとめ


感想、改良提案、バグ報告などありましたら、げんまで。一応私のマシンで実際に走っているコードを載せていますが、このページにある内容を実行する時は各自の責任の下で行ってください。


First upload : 97/05/20(火) 05:08:05
Modified : 97/09/11(木) 15:40:36 「Delphi同士でコールバック」追加
Last Modified :