JavaFX にはダイアログを表示するための Alert というクラスがあります。とても、 便利なのですが、 確認ダイアログを表示したときのボタンはいまいちだと思うのです。
ボタンのキャプションが 「取消」 ですよ。AWT/Swing の時代は 「了解」 ・ 「取消し」 という表示だったので、 「了解」 が 「OK」 に変わっただけでも改善が見られるわけですが…。どうせなら 「取消し」 も 「キャンセル」 に変更しておいて欲しかったです。
文句を言っても仕方ありません。自前で JavaFX ダイアログのボタン 「取消」 を 「キャンセル」 に置き換えてしまいましょう。
JavaFX のボタン表示を変更する方法は (私の知る限り) 3 つあります。プログラムで指定する方法が 2 つ、 リソース ・ ファイルで指定する方法が 1 つです。それぞれの方法を順番に紹介していきます。
ベースとなるサンプル・プログラム
はじめに、 変更前のサンプル ・ プログラムを提示しておきます。
- src
- com
- example
- Main.java
- Main.fxml
- example
- com
Main.fxml<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<StackPane xmlns:fx="http://javafx.com/fxml/1"
prefWidth="300"
prefHeight="200">
<Button
fx:id="btn1" onAction="#btn1_onAction"
text="Click Me!" />
</StackPane>
Main.javapackage com.example;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
loader.setController(this);
Parent root = (Parent)loader.load();
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
@FXML
protected void btn1_onAction(ActionEvent event) {
Alert dialog = new Alert(AlertType.CONFIRMATION);
dialog.setTitle("ダイアログのタイトル");
dialog.setHeaderText(null);
dialog.setContentText("処理を実行しますか?");
dialog.showAndWait();
}
}
ウィンドウに 「Click Me!」 というボタンが 1 つ配置されていて、 このボタンをクリックするとダイアログが表示されます。
ダイアログを表示している btn1_onAction
メソッドは以下のようになっています。Alert
クラスのコンストラクタに AlertType.CONFIRMATION
を指定すると 「OK」 と 「取消」 のボタンを持つ確認ダイアログが自動的に構成されます。
ダイアログを表示するAlert dialog = new Alert(AlertType.CONFIRMATION);
dialog.setTitle("ダイアログのタイトル");
dialog.setHeaderText(null);
dialog.setContentText("処理を実行しますか?");
dialog.showAndWait();
方法1. ボタンのキャプションを明示的に指定する
もっとも簡単なのは、 Alert
インスタンスを生成するときに、 ボタンのキャプションを指定する方法です。
Alert
クラスには 3 つの引数を指定するコンストラクタがあります。
- Alert(AlertType alertType, String contentText, ButtonType… buttons)
第 3 引数には ButtonType
配列を指定します。ButtonType
はボタンのデータ構造を表すオブジェクトでキャプションも保持しています。キャプションを明示的に指定した ButtonType
インスタンスを作成し、 それを Alert
クラスのコンストラクタに指定します。
ButtonTypeを指定してAlertインスタンスを生成する(配列指定)Alert dialog = new Alert(AlertType.CONFIRMATION, null, new ButtonType[] {
new ButtonType("OK", ButtonData.OK_DONE),
new ButtonType("キャンセル", ButtonData.CANCEL_CLOSE) });
第 3 引数 ButtonType...
は可変長引数になっているので new ButtonType[] { }
配列を作成せずに、 ButtonType
インスタンスを並べて指定することもできます。
ButtonTypeを指定してAlertインスタンスを生成する(可変長引数指定)Alert dialog = new Alert(AlertType.CONFIRMATION, null,
new ButtonType("OK", ButtonData.OK_DONE),
new ButtonType("キャンセル", ButtonData.CANCEL_CLOSE));
上記の変更を加えてプログラムを実行すると、 ボタンのキャプションが変わります。
この方法は 「キャンセル」 ボタンだけでなく 「OK」 ボタンも指定しないといけないのが手間ですね。
方法2. キャプションを置き換える
次に紹介するのはデフォルトの確認ダイアログを生成してからボタンのキャプションを置き換える方法です。
Alert
インスタンスの生成には、 AlertType
のみを指定するコンストラクタを使います。
Alert dialog = new Alert(AlertType.CONFIRMATION);
これでデフォルトの確認ダイアログが作成されます。ボタンのキャプションは 「OK」 と 「取消」 になっているので、 「取消」 ボタンの参照を取得して、 ボタンのキャプションに直接 「キャンセル」 をセットします。
まずは 「取消」 ボタンの参照を取得しなければなりません。Alert
が内包している DialogPane
には ButtonType
を指定してボタンへの参照を取得する lookupButton
メソッドがあります。これを使います。
AlertType.CONFIRMATION
を指定して Alert
インスタンスを生成すると、 自動的に ButtonType.OK
を持つ 「OK」 ボタンと ButtonType.CANCEL
を持つ 「取消」 ボタンが作成されます。ButtonType.CANCEL
を指定して lookupButton
メソッドを呼び出せば、 「取消」 ボタンへの参照が取得できます。
「取消」 ボタンへの参照が取得できれば、 あとは setText
メソッドを呼び出して 「キャンセル」 をセットするだけです。
ButtonType.CANCELのボタンを取得してキャプションを置き換えるAlert dialog = new Alert(AlertType.CONFIRMATION);
Node button = dialog.getDialogPane().lookupButton(ButtonType.CANCEL);
if(button instanceof Button) {
((Button)button).setText("キャンセル");
}
この変更を加えてプログラムを実行すると、 ボタンのキャプションが変わります。
方法 2 の出番はあまりないかもしれません。Alert
インスタンスの生成処理に介入できず、 あとからキャプションを変えなければならないケースでは方法 2 が役に立つかもしれません。
方法3. リソースファイルで指定する
最後に紹介するのはリソースファイルで指定する方法です。この方法はソースコードを書き換える必要がなく、 単純にリソースファイルを追加するだけなのでとてもスマートです。
controls_ja_JP.propertiesDialog.cancel.button=キャンセル
上記内容のファイルを controls_ja_JP.properties
という名前で、 com/sun/javafx/scene/control/skin/resources
に配置します。
- src
- com
- example
- Main.java
- Main.fxml
- sun
- javafx
- scene
- control
- skin
- resources
- controls_ja_JP.properties
- resources
- skin
- control
- scene
- javafx
- example
- com
ソースツリーに含めず、 実行時のクラスパスに com/sun/javafx/scene/control/skin/resources/controls_ja_JP.properties
を配置しても構いません。
Java 9 以降であればファイルの文字コードに UTF-8
を使えます。以前は、 ResourceBundle
が使用するデフォルト ・ エンコーディングは ISO-8859-1
でした。ISO-8859-1
を使用する場合は、 以下のように Unicode エスケープ形式で記述する必要があります。
controls_ja_JP.properties(Unicodeエスケープ形式の場合)Dialog.cancel.button=¥u30AD¥u30E3¥u30F3¥u30BB¥u30EB
適切なパスに、 適切なファイル名で、 適切なキー名を持つリソースファイルを配置した状態でプログラムを実行すると、 リソースが適用されボタンのキャプションが変わります。
リソースを適用するために以下の点に注意してください。
言語と国を含めてロケールを指定する
リソースファイルの名前には ja_JP
のようなロケールを含めてください。ファイル名をロケールなしで controls.properties
としたり、 国 (JP) を省略して controls_ja.properties
としてしまうと、 システム ・ デフォルトの controls_ja_JP.properties
が適用されてしまいます。デフォルトのリソースよりも優先して自前のリソースファイルを適用させるために言語と国を含む明確なロケール指定が必要です。
リソースファイルのパスを変えることはできない
リソースファイルのパスは com/sun/javafx/scene/control/skin/resources/
でなければなりません。他のパスに変更することはできません。
ControlResources.javapackage com.sun.javafx.scene.control.skin.resources;
import java.util.ResourceBundle;
public final class ControlResources {
// Translatable properties
private static final String BASE_NAME
= "com/sun/javafx/scene/control/skin/resources/controls";
JavaFX のリソース参照処理を担っている ControlResources
クラスで、 リソースを参照するベース名が固定で指定されているからです。(パッケージ名が com.sun
で始まっているので将来、 変更されるかもしれません…。)
リソースのキー名はどうやって知ることができるのか?
キー名 Dialog.cancel.button
でキャンセルボタンのキャプションを指定することができました。このキー名はどうやって知ることができるのでしょうか? 他にはどのようなキー名が存在しているのでしょうか?
残念ながら JavaFX の UI をカスタマイズするためのリソースのキー名を体系的にまとめた情報はなく、 いまのところ、 JavaFX のソースコードを地道に追いかけてリソースのキー名を探していくしかないようです。
キャプションを 「キャンセル」 に変更する方法には以下の手順で辿り着きました。
Alert.java
を読んで、ButtonType.java
でボタンのキャプションを決めていることが分かる。
ButtonType.java
で、ControlResources.getString(key)
でキャプションを取得していることが分かる。このとき、ButtonType.CANCEL
ならkey
に渡されるのは"Dialog.cancel.button"
であることも分かる。ControlResources.java
で、ResourceBundle.getBundle(BASE_NAME)
でリソースバンドルを取得していることが分かる。BASE_NAME
に"com/sun/javafx/scene/control/skin/resources/controls"
を指定していることが分かる。ResourceBundle
は JavaFX 固有の仕組みではなく Java 汎用のリソース解決の仕組み。上記で判明したベース名とキー名を元にしてリソースファイルを作成すれば、 自動的に読み込まれるはず。
JavaFX のソースコードを読むといっても、 リソースのキー名を探すだけなら難しいことははありません。正確にロジックを読み解いていく必要はないのですから。ちなみに、 Eclipse なら Ctrl キーを押しながらクラス名やメソッド名をクリックするだけで簡単に Java の標準ライブラリや JavaFX ライブラリのソースコードを辿っていくことができます。
- 2019-12-02 追記
- Java 実行環境にはよっては
controls_ja_JP.properties
を配置する方法は上手くいかないようです。AdoptOpenJDK 11 + OpenJFX 11 では問題なく動作したのですが、 LibericaJDK では正しく動作せず、 ボタンキャプションが置き換えられませんでした。
SPIを使ったリソースの差し替えではうまくいかない
Java にはサービスプロバイダインタフェース (SPI) と呼ばれる機能拡張 (依存性注入) のための仕組みがあります。リソースに関連するプロバイダとして以下の 2 つのインターフェースがありますが、 どちらも、 JavaFX のリソースを置き換えることはできませんでした。
java.util.spi.ResourceBundleControlProvider
java.util.spi.ResourceBundleProvider
ResourceBundleControlProvider
ResourceBundleControlProvider の Javadoc には以下の記載があります。
名前付きモジュールでは、 すべての ResourceBundleControlProvider が無視されます。
ResourceBundleControlProvider
が無視されて機能しないケースもあるということみたいですが、 モジュールというのが何を指しているの分かりにくいですね。自作のアプリケーションのことなのか? JavaFX モジュール (javafx.controls
) のことなのか? 自作アプリケーションのことを指しているのであれば、 module-info.java
を含めなければ回避できそうです。JavaFX モジュール (javafx.controls
) のことを指しているのであれば、 JavaFX では ResourceBundleControlProvider
は機能しない、 ということになります。
ソースコードを追ってみましょう。
java.util.ResourceBundle.javaprivate static Control getDefaultControl(Module targetModule, String baseName) {
return targetModule.isNamed() ?
Control.INSTANCE :
ResourceBundleControlProviderHolder.getControl(baseName);
}
これが ResourceBundle.Control
を取得する処理です。モジュールが名前付き (isNamed) であれば、 ResourceBundleControlProvider
を検索せずに Control.INSTANCE
という固定の ResourceBundle.Control
インスタンスを返しています。「名前付きモジュールでは、 すべての ResourceBundleControlProvider が無視されます。」 という Javadoc の記載はこのことですね。
仮引数 targetModule
が何者なのかを遡って見ていきます。
java.util.ResourceBundle.java@CallerSensitive
public static final ResourceBundle getBundle(String baseName)
{
Class<?> caller = Reflection.getCallerClass();
return getBundleImpl(baseName, Locale.getDefault(),
caller, getDefaultControl(caller, baseName));
}
仮引数 targetModule
に渡されているのは Reflection.getCallerClass
メソッドの戻り値でした。これは 「呼び出し元のクラス」 を返す特殊なメソッドです。つまり、 呼び出し元クラスのモジュールが名前付きかどうかによって ResourceBundleControlProvider
が検索されるかどうかが決まるということになります。
呼び出し元クラスは com.sun.javafx.scene.control.skin.resources.ControlResources
です。このクラスは javafx.controls
モジュールに属しています。結局、 JavaFX のリソース検索では ResourceBundleControlProvider
は使われないということです。自作アプリケーションのモジュール定義 (module-info.java
) ではどうにもなりません。
ResourceBundleProvider
ResourceBundleProvider
というインターフェースもあります。(ResourceBundleControlProvider
と名前が似ていますが異なるものです。)
ResourceBundleProvider
の作り方 ・ 使い方については Javadoc に詳しく書かれています。
簡単に説明するとこうです。
ResourceBundle.getBundle("com.example.app.MyResource")
このようなリソースバンドルを取得するコードがあるとき、 com.example.app.spi.MyResourceProvider
という名前のプロバイダーを探して、 それが見つかれば ResourceBundle
の取得処理をそのプロバイダーに委譲してくれます。(MyResourceProvider
は ResourceBundleProvider
インターフェースを実装していなければなりません。)
この ResourceBundleProvider
の残念なところは、 リソースのベース名に基づいてプロバイダーの完全修飾クラス名が決められてしまう点にあります。
プロバイダーの命名規則<package of baseName> + ".spi." + <simple name of baseName> + "Provider"
リソースのベース名が com.example.app.MyResource
ならば、 プロバイダーの完全修飾クラス名は com.example.app.spi.MyResourceProvider
でなければなりません。これによって、 リソースのベース名が制約されることになります。
- リソースのベース名にはパッケージ名 (ドット) を付ける
- リソースのベース名にはクラス名に使用できない文字を使わない
たとえば、 リソースのベース名を com.example.app.1Resource
とします。
ResourceBundle.getBundle("com.example.app.1Resource")
これに対応する ResourceBundleProvider
の完全修飾クラス名は com.example.app.spi.1ResourceProvider
ということになります。しかし、 1ResourceProvider
という数字で始まるクラス名は命名規約に反していますから実際にこのようなクラスを用意することはできません。よって、 リソースのベース名 com.example.app.1Resource
に対応する ResourceBundleProvider
は作成できません。
ResourceBundleProvider
が使えるかどうかはリソースのベース名次第です。
さて、 JavaFX のリソース検索に話を戻しましょう。JavaFX では以下のベース名を使用してリソースバンドルを取得していました。
private static final String BASE_NAME
= "com/sun/javafx/scene/control/skin/resources/controls";
ResourceBundle.getBundle(BASE_NAME);
区切り文字にドット (.
) ではなくスラッシュ (/
) が使われています。これではパッケージ名とは認められません。このベース名はパッケージ名を持たず、 com/sun/javafx/scene/control/skin/resources/controls
全体が単純名 (クラス名) ということになってしまいます。
クラス名にスラッシュ (/
) を含めることはできませんから、 結局、 このベース名に対応する ResourceBundleProvider
は作成できないという結論になります。
というわけで、 SPI (ResourceBundleControlProvider
、 ResourceBundleProvider
) を使って JavaFX のリソースを置き換えるという試みは失敗したのでした。
リソースファイルを配置する方法 3 のほうがずっと簡単ですから、 SPI が使えなくても、 まあ良しとしましょう。
サンプル・プログラム
サンプル ・ プログラムのソースコードは下記のリンクからダウンロードできます。