FireMonkey で Vcl の TForm.KeyPreview と同じ事をしたい

# VCL の TForm.KeyPreview とは

[VCL](http://docwiki.embarcadero.com/RADStudio/Rio/ja/VCL) は Windows に特化したライブラリで、[TForm](http://docwiki.embarcadero.com/Libraries/Rio/ja/Vcl.Forms.TForm) も､その上に乗っているコントロールも、OS から見て全て「Window」です。
そして、その Window がフォーカスを持っている場合、[WM_KEYDOWN](https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-keydown) などのメッセージは直接 Window に飛びます（[EDIT](https://docs.microsoft.com/en-us/windows/desktop/controls/edit-controls) などのフォーカスを持つコントロールの場合）。
メッセージを受け取った個々の Window が Key に対して応答します（例えば EDIT の場合、文字が入力されたり）。

ですが、Key の状態を一元的に管理したい場合もあります。
例えば Photoshop のようにどのコントロールにフォーカスがあってもショートカットでツールを切り替える、などの動作を実現するためには、Key の一元的な管理が必要です[^1]。

その時に有効なのが [TForm.KeyPreview プロパティ](http://docwiki.embarcadero.com/Libraries/Rio/ja/Vcl.Forms.TForm.KeyPreview)です。

TForm.KeyPreview が True の時は Form の [OnKeyDown](http://docwiki.embarcadero.com/Libraries/Rio/ja/Vcl.Controls.TWinControl.OnKeyDown)/[OnKeyUp](http://docwiki.embarcadero.com/Libraries/Rio/ja/Vcl.Controls.TWinControl.OnKeyUp)/[OnKeyPress](http://docwiki.embarcadero.com/Libraries/Rio/ja/Vcl.Controls.TWinControl.OnKeyPress) が呼ばれるようになり、Key の状態を一元的に管理できます。

# FireMonkey には KeyPreview は無い

VCL が OS から見て実体を持った Window だったのと対照的に、FireMonkey のコントロールは OS から見て存在しません。単なる画像です[^2]。
ですので、OS から直接キーイベントが飛んだりしません。
Key を受け取った誰かが各コントロールのイベントを呼び出しています。

## FireMonkey の TForm
Key を受け取るのは誰かというと、それは [TForm](http://docwiki.embarcadero.com/Libraries/Rio/ja/FMX.Forms.TForm) です。
TForm は各 OS のネイティブウィンドウとして生成されるため、OS から見て実体が存在するのです。

TForm が Key を受け取った時にフォーカスを持っているコントロールがある場合、そのコントロールの OnKeyDown といったイベントを TForm が呼び出します。

また、同時に**自分自身の OnKeyDown イベントも呼び出します**。
そのため、FireMonkey には KeyPreview の概念すらありません。

# FireMonkey の Key イベントの問題
FireMonkey の TForm が自分自身のイベントも呼び出すのならば特に問題が無いように思いますが、実装が微妙なため困った問題を引き起こします。
次のコードは FireMonkey の TCommonCustomForm.KeyDown メソッドの一部で、イベントを呼び出す部分です。

```pascal
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 渡し](http://docwiki.embarcadero.com/RADStudio/Rio/ja/%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%EF%BC%88Delphi%EF%BC%89#.E5.80.A4.E3.83.91.E3.83.A9.E3.83.A1.E3.83.BC.E3.82.BF.E3.81.A8.E5.A4.89.E6.95.B0.E3.83.91.E3.83.A9.E3.83.A1.E3.83.BC.E3.82.BF)になっているため中身を書き換えられます。
もしも最初の 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 の定義を見てみると…

```pascal
public procedure KeyDown(var Key: Word; var KeyChar: System.WideChar; Shift: TShiftState); virtual;
```

となっていて、可視性は Public でしかも仮想メソッドです。
つまり、override するだけです。

```pascal
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 では全く別の解法になるという良い例かと思います。

[^1]: VCL では簡単な手段として TPopupMenu を使うという方法もあります。TPopupMenu を非表示にして メニューアイテムに ShortCut キーを設定する、という方法です。

[^2]: [ControlTypes プロパティによるネイティブコントロール](http://docwiki.embarcadero.com/Libraries/Rio/ja/FMX.Controls.Presentation.TPresentedControl.ControlType#.E3.83.8D.E3.82.A4.E3.83.86.E3.82.A3.E3.83.96_.E3.82.B3.E3.83.B3.E3.83.88.E3.83.AD.E3.83.BC.E3.83.AB)は実体があります。

[^3]: FMX.ListBox.pas の TCustomListBox.KeyDown メソッドをご覧ください。恐らく後続の case 文で else 節に飛ばすためだと考えられます。