概要
JavaFX の標準で用意されている Label には Action やキーボードショートカットが設定できません。文字を表示するためだけの Control なので当たり前といえばそうなのですが、どうしても設定したい事情(後述)があったので、どうにかならないかと調べてみたところ、自分で Label を継承して定義するしかないとわかりました。
実行環境
Java | SE 1.8.0_102 |
---|---|
OS | Windows 10 |
Eclipse | Mars 4.5.2 |
JFoenix | 1.0.0 |
実装
2つクラスを作る必要があります。実装全体はリンク先(GitHub Repository)をご覧ください。
- Label を継承するクラス……MenuLabel
- 1 の Skin を定義するクラス……MenuLabelSkin
以下、ポイントを紹介します。
1. Label を継承するクラス……MenuLabel
2つのコピペ
コピペなので何も工夫しておらずコメントのしようもないです。
- Action を使うための部分……Buttonクラスからコピペ
- キーボードショートカットを使うための部分……MenuItemクラスからコピペ
Skin の指定
設定されるキーボードショートカットを scene のキーボードショートカット一覧に登録するため、2のCustomスキンを指定します。
@Override
protected Skin<?> createDefaultSkin() {
return new MenuLabelSkin(this);
}
引数なしコンストラクタ
FXML からは引数なしコンストラクタが呼ばれるので、忘れずに定義しておきます。
public MenuLabel() {
this("");
}
public MenuLabel(final String text) {
super(text);
setOnMouseClicked(event -> getOnAction().handle(new ActionEvent()));
}
2. 1 の Skin を定義するクラス……MenuLabelSkin
表示するテキストの調整(ショートカットの追加表示)と、キーボードショートカットの登録に必要でした。
下記のコードをご覧になるとわかる通り、Skin を設定する label オブジェクトが Accelerator(キーボードショートカット)を持っている場合は表示テキストにショートカットを追加し、持っているショートカットを scene オブジェクトの Accelerators という Map に登録します。
if (label.getAccelerator() == null) {
return;
}
label.setText(label.getText() + "\t|\t" + label.getAccelerator().getDisplayText());
getSkinnable().getScene().getAccelerators()
.put(label.getAccelerator(), () -> label.getOnAction().handle(new ActionEvent()));
ショートカットの表示は、 MenuItem のコードを辿っていくとよりきれいに表示させる実装方法が見つかったのですが、今回やった限りでは上手くいかなかったので、上記の実装で妥協しています。
おもちゃ
今回作成した MenuLabel を使ったおもちゃのスクリーンショットは下記の通りです。
スクリーンショット
起動直後はこんな感じで、Hello の領域をクリックするか、 Ctrl+H を入力すると
このようにポップアップが出ます。
このやり方を知っていれば、どんな Control にも Action とキーボードショートカットを設定できるようになります。どんな時に役立つのかはさっぱりわかりません。
なぜこんなものを作ってしまったのか ~ どうしても設定したい事情
普通に考えたら Button や MenuItem を使えばよいので、今回やったことはまったく無意味に思えるでしょう。
JavaFX で Material design を実装するためのライブラリで JFoenix というものがあります。先ほどのサンプルアプリケーションで使ったポップアップはこのライブラリの JFXSnackbar というコンポーネントを使いました。この JFoenix には ListView を拡張した JFXListView というものがありまして、これは Labeled を継承した Control を要素に用いることができます。これと JFXTabPane(TabPane の拡張)を組み合わせて、 Material designっぽい Menu を作ってみようと考えて、実装してみたところ、2つ問題が発生しました。そう、Action を設定できないこと、キーボードショートカットを設定できないことです。Menu の代替として使うにはその2つが必須でした。
Action だけなら Button を使えば、見た目はおかしいですが何とか実現できます。しかし Button はキーボードショートカットを設定できないので、それであれば Label を拡張してしまおう、という判断に至りました。
備考
この JFXTabPane を、例えば Drawer の下に入れた(隠した)場合、起動直後はキーボードショートカットが使えません。どうやら Skin オブジェクトの初期化はそのコンポーネントが最初に表示されたタイミングで実行されるようです。それでは Menu の代替として使えることにならないので、対策を考えてみましたが、どうもなさそうでしたので、手動で JFXTabPane が持っている MenuLabel
を全件調べて Accelerator
を put する、という方法を採りました。
@FXML
private TabPane menuTabs;
......
final ObservableMap<KeyCombination, Runnable> accelerators
= this.stage.getScene().getAccelerators();
menuTabs.getTabs().forEach(tab -> {
@SuppressWarnings("unchecked")
final JFXListView<ActionLabel> labels = (JFXListView<ActionLabel>) tab.getContent();
labels.getItems().forEach(l -> {
if (l.getAccelerator() == null) {
return;
}
accelerators.put(l.getAccelerator(), () -> l.getOnAction().handle(new ActionEvent()));
});
});
リンク
- ソースコード全体
- JavaFX で Snackbar を表示する……今回使ったポップアップ表示用のコンポーネントについてはこちらの記事をご覧くださいますと幸いです。