プロジェクト

全般

プロフィール

JavaFXとバインディング

はじめに

JavaFXでは、コントロール等の持つ属性(ラベルのテキストなどの値や、幅・高さ、ディセーブル等)を「プロパティ」という概念を導入して他のオブジェクトの属性と「バインディング」し、片方の属性が変更されるとバインディングした他方の属性を連動して変更されるようにする仕組みが提供されています。

Swingまでの仕様・設計では、オブジェクトの属性の変更を連動させたいときはリスナーを設定してコールバックを受ける方法で実現していました。そのためにはリスナーオブジェクトを生成し(クラスにリスナーインタフェースをimplementsするか、リスナーインタフェースをimplemetsする内部クラスを作るか、リスナーインスタンスを匿名クラスでインスタンス化する)、リスナーメソッドで値を取得して連動させる箇所にセットするという手間をかけていました。

JavaFXでは、その仕組みが簡単にかけるようになりました、というところです。このプロパティという概念は馴染みがないと最初違和感、異質感が半端ないですが、馴染むとそれなりに受け入れて使えるようになります。使っているうちに、オブザーバーパターン(リスナー)の実装を使うことが手間になって、バインディングが便利すぎて手放せなくなってしまいます。

基本

バインディングは、あるオブジェクトのプロパティと、別なプロパティを結び付ける仕組みです。
オブジェクトのプロパティに変化が生じると、バインディングされているプロパティに変更通知を行い、値の更新を伝搬します。

プロパティの種類

JavaFXがAPIで提供するプロパティは次となります。

プロパティの型 読み込み専用プロパティの型 プロパティの値の型
BooleanProperty ReadOnlyBooleanProperty boolean
IntegerProperty ReadOnlyIntegerPropety int
FloatProperty ReadOnlyFloatProperty float
DoubleProperty ReadOnlyDoubleProperty double
StringProperty ReadOnlyStringProperty String
ObjectProperty<T> ReadOnlyObjectProperty<T> T extends Object
ListProperty<E> ReadOnlyListProperty<E> ObservableList<E>
MapProperty<K,V> ReadOnlyMapProperty<K,V> ObservableMap<K,V>
SetProperty<E> ReadOnlySetProperty<E> ObservableSet<E>

プロパティの操作

プロパティに値をセットする

2つのメソッド、void set(T value)void setValue(T value)があります。
プリミティブの値のプロパティでは、setメソッドの引数はプリミティブ型、setValueメソッドの引数はラッパー型となっています。

  • IntegerProperty
    void set​(int value)
    void setValue​(Number v)
    

プロパティの値を取得する

2つのメソッド、T get()T getValue()があります。
プリミティブの値のプロパティでは、getメソッドの戻り値はプリミティブ型、getValueメソッドの戻り値はラッパー型となっています。

  • IntegerProperty
    int get()
    Integer getValue()
    

バインディング

バインディングの例(片方向)

2つのプロパティ alfaProperty, omegaPropertyを考えます。どちらもString型のオブジェクトを持つことにします。

// プロパティの宣言と初期化
StringProperty alfaProperty = new SimpleStringProperty("alfa");
StringProperty omegaProperty = new SimpleStringProperty("omega");

alfaPropertyの変化をomegaPropertyに伝搬したいとします。つまり、alfaPropertyの値を変更すると、その値がomegaPropertyにセットされるという具合です。
プロパティクラスのbindメソッドを使って、2つのプロパティを片方向結合します。bindメソッドの書式は次です。

void bind(ObservableValue observable)

引数に指定するプロパティが「Observable」(状態変化をbind先に伝搬する)です。

omegaProperty.bind(alfaProperty);

ここで、alfaProperty.set("ALFA"); を実行するとbindによりomegaPropertyの値も"ALFA"に変更されます。
逆に、omegaPropertyの値を変更しようとすると、bindがかかっているので矛盾が生じるため 実行時例外が発生します。

Exception in thread "main" java.lang.RuntimeException: A bound value cannot be set.
    at javafx.base/javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:141)
    at javafx.base/javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:50)
    at ExampleProperty1.bindStringsOmegaAlfa(ExampleProperty1.java:20)

バインディングの例(双方向)

2つのプロパティ alfaProperty, omegaPropertyを考えます。どちらもString型のオブジェクトを持つことにします。

// プロパティの宣言と初期化
StringProperty alfaProperty = new SimpleStringProperty("alfa");
StringProperty omegaProperty = new SimpleStringProperty("omega");

alfaPropertyの変化をomegaPropertyに伝搬し、またomegaPropertyの変化をalfaPropertyに伝搬したいとします。つまり、alfaPropertyとomegaPropertyのどちらかの値を変更すると、その値がもう一方のプロパティにセットされるという具合です。
プロパティクラスのbindBidirectionalメソッドを使って、2つのプロパティを双方向結合します。bindBidirectionalメソッドの書式は次です。

void bindBidirectional(Property<T> other)

双方向結合なので、どちらをメソッドのレシーバー、引数にしても違いはないかと思います。

omegaProperty.bindBidirectional(alfaProperty);

ここで、alfaProperty.set("ALFA"); を実行するとbindBidirectionalによりomegaPropertyの値も"ALFA"に変更されます。
逆に、omegaProperty.set("OMEGA"); を実行するとbindBidirectionalによりalfaPropertyの値も"OMEGA"に変更されます。

バインディングのカスタマイズ

  • IntegerPropertyなどのサブクラスを作成
  • Bindingsクラスのstaticメソッドを使用
  • fluent interface APIを使用(IntegerExpressionクラス等)

やりたいこと別

型の違うプロパティをバインディングしたい

数値から文字列へのバインディング

例えば、スライダー(Slider)の値をラベルに表示したいとします。スライダーの値はDoubleProperty型で、ラベル(Label)の値はStringProperty型です。これを直接バインドするとコンパイルエラーとなってしまいます。

Slider slider;
Label label;
    :
label.textProperty().bind(slider.valueProperty());  // コンパイルエラー

数値型のプロパティのasStringメソッド

数値型のプロパティには、StringBindingを返却するasStringメソッドが用意されています。次にシグニチャを示します。

public StringBinding asString()
public StringBinding asString(String format)
public StringBinding asString(Locale locale, String format)

引数なしのasStringは、デフォルトの文字列(数値型プロパティの値をtoString()したもの)を生成します。

label.textProperty().bind(slider.valueProperty().asString());

書式を指定して文字列化する場合は、次のコードとなります。

label.textProperty().bind(slider.valueProperty().asString("%6.2f"));

Bindings.convertメソッドを使う

Bindingsクラスのメソッドconvertは、各種型のプロパティから文字列表現を生成するStringExpressionインスタンスを生成します。
シグニチャを次に示します。

public static StringExpression convert(ObservableValue<?> observableValue)

JavaFXの実装では、プロパティの実の値にtoString()を呼び出しています。

コードは次となります。

label.textProperty().bind(Bindings.convert(slider.valueProperty()));

Bindings.formatメソッドを使う

Bindingsクラスのメソッドformatは、第1引数に書式を指定し、第2引数にプロパティ(ObservableValue型)を指定すると、第2引数のプロパティが変化すると対応する文字列が生成されます。シグニチャを次に示します。

public static StringExpression format(String format, Object... args)

コードは次となります。

label.textProperty().bind(Bindings.format("%6.2f", slider.valueProperty()));

ラベルの文字列プロパティにNumberStringConverterを使ってスライダーの値プロパティをバインド
label.textProperty().bindBidirectional(slider.valueProperty(), new NumberStringConverter());
BindingsクラスのbindBidirectionalメソッドを用い、NumberStringConverterを第3引数に指定
Bindings.bindBidirectional(label.textProperty(), slider.valueProperty(), new NumberStringConverter());
  • 注記
    最初、バインドする数値のプロパティがDoubleProperty型だからという理由でDoubleStringConverterを使ったのですが、コンパイルエラーになってしまいました。DoubleProperty型は、Property<java.lang.Number>をimplementsしているので、NumberStringConverterでないと合致しないためと思われます。
BindingsクラスのcreateStringBindingメソッドでStringへのBindingインスタンスを生成
label.textProperty().bind(
    Bindings.createStringBinding( () -> Double.toString(slider.getValue()), slider.valueProperty()),
);

createStringBindingメソッドのシグニチャは次です。

public static StringBinding createStringBinding(Callable<String> func, Observable... dependencies)

第1引数に、プロパティが変化したときに実行する処理(戻り値型がString)を記述したCallable(ラムダ式でも可)を指定、 第2引数に変化を監視するプロパティを指定します。

真偽値から文字列へのバインディング

Booleanプロパティの真偽値に応じた文字列を表示したいとします。

真偽値型のプロパティのasStringメソッド

真偽値型のプロパティには、StringBindingを返却するasStringメソッドが用意されています。次にシグニチャを示します。

public StringBinding asString()

引数なしのasStringは、デフォルトの文字列(真偽値型プロパティの値をtoString()したもの)を生成します。

label.textProperty().bind(checkBox.selectedProperty().asString());

結果は"true"、"false"が表示されます。

BindingsクラスのcreateStringBindingメソッドを使う

Bindingsユーティリティクラスには、StringExpressionを返すcreateStringBindingsメソッドがあります。

public static StringBinding createStringBinding(Callable<String> func, Observable... dependencies);

label.textProperty().bind(
    Bindings.createStringBinding( ()-> checkBox.selectedProperty().get() ? "On" : "Off", checkBox.selectedProperty())
);
when, then, otherwiseを使う
label.textProperty().bind(
    Bindings.when(checkBox.selectedProperty()).then("On").otherwise("Off")
);

基本系は、Bindings.when(条件).then(値1).otherwise(値2) となります。値1と値2は同じ型とします。
条件には、ObservableBooleanValue型の値を指定します。

直接 new When(..)とすることもできますが、Bindings.when(..)とするのがよいとされています。

値を計算した結果でバインディングしたい

数値のプロパティに定数を加減算してバインディング

ある数値プロパティに定数を足す、または引いて別な数値プロパティに結び付けます。

@FXML
private AnchorPane rootPane;
@FXML
private Canvas canvas;

@Override
public void initialize(URL url, ResourceBundle rb) {
    canvas.widthProperty().bind(rootPane.widthProperty().subtract(200));
    canvas.heightProperty().bind(rootPane.heightProperty().subtract(120));
}

活性化・非活性化を連動したい

チェックボックスのチェック有無でラベルの活性化・非活性化を行いたい場合などです。

チェックボックスにチェックが付くと自動でテキストフィールドを活性化

@FXML CheckBox editCheckBox;
@FXML TextField editField;
  :
@Override
public void initialize(URL url, ResourceBundle rb) {
    editField.disableProperty().bind(Bindings.not(editCheckBox.selectedProperty()));
}

TextFieldのdisabledPropertyをtrueにすると、TextFieldが非活性化します。
チェックボックスにチェックが付くとCheckBoxのselectedPropertyがtrueになります。チェックが付いたときにTextFieldを活性化し、チェックが外れたときにTextFieldを非活性化するので、論理を反転させるためBindings.notを入れています。

プロパティを1:多でbindしたい

2つのBooleanPropertyとbindする

2つのプロパティが共にtrueの時に伝搬したい場合を想定します。
alfaPropertyとbravoPropertyと2つのBooleanがあり、両方ともtrueの時にsummaryPropertyがtrueになるようにしたい、というケースを検討します。

BooleanProperty summaryProperty = new SimpleBooleanProperty(false);
BooleanProperty alfaProperty = new SimpleBooleanProperty(false);
BooleanProperty bravoProperty = new SimpleBooleanProperty(false);

次のように、BooleanProperty(正確には継承元のBooleanExpression)のorメソッドを使い、2つのBooleanPropertyのどちらかが真の時に真を取る BooleanBindingを生成します。

summaryProperty.bind(alfaProperty.or(bravoProperty));

alfaまたはbravoのプロパティのどちらかが真の時、summaryが真となります。

3つ以上のBooleanPropertyとbindする

では、3つ以上の時は、

summaryProperty.bind(alfaProperty
        .or(bravoProperty)
        .or(charlieProperty)
        .or(deltaProperty)
);


約1ヶ月前に更新