戻る ホーム 上へ

3.5 Active & Focus

Windows の 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 ではフォームがアクティブ化する際、以下のようにフォーカスを制御します。

フォームの ActiveMDIChild が Nil で無い(つまりフォームがMDI親フォームで子フォームを持つ場合)、ActiveMDIChild の示す子フォームをアクティブ化します。
子フォームを持つ MDI親フォーム以外の場合
ActiveControl Property が Nil でないなら、ActiveControl が示すウィンドウコントロールにフォーカスを移します。
フォームのActiveControl Property が Nil ならタブ順にしたがって最初のフォーム上のウィンドウコントロールにフォーカスを移します。
フォーム上にウィンドウコントロールが無ければ、フォーム自身がフォーカスを受け取ります。この場合 ActiveControl Property は Nil になります。

つまりフォームの ActiveControl はフォームがアクティブになったときどのコントロールがフォーカスが受け取るのかを示す property です(FocusedControl という名にしなかったのはこの辺が理由でしょうか?)。フォーカスを持っているコントロールを表すわけでは無いので注意してください。

フォームの ActiveControl property は書き込みもできます。ただし書き込んでも、常にフォーカスが移動するわけではありません。フォームの Active Property = True の場合だけすぐにフォーカスがコントロールに移ります。ですから ActiveControl は フォームがまだそれ自身のウィンドウを作成する前の場合やフォームが非表示の場合でも設定できます。たとえばフォームの OnCreate イベントでフォーカスを得るコントロールを設定しておくことができます。後述する SetFocus メソッドではこれができません。

3.5.3 Application 変数の Active Property

Application 変数の 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 Property

Screen 変数のこれらの property は アプリケーション内のコントロールに届いた WM_SETFOCUS メッセージを追跡した結果の記録です。

ActiveControl はアプリケーション内で最後に WM_SETFOCUS を受け取ったコントロールです。
ActiveCustomForm(Delphi 3 以降 Only) はアプリケーション内で最後に WM_SETFOCUS を受け取ったコントロールの親フォームです。
ActiveForm はアプリケーション内で最後に WM_SETFOCUS を受け取ったコントロールの親フォームで、親フォームが TForm またはその派生クラスの場合、そのフォームがこの property にセットされます。TForm またはその派生クラスではない場合 Nil がセットされます。

これらの property は WM_KILLFOCUS を追跡していないため、フォーカスが他のアプリケーションに移っても変化しません。

また、前に書いたように MDI 親フォームの Active Property は決して True にはなりませんが、MDI親フォームが子フォームを持たない場合 Screen.ActiveCustomForm や Screen.ActiveForm は MDI 親フォームを指すことが有り得ます。これは これらの property が WM_SETFOCUS の受信先の記録で、フォームの Active property とは別のものだからです。

3.5.6 フォーカスの移動とイベントの関係

VCL のフォーカスとアクティブに関連するイベントには

  1. Application 変数の OnActivate/OnDeactivate イベント
  2. Screen 変数の OnActiveControlChange/OnActiveFormChange イベント
  3. TWinControl の継承クラスが持つ OnEnter/OnExit イベント
  4. フォームの OnActivate/OnDeactivate イベント

の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 を持つとします。(下図参照)

Figure 3.5-1

図3.5-1 OnEnter/OnExit イベント 説明図(1)

最初の状態では ボタン b1 が WM_SETFOCUS メッセージを最後に受け取ったコントロールだとすると、ボタン b2 が WM_SETFOCUS メッセージを受け取ると b1 に OnExit イベントが起こり、b2 には OnEnter イベントが起こります。パネルには何もイベントは起きません。(下図参照)

Figure 3.5-2

図3.5-2 OnEnter/OnExit イベント 説明図(2)

しかし、最後に WM_SETFOCUS メッセージを受け取ったのが b1 で 次に c1 が WM_SETFOCUS メッセージを受け取ると b1 と B に OnExit イベントが起こり、C と c に OnEnter イベントが起こります。

Figure 3.5-3

図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種類になります。

  1. Windows のアクティブ、フォーカスをそのまま使っているもの
    TWinControl の Focused Property: Windows でいうところのフォーカスにそのまま対応しています。
    フォーム の Active Property: MDI 親/子Windows では無い場合、Windows でいうところのトップレベルウィンドウのアクティブにそのまま対応している。また MDI 子フォームの場合は Windows でいうところの MDI子フォームのアクティブにそのまま対応しています。
    Application 変数の Active Property: Windows でいうところのアプリケーションのアクティブにそのまま対応しています。
    Application 変数の OnActivate/OnDeactivate イベント: WM_ACTIVATEAPP メッセージで起きるイベントです。
  2. WM_SETFOCUS メッセージを受け取ったコントロールを追跡した結果を表すもの
    Screen変数の ActiveCustomForm(Delphi 3 以降 Only), ActiveForm property: WM_SETFOCUS を最後に受け取ったコントロールの親フォームを表します。WM_SETFOCUS の追跡はアプリケーション内の全てのコントロールに対して行われます。
    Screen変数の ActiveControl property: WM_SETFOCUS を最後に受け取ったコントロールを表します。WM_SETFOCUS の追跡はアプリケーション内の全てのコントロールに対して行われます。
    Screen変数の OnActiveFormChange イベント: 「WM_SETFOCUS を最後に受け取ったコントロールの親フォーム」が変わったときに起きます。WM_SETFOCUS の追跡はアプリケーション内の全てのコントロールに対して行われます。
    TCustomFormの OnActive, OnDeactivate イベント: 「WM_SETFOCUS を最後に受け取ったコントロールの親フォーム」が変わったときに起きます。WM_SETFOCUS の追跡はアプリケーション内の全てのコントロールに対して行われます。
    TWinControl の OnEnter/OnExit イベント: フォーム内の「WM_SETFOCUS を最後に受け取ったコントロール」が変化したときに起きます。WM_SETFOCUS の追跡はフォーム内コントロールに対してのみ行われます。
  3. フォーカスの予約をするもの

  4. フォームの ActiveControl property: フォームがアクティブになったときにフォーカスを受け取るコントロールを指定します。

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 するとキャレットが表示されます。キーボードを受け取らないのにもかかわらずそうなるのです(下図参照)。

Figure 3.5-4

図3.5-4 アクティブでないのにエディットコントロールにキャレットが有る不気味なフォーム

ですからSetFocus メソッドはキーボード入力関連のイベントハンドラなど、明らかにプログラムがフォアグラウンドであることが判る場合のみ使い、そうでない場合はフォームの ActiveControl property を使ってフォーカスの予約を行うようにしたほうがよいでしょう。

どうしても SetFocus を使いたい場合は プログラムがフォアグラウンドであることを確かめて使うことをお勧めします。以下の関数でフォアグラウンドかどうかを判定できます。


functiom IsForegound: Boolean;
begin
  Result := 
    GetCurrentThreadID  = GetWindowThreadProcessId(GetForeGroundWindow, Nil)
end;

3.5.8 まとめ

VCL では アクティブ/フォーカス関連の property は3種にに分けられます。
Windows のアクティブ/フォーカスをそのまま表す property
Application 変数の Active Property
フォームの Active Property
TWindcontrol の Focused Property
WM_SETFOCUS を最後に受信したコントロール、及びその親フォームを表す property
Screen変数の ActiveControl Property
Screen変数の ActiveCustomForm Property(Delphi 3 以降 Only)
Screen変数の ActiveForm Property
フォームがアクティブになったときにフォーカスを得るコントロールを予約するもの
フォームの ActiveControl Property
VCL ではアクティブ/フォーカス関連のイベントは以下の2種が有ります。
Windows のアクティブ/フォーカスの変化で起きるイベント
Application の OnActivate/OnDeactivate イベント(WM_APPACTIVATE に対応)
「WM_SETFOCUS を受信したコントロール」の変化により発生するイベント
Screen 変数の OnActiveControlChange イベント
Screen 変数の OnActiveFormChange イベント
フォームの OnExit, OnEnter イベント

戻る ホーム

inserted by FC2 system