JavaFxのコンボボックスを極める

# はじめに

今回のサンプルコードは[GitHub](https://github.com/yokoi-engineering/javafx_locate_combo)にあります。
コミット履歴で内容を追えるようにしてあります。

# コンボボックスの選択肢を列挙型で表示したい

Java1.5より使える列挙型にて、手軽に一連の値を定義することができます。
そしてそのままコンボボックスの選択肢として並べて表示したいのはごく自然なことですね。


```java:ParakeetEnum.java
package com.exsample.javafxlocatecombo;

public enum ParakeetEnum {
    PEACH_FACED_LOVEBIRD,
    COCKATIEL,
    BUDGERIGAR;
}
```

```java:MainSceneFXMLController.java
package com.exsample.javafxlocatecombo;

import java.net.URL;
import java.util.Objects;
import java.util.ResourceBundle;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;

public class MainSceneFXMLController implements Initializable {

    @FXML
    private ComboBox<ParakeetEnum> comboParakeet;
    @FXML
    private Label label;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        ObservableList<ParakeetEnum> list =
                FXCollections.observableArrayList(ParakeetEnum.values());
        comboParakeet.getItems().addAll(list);
        comboParakeet.valueProperty().addListener(
                (ObservableValue<? extends ParakeetEnum> observable,
                 ParakeetEnum oldValue, ParakeetEnum newValue) -> {
            if (Objects.nonNull(newValue)) {
                label.setText(newValue.name());
            }
        });
    }
}
```

上記は、コンボボックスの値が変わったら、それをラベルに表示するサンプルです。

* ComboBoxの総称型には、作成した列挙型を指定します
* FXCollections.observableArrayListに列挙型の配列を入れます
* ComboBoxの valueProperty().addListener() で値の変更を受けています
* なお列挙型の values() や name() は暗黙に定義されているメソッドです

で、画面には列挙型の定義名がそのまま表示されるので悲しい感じになります。

![combo1.png](https://qiita-image-store.s3.amazonaws.com/0/114529/24d0f92d-cd4f-0518-675f-fbdf19da3303.png)

# 列挙型を使いつつも、狙った文字列を表示する技

Javaの列挙型はフィールドやメソッドを定義できます。
ここに表示したい文字列を入れてあげます。

```java:ParakeetEnum.java
package com.exsample.javafxlocatecombo;

public enum ParakeetEnum {
    PEACH_FACED_LOVEBIRD("小桜インコ"),
    COCKATIEL("オカメインコ"),
    BUDGERIGAR("セキセイインコ");

    final private String name;

    private ParakeetEnum(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
```

表示側のコンボボックスは少し面倒ですが、レンダリングをカスタマイズしてあげます。
ここで注意なのが、レンダリングは「ListView」と「Button」が別々ということです。
よく setCellFactory() だけを実装している例がありますが、それでは「ListView」しかレンダリングしません。
そしてレンダリングの setText() やラベルを更新するときに列挙型のメソッド getName() を呼んでいます。

```java:MainSceneFXMLController.java
package com.exsample.javafxlocatecombo;

import java.net.URL;
import java.util.Objects;
import java.util.ResourceBundle;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;

public class MainSceneFXMLController implements Initializable {

    @FXML
    private ComboBox<ParakeetEnum> comboParakeet;
    @FXML
    private Label label;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        Callback<ListView<ParakeetEnum>, ListCell<ParakeetEnum>> cellFactory
                = (ListView<ParakeetEnum> param) -> new ListCell<ParakeetEnum>() {
            @Override
            protected void updateItem(ParakeetEnum item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    setText(item.getName());
                }
            }
        };
        ObservableList<ParakeetEnum> list
                = FXCollections.observableArrayList(ParakeetEnum.values());
        comboParakeet.getItems().addAll(list);
        comboParakeet.setButtonCell(cellFactory.call(null));
        comboParakeet.setCellFactory(cellFactory);
        comboParakeet.valueProperty().addListener((
                ObservableValue<? extends ParakeetEnum> observable,
                ParakeetEnum oldValue, ParakeetEnum newValue) -> {
            if (Objects.nonNull(newValue)) {
                label.setText(newValue.getName());
            }
        });
    }
}
```

列挙型のコンストラクタに入れた文字が表示されました。

![combo2.png](https://qiita-image-store.s3.amazonaws.com/0/114529/bedd38bc-5d90-7ef4-e7fb-60feca09688c.png)

# 多言語対応で極める！！

GUIアプリケーションなので多言語対応したいことはあるでしょう。
上記まではお決まりのパターンなので、探せば解説がありますが、今回の多言語対応はオリジナル実装です。でも外していないと思います。

まずロケールファイルを用意します。好きなだけ用意してください。

```locale.properties
key.PeachFacedLovebird=Peach-faced Lovebird
key.Cockatiel=Cockatiel
key.Budgerigar=Budgerigar
```

```locale_ja_JP.properties
key.PeachFacedLovebird=小桜インコ
key.Cockatiel=オカメインコ
key.Budgerigar=セキセイインコ
```

列挙型はリソースキーを定義するように変更します。

```java:ParakeetEnum.java
package com.exsample.javafxlocatecombo;

public enum ParakeetEnum {
    PEACH_FACED_LOVEBIRD("key.PeachFacedLovebird"),
    COCKATIEL("key.Cockatiel"),
    BUDGERIGAR("key.Budgerigar");

    final private String resourceKey;

    private ParakeetEnum(String resourceKey) {
        this.resourceKey = resourceKey;
    }

    public String getResourceKey() {
        return resourceKey;
    }
}
```

画面は ResourceBundle.getString() で指定したリソースキーの文字を取得するように変えます。

```java:MainSceneFXMLController.java抜粋
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        Callback<ListView<ParakeetEnum>, ListCell<ParakeetEnum>> cellFactory
                = (ListView<ParakeetEnum> param) -> new ListCell<ParakeetEnum>() {
            @Override
            protected void updateItem(ParakeetEnum item, boolean empty) {
                super.updateItem(item, empty);
                if (item != null && !empty) {
                    setText(rb.getString(item.getResourceKey()));
                }
            }
        };
        ObservableList<ParakeetEnum> list
                = FXCollections.observableArrayList(ParakeetEnum.values());
        comboParakeet.getItems().addAll(list);
        comboParakeet.setButtonCell(cellFactory.call(null));
        comboParakeet.setCellFactory(cellFactory);
        comboParakeet.valueProperty().addListener((
                ObservableValue<? extends ParakeetEnum> observable,
                ParakeetEnum oldValue, ParakeetEnum newValue) -> {
            if (Objects.nonNull(newValue)) {
                label.setText(rb.getString(newValue.getResourceKey()));
            }
        });
    }
```

画面を用意する際に FXMLLoader.load() に ResourceBundle を指定してあげます。
ロケールの切り替えは Locale.setDefault() で行っていますが、これは設定ファイルなり設定ダイアログなり、システム設定に任すなり、好きにしてください。サンプルで切り替えて確認できるようにしてあるだけです。


```java:MainApp.java
package com.exsample.javafxlocatecombo;

import java.util.Locale;
import java.util.ResourceBundle;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class MainApp extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Locale.setDefault(Locale.ENGLISH);
        ResourceBundle rb = ResourceBundle.getBundle("locale.locale", Locale.getDefault());
        Parent root = FXMLLoader.load(getClass().getResource("/fxml/MainScene.fxml"), rb);
        Scene scene = new Scene(root);
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setTitle("JavaFX and Maven");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
```

出ましたー。

![combo3.png](https://qiita-image-store.s3.amazonaws.com/0/114529/46589bc4-3652-076e-4fd9-7594f457fa3b.png)