JavaFXダイアログのボタン表示を変更する

JavaFX にはダイアログを表示するための Alert というクラスがあります。とても 便利なのですが 確認ダイアログを表示したときのボタンはいまいちだと思うのです。

ボタンのキャプションが 取消 ですよ。AWT/Swing の時代は 了解 取消し という表示だったので 了解 OK に変わっただけでも改善が見られるわけですが…。どうせなら 取消し キャンセル に変更しておいて欲しかったです。

文句を言っても仕方ありません。自前で JavaFX ダイアログのボタン 取消 キャンセル に置き換えてしまいましょう。

JavaFX のボタン表示を変更する方法は 私の知る限り 3 つあります。プログラムで指定する方法が 2 リソース ファイルで指定する方法が 1 つです。それぞれの方法を順番に紹介していきます。

ベースとなるサンプル プログラム

はじめに 変更前のサンプル プログラムを提示しておきます。

  • src
    • com
      • example
        • Main.java
        • Main.fxml
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.java
package 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.properties
Dialog.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

ソースツリーに含めず 実行時のクラスパスに 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.java
package 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 のソースコードを地道に追いかけてリソースのキー名を探していくしかないようです。

キャプションを キャンセル に変更する方法には以下の手順で辿り着きました。

  1. Alert.java を読んで ButtonType.java でボタンのキャプションを決めていることが分かる。

  2. ButtonType.java ControlResources.getString(key) でキャプションを取得していることが分かる。このとき ButtonType.CANCEL なら key に渡されるのは "Dialog.cancel.button" であることも分かる。

  3. ControlResources.java ResourceBundle.getBundle(BASE_NAME) でリソースバンドルを取得していることが分かる。BASE_NAME "com/sun/javafx/scene/control/skin/resources/controls" を指定していることが分かる。

  4. 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.java
private 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 が使えなくても まあ良しとしましょう。

サンプル プログラム

サンプル プログラムのソースコードは下記のリンクからダウンロードできます。

dialog-sample.zip (58KB)

この記事を共有しませんか?