戻る ホーム 上へ 進む

3.3 キーボードメッセージの処理

Delphi でプログラミングをしていると最初はあまり気にならないのですが、ある程度使い込んでゆくと Delphi で作ったアプリケーションのキーボードメッセージの処理が気になってくるはずです。例えば TEdit コントロールをフォームに貼り付けて、OnKeyDown ハンドラで TAB キーを捕まえて処理しようと思ったのに、TAB キーが捕まらないとか、ALT キーをポンと押したら突然メインフォームが前面に表示されるとか、VCL が裏でいったい何をやっているのか判らずにいらいらすることがあります。本節では、こうしたもやもやを晴らし、アプリケーションやコントロールが正しく書けるようにするために、Delphi のキーボードメッセージの処理に対するポリシーを説明し、また、キーボードの処理の詳細を解説します。

3.3.1 VCL がキーボードメッセージに対して行っていること

VCL のメッセージ処理は単純では有りません。VCL は単純にキーボードメッセージをコントロールに届けるのではなく、かなり複雑な処理を行っています。VCL の提供するキーボードに関する機能は以下の通りです。

  1. メニュー関連のメッセージのメインフォームへの転送。
  2. ダイアログボックスのシミュレート
    特殊キー(Return, ESC, 矢印、TAB、 Ctrl+Break)の処理
    コントロールのニーモニックの処理
  3. ショートカット

3.3.1.1 メニュー関連のメッセージのメインフォームへの転送

これは私が勝手に考えたことですが VCL が想定するアプリケーションの形態は大きく分けて3種あると思われます。MDI アプリケーション、MDI ライクなアプリケーションとMDI ライクでないアプリケーションです(これらの名前は私の勝手な造語です)。MDI アプリケーションは説明不要と思われますが一個のメインフォームと子フォームからなるアプリケーションです。MDI ライクなアプリケーションとは、複数のフォームからなり、メインフォーム以外のフォームがメニューを持たないようなアプリケーションです。Delphi の IDE のようなアプリケーションと考えていいでしょう。MDI ライクでないアプリケーションとは、それぞれのフォームがメニューを持つアプリケーションです。

MDI ライクなアプリケーションとは、メインフォームでないフォームのメインメニューコンポーネントの AutoMerge Property を True にしたアプリケーションなのですが、こうするとメインフォームでないフォームのメニューがメインフォームのメニューにマージされるだけではなく、メニューに関するメッセージの流れも変わります(重要!)。

試しに、フォームを2個持つプログラム作ってみてください。両方にメインメニューコンポーネントを貼り付け、適当なメニューを作ってメインフォームでない方のメインメニューコンポーネントの AutoMerge Property を True にしてください。プログラムを実行してメインフォームでない方のフォームをアクティブにし、ALT キーを叩いてみてください。メインフォームがアクティブになり、適当なキーを押すと、元のフォームがアクティブになるはずです。Delphi の IDE でも同じ事が起きます。オブジェクトインスペクタで作業中に ALT+S を押すとメインフォームの検索メニューがドロップダウンします。ここで ESC を2回押せばオブジェクトインスペクタが再びアクティブになったはずです。

今度は先ほど作ったプログラムで、AutoMerge Property を False に変えて見てください。今度は ALT を叩いてもメインフォームはアクティブにならなかったはずです。

このように、メニューの操作に関連するキーを検出してアクティブウィンドウを切り替え、メッセージをメインフォームに送り込んでいるのは VCL です。このメッセージの流れの変化はアプリケーションの使い勝手に大きく影響するのは明らかでしょう。プログラム作成時には十分に考慮すべき事項です。

3.3.1.2 ダイアログボックスのシミュレート

Delphi を使いはじめてすぐに気が付くのは、フォームがダイアログボックスに似ているという点です。例えばコントロール間を TAB キーや矢印キーで移動できます。TEdit コントロールにフォーカスが有るときに Return(Enter)キーを押すと Default Property = True の TBitton コントロールが押されます。ESC キーを押すと Cancel Property = True の TButton コントロールが押されます。ボタンのキャプションがが下線が引かれた文字(ニーモニック)を持つ場合、その文字をキーボードから入力するとボタンが押されます。

こうした機能は本来 Windows のダイアログボックスウィンドウとメッセージループ中の IsDialog API が提供している機能で、普通の単なるウィンドウはこうした機能を持っていません。ダイアログボックスウィンドウは CreateDialog や DialogBox API で作成される特別なウィンドウですが、フォームはこの API で作成されません。このため、VCL はメッセージループ中の 「キーボードメッセージ処理」(TApplication の IsKeyMsg メソッド)と VCL 中の各種のキーボードメッセージ処理が連携してダイアログボックスをシミュレートしています。VCL はダイアログボックスリソースを使うことなく、ダイアログボックスのようなウィンドウ(フォーム)を提供します。

3.3.1.3 ショートカット

VCL のキーボード処理の非常に重要な処理としてショートカット(アクセラレータ)の処理があります。VCL はアクセラレータリソースを使うことなくショートカットを実現しています。Delphi でプログラミングするにはショートカットの理解が非常に重要です。

通常の Windows のプログラミングでは、ショートカットはアクセラレータリソースとしてメニューとは独立に管理されます。一方、Delphi のプログラミングでは、ショートカットはメインメニューとポップアップメニューのショートカットとして整理されています。VCL の提供するショートカットの機能は独特であり強力です。

Delphi ではフォーム毎にショートカットを定義でき、これらはメインメニューと、フォームに関連付けられたポップアップメニューで定義されます。

アプリケーション全体で共通のショートカットはメインメニューのショートカットで定義します。

コントロールやコントロールのコンテナのショートカットはそれらに関連付けられたポップアップメニューで定義されます。

これだけでは判らないと思うので、ショートカットの探索ロジックを紹介しましょう。

まず、キーが押されると、VCL はフォーカスを持つコントロールに関連付けられたポップアップメニューの全てのメニュー項目の ShortCut Property をチェックします。もし押されたキーに該当するショートカットがあれば、それのイベントハンドラが実行されます。

もしショートカットが見つからなければ、そのコントロールの Parent property の指すコントロール(コンテナ)に関連付けられたポップアップメニューを調べ、ショートカットが見つかればそれのイベントハンドラが実行されます。

以上を再帰的に繰り返してフォームに到達し、フォームのポップアップメニューにもショートカットが見つからないと、フォームのメインメニューの全メニュー項目の ShortCut Property が調べられ、該当するショートカットが有れば、それのイベントハンドラが実行されます。

最後に、これは非常に重要な点ですが、フォームのメインメニューにもショートカットが見つからなければ、メインフォームのメインメニューのショートカットが調べられます(下図参照)。このロジックはポップアップメニューの AutoPopup property とは無関係に動くことに注意してください(重要!)。

Figure 3.3-1

図3.3-1 ショートカットの探索ルート

もうお判りかと思いますが、Delphi で作成したプログラムのショートカットは上記の様に階層構造をなしており、フォーカスを持っているコントロールが変わるとショートカットの探索ルートが動的に変化します。

VCL のコントロールに関連付けられたポップアップメニューはショートカットと同様に階層構造を持ちます。つまり、フォームにポップアップメニューを関連付け、フォーム上のコントロールにポップアップメニューを関連付けなければ、コントロールのポップアップメニューはフォームのものが使われます。つまり、ポップアップメニューの階層構造とショートカットの階層構造が連動しているわけです。ポップアップメニューは AutoPopup property を False にすれば、右クリックで表示されませんから、プログラマはメニューに現れないショートカットを定義することができます。

VCL のショートカットの階層構造を使って、アプリケーション共通のショートカットやフォーム固有のショートカットを定義でき、コントロール固有のローカルなショートカットを定義できます。この機能は Windows が提供する単純でフラットなキーボードアクセラレータよりもはるかにパワフルで柔軟です。

3.3.2 説明の前に

そろそろ細かな説明に入って行きますが、その前に説明を読むための若干の基礎知識や前提条件をお話します。

3.3.2.1 ニーモニック、ショートカット

ニーモニックとはメニューやコントロールのキャプション等で下線が引かれた文字のことで、ショートカットの一種として働きます。しかし、用語の混乱を避けるため、以後の説明ではショートカットはアクセラレータを差すこととし、ニーモニックとは区別することにします。都合のいいことに、VCL の Shortcut Property は常にアクセラレータ(ショートカット)を意味します。

Figure 3.3-2

図3.3-2 メニューのニーモニック、アクセラレータ、コントロールのニーモニック

3.3.2.2 VCL Control Message/VCL Control Notification Message

VCL はさまざまな内部処理のために、VCL Control Message と VCL Control Notification Message を使います。これらは、Control Unit 内で定義されています。VCL Control Message は メッセージ番号が CM_ で始まるメッセージで、VCL Control Notification Message は メッセージ番号が CN_ で始まるメッセージです。面倒なので、これらを Control Message と呼ぶことにします。Control Message を使うのは、メッセージ処理を複数のコントロールで分散して より OOP 的に行うためです。VCL ではコントロールのメソッドを直接呼び出さずにメッセージで処理を依頼する手法がよく使われています。 メッセージをより高位のウィンドウに通知してアプリケーション全体に関わるような処理を依頼したり、逆に高位のコントロールが配下のコントロールにメッセージをブロードキャストして、各コントロールに情報を伝達したり、コントロール毎の細かな処理を依頼したりします。

キーボードメッセージで重要な Control Message は

CM_APPKEYDOWN, CM_APPSYSCOMMAND, CM_DIALOGKEY, CM_DIALOGCHAR, CM_WANTSPECIALKEY, CM_CHILDKEY, CN_KEYDOWN, CN_KEYUP, CN_CHAR, CN_SYSKEYDOWN, CN_SYSCHAR

の12個です

また、 WM_GETDLGCODE と WM_SYSCOMMAND の処理が重要な役割を果たします。

3.3.2.3 メッセージループ

キーボードメッセージ処理の中心はもちろんメッセージループです。以下に 3.2 で説明したメッセージループの図を再掲します。

Figure 3.2-4

図3.2-4 メッセージループ(再掲)

この中で、メッセージはいくつかの TApplication のメソッドで処理されるわけですが、「OnMessage ハンドラの呼び出し」はユーザ定義のハンドラの呼び出しですし、「ヒントメッセージ処理」はキーボードメッセージ処理に影響を与えず、常に「処理せず」を返します。また「ダイアログ関連メッセージ処理」は、Windows 本来のダイアログボックスを VCL が使う時の処理で、フォーム向けのキーボードメッセージの処理では常に「処理せず」を返します。

従って、以降の説明(3.3節)では、「MDI アクセラレータの処理」と「キーボードメッセージ処理」(TApplication の IsKeyMsg メソッド)および TranslateMessage/DiapcthMessage API のみを解説し、他は無いものとして無視します。

3.3.3 IsKeyMsg

ここから本格的にキーボードメッセージ処理の核心に入っていきます。

キーボードメッセージのさまざまな処理を行うために呼ばれるのは、図3.2.4 のメッセージループ中の「キーボードメッセージの処理」(IsKeyMsg メソッド)です。

IsKeyMsg メソッドの処理は簡単で、キーボードのメッセージ(WM_KEYFIRST から WM_KEYLAST までのメッセージ)を対応する Control Message に直し、メッセージのウィンドウハンドルの示すウィンドウコントロールに SendMessage API で送ります。メッセージ番号の変換は、元のメッセージ番号に CN_BASE($B000) を加えるだけの簡単なものです。詳細は Forms.pas を見てください。

IsKeyMsg は SendMessage API の戻り値が非ゼロなら、キーボードメッセージは処理済みだとみなし、メッセージループに True を返します。この場合、メッセージループは、TranslateMessage や DispatchMessage をスキップします。以後「処理済みにする」とはこの SendMessage API の戻り値が 非ゼロになるようにメッセージを処理することを意味することとします。

3.3.4 CN_KEYDOWN メッセージの処理

キーを押した時、通常は、メッセージループに WM_KEYDOWN が届きます。IsKeyMsg はこれを CN_KEYDOWN に変換してコントロールに届けます。

CN_KEYDOWN を受け取るのは必ず TWinControl.CNKeyDown メソッドです。

3.3.4.1 アクセラレータ(ショートカット)の処理

CNKeyDown はまず最初に、アクセラレータ(ショートカット)を調べるため IsMenuKey メソッドを呼びます。

IsMenuKey はコントロールにポップアップメニューがないか調べ、有ればメニュー項目の Shortcut Property を調べて該当するショートカットが有れば、それにむすびつけられた処理を実行します。もし無ければ、さらに親のコントロールのメニューのショートカットを再帰的に調べてゆきます。どこかでショートカットが有れば、メッセージは「処理済み」になります。ショートカットが有ればそれに関連付けられた処理が無くても「処理済み」になることに注意してください。

IsMenuKey はポップアップメニューのショートカットを発見できなければ、今度はコントロールの属するフォームのメニューのショートカットを調べます。もし有れば該当処理を実行し、メッセージを「処理済み」にします。

IsMenuKey はフォームのメニューでもショートカットを発見できなければ、アプリケーションウィンドウに CM_APPKEYDOWN メッセージを送り処理を依頼します。アプリケーションウィンドウはメインフォームのメニューのショートカットを調べ、該当するショートカットが有れば該当処理を実行しメッセージを「処理済」にします。

Figure 3.3-3

図3.3-3 アクセラレータ(ショートカット)の処理

以上のように、アクセラレータ(ショートカット)の探索はアプリケーションウィンドウを中継点にして、フォームを越えて行われることに注意してください。また、メインメニューのショートカットのチェックはメインメニューの AutoMerge property が True の場合、Merge 後のイメージでチェックされることに注意してください。Merge で隠されたメニュー項目のショートカットは無視されます。

ショートカットが発見されると、CNKeyDown メソッドは非ゼロを返すため、3.3.3 で述べたように、WM_KEYDOWN メッセージはコントロールには渡りません。

CNKeyDown はショートカットが見つからないと、次に、CM_CHILDKEY を自分自身に送ります。TWinControl.CMChildkey はこのメッセージをそのまま親のコントロールに送るのでほとんどの場合フォームまで到達し未処理のまま戻ってきます。このメッセージをトラップしているのは TDBCtrlGrid コントロールだけです。このメッセージの意味はまだ調査中です。

3.3.4.2 特殊キーの処理

CNKeyDown メソッドは、CM_CHILDKEY の処理後、特殊キーの処理に進みます。ここでいう特殊キーとは以下のキーのことです。

仮想キーコード対応するキー
VK_TABTAB キー
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN上下左右の矢印キー
VK_RETURNReturn 、又は Enter キー
VK_EXECUTE実行キー(このキーを実装したキーボードは無い)
VK_ESCAPEESC キー
VK_CANCELCtrl+Break

CNKeyDown メソッドはメッセージ中の仮想キーが上記の値では無い場合、直ちにリターンします。この場合、コントロールには WM_KEYDOWN メッセージが送られることになります。メッセージ中の仮想キーが上記のどれかである場合、フォームがダイアログボックスをシミュレートするため、WM_KEYDOWN メッセージをコントロールに渡すか、CN_KEYDOWN メッセージをフォームで処理するかを決めなくてはなりません。

CNKeyDown メソッドは2回に分けて自分自身にメッセージを送り、特殊キーをコントロールが受け取るかを尋ねます。つまり、キーが欲しいと答えないとコントロールには WM_KEYDOWN メッセージが送られません。尋ねるために送られるメッセージは CM_WANTSPECIALKEY と WM_GETDIALOGCODE です。

前者は VCL の Control Message です。WParam に CNKeyDown の 仮想キーコードをセットするので、コントロールは仮想キーコード毎にキーが欲しいかどうかを返せます。コントロールがこのメッセージの戻り値を非ゼロにすれば、そのキーの WM_KEYDOWN メッセージが欲しいことになります(WM_KEYUP の時は意味が逆になります。後述)。

後者は、本来 Windows のダイアログボックスがコントロールに送る SDK の Reference に載っているメッセージで、やはりその応答で欲しいキーを指定します。CM_WANTSPECIALKEY とは異なり、WM_GETDLGCODE の戻り値は、欲しいキーをフラグビットの集まりで示します。VCL で重要なフラグビットは以下の通りです。

戻り値のフラグビット意味
DLGC_WANTARROWS = 1WM_KEYDOWN で 矢印キー(VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN)が欲しい
DLGC_WANTTAB = 2WM_KEYDOWN で TAB キー(VK_TAB)が欲しい
DLGC_WANTALLKEYS = 4WM_KEYDOWN で VK_RETURN, VK_EXECUTE, VK_ESCAPE, VK_CANCEL キーが欲しい
DLGC_WANTCHARS = $80WM_CHAR が欲しい

コントロールは欲しいキーをどちらのメッセージで答えても構いません。どちらかで欲しいキーを希望した場合、後で WM_KEYDOWN メッセージが通知されます。WM_GETDLGCODE を使うのは、TEdit など、Windows であらかじめ用意されているコントロールのためで、こうすることで、TEdit 等の実装が簡単になるからです。

CM_WANTSPECIALKEY と WM_GETDLGCODE の守備範囲が違ことに注意してください。ここでは特殊キーに関して論じているため機能的には両者はあまり変わりませんが、WM_GETDLGCODE はコントロールが WM_CHAR を受け取りたいかチェックするのにも使います。それに比べて CM_WANTSPECIALKEY は特殊キー専門です。WM_CHAR に関してはあとで CN_CHAR の説明で触れます。

特殊キーをコントロールが欲しくないと答えた場合、CNKeyDown メソッドはフォームへ CM_DIALOGKEY メッセージを送ります。CN_DIALOGKEY の wParam には CN_KeyDown の仮想キーコードがセットされます。フォームの CMDialogKey メソッドは、仮想キーコードが VK_TAB で Ctrl キーが押されていなければコントロールフォーカスをタブオーダに従って次に移します。 VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN ならばコントロールフォーカスをタブオーダに従って前か次に移します。それ以外の場合は、フォーム内の全コントロールに対して CM_DIALOGKEY メッセージをブロードキャストします。

幾つかのコントロールは CM_DIALOGKEY に応答して、ダイアログボックスをシミュレートするのに協力します。標準で付いてくるコントロールの応答は以下の通りです。

コントロール応答
TButton仮想キーがVK_RETURN で、自分が Default ボタンか、自分にフォーカスが有ればボタンを押します。仮想キーが VK_ESCAPE で自分が Cancel ボタンならばボタンを押します。
TPageControl仮想キーが VK_TAB で Ctrl キーが押されている場合、ページをめくります。
TOleControlすみません。何をやっているか良く分かりません (^^;

もし、フォームおよびその配下のコントロールがいずれも特殊キーを処理しなかった場合や、処理しても戻り値を非ゼロにしなかった場合は「処理済み」とはならず、コントロールに WM_KEYDOWN メッセージが届きます。例えば、TAB キーと一緒に Ctrl キーも押されていた場合とか、矢印キーを押したがフォームにコントロールが無い時は処理されないので、コントロールには WM_KEYDOWN が届きます。

Figure 3.3-4

図3.3-4 CNKeyDown の特殊キーの処理

3.3.5 CN_KEYUP メッセージの処理

CN_KEYDOWN の次は CN_KEYUP メッセージの処理です。メッセージループの IsKeyMsg メソッドは WM_KEYUP メッセージを CN_KEYUP に変換し TWinControl.CNKeyUp メソッドに送ります

CNKeyUp メソッドの処理は非常に簡単で、仮想キーコードが特殊キーの場合、自分自身(コントロール)に CM_WANTSPECIALKEY を送って、WM_KEYUP が欲しくないか尋ねます。意味が CN_KEYDOWN と逆転していることに注意してください。つまり、CM_WANTSPECILKEY で 特殊キーの WM_KEYDOWN が欲しいこと(戻り値が非ゼロ)は WM_KEYUP を欲しくないことを意味します。逆に WM_KEYDOWN が欲しくないこと(戻り値がゼロ)は WM_KEYUP が欲しいことを意味します。CM_WANTSPECIALKEY を受け取ったコントロールはそれが CNKeyDown と CNKeyUp のどちらから来たのか判りませんから、コントロールは特殊キーに関しては WM_KEYDOWN と WM_KEYUP の片方を希望することになります。ただし、WM_KEYUP を希望した場合、両方が来ることは有り得ます。

CNKeyUp は WM_GETDLGCODE メッセージを使いません。CNKeyDown は 特殊キーの WM_KEYUP が欲しいかコントロールに尋ねるだけで処理を終わります。CNKeyUp が 0 を返せばコントロールに WM_KEYUP が届きます。CNKeyUp は 特殊キー以外の CN_KEYUP メッセージに対しては常に 0 を返します(下図参照)。

Figure 3.3-5

図3.3-5 CNKeyUp の特殊キーの処理

3.3.6 CN_CHAR の処理

WM_KEYDOWN メッセージが 3.3.3 の数々の処理を潜り抜け、結局処理されずに戻ってくると、つまり、該当するショートカットがなく、特殊キーであっても、コントロールが WM_KEYDOWN メッセージの発行を希望した場合や、特殊キーの処理をフォームに依頼する CM_DialogKey メッセージを誰も処理しなかった場合、 IsKeyMsg が 0で戻り、ようやく WM_KEYDOWN メッセージは TranslateMessage API にたどり着きます。TranslateMessage は 可能なら WM_KEYDOWN メッセージに対応する WM_CHAR メッセージをメッセージキューに Post し、DispatchMessage API が WM_KEYDOWN メッセージをコントロールに送ります。コントロールが WM_KEYDOWN メッセージを処理するとメッセージループが回り、IsKeyMsg に WM_CHAR メッセージが届きます。

以上から判るように、WM_CHAR は WM_KEYDOWN が IsKeyMsg で処理された場合は Post されません。つまりコントロールが WM_KEYDOWN を受け取った場合だけ WM_CHAR がポストされます。

IsKeyMsg メソッドは WM_CHAR メッセージを CN_CHAR メッセージに変換し、TWinControl.CNChar メソッドに送ります。

CN_CHAR の処理は単純ですが重要な処理をします。CNChar メソッドは自分自身(コントロール)に WM_GETDLGCODE メッセージを送り、WM_CHAR が欲しいか尋ねます。コントロールが「欲しくない」と答えた場合、CNChar メソッドはフォームに CM_DIALOGCHAR メッセージを送ります。フォームは配下のコントロールに CM_DIALOGCHAR メッセージをブロードキャストします。

CM_DIALOGCHAR メッセージは各コントロールのニーモニックの処理に使われます。ニーモニックと CM_DIALOGCHAR の文字コードが一致した場合、標準のコントロールは以下のように応答します。

コントロール応答
TGroupBoxグループボックス内の最初のコントロールにフォーカスを移します。
TLabelFocusControl Property が指定されていれば、そのコントロールにフォーカスを移します。
TButtonボタンをクリックします。
TCheckBoxチェックをトグルします。
TRadioButtonフォーカスを獲得し、自分をチェックします。
TTabNoteBookタブのニーモニックでノートのページを選択します。
TTabSetタブのニーモニックでタブを選択します。

CNChar メソッドは、コントロールが WM_CHAR を「欲しい」と答えた場合や、CM_DIALOGCHAR メッセージに対してどのコントロールもニーモニックの処理を行わなかった場合、コントロールには WM_CHAR が届きます(下図参照)。

Figure 3.3-6

図3.3-6 CNChar の特殊キーの処理

3.3.7 CN_SysKeyDown の処理

WM_SYSKEYDOWN メッセージは、メニューキー(ALT キー)が押された時や、メニューキーに続いて他のキーが押された時に発生します。通常はまず、 VK_MENU を持つ WM_SYSKEYDOWN メッセージが送られ、その後、後続のキーの仮想キーコードを持つ WM_SYSKEYDOWN が発生します。但し、ALT キーのみを押して一定時間経つと、キーがメニュー側のメッセージループに奪われるようになるので、TApplication のメッセージループにはキーが入ってこなくなります。これはメニューの何らかの選択操作をし(ESC キーを押すなど)、操作が終了すれば、メッセージループに再びメッセージが来るようになります。

WM_SYSKEYDOWN を IsKeyMsg が受け取ると、IsKeyMsg は CN_SYSKEYDOWN に変換し、コントロールに送ります。このメッセージを受け取るのは TWindControl.CnSysKeyDown メソッドです。

CnSysKeyDown メソッドの処理は CnKeyDown の処理と良く似ています、CnSysKeyDown は CnKeyDown と全く同じようにショートカットを処理します。つまり、ALT と組み合わされたショートカットはここで処理されます。

CnSysKeyDown は CnKeyDown と同じように、ショートカットが発見されなかった場合、CM_CHILDKEY を自分自身に送ります。CM_CHILDKEY が処理されなかった場合、 CnKeyDown とは異なり、必ず CM_DIALOGKEY をフォームに送ります。つまり、CnKeyDown が特殊キーの処理だけをフォームに依頼したのに対し、CnSyskeyDown は全てのキーの処理をフォームに依頼します。フォームは ALT キーが押されている時は必ず CM_DIALOGKEY を配下のコントロールにブロードキャストします。各コントロールの処理は CnKeyDown が CM_DIALOGKEY を送った時とだいたい同じですが、標準のコントロールでは、TButton は ALT/Ctrl/Shift キーが押下されている場合は反応しないようになっています。

3.3.8 CN_SYSCHAR の処理

IsKeyMsg は WM_SYSKLEYUP を受け取ると CN_SYSKEYUP をコントロールに送るのですが、VCL はこのメッセージを全く無視するので、CN_SYSCHAR の説明に移ります。

メッセージループの TranslateMessage API は WM_SYSKEYDOWN から可能ならば WM_SYSCHAR を作りメッセージキューに Post します。従って、CN_SYSKEYDOWN が「処理済み」になった場合は WM_SYSCHAR は発生しません。

メッセージループの IsKeyMsg メソッドは WM_SYSCHAR を受け取ると CN_SYSCHAR メッセージに変換しコントロールに送ります。CN_SYSCHAR を受け取るのは TWinControl.CnSysChar メソッドです。

CnSysChar メソッドは、仮想キーコードが VK_SPACE、 つまり ALT+スペース(システムメニューのドロップ)を除いて、フォームに CM_DIALOGCHAR を送ります。これは CN_CHAR の場合と同様に、コントロールのニーモニックの処理に使われます。

3.3.9 WM_SYSCOMMAND(SC_MENU) の処理

WM_SYSKEYDOWN(CharCode = VK_MENU) のみかあるいは、WM_SYSKEYDOWN(CharCode = VK_MENU) + WM_SYSCHAR をデフォルトのウィンドウプロシージャが受け取ると、ウィンドウプロシージャはそのウィンドウに WM_SYSCOMMAND(WParam = SC_KEYMENU, LParam = KeyCode) を送ります(WM_SYSKEYDOWN(CharCode = VK_MENU) のみの場合は KeyCode は 0 になります)。このメッセージは本来メニューをドロップさせるためのメッセージですが、VCL はこのメッセージに対し複雑な処理をします。

まず、このメッセージを受け取ったコントロールはフォームへ CM_APPSYSCOMMAND を送ります。CM_APPSYSCOMMAND の LParam には WM_SYSCOMMAND メッセージへのポインタがセットされます。但し、WM_SYSCOMMAND の LParam が VK_SPACE か '-' の場合は CM_APPSYSCOMMAND は送られません。この組み合わせがシステムメニューをドロップするからです(後者はMDI 子ウィンドウのシステムメニュー)。

このメッセージを受け取る TForm.CMAppSysCommand は 自分が MDI 子フォームか、メインメニューを持っていないか、メインメニューの AutoMerge Property が True なら、CM_APPSYSCOMMAND をさらに、アプリケーションウィンドウに送ります。この時、CM_APPSYSCOMMAND の WParam と LParam には WM_SYSCOMMAND の WParam と LParam がそのままセットされます。条件に合わなければ、フォームは CM_APPSYSCOMMAND を未処理で返すため、WM_SYSCOMMAND はデフォルトのウィンドウプロシージャに渡りそのフォームのメニューが反転するかキーに該当するメニューがドロップします。

アプリケーションウィンドウは、CM_APPSYSCOMMAND を受け取ると、メインフォームをアクティブにし、CM_APPSYSCOMMAND を WM_SYSCOMMAND に直してメインフォームに送ります。WM_SYSCOAMMND を受け取ったメインフォームはそれをデフォルトのウィンドウプロシージャに送るため、メインフォームのメニューが反転するか、キーに該当するメニューがドロップします。(下図参照)

Figure 3.3-7

図3.3-7 WM_SYSCOMMAND(SC_MENU) の処理

ようするに、WM_SYSCOMMAND(SC_MENU)の処理は、WM_SYSCOMMAND(SC_MENU) を上記の条件にしたがって、アプリケーションウィンドウを介してメインフォームに転送します。従って、メインメニューの AutoMerge Property が True かメニューを持たないフォームでメニューのキー操作(ALT + キー)を行うと、メインフォームがアクティブになり、メインフォームのメニューに対してキー操作を行うことになります。

3.3.10 WM_KEYDOWN, WM_KEYUP, WM_CHAR, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_SYSCHAR の処理

この6種のメッセージは幾つかのコントロール(グリッドコントロール等)では独自のメッセージ処理メソッドを持っていますが、ほとんどのコントロールは TWinContol のメソッドで処理されます。

WM_KEYDOWN, WM_KEYUP, WM_CHAR, WM_SYSKEYDOWN, WM_SYSKEYUP の共通の処理は TWinControl の WMKeyDown, WMKeyUp, WMChar, WMSysKeyDown, WMSysKeyUp メソッドで行われます。これらのメソッドが呼び出すメソッド及びイベントを下図に示します。

Figure 3.3-8

図3.3-8 WMKeyDown, WMKeyUp, WMChar, WMSysKeyDown, WMSysKeyUp, メソッド の処理

WM_SYSCHAR に対応するメッセージ処理メソッドは無く、従って、イベントも起きないことに注意してください。 例えば ALT+A を押した時は

  1. WM_SYSKEYDOWN(VK_MENU)
  2. WM_SYSKEYDOWN(VK_A)
  3. WM_SYSCHAR('a')
  4. WM_SYSKEYUP(VK_A)
  5. WM_SYSCOMMAND(CmdType = KeyMenu Key = 97)
  6. WM_MENUCHAR('a')
  7. WM_SYSKEYUP(VK_A)
  8. WM_KEYUP(VK_MENU)

計8個のメッセージが発生しますが、この時の WM_SYSCHAR を OnKeyPress イベントで捕らえることは出来ません。

DoKeyDown/DoKeyUp/DokeyPress の主要な処理はコントロールの ControlStyle Property に csNoStdEvent が含まれているか調べ、含まれていなければ KeyDown/keyUp/KeyPress メソッドを介して OnKeyDown/OnKeyUp/OnKeyPress イベントハンドラを呼び出すことです。従って csNoStdEvent が ControlStyle Property に含まれている場合は、キーボードイベントは発生しません。

DoKeyDown/DoKeyUp/DoKeyPress は戻り値を持っています。この戻り値が True の場合は、コントロールはキーボードメッセージ処理を打ち切り、後続の処理にメッセージを渡しません。DoKeyDown/DoKeyUp/DoKeyPress がイベントハンドラ OnKeyDown/OnKeyUp/OnKeyPress を呼び出し、イベントハンドラが 参照渡しの引数 Key(OnKeyDown, OnKeyUp では仮想キーコード、OnKeyPress では文字コード) を 0(又は#0) に変更した場合、 False が返るようになっています。つまり、コントロールのこれらのイベントハンドラで Key を 0(又は#0)に変更すると、キーボードメッセージがコントロールの後続のキーボードメッセージ処理に渡らなくなります。例えばメモコントロールに Ctrl+J(#10) の WM_CHAR メッセージを処理させないようにするには、メモコントロールの OnKeyPress ハンドラ内で、引数 Key が #10 の場合だけ、 Key を #0 に書き換えればOKです。これはよく知られたテクニックですが、ヘルプには載っていないので覚えておきましょう。

フォームの KeyPreview Property が True の場合は、DoKeyDown/DoKeyUp/DoKeyPress は、自分のイベントハンドラを呼び出す前に、フォームの DoKeyDown/DoKeyUp/DoKeyPress を呼び出します。従って、KeyPreview を True にしておけばフォーム内のコントロールに来た上記の6種のキーボードメッセージをコントロールよりも先にフォームで処理できます。もちろんフォームの イベントハンドラ(OnKeyDown/OnKeyUp/OnKeyPress)で Key 引数を 0(#0) に設定すればコントロールの後続のメッセージ処理にメッセージは渡らなくなります。

以上から判るようにイベント OnKeyDown/OnKeyUp/OnKeyPress は上記の6種のメッセージに対応したものであり、ショートカットの処理、ニーモニックの処理、ダイアログボックスのシミュレートのためメッセージが届かなかった場合はイベントが発生しないことに注意してください。例えばボタンコントロールに TAB キーの OnKeyPress イベントが起きることは絶対にありません。

DoKeyDown だけが行っている特殊な処理があります。DoKeyDown は WM_KEYDOWN の仮想キーコードが VK_APP(最近のキーボード Windows 95 用に新設されたアプリケーションキー)の場合、コントロールのポップアップメニューを表示します。

KeyDown/KeyUp/keyPress は純粋にイベントハンドラを呼び出すだけのメソッドでイベントディスパッチャとも呼ばれます。これらのメソッドは dynamic なので、コントロールのキーボードメッセージ処理を継承先のコントロールで変更する場合、これらのメソッドをオーバライドするのが最も楽です。こうすれば、上記のフォームのキーボードプレビューの処理の後の、OnKeyDown/OnKeyUp/OnKeyPress イベントハンドラの処理の前後に、継承先のコントロールの処理を追加できます(重要!!)。実際ほとんど VCL の標準のコントロールは KeyDown/KeyUp/keyPress メソッド を override して処理を追加しています。

3.3.11 まとめ

以上長々と書いてきましたがここで、VCL のキーボードメッセージの処理をまとめておきます。

まず、アプリケーション全体に関わる処理としては
ショートカットの処理
VCL のキーボードメッセージ処理ではショートカットが最優先で処理されます。ショートカットはメインメニューとポップアップメニューで定義され、ショートカットの探索は現在アクティブなコントロールがどれかによって動的に変化します。
メニュー操作関連キーの処理
WM_SYSCOMMAND(SC_MENU)は フォームが MDI CHILD か、フォームのメインメニューの Auotmerge Property が Trueか、またはフォームがメニューを持たない場合メインフォームに転送されます。

次にコントロールに関わる処理をまとめます。以下のメッセージに適切に応答するようにコントロールを作成しないとコントロールは正常に動作しません。

WM_KEYDOWN で特殊キーを処理したいコントロールは CM_WANTSPECIALKEY か WM_GETDLGCODE に適切に応答しなければなりません。CM_WANTSPECIALKEY に応答する場合は、メッセージ中の仮想キーコードを調べて WM_KEYDOWN が必要なら非ゼロを返します。WM_GETDLGCODE に応答する場合は以下のようにします。
欲しい WM_KEYDOWN の仮想キーコードWM_GETDLGCODE の応答に含めるフラグビット
VK_LEFT, VK_RIGHT, VK_UP, VK_DOWNDLGC_WANTARROWS
VK_TABDLGC_WANTTAB
VK_RETURN, VK_EXECUTE, VK_ESCAPE, VK_CANCELDLGC_WANTALLKEYS

コントロールが CM_DIALOGKEY を受け取った場合、 コントロールはフォームがダイアログボックスとして動作することに協力しなければなりません。CM_DIALOGKEY は特殊キーか ALT+キー が押されたことを通知してくるため、コントロールは適切に応答しなければなりません。応答の仕方は 3.3.4.2 を参考にしてください。もし、CM_DIALOGKEY を処理したなら、戻り値を非ゼロにします。
コントロールが WM_CHAR を処理したいのなら、 WM_GETDLGCODE の戻り値に DLGC_WANTCHAR を含めなければなりません。
コントロールが CM_DIALOGCHAR を受け取った場合は、コントロールは自分がニーモニックを持つなら、ニーモニックと CM_DIALOGCHAR の文字コードを比較し、一致したなら適切な処理をしなければなりません。 CM_DIALOGCHAR を処理したなら、戻り値を非ゼロにします。
新しいコントロールを作成する時、キーボードメッセージの処理を変更するには、メッセー処理メソッドを override するより、dynamic なメソッド KeyDown/KeyUp/KeyPress を Override すべきです。

最後にキーボード関連のイベントに関してまとめます。

OnKeyDown/OnKeyUp/OnKeyPress イベントは WM_KEYDOWN, WM_KEYUP, WM_CHAR, WM_SYSKEYDOWN, WM_SYSKEYUP メッセージに対応します。従って、ショートカット、ニーモニック、ダイアログボックスのシミュレートのためにコントロールにこれらのメッセージが渡らない場合にはこれらの OnKeyDown/OnKeyUp/OnKeyPress イベントは起きません。
コントロールの ControlStyle Property に csNoStdEvent が含まれている場合、OnKeyDown/OnKeyUp/OnKeyPress イベントは起きません。
フォームの KeyPreview Property が True の場合、コントロールの キーボードイベント(OnKeyDown/OnKeyUp/OnKeyPress) よりも前にフォームのキーボードイベントが発生し、その後コントロールのキーボードイベントが発生します。
キーボードイベント中で参照渡しのパラメータ Key に 0(#0) を代入すると、コントロールのキーボードメッセージの処理をキャンセルできます。

戻る ホーム 上へ 進む

inserted by FC2 system