3.5 Active & FocusWindows の API をよく知っている方なら
であることはよくご存知と思います。 一方、VCL には ActiveControl や ActiveForm 等 Active が名の頭にが付く property が目に付きます。OnActivate イベント や SetFocus メソッド等 フォーカスやアクティブに関連しそうなメソッド/イベントも有ります。しかしこれらは Windows でいうところのフォーカスやアクティブとはかなり意味が異なっていますが、Delphi のドキュメントはこの違いを教えてくれません。 そこで、この節では VCL のフォーカス、アクティブに関するプロパティ/メソッド/イベントについて 関連するのメッセージ処理も含め解説します。 尚、以下でカタカナで記した「フォーカス」や「アクティブ」は Windows での意味で使います。より正確に言うと Primary Thread の Local Input Status を表します。また カタカナで表記したフォームはDelphi 2 では TForm または TFormの継承クラスのインスタンスを表しますが、Delphi 3 以降では TCustomForm または TCustomFormの継承クラスのインスタンスを表すこととします。Delphi 3 以降ではTCustomForm からは TForm の他に TActiveForm や TPropertyPage 等が派生していますので 単純に TForm または TForm の継承クラスのインスタンスことでは有りません。読む際に注意してください。 3.5.1 TWinControl の Focusedまず最も簡単なものからはじめましょう。 TWindControl およびその継承クラスの Focused Property はそのコントロールがフォーカスを持っているかどうかを表します。読み出し専用の property で、フォーカスを持つ場合 True になります。 Focused Property は GetFocus API の返すウィンドウハンドルとコントロールのウィンドウハンドルを比較して真偽を返しているだけで、Windows の「フォーカス」と完全に同じものです。 3.5.2 フォーム の Active Property と ActiveControl Propertyこれも比較的単純な property で フォームが アクティブかどうかを表す読み出し専用の Property です。 フォームが MDI の親/子フォームではない場合、Active Property = True は Windows でいうところのトップレベルウィンドウがアクティブになったことと同じ意味になります。つまり、フォーム自身か、フォームの子、孫...ウィンドウのいずれかがフォーカスを持つことを表します。言い方をかえるとActive Proeprty は Windows から通知される WM_ACTIVATE メッセージの WParam の内容をそのまま反映したものです。フォームが MDI 子フォームの場合は Active Property はいわゆるアクティブを表すものではなく MDI親ウィンドウが管理する Active子フォームで有ることを表します。言い方をかえるとWM_MDIACTIVATE メッセージの内容を反映したものです。つまり Active Property はMDI親フォームが記憶している「フォーカスを持つべき子フォーム(より正確に言えば、子フォーム自身、あるいはその子孫のウィンドウがフォーカスを持つべき子フォーム)」を表しています。 この場合は子フォームやその子孫のウィンドウがフォーカスを持っているとは限りません。MDI 親フォームがアクティブになったときに子フォームかその子孫がフォーカスを受け取るということです。 フォームが MDI 親フォームの場合、Active Property は常に False になります。 MDI親フォームが子フォームを持たない場合でも Active になりません。変な仕様ですが覚えておいてください。 フォームがアクティブ化する時(子フォームのアクティブ化も含む)、フォーム自身がフォーカスを持つとは限りません。VCL ではフォームがアクティブ化する際、以下のようにフォーカスを制御します。
つまりフォームの ActiveControl はフォームがアクティブになったときどのコントロールがフォーカスが受け取るのかを示す property です(FocusedControl という名にしなかったのはこの辺が理由でしょうか?)。フォーカスを持っているコントロールを表すわけでは無いので注意してください。 フォームの ActiveControl property は書き込みもできます。ただし書き込んでも、常にフォーカスが移動するわけではありません。フォームの Active Property = True の場合だけすぐにフォーカスがコントロールに移ります。ですから ActiveControl は フォームがまだそれ自身のウィンドウを作成する前の場合やフォームが非表示の場合でも設定できます。たとえばフォームの OnCreate イベントでフォーカスを得るコントロールを設定しておくことができます。後述する SetFocus メソッドではこれができません。 3.5.3 Application 変数の Active PropertyApplication 変数の Active property は アプリケーションにアクティブなトップレベルウィンドウが有るかどうかを示す読み出し専用の property で WM_ACTIVATEAPP メッセージ内容をそのまま反映したものです。 3.5.4 SetFocus メソッドSetFocus メソッドは、 ActiveControl Property とは違い即フォーカスを移動させるメソッドです。SetFocus メソッドは TWincontrol から継承されるメソッドで、コントロールがフォームの場合とそうでない場合で微妙に動きが異なります。 コントロールがフォームの場合、SetFocusメソッドを呼ぶとフォームがアクティブ化されます。フォーカスの移る先は 3.5.2 で説明したアクティブ化の際のフォームのフォーカスの制御に従います。 コントロールがフォームではない場合は 親フォームの ActiveControl Property にそのコントロールがセットされフォームがアクティブ化されます。結果的に そのコントロールがフォーカスを受け取ります。 3.5.5 Screen 変数の ActiveControl/ActiveCustomForm/ActiveForm PropertyScreen 変数のこれらの property は アプリケーション内のコントロールに届いた WM_SETFOCUS メッセージを追跡した結果の記録です。
これらの property は WM_KILLFOCUS を追跡していないため、フォーカスが他のアプリケーションに移っても変化しません。 また、前に書いたように MDI 親フォームの Active Property は決して True にはなりませんが、MDI親フォームが子フォームを持たない場合 Screen.ActiveCustomForm や Screen.ActiveForm は MDI 親フォームを指すことが有り得ます。これは これらの property が WM_SETFOCUS の受信先の記録で、フォームの Active property とは別のものだからです。 3.5.6 フォーカスの移動とイベントの関係VCL のフォーカスとアクティブに関連するイベントには
の4種類があります。 3.5.6.1 Application 変数の OnActivate/OnDeactivate イベントこのイベントは WM_ACTIVATEAPP に対応した単純なイベントです。アプリケーション内のいずれかのトップレベルウィンドウがアクティブになると OnActivate イベントが起こり、アプリケーション内の全てのトップレベルウィンドウがアクティブで無くなると OnDeactivate イベントが起こります。 3.5.6.2 Screen 変数の OnActiveControlChange/OnActiveFormChange イベントこのイベントはアプリケーション内のコントロールに届いた WM_SETFOCUS メッセージを追跡して起こされるイベントです。つまり、WM_SETFOCUS メッセージによって Screen 変数の ActiveControl Property や ActiveCustomForm Property が変化するとき OnActiveControlChange イベントや OnActiceFormChange が起こります。 繰り返しますが、これらのイベントは WM_SETFOCUS のみを追跡し、WM_KILLFOCUS は見ていないので、アプリケーションが非アクティブ状態になることを検知しません。注意してください 3.5.6.3 TWinControl の継承クラスが持つ OnEnter/OnExit イベントこのイベントもやはり WM_SETFOCUS メッセージのみを追跡してフォーカスを持つコントロールの変化からイベントを作りますが、WM_SETFOCUS を受信するコントロールを調べる範囲が「フォーム内」に限られます。つまり、フォーム内のコントロールのことしか見ていません。フォームはフォーム内の最後に WM_SETFOCUS を受信したコントロールを覚えていて、このコントロールが変化すると、以前 WM_SETFOCUS を受けたコントロールには OnExit イベントを起こし、新たに WM_SETFOCUS を受けた(フォーカスを受け取った)コントロールには OnEnter イベントが起こります。 OnEnter/OnExit は WM_SETFOCUS を受けたコントロール以外でも起こります。OnEnter/OnExit イベントは Parent Property を介して伝播するようになっています。 例えば フォーム A が パネル B とパネル C を持ち、パネル B は ボタン b1 と b2, パネル C は チェックボックス c1 を持つとします。(下図参照) 図3.5-1 OnEnter/OnExit イベント 説明図(1) 最初の状態では ボタン b1 が WM_SETFOCUS メッセージを最後に受け取ったコントロールだとすると、ボタン b2 が WM_SETFOCUS メッセージを受け取ると b1 に OnExit イベントが起こり、b2 には OnEnter イベントが起こります。パネルには何もイベントは起きません。(下図参照) 図3.5-2 OnEnter/OnExit イベント 説明図(2) しかし、最後に WM_SETFOCUS メッセージを受け取ったのが b1 で 次に c1 が WM_SETFOCUS メッセージを受け取ると b1 と B に OnExit イベントが起こり、C と c に OnEnter イベントが起こります。 図3.5-3 OnEnter/OnExit イベント 説明図(3) つまり、有るコントロールに OnEnter イベントが起きるということは、最後に WM_SETFOCUS を受けたコントロールが そのコントロールの「外」に有り、そのコントロールかその「配下」のコントロールが WM_SETFOCUS メッセージを受け取ったということを表します。OnExit はこの逆の意味になります。 繰り返しますが、OnExit/OnEnter イベントは WM_KILLFOCUS を追跡しておらず。また WM_SETFOCUS でフォーカスを受けたコントロールの追跡はフォームに閉じています。つまり各フォームは最後に WM_SETFOCUS を受けたフォーム内のコントロールを覚えています。 従って、別のフォーム内のコントロールが WM_SETFOCUS を受けても元のフォームにはイベントは起きません。またフォーカスの移動先のコントロールがたまたま移動先のフォームが覚えている最後に WM_SETFOCUS を受けたコントロールと一致する場合もフォーカスの移動先のフォームでイベントは起きません。つまり Screen 変数の OnActiveFormChange/OnActiveControlChange イベントとはことなり、OnExit/OnEnter はフォーム内の WM_SETFOCUS を受けたコントロールの変化だけを検出するのです。 以上から当然ですがフォームには OnExit/OnEnter イベントはありません (^^ 3.5.6.4 フォームの OnActivate/OnDeactivate イベントOnActivate/OnDeactivate イベント も WM_SETFOCUS メッセージを追跡することで作られます。最後に WM_SETFOCUS メッセージを受け取ったコントロールを持つフォームは Screen 変数内に保持されていて、これの変化が検出されるとフォームに OnDeactivate/OnActivate が起こります。つまりこのイベントは Screen 変数の OnActiveFormChange とほぼ同じものです。ただしフォームのイベントで有る点が違います。WM_KILLFOCUS の追跡はしていないので、アプリケーション間でフォーカスが移動してもこのイベントは起きません。 ShowModal でフォームを表示する際は少し特殊な動きになります。ShowModal では上記の WM_SETFOCUS の機構を止めて、フォーム表示直後に無条件に OnActivate イベントが起こります。またフォームが非表示になる直前に無条件に OnDeactivate イベントが起こります。従って、モーダルなフォームから別のモーダルなフォームを作っても余分なイベントが発生しないようになっています。おそらくこの方が便利だと Inprise の技術者は考えたのでしょう。 3.5.7 VCL のアクティブとフォーカスの取り扱いに関する考察3.5.7.1 アクティブとフォーカスに関係するプロパティ/イベントの分類長々と説明してきましたが、考察のためまず VCL のアクティブ&フォーカス関連の プロパティ、メソッド、分類しておきます。おおきく以下の3種類になります。
3.5.7.2 WM_KILLFOCUS を無視する理由3.5.7.1 の分類の 2 で示したプロパティ、イベントは WM_SETFOCUS メッセージに基づき、WM_KILLFOCUS は無視しています。これは VCL の大きな特徴です。なぜなのでしょうか。 私見ですが、Windows のフォーカスの考え方はアプリケーションには少し神経質過ぎる面が有ります。例えばダイアログボックス上のコントロール間でフォーカスを TAB キーで移動することを考えてみましょう。エディットコントロールからフォーカスが外れるときにエラーチェックを行ってエラーがあればフォーカスを取り返しエラーダイアログを表示するような場合、Windows のフォーカスではダイアログが表示されたときにフォーカスが外れてしまいます。しかし、VCL ではこの場合 OnExit イベントは起きませんし、エラーダイアログが閉じた後のフォーカスの移動先を ActiveControl メソッドで予約することもできます。もしこの段階で OnExit が起きるとフォーカスの細かい処理が面倒なことになるでしょう。OnExit イベントや ActiveControl property がフォームに閉じていて互いに干渉しないのはこういう時非常に便利です。 同様に ALT+TAB でアプリケーションを切り替えたとき、アプリケーションは Windows で言うところのアクティブではなくなりフォーカスも失われますが、分類2のイベントは起こりません。通常アプリケーションは自分が切り替えられたことを意識して処理を行うことが少ないですからこちらの方が好都合です。 つまり、Screen の ActiveControl/ActiveForm/ActiveCustomForm property、Screen のOnActiveControlChange/OnActiveFormeChange イベントとフォームの OnActivate/OnDeactivate イベントは アプリケーション内に閉じて処理を進めるときに便利です。またフォームの OnExit/OnEnter イベントはフォームに閉じて処理を進めるときに便利だと言えます。 この VCL のアクティブ/フォーカス関連のイベントの考え方はなかなか有用ではないでしょうか? 私は気に入ってます。 もちろんアプリケーションでは WM_KILLFOCUS を厳密に意識しなければならないことも有ります。そう言うときは分類1の Property を使うなり、メッセージや API(GetFocus, GetActiveWindow 等) を利用すればよいでしょう。 3.5.7.3 SetFocus を使うときの注意フォーカスを移動させるための方法として VCL は ActiveControl と SetFocus メソッドを提供しています。ただし SetFocus メソッドの使用にあたっては若干の注意が必要です。 これは VCL に限ったことでは有りませんが、SetFocus メソッドが使っている Windows の SetFous API を何も考えずに使うと多くの場合問題が起きます。これらの API は スレッドの Local Input Status を変更しますが スレッドがフォアグラウンドでない場合フォアグラウンドになるまで表示上の外観は変化しないからです。 言いかえるとバックグラウンドで走行しているプログラムで SetFocus メソッドを実行しても、キーボードメッセージは来ませんし、ウィンドウは前面に移動せずタイトルバーの色も変わりません。タスクバーや ALT+TAB でそのアプリケーションをフォアグラウンドにすると初めて SetFocus の効果が現れます。 しかもまずいことに、このようにフォームレベルでは効果が現れないのにもかかわらず、コントロールの外観は変化してしまいます。例えば エディットコントロールを SetFocus するとキャレットが表示されます。キーボードを受け取らないのにもかかわらずそうなるのです(下図参照)。 図3.5-4 アクティブでないのにエディットコントロールにキャレットが有る不気味なフォーム ですからSetFocus メソッドはキーボード入力関連のイベントハンドラなど、明らかにプログラムがフォアグラウンドであることが判る場合のみ使い、そうでない場合はフォームの ActiveControl property を使ってフォーカスの予約を行うようにしたほうがよいでしょう。 どうしても SetFocus を使いたい場合は プログラムがフォアグラウンドであることを確かめて使うことをお勧めします。以下の関数でフォアグラウンドかどうかを判定できます。 functiom IsForegound: Boolean; begin Result := GetCurrentThreadID = GetWindowThreadProcessId(GetForeGroundWindow, Nil) end; 3.5.8 まとめ
|