⌨️

Flutterでキーボードショートカット【初級編】

2022/04/22に公開

Flutterでキーボードショートカットを使うには、いくつか方法がある。
今回は、FocusonKeyEventを使って、Widgetを開き1つのキーが押されたときの処理を実装する。
(Widgetを開かない通常画面でショートカットを使ったり、複数キーを使ったショートカットなど、より複雑なキー入力処理については、今後【中級編】【上級編】という形で扱うかも?)
正直よくわかってないが、一応動作したのでまとめておく。

今回の記事でできること

開いたWidgetで1つのキーが押されたときの処理ができる。
以下の六法アプリでは、右下のプラスボタンから法令検索し法令を追加した後、条文が表示されているエリアをダブルクリックすると条番号検索するモーダルが現れる。
このモーダルで、表示されているボタンを使わずに、数字キーで数字を入力、BackSpaceキーで数字を一つ削除、EnterまたはSpaceキーで条番号へジャンプできる。
また、Sキーを押してモーダルを閉じることもできる。
この記事ではこうしたキー入力処理を実現する。
https://codes.enoiu.com/

Flutterにおけるキーボードショートカットの取り扱い

公式ページは以下のリンク。
https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts
日本語での情報としては、以下の記事が参考になります。
https://zenn.dev/inari_sushio/articles/746ad8a0470594
とりあえず、キー入力を受け付けるにはFocus状態にある必要があるということを理解しておけば大丈夫です。
※Focus状態は、TextFieldとかであるautofocusをイメージするとわかりやすいかも。autofocusを指定すると、TextFieldが表示されたときに、TextFieldで入力が受け付けられる状態になるが、この入力が受け付けられる状態というのがまさにFocus状態。

使い方

Focus Widget

今回の記事では、既述の通り、FocusonKeyEventを使って、1つのキーが押されたときの処理を実装する。
Focusクラスについての公式ページ↓
https://api.flutter.dev/flutter/widgets/Focus-class.html
Focusを使ったコードは以下のような感じ。

Focus(
  autofocus: true,
  onKeyEvent: (node, event) {},
  child: Widget()
)

まず、キーボードショートカットを使いたいWidgetを包むFocusを作る。
そして、Widgetが表示されたときにFocus状態になるようにautofocusをtrueにする。
キー入力の処理は、onKeyEventで行う。

onKeyEvent

onKeyEventは以下のような感じ。

onKeyEvent: (node, event) {
  if (event is KeyDownEvent) {
    if (event.logicalKey == LogicalKeyboardKey.keyQ) {
      //処理
      return KeyEventResult.handled;
    }
  }
  return KeyEventResult.ignored;
}

if (event is KeyDownEvent) {}というif文は、キーが押し下げられたときにのみ処理するためのもの。これがないと、キーが押し下げられたときとキーが戻ってくるときの2回処理がされてしまう。
そして、if (event.logicalKey == LogicalKeyboardKey.keyQ) {}で、Qキーが押されたときの処理を行っている。
ここではlogicalKeyを使っているが、physicalKeyというのもある。
logicalKeyは実際のキー、physicalKeyはQWERTYキーボードでの特定の位置のキーを指す。
キーの印字通りにするならlogicalKeyでOK。
使えるキーについては以下の公式ドキュメントを参照。
スペースバーはLogicalKeyboardKey.space、エンターキーはLogicalKeyboardKey.enterみたいに使える。
https://api.flutter.dev/flutter/services/LogicalKeyboardKey-class.html
https://api.flutter.dev/flutter/services/PhysicalKeyboardKey-class.html
また、onKeyEventでは、返り値としてKeyEventResultが必要。正直よくわかってないが、Focus公式ドキュメント通り、keyが押された処理のif文ではKeyEventResult.handledを、onKeyEventの最後のところにはKeyEventResult.ignoredをreturnしている。

使用例

この記事の最初に紹介した六法アプリでの使用例。
キーを押したときの処理などは省略している。

//
String s = '';
//
Focus(
  autofocus: true,
  onKeyEvent: (node, event) {
    final key = event.logicalKey;
    final keyl = key.keyLabel;
    if (event is KeyDownEvent) {
      if (key == LogicalKeyboardKey.space || 
          key == LogicalKeyboardKey.enter ||
	  key == LogicalKeyboardKey.numpadEnter) {
        //スペース、エンター、テンキーのエンターが押されたときの処理
	return KeyEventResult.handled;
      } else if (key == LogicalKeyboardKey.backspace) {
        //バックスペースが押されたときの処理
	return KeyEventResult.handled;
      } else if (keyl.contains(RegExp(r'[0-9]'))) {
        //数字キー、テンキーの数字キーが押されたときの処理
	setState(() {
	  s += keyl[keyl.length - 1];
	});
	return KeyEventResult.handled;
      } else if (key == LogicalKeyboardKey.keyS) {
        //Sキーが押されたときの処理
	Navigator.of(context).pop();
	return KeyEventResult.handled;
      }
    }
    return KeyEventResult.ignored;
  },
  child: //
)
//

まず、このFocusはshowModalBottomSheetのbuilder > BottomSheet > StatefulBuilder内に置き、Focusのchildにはボタンとかを包んだContainerを指定している。
そして、onKeyEventのはじめのとこで、event.logicalKeykey.keyLabelをそれぞれkey, keylとして、コードを若干短縮している。
また、通常のキーとテンキーのキーは別キーとして扱われるため、テンキーで使うにはLogicalKeyboardKey.numpadEnterみたいな感じで指定が必要。
数字キーも同様に、通常はdigit0とかで指定できるが、テンキーではnumpad0みたいな指定になる。
今回の使用では、押された数字キーの数字を画面に反映させたかったため、keyLabelを使った処理にした。

keyLabelを使った数字キーの処理について

まず、if文の条件でkeyl.contains(RegExp(r'[0-9]'))としている。
これは、keyLabelに0~9の数字が含まれているかどうかを判定している。
keyLabelはキーの名前として設定されている値で、onKeyEvent内ではevent.logicalKey.keyLabelとして取得できる。
通常の数字キーでのkeyLabelはそのままの数字として取得できる(0とか)が、テンキーの数字キーでのkeylabelはNumpad 0みたいにNumpadがついてしまうため、0~9が含まれるかどうかという条件で判定している。

同様の理由で、押されたキーの数字を取得するためにkeyl[keyl.length - 1]を使用した。
これは、keyLabelの最後の文字を取得するもの。
テンキーでのkeyLabelがNumpad 0という感じなので、最後の文字を取得すればキーの数字を取得できる。
問題として、数字が含まれるファンクションキーにも反応してしまうという点があるが、ファンクションキーはこのアプリで使用しないのでよしとした。ファンクションキーに反応しないようにするには、keyLabelにFが含まれる場合を除くとかすればいいかも?

まとめ

ということで、FocusonKeyEventを使って、Widgetを開き1つのキーが押されたときの処理のやり方をまとめた。
次回の中級編?では、ModalとかのWidgetを開かずに(最初の画面とか)でショートカットを使ったり、TextFieldなど他のfocusがあるところでショートカットを使う方法をまとめたい。(ショートカットの使い方というよりもFocusNodeの説明になるかも)
中級編の記事の内容によって、以下の六法アプリで使ったショートカット機能がすべて実装できる予定です。
(以下の六法アプリでは、Cを押すとショートカットを確認できます。)
https://codes.enoiu.com/

Discussion