API の方の第6章から第14章までは標準コントロールについて、また第25章から第29章まではコモンコントロールについて調べた。これらのコントロールの大半は、子ウィンドウであり、ユーザからの操作により重要な状態変化が起こると、そのことを親ウィンドウにメッセージを送って知らせていた。一方、これらのコントロールのラッパーであるVCLのクラスは、自分自身のイベントとしてユーザに知らせることができる。この仕組みはどのようになっているのだろうか?
また、コントロールを操作する際にも、アプリケーションはそのコントロール特有のメッセージを送って状態を変化させることができることが分かった。これらは、VCL のプロパティーやメソッドではどのように実現されているのだろうか。この章では、まず最初に、コントロールが親ウィンドウに送るメッセージを捕まえて、それがどのように処理されてイベントハンドラが起動されるのかについて、VCL の仕組みを調べてみよう。これが理解できるようになると、VCL のコントロールの動作の仕組みが理解できるようになるだろう。また、動作をカスタマイズしたり、新しい派生コンポーネントを作成するときに非常に役に立つとこが分かるだろう。
第2章 コントロールのメッセージ処理(その1) |
2−1 WM_COMMAND での通知メッセージを捕まえる |
2−2 親ウィンドウがフォームでないとき |
2−3 ボタンの OnClick イベント |
2−4 WM_NOTIFY での通知メッセージ |
2−5 どのコントロールがどんな通知メッセージを送るのか |
2−6 スクロールのメッセージ |
2−7 まとめ |
TButton、TCheckBox、TRadioButton、TEdit、TMemo、TListBox、TComboBox などの標準コントロールでは、ユーザからの操作による状態変化が起こると、親ウィンドウに WM_COMMAND メッセージを送ってそのことを知らせる。ここでは、TButton を例にしてこのことを確認してみよう。新規で Form1 に Button1 と Button2 と Memo1 を右図のように配置して、下記のようにメッセージ指令によって WM_COMMAND を捕まえてみよう。
List 1
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; Button2: TButton; procedure FormCreate(Sender: TObject); private { Private 宣言 } public { Public 宣言 } procedure WMCommand(var Msg:TWMCommand);message WM_COMMAND; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin Memo1.ScrollBars := ssVertical; end; procedure TForm1.WMCommand(var Msg: TWMCommand); var WC:TWinControl; begin inherited; WC := FindControl(Msg.Ctl); if WC is TButton then begin Memo1.Lines.Add(WC.Name); end; end;
実行してボタンをクリックすると右図のようになる。確かに、親である Form1 に WM_COMMAND を送っていることが分かるだろう。ダブルクリックすると、クリックが2回起こる。
コードを見てみよう。メッセージ指令を持った procedure WMCommand( ) メソッドにより、このオブジェクトの中を流れる WM_COMMAND メッセージを捕まえている。一般に、メッセージ指令によるメッセージ処理メソッドの引数は、TMessage 型のレコードである。
TMessage = packed record Msg: Cardinal; case Integer of 0: ( WParam: Longint; LParam: Longint; Result: Longint); 1: ( WParamLo: Word; WParamHi: Word; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end;
これは、もっとも一般的なメッセージの形をレコードにしたものである。これに対して、WM_COMMAND メッセージは、ヘルプによると
WM_COMMAND wNotifyCode = HIWORD(wParam); // 通知コード(メニューからは0,アクセレレータからは1) wID = LOWORD(wParam); // コントロールID (IDE が付けた16ビットの数値) hwndCtl = (HWND) lParam; // コントロールの時はそのハンドル、その他の場合はゼロ
となっている。メッセージ処理メソッドで、TMessage 型で受信すると、上記のように WParam と LParam をデコードして情報を取り出さなくてはならない。これに対して、Message.pas で定義されている同サイズのレコード TWMCommand で受けとると
TWMCommand = packed record Msg: Cardinal; // メッセージ本体。いまの場合は WM_COMMAND ItemID: Word; // コントロールのID NotifyCode: Word; // 通知コード Ctl: HWND; // コントロールのハンドル Result: Longint; // 戻り値 end;
になっているので、情報が容易に取り出せる。それではこのメソッドのコードを見てみよう。
procedure TForm1.WMCommand(var Msg: TWMCommand); var WC:TWinControl; begin inherited; WC := FindControl(Msg.Ctl); if WC is TButton then begin Memo1.Lines.Add(WC.Name); end; end;
最初に inherited; により継承メソッドを呼び出している。これをしないと OnClick イベントが起きない。このように、メッセージ処理メソッドでは、override しなくても inherited; で継承メソッドを実行できる。つぎに FindControl( ) を使って、このメッセージを送ったコントロールを見つけている。
function FindControl(Handle: HWnd): TWinControl;
この関数は、ウィンドウハンドルから VCL のコントロールを見つけて TWinControl の形で返してくれる。該当するものがないときは、nil を返す。このコードでは、メッセージの送り手が TButton クラスであることを確認してから、その名前を Memo1 に表示する。ここで、Memo1 からのメッセージを捕まえると無限ループに陥るので注意すること。Memo1 はあくまでモニタ用である。
ここで確認できたように TButton がつくるコントロールは、通知メッセージとしてクリックだけしか送らない。本来、Win32 の 'BUTTON' クラスの子ウィンドウで BS_PUSHBUTTON スタイルをもつコントロールは、このようにクリックだけを親ウィンドウに通知するのである。これに BS_NOTIFY = $4000 スタイルを追加すると
{ Button Notification Codes } const BN_CLICKED = 0; BN_DBLCLK = 5; BN_SETFOCUS = 6; BN_KILLFOCUS = 7;
のすべての通知メッセージを送ってくれるようになる。詳細は、API の 第6章 コントロール : プッシュボタンコントロール の項を見てほしい。それでは Button1 に BS_NOTIFY を追加して試してみよう。
List 2
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; Button2: TButton; procedure FormCreate(Sender: TObject); private { Private 宣言 } public { Public 宣言 } procedure WMCommand(var Msg:TWMCommand);message WM_COMMAND; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); var Style:LongInt; begin Memo1.ScrollBars := ssVertical; Style := GetWindowLong(Button1.Handle,GWL_STYLE); SetWindowLong(Button1.Handle,GWL_STYLE,Style or BS_NOTIFY); end; procedure TForm1.WMCommand(var Msg: TWMCommand); var WC:TWinControl; NCode:string; begin inherited; WC := FindControl(Msg.Ctl); if WC is TButton then begin case Msg.NotifyCode of BN_CLICKED :NCode := 'Clicked'; BN_DBLCLK :NCode := 'DblClicked'; BN_SETFOCUS :NCode := 'SetFocus'; BN_KILLFOCUS:NCode := 'KillFocus'; else NCode := 'UnKnown'; end; Memo1.Lines.Add(Format('%s : NotifyCode = %s',[WC.Name,NCode])); end; end;
実行すると右図のようになる。
FormCreate( ) で GetWindowLong( ) で Button1 のウィンドウスタイルを取り出し、それと BS_NOTIFY の論理和をとって、SetWindowLong( ) でスタイルをセットしている。結果を見るとわかるように、クリックイベントの他に、フォーカスの移動とダブルクリックも通知してくる。これに対して、Button2 では通知してくるのはやはりクリックイベントだけである。VCL の TButton には OnEnter と OnExit イベントがあって、やはりフォーカスの移動を知ることはできるが、これは上位クラスの TWinControl でもうけられており、ボタンクラスの通知メッセージとは無関係に VCL で独自に実装したものである。したがって、ウィンドウズの標準コントロールのそれとは異なった動作をすることがある。例えば、Form2 をクリックしてフォーカスを移動しても OnExit イベントは発生しないが、上記の BN_KILLFOCUS 通知メッセージはちゃんと届けられる。VCL でのフォーカス管理は、同一の Form 内での SetFocus 部分だけを追跡しているので、ウィンドウズの標準動作とは異なる動作をするのである。
デフォルトで作られる TForm1 は、コードエディタを見ると分かるように TForm の派生クラスとして作成される。したがって、IDE を使ってフォームデザイナでコンポーネントを配置して、そのイベントハンドラにコードを書く、ということは、TForm の派生クラスを作成しているのと等価である。このように、フォームデザイナとコードエディタは連携して、設計時に TForm のクラスを作成している。つまり、TForm の派生クラスだけが設計時に作成される。したがって、前節で見たように TForm の派生クラスに対しては、設計時にメッセージ指令をつかってメッセージ処理メソッドを簡単に追加することができる。もし、ボタンがパネルの上にあったらどうなるのだろうか? 通知メッセージは親であるパネルに送られるので、当然 Form1 で捕まえられなくなるはずだ。ちょっと、試してみよう。
右図のように Button2 を Panel1 の上に置いてみよう。コードは、前節の最初のものと同じである。
List 3
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; Panel1: TPanel; Button2: TButton; procedure FormCreate(Sender: TObject); private { Private 宣言 } public { Public 宣言 } procedure WMCommand(var Msg:TWMCommand);message WM_COMMAND; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin Memo1.ScrollBars := ssVertical; end; procedure TForm1.WMCommand(var Msg: TWMCommand); var WC:TWinControl; begin inherited; WC := FindControl(Msg.Ctl); if WC is TButton then begin Memo1.Lines.Add(WC.Name); end; end;
実行すると右図のようになり、たしかに Button2 からの通知メッセージを Form1 で受信することはできないことが確認できる。
では、Panel1 の中を流れるメッセージはどうやって捕まえたらよいだろうか。 Form1 の場合とはちがって設計時に派生クラスが作成されるわけではないので、簡単にメッセージ処理メソッドを追加できない。一番すっきりするのは TPanel から派生クラスを作成して、メッセージ処理ハンドラを追加して、新しいコンポーネントを作成することだ。しかし、一々そのためだけに新しくコンポーネントを作成するのは面倒である。ここでは、WindowProc を取り替えて、いわゆる「サブクラス化」によりメッセージを捕まえてみよう。上記のプログラムにコードを付け加える。
List 4
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; Panel1: TPanel; Button2: TButton; procedure FormCreate(Sender: TObject); private { Private 宣言 } public { Public 宣言 } OriginalProc:TWndMethod; procedure WMCommand(var Msg:TWMCommand);message WM_COMMAND; procedure SubclassProc(var Msg:TMessage); end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin Memo1.ScrollBars := ssVertical; OriginalProc := Panel1.WindowProc; Panel1.WindowProc := SubclassProc; end; procedure TForm1.SubclassProc(var Msg: TMessage); begin OriginalProc(Msg); case Msg.Msg of WM_COMMAND:WMCommand(TWMCommand(Msg)); end; end; procedure TForm1.WMCommand(var Msg: TWMCommand); var WC:TWinControl; begin inherited; WC := FindControl(Msg.Ctl); if WC is TButton then begin Memo1.Lines.Add(WC.Name); end; end;
実行してみると右図のように、たしかに Button2 をクリックしたときの WM_COMMAND を捕まえることができることが分かる。コードを見てみよう。
TControl の派生クラスは、WindowProc プロパティーを持っている。これには、API レベルのウィンドウ関数に相当するメソッドの関数ポインタが入っている。のちに示すように、必ずしも TWinControl の派生クラスに限らず、それ以外のクラスにもこのプロパティーがあるので、VCL のクラスのインスタンスを流れるメッセージを処理する関数へのポインタであるとした方が正しいだろう。
type TWndMethod = procedure(var Message: TMessage) of object; property WindowProc: TWndMethod;
このプロパティーに TWndMethod 型のハンドラを代入して、サブクラス化する。ここでは、FormCreate( ) でそれを行っている。入れ替えた SubclassProc( ) では、必ずもとの関数を呼び出すようにすること。ここでは最初に呼び出し、それから WM_COMMAND を捕まえて、WMCommand() に転送している。
前節までで、標準コントロールから WM_COMMAND を通じた通知メッセージが親コントロールに届いていることが TButton を例にして確かめられた。しかし、VCL では、OnClick イベントは、そのコントロールのイベントになっている。決して、親コントロールのイベントではない。TButton は自分がクリックされた時、親に WM_COMMAND を送るが、そのことを自分自身でどうやって知るのだろうか?
これについては以前 Delphi-ML に投稿したことがある。
http://www2.big.or.jp/~osamu/Delphi/delphi-browse.cgi?index=061329
以下に再録する。
[Delphi-ML:61329] Button1 を押すと、なぜ Button1Click が呼び出されるのか? HalbowTue, 16 Oct 2001 13:51:06 +0900 -------------------------------------------------------------------------------- Delphi の IDE で Form1 に Button1 を配置して、実行時に Button1 をマウスでクリックすると Button1Click( ) が実行されます。これは、どんな仕組みなのでしょうか? わたしなりに D3.1 のソースを見て、調べてみましたのでここに書かせてください。 コンポーネントパレットの Standard や Win32 のページにあるほとんどのコンポーネントは、 Win32 がOSとして供給している標準コントロールやコモンコントロールの機能を ObjectPascal のコードで包み込んで使いやすくした物です。これらは、メニューや TImageList や TLabel など を除くと、ほとんど子ウィンドウとしてのウィンドウ(コントロール)を作ります。ウィンドウに はウィンドウ関数というOSから呼び出される関数があって、これにコードを書くとことにより メッセージの処理が行われ、ボタンはボタンらしく、リストボックスはリストボックスらしく動作 します。OSから供給されているコントロールはこのウィンドウ関数が公開されていなくて、ユーザ はそれに直接コードを書くことはできません。しかし、コントロールに何か重要な出来事が起こる と、標準コントロールの場合は親ウィンドウに WM_COMMAND、コモンコントロールの場合は WM_NOTIFY を送って、そのことを知らせます。スクロールバーとトラックバーは例外で、親ウィン ドウに WM_VSCROLL か WM_HSCROLL メッセージを送ります。また、コードでコントロールを操作し たいときは、各コントロールにそれぞれ定義されているメッセージを SendMessage( ) で送信する 事により遠隔操作します。 さて、VCLを使わないで MainWindow と Button1 を作ることができますが、この場合は、 Button1 を押すと、MainWindow のウィンドウ関数に WM_COMMAND が送られてきます。 WM_COMMAND wNotifyCode = HIWORD(wParam); // notification code wID = LOWORD(wParam); // item, control, or accelerator identifier hwndCtl = (HWND) lParam; // handle of control このとき、wNotifyCode には { Button Notification Codes } const BN_CLICKED = 0; BN_DBLCLK = 5; BN_SETFOCUS = 6; BN_KILLFOCUS = 7 のうち、BN_CLICKED = 0 が設定されており、 wID には Button1 のID(16ビットの数値)、 hwndCtl には Button1 のウィンドウハンドルが設定されています。MainWindow のウィンドウ関数 ではこの WM_COMMAND を受け取って Button1 がクリックされたことを知り、その処理コードを実行 します。と、いうふうに、事態は単純です。しかし、VCL の場合は、簡単にウィンドウを作った り、イベントハンドラが書けたりするようになっていて非常に便利ですが、そのかわり VCLは裏 で大きな仕事をしています。Form1 の上の Button1 をクリックすると、Win32 の標準コンロールの 動作として、親である Form1 ウィンドウに、上記と同じように WM_COMMAND を送ります。VCLで は、ウィンドウ関数をインスタンス化するためにウィンドウ関数が MakeObjectInstance() 関数に よりMainWindowProc() になっていて、これに TMessage の形で届けられます。 { Generic window message record } PMessage = ^TMessage; TMessage = record Msg: Cardinal; case Integer of 0: ( WParam: Longint; LParam: Longint; Result: Longint); 1: ( WParamLo: Word; WParamHi: Word; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end; Form1 は MainWndProc(var Message: TMessage); で受け取って、自分の WindowProc() に WindowProc(Message); として渡します。 WindowProc は、デフォルトでは WndProc なので、 WndProc で受け取り、自分自身に Dispatch(Message) します。ここで、ObjectPascal のクラスイ ンスタンスにメッセージが渡った訳です。そして、 procedure WMCommand(var Message:TWMCommand); message WM_COMMAND; でこれを捕獲します。 TWMCommand = record Msg: Cardinal; // 今の場合は WM_COMMAND ItemID: Word; // IDE が自動的に付けたID NotifyCode: Word; // BN_CLICKED = 0; Ctl: HWND; // Button1 のハンドル Result: Longint; // 処理結果(通常ゼロ) end; 次にここで Message.Ctl を参照して FindControl( ) によりそのハンドルを持つ TWinControl を探し、見つかったらそのコントロールの Perform( ) により Perform(Msg + CN_BASE, WParam, LParam); として、ようやく Button1 の WindowProc()に CN_COMMAND が渡されます。 const CN_BASE = $BC00; CN_COMMAND = CN_BASE + WM_COMMAND; そして同じように、Button1 の WndProc() → Dispatch() → procedure CNCommand(var Message: TWMCommand); message CN_COMMAND; としてクラスインスタン スの中で捕獲されます。このメソッドの中で Message.NotifyCode が BN_CLICKED であるならば Click メソッドが呼ばれ、ついにイベントハンドラ OnClick が ( TControl.Click から )呼び 出されます。 まとめると、VCLではコントロールから親ウィンドウに SendMessage() で送られる WM_COMMAND は、親ウィンドウが CN_COMMAND として送り返すことにより、クラスインスタンスで捕獲して、イ ベントハンドラを呼び出すことができるようになっています。他のコントロールの場合も同様です。 VCLでは上記のようにメッセージの流れを追跡してゆくと非常にややこしいことになりますが、そ のことを意識しないようにうまく隠蔽することにより、とても使いやすいクラスライブラリとなって います。しかし、VCLはメッセージ処理の全てをイベントやプロパティー、メソッドとして公開し ているわけではありません。 ちょっと便利な機能を付け足すには、やはりメッセージ処理周りの知 識は必要だと思うのです。 長文失礼しました。 ------------------------------------------- Halbow halbow@cool.ne.jp http://halbow.cool.ne.jp/ -------------------------------------------
以上のように、親ウィンドウが CN_COMMAND として送り返してくることが TButton のインスタンスでイベントとして公開できることの理由である。本当にそうなっているか確かめてみよう。
List 5
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } OriginalProc:TWndMethod; procedure SubclassProc(var Msg:TMessage); end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin Memo1.ScrollBars := ssVertical; OriginalProc := Button1.WindowProc; Button1.WindowProc := SubclassProc; end; procedure TForm1.SubclassProc(var Msg: TMessage); begin OriginalProc(Msg); case Msg.Msg of CN_COMMAND:Memo1.Lines.Add('CN_COMMAND'); end; end; procedure TForm1.Button1Click(Sender: TObject); begin Memo1.Lines.Add('OnClick'); end;
実行すると右図のように、CN_COMMAND メッセージが Button1 に届いていることが確かめられる。このように、TButton の時にはあまり役に立たないが、どのコントロールの場合も自分が親に送ったメッセージが送り返されてくることを知っていることは重要である。CN_COMMAND のほかにも Controls.pas にはこのようなメッセージが定義されている。
{ VCL control notification IDs } const CN_BASE = $BC00; CN_CHARTOITEM = CN_BASE + WM_CHARTOITEM; CN_COMMAND = CN_BASE + WM_COMMAND; CN_COMPAREITEM = CN_BASE + WM_COMPAREITEM; CN_CTLCOLORBTN = CN_BASE + WM_CTLCOLORBTN; CN_CTLCOLORDLG = CN_BASE + WM_CTLCOLORDLG; CN_CTLCOLOREDIT = CN_BASE + WM_CTLCOLOREDIT; CN_CTLCOLORLISTBOX = CN_BASE + WM_CTLCOLORLISTBOX; CN_CTLCOLORMSGBOX = CN_BASE + WM_CTLCOLORMSGBOX; CN_CTLCOLORSCROLLBAR = CN_BASE + WM_CTLCOLORSCROLLBAR; CN_CTLCOLORSTATIC = CN_BASE + WM_CTLCOLORSTATIC; CN_DELETEITEM = CN_BASE + WM_DELETEITEM; CN_DRAWITEM = CN_BASE + WM_DRAWITEM; CN_HSCROLL = CN_BASE + WM_HSCROLL; CN_MEASUREITEM = CN_BASE + WM_MEASUREITEM; CN_PARENTNOTIFY = CN_BASE + WM_PARENTNOTIFY; CN_VKEYTOITEM = CN_BASE + WM_VKEYTOITEM; CN_VSCROLL = CN_BASE + WM_VSCROLL; CN_KEYDOWN = CN_BASE + WM_KEYDOWN; CN_KEYUP = CN_BASE + WM_KEYUP; CN_CHAR = CN_BASE + WM_CHAR; CN_SYSKEYDOWN = CN_BASE + WM_SYSKEYDOWN; CN_SYSCHAR = CN_BASE + WM_SYSCHAR; CN_NOTIFY = CN_BASE + WM_NOTIFY;
その他に、ウィンドウコントロール以外でも使われている VCL 独自のメッセージも定義されている。
{ VCL control message IDs } const CM_BASE = $B000; CM_ACTIVATE = CM_BASE + 0; CM_DEACTIVATE = CM_BASE + 1; CM_GOTFOCUS = CM_BASE + 2; CM_LOSTFOCUS = CM_BASE + 3; CM_CANCELMODE = CM_BASE + 4; CM_DIALOGKEY = CM_BASE + 5; CM_DIALOGCHAR = CM_BASE + 6; CM_FOCUSCHANGED = CM_BASE + 7; CM_PARENTFONTCHANGED = CM_BASE + 8; CM_PARENTCOLORCHANGED = CM_BASE + 9; CM_HITTEST = CM_BASE + 10; CM_VISIBLECHANGED = CM_BASE + 11; CM_ENABLEDCHANGED = CM_BASE + 12; CM_COLORCHANGED = CM_BASE + 13; CM_FONTCHANGED = CM_BASE + 14; CM_CURSORCHANGED = CM_BASE + 15; CM_CTL3DCHANGED = CM_BASE + 16; CM_PARENTCTL3DCHANGED = CM_BASE + 17; CM_TEXTCHANGED = CM_BASE + 18; CM_MOUSEENTER = CM_BASE + 19; CM_MOUSELEAVE = CM_BASE + 20; CM_MENUCHANGED = CM_BASE + 21; CM_APPKEYDOWN = CM_BASE + 22; CM_APPSYSCOMMAND = CM_BASE + 23; CM_BUTTONPRESSED = CM_BASE + 24; CM_SHOWINGCHANGED = CM_BASE + 25; CM_ENTER = CM_BASE + 26; CM_EXIT = CM_BASE + 27; CM_DESIGNHITTEST = CM_BASE + 28; CM_ICONCHANGED = CM_BASE + 29; CM_WANTSPECIALKEY = CM_BASE + 30; CM_INVOKEHELP = CM_BASE + 31; CM_WINDOWHOOK = CM_BASE + 32; CM_RELEASE = CM_BASE + 33; CM_SHOWHINTCHANGED = CM_BASE + 34; CM_PARENTSHOWHINTCHANGED = CM_BASE + 35; CM_SYSCOLORCHANGE = CM_BASE + 36; CM_WININICHANGE = CM_BASE + 37; CM_FONTCHANGE = CM_BASE + 38; CM_TIMECHANGE = CM_BASE + 39; CM_TABSTOPCHANGED = CM_BASE + 40; CM_UIACTIVATE = CM_BASE + 41; CM_UIDEACTIVATE = CM_BASE + 42; CM_DOCWINDOWACTIVATE = CM_BASE + 43; CM_CONTROLLISTCHANGE = CM_BASE + 44; CM_GETDATALINK = CM_BASE + 45; CM_CHILDKEY = CM_BASE + 46; CM_DRAG = CM_BASE + 47; CM_HINTSHOW = CM_BASE + 48; CM_DIALOGHANDLE = CM_BASE + 49; CM_ISTOOLCONTROL = CM_BASE + 50; CM_RECREATEWND = CM_BASE + 51; CM_INVALIDATE = CM_BASE + 52; CM_SYSFONTCHANGED = CM_BASE + 53; CM_CONTROLCHANGE = CM_BASE + 54; CM_CHANGED = CM_BASE + 55; CM_DOCKCLIENT = CM_BASE + 56; CM_UNDOCKCLIENT = CM_BASE + 57; CM_FLOAT = CM_BASE + 58; CM_BORDERCHANGED = CM_BASE + 59; CM_BIDIMODECHANGED = CM_BASE + 60; CM_PARENTBIDIMODECHANGED = CM_BASE + 61; CM_ALLCHILDRENFLIPPED = CM_BASE + 62; CM_ACTIONUPDATE = CM_BASE + 63; CM_ACTIONEXECUTE = CM_BASE + 64; CM_HINTSHOWPAUSE = CM_BASE + 65; CM_DOCKNOTIFICATION = CM_BASE + 66; CM_MOUSEWHEEL = CM_BASE + 67;
これがどのような時に送られてくるかは、ソースを調べると簡単に分かるときもあるし、コントロールをサブクラス化して確かめることもできる。コンポーネントをつくるときにはこれらの知識が非常に役に立つ。
標準コントロールが親ウィンドウに WM_COMMAND とおして通知メッセージを送るのに対し、コモンコントロールでは WM_NOTIFY メッセージを送る。ここでは、このことを確かめよう。簡単な例としてステータスバーを取りあげよう。
List 6
type TForm1 = class(TForm) StatusBar1: TStatusBar; Memo1: TMemo; Button1: TButton; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } procedure WMNotify(var Msg:TWMNotify);message WM_NOTIFY; end; var Form1: TForm1; implementation {$R *.DFM} uses Commctrl; procedure TForm1.FormCreate(Sender: TObject); var SP:TStatusPanel; i:integer; begin StatusBar1.SimpleText := 'This is SimpleText'; for i := 0 to 3 do begin SP := StatusBar1.Panels.Add; SP.Width := 100; SP.Text := Format('Panel : %d',[i]); end; Memo1.ScrollBars := ssVertical; end; procedure TForm1.WMNotify(var Msg:TWMNotify); var pNMM:PNMMouse; WC:TWinControl; hWindow:HWND; s:string; Section:integer; begin inherited; pNMM := PNMMouse(Msg.NMHdr); case pNMM^.hdr.code of NM_CLICK :s := 'Click'; NM_DBLCLK :s := 'DblClick'; NM_RCLICK :s := 'RClick'; NM_RDBLCLK :s := 'RDblClick'; SBN_SIMPLEMODECHANGE:s := 'SimpleChange'; else s := 'UnKnown'; end; hWindow := pNMM^.hdr.hwndFrom; WC := FindControl(hWindow); if s = 'SimpleChange' then begin if SendMessage(hWindow,SB_ISSIMPLE,0,0) <> 0 then s := 'from '+WC.Name+' :'+s+' : SimpleMode' else s := 'from '+WC.Name+' :'+s+' : NotSimpleMode'; end else begin Section := pNMM^.dwItemSpec; s := Format('from %s : %s at Section %d',[WC.Name,s,Section]); end; Memo1.Lines.Add(s); end; procedure TForm1.Button1Click(Sender: TObject); begin StatusBar1.SimplePanel := not StatusBar1.SimplePanel; end;
実行すると、右図のようになる。
WM_COMMAND より WM_NOTIFY のほうが情報量が多く、それをデコードして取り出すのはすこしややこしい。
TWMNotify = packed record Msg: Cardinal; // いまの場合は WM_NOTIFY IDCtrl: Longint; // このメッセージを送ったコントロールのID NMHdr: PNMHdr; // TNMHdr へのポインタ(以下を参照) Result: Longint; // 処理結果(通常ゼロ) end;
もっとも簡単な WM_NOTIFY メッセージでは NMHdr 部分は次のようなレコード型へのポインタである。
type PNMHdr = ^TNMHdr; tagNMHDR = packed record hwndFrom: HWND; // コントロールのウィンドウハンドル idFrom: UINT; // このメッセージを送ったコントロールのID code: Integer; // NM_ code で示される通知コード end; TNMHdr = tagNMHDR; NMHDR = tagNMHDR;
ここで最も重要なのは、出来事の内容を示す code の部分である。ステータスバーの場合は Commctrl.pas に以下のように定数が定義されている。
const NM_FIRST = 0- 0; { generic to all controls } SBN_FIRST = 0-880; NM_CLICK = NM_FIRST-2; NM_DBLCLK = NM_FIRST-3; NM_RCLICK = NM_FIRST-5; NM_RDBLCLK = NM_FIRST-6; SBN_SIMPLEMODECHANGE = SBN_FIRST - 0;
これでコントロールにどんな出来事が起こったのかが分かる。しかしさらにやっかいなのは、各々の出来事によって TWMNotify の NMHdr の部分の情報が異なる点である。ステータスバーの場合は、例えば NM_CLICK のとき、
NM_CLICK lpnm = (LPNMMOUSE) lParam;
となっていて、NMHdr の部分が以下のようなレコード型へのポインタであることが分かる。
type tagNMMOUSE = packed record hdr: TNMHdr; dwItemSpec: DWORD; dwItemData: DWORD; pt: TPoint; dwHitInfo: DWORD; end; PNMMouse = ^TNMMouse; TNMMouse = tagNMMOUSE;
このように、NMHdr の部分を読み替えるレコード型の最初のフィールドは必ず TNMHdr 型になっていて、どの通知メッセージでも TWMNotify の NMHdr の部分は PNMHdr として読みとれるようになっている。さらに Statusbar では、このレコードの dwItemSpec にどの Part をクリックしたかのインデックスが入っている。SimpleText のときは -2 が入っているようだ。これは他のクリック通知のときも同じである。最後の通知メッセージ SBN_SIMPLEMODECHANGE については追加情報はない。現在のモードについては、SB_ISSIMPLE メッセージを送って問い合わせることができる。
この章の前節までで WM_COMMAND と WM_NOTIFY メッセージを通じてコントロールから親ウィンドウに送られる通知メッセージの取得方法が分かっただろう。しかし、このような通知メッセージを取得することが、どんなことに役に立つのだろうか?
VCL の TButton では、OnDblClick に相当するイベントはないが、WM_COMMAND を捕まえるとそのタイミングを知ることができる。うーむ、これはあまり役に立ちそうもなく、たいしてうれしくない。TStatusBar には、OnClick や OnDblClick イベントがあるが、これらは TControl から引き継いだものなので、どのパートをクリックしたのか分からない。WM_NOTIFY からの情報ではこれを知ることができる。また、SimpleText の true/false の切り替えのタイミングについてもイベントで知ることができる。OnMouseDown などのイベントハンドラでは、マウスカーソルの位置がわかるので、これと SB_GETRECT を送って取得できる各パートの Rect を比較するとどのパートがクリックされたのか知ることができるが、面倒である。TButton のような単純なコントロールのときはあまり役に立たないが、コントロールからの通知メッセージを取得できれば、VCL がそのコードでラップし忘れた情報を取得できるので、ある場合には非常に役に立つ。VCL では、コントロールの通知メッセージに対応するイベントをすべて備えているわけではないのである。
そこで重要なのは、どのコントロールがどのような通知メッセージを送るのかを知ることである。この情報は、API のヘルプだけでは不足しているので、筆者の場合は、SDK を参照するか、WEB で MSDN を見ることになる。SDK は、core の部分だけであれば、簡単にダウンロードできる。2002/11/04 現在では、以下の URL からダウンロードできる。
http://www.microsoft.com/msdownload/platformsdk/sdkupdate/SDKInfo.htmまた、WEB から情報を得るには、
http://msdn.microsoft.com/default.aspからメニューを選んで
MSDN Home > MSDN Library > User Interface Design and Development > Windows Controls > Individual Control Information
を辿って行く。これらの情報と、VCL の対応するコントロールのクラスのイベントを見比べてみると、VCL が提供するイベントが通知メッセージの大半をラップしているのであるが、かなり重要と思われるものでラップし忘れているものも結構あることに気づくだろう。VCL のユーザとしても、コントロールのメッセージ処理の詳細を理解できれば、プログラミングコードを書くための幅が広がるであろう。
TScrollBox などのそれ専用のコントロール以外では、VCL はスクロールメッセージをほとんど無視していてイベントに採用していない。TForm はもちろん、TListBox、TMemo、TTreeView、TListView、TRichEdit、TStringGrid などの標準スクロールバーを表示するコントロールでもスクロールに関係したイベントはまったく用意されていない。標準スクロールバーは入力専用のコントロールであり、取り付けられている親ウィンドウに WM_VSCROLL または WM_HSCROLL メッセージを送る。標準スクロールバーのほかに、スクロールバーコントロール TScrollBar や、TUpDown、TTrackBar などは同様のメッセージを親ウィンドウに送る。ここでは、まず、TForm について WM_VSCROLL メッセージを捕まえてみよう。各コントロールのスクロールメッセージについては、次章以降で調べることにする。
右図のように配置して以下のコードを書く。
List 7
type TForm1 = class(TForm) Memo1: TMemo; Button1: TButton; Label1: TLabel; procedure FormCreate(Sender: TObject); procedure Button1Click(Sender: TObject); private { Private 宣言 } public { Public 宣言 } procedure WMVScroll(var Msg:TWMScroll);message WM_VSCROLL; end; var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.FormCreate(Sender: TObject); begin Memo1.ScrollBars := ssVertical; Label1.Top := 800; end; procedure TForm1.WMVScroll(var Msg: TWMScroll); var s:string; begin inherited; case Msg.ScrollCode of SB_LINEUP :s := 'LINEUP'; SB_LINEDOWN :s := 'LINEDOWN'; SB_PAGEUP :s := 'PAGEUP'; SB_PAGEDOWN :s := 'PAGEDOWN'; SB_THUMBPOSITION :s := 'THUMBPOSITION'; SB_THUMBTRACK :s := 'THUMBTRACK'; SB_TOP :s := 'TOP'; SB_BOTTOM :s := 'BOTTOM'; SB_ENDSCROLL :s := 'ENDSCROLL'; else s := 'UNKNOWN'; end; Memo1.Lines.Add(Format('%s : Pos = %d',[s,Msg.Pos])); end; procedure TForm1.Button1Click(Sender: TObject); begin Memo1.Clear; end;
実行すると右図のようになる。いろいろ試してみて欲しい。
WM_VSCROLL メッセージについて API のヘルプで調べると
WM_VSCROLL nScrollCode = (int) LOWORD(wParam); // scroll bar value nPos = (short int) HIWORD(wParam); // scroll box position hwndScrollBar = (HWND) lParam; // handle of scroll bar
ということになっているが、Messages.pas に宣言されている
TWMScroll = packed record Msg: Cardinal; ScrollCode: Smallint; { SB_xxxx } Pos: Smallint; ScrollBar: HWND; Result: Longint; end; TWMHScroll = TWMScroll; TWMVScroll = TWMScroll;
をつかうと、デコードが簡単である。 ScrollCode では、スクロールバーをユーザがどのように操作したかを以下の数値で知らせる。TForm のウィンドウ関数は、このメッセージにしたがってウィンドウの内容をスクロールするわけだ。
const { Scroll Bar Commands } SB_LINEUP = 0; SB_LINELEFT = 0; SB_LINEDOWN = 1; SB_LINERIGHT = 1; SB_PAGEUP = 2; SB_PAGELEFT = 2; SB_PAGEDOWN = 3; SB_PAGERIGHT = 3; SB_THUMBPOSITION = 4; SB_THUMBTRACK = 5; SB_TOP = 6; SB_LEFT = 6; SB_BOTTOM = 7; SB_RIGHT = 7; SB_ENDSCROLL = 8;
同様な定義が二通りあるのは、垂直と水平スクロールに対応しているからである。Pos は、これが SB_THUMBPOSITION or SB_THUMBTRACK のときにつまみ(サム:Thumb)の位置を示している。以上のように、スクロール通知メッセージを捕まえられると、スクロールイベントは簡単に公開できるようになるだろう。また、Form にこれらのスクロールメッセージを送ると、コードでスクロールさせることができる。
ユーザが何らかの働きかけを行ったりシステムから通知があったりして、コントロールに重要な変化が起きると VCL のイベントとして通知される。イベントハンドラにコードを書いてプログラムを作成することは本質的には、イベントハンドラのコードでメッセージ処理をしていることと同じであるから、メッセージについて知っておくことは VCL のユーザとしても重要である。本章では、おもにコントロールがどのようにしてメッセージを送受信しているかを調べ、とくに WM_COMMAND と WM_NOTIFY メッセージを通じて親コントロールに送る通知メッセージを取得し、処理する方法について調べた。VCL のイベントとしてラップされていない通知メッセージがかなりあることも分かった。次章以降で各コントロールごとに個別に調べてゆくことにする。