VCL の TForm.KeyPreview とは
VCL は Windows に特化したライブラリで、TForm も、その上に乗っているコントロールも、OS から見て全て「Window」です。
そして、その Window がフォーカスを持っている場合、WM_KEYDOWN などのメッセージは直接 Window に飛びます(EDIT などのフォーカスを持つコントロールの場合)。
メッセージを受け取った個々の Window が Key に対して応答します(例えば EDIT の場合、文字が入力されたり)。
ですが、Key の状態を一元的に管理したい場合もあります。
例えば Photoshop のようにどのコントロールにフォーカスがあってもショートカットでツールを切り替える、などの動作を実現するためには、Key の一元的な管理が必要です1。
その時に有効なのが TForm.KeyPreview プロパティです。
TForm.KeyPreview が True の時は Form の OnKeyDown/OnKeyUp/OnKeyPress が呼ばれるようになり、Key の状態を一元的に管理できます。
FireMonkey には KeyPreview は無い
VCL が OS から見て実体を持った Window だったのと対照的に、FireMonkey のコントロールは OS から見て存在しません。単なる画像です2。
ですので、OS から直接キーイベントが飛んだりしません。
Key を受け取った誰かが各コントロールのイベントを呼び出しています。
FireMonkey の TForm
Key を受け取るのは誰かというと、それは TForm です。
TForm は各 OS のネイティブウィンドウとして生成されるため、OS から見て実体が存在するのです。
TForm が Key を受け取った時にフォーカスを持っているコントロールがある場合、そのコントロールの OnKeyDown といったイベントを TForm が呼び出します。
また、同時に自分自身の OnKeyDown イベントも呼び出します。
そのため、FireMonkey には KeyPreview の概念すらありません。
FireMonkey の Key イベントの問題
FireMonkey の TForm が自分自身のイベントも呼び出すのならば特に問題が無いように思いますが、実装が微妙なため困った問題を引き起こします。
次のコードは FireMonkey の TCommonCustomForm.KeyDown メソッドの一部で、イベントを呼び出す部分です。
if FFocused <> nil then
FFocused.KeyDown(Key, KeyChar, Shift);
if ((Key <> 0) or (KeyChar <> #0)) and Assigned(FOnKeyDown) then
FOnKeyDown(Self, Key, KeyChar, Shift);
ここで Key と KeyChar は var 渡しになっているため中身を書き換えられます。
もしも最初の if 文から呼ばれる FForcused.KeyDown の中で中身が書き換えられていたらどうなるでしょう?
特に 0 や #0 になっていると次の if 文で弾かれてしまい、TForm 自身のイベント FOnKeyDown が呼び出されなくなってしまいます。
0 や #0 で無くても実データではなく変更された値が渡される事になります。
Key, KeyChar が変更されるとき
実際の所、Key, KeyChar が意図せず書き換わる事はあるのか?というと、あります。
一番身近な例では TListBox です。
TListBox はカーソルキーでアイテムを切り替えられますが、その時に KeyChar を #0 で書き換えています3。
そのため、TListBox がフォーカスを持っている場合、キーが書き換えられ想定した動作が出来なくなります。
FireMonkey で KeyPreview をする
ということで、VCL の KeyPreview と同様の事をする場合、FFocused.KeyDown より先に Key を入手する必要があります。
と、まあ勿体付けましたが解決方法は非常に簡単です。
KeyDown の定義を見てみると…
public procedure KeyDown(var Key: Word; var KeyChar: System.WideChar; Shift: TShiftState); virtual;
となっていて、可視性は Public でしかも仮想メソッドです。
つまり、override するだけです。
TForm1 = class(TForm)
public
procedure KeyDown(var Key: Word; var KeyChar: System.WideChar; Shift: TShiftState); override;
end;
procedure TForm1.KeyDown(var Key: Word; var KeyChar: System.WideChar; Shift: TShiftState);
begin
// ここに KeyPreview でやりたい処理を書く
inherited;
end;
ある意味 VCL よりはまっとうな手段という気がします。
まとめ
今回の KeyPreview は、VCL のアレがやりたい!を考えても FireMonkey では全く別の解法になるという良い例かと思います。
-
VCL では簡単な手段として TPopupMenu を使うという方法もあります。TPopupMenu を非表示にして メニューアイテムに ShortCut キーを設定する、という方法です。 ↩
-
ControlTypes プロパティによるネイティブコントロールは実体があります。 ↩
-
FMX.ListBox.pas の TCustomListBox.KeyDown メソッドをご覧ください。恐らく後続の case 文で else 節に飛ばすためだと考えられます。 ↩