JavaFX を勉強したときのメモ
環境
OS
Windows 10
Java
>java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
Scene Builder
JavaFX Scene Builder 8.4.1
Gluon が開発してるやつ
JavaFX プロパティ
JavaFX では、オブジェクトのプロパティを表現する方法として JavaBeans を拡張した JavaFX プロパティ というものが使用されている。
JavaFX プロパティでは、オブジェクトのプロパティを次のように実装する。
package sample.javafx.property;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class MyClass {
    private DoubleProperty value = new SimpleDoubleProperty();
    
    public final double getValue() {
        return this.value.get();
    }
    
    public final void setValue(double value) {
        this.value.set(value);
    }
    public DoubleProperty valueProperty() {
        return this.value;
    }
}
- プロパティ(value)をprivateで定義する- プロパティの型はプリミティブ型ではなく、 DoublePropertyのように JavaFX が用意しているラッパー型を使用する
- ラッパー型は javafx.beans.property パッケージ以下に用意されている
- 
DoubleProperty自体はインターフェースで、実装クラスとしてSimpleDoublePropertyが用意されている
 
- プロパティの型はプリミティブ型ではなく、 
- Getter, Setter を定義する
- メソッド名は JavaBeans と同じ規則で命名する
- 型はラッパー型ではなく、中身の値を出し入れするようにする
- メソッドは finalで定義する
 
- 
プロパティ名Propertyというメソッドを定義する- これはラッパー型をそのまま返す
 
JavaFX に用意されている多くのクラスは、この JavaFX プロパティを実装している。
例えば、 Label クラスの text プロパティに対応する各メソッドは以下になる(実際に定義されているのは Labeled クラス)。
Observable
JavaFX プロパティは単にメソッドの宣言を決めているだけでなく、 JavaBeans には無い特別な機能をいくつか提供している。
そのうちの1つが、 Observable になる。
Observable を実装したクラスは、内容が無効になったことを監視できる機能を提供している。
前述の JavaFX プロパティで使用した DoubleProperty も Observable を継承している。
この Observable には addListener(InvalidationListener) というメソッドが定義されており、内容が無効になったときに通知を受け取るリスナーを登録できる。
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty();
        a.addListener(observable -> {
            System.out.println("observable=" + observable);
        });
        a.set(1.0);
        a.set(2.0);
    }
}
observable=DoubleProperty [value: 1.0]
observable=DoubleProperty [value: 2.0]
リスナーには、変更された Observable 自身が引数として渡される。
無効と変更
Observable を実装したクラスの中には、内容の変更が要求されても(例えば Setter メソッドが呼ばれても)すぐには計算が行われず、次に内容が要求されたときに初めて計算をするようなものがある(遅延計算)。
つまり、内容の変更が要求されてから実際に計算されるまでの間、内容が確定していない期間がありえることになる。
この内容が確定していないことを 無効 と表現する。
そして、実際に計算が行われて内容が確定したときを 変更 と表現する。
InvalidationListener は、内容が無効になったときにコールバックされるようになっている。
なお、実装クラスによっては内容が即座に計算されるものもあるので、その場合は無効と変更は同時に発生することになる(無効になったあと、すぐ内容が確定(変更)する)。
ObservableValue
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty();
        a.addListener((observableValue, oldValue, newValue) -> {
            System.out.println("observableValue=" + observableValue + ", oldValue=" + oldValue + ", newValue=" + newValue);
        });
        
        a.set(1.0);
        a.set(2.0);
    }
}
observableValue=DoubleProperty [value: 1.0], oldValue=0.0, newValue=1.0
observableValue=DoubleProperty [value: 2.0], oldValue=1.0, newValue=2.0
Observable を継承したインターフェースとして、 ObservableValue インターフェースが存在する。
これには addListener(ChangeListener) というメソッドが用意されており、値が変更されたときに通知を受け取るリスナーを登録できる。
リスナーには、変更された ObservableValue 自身と、変更の前後の生の値が渡される。
DoubleProperty を含め JavaFX で用意されているプロパティ用のクラスは ObservableValue を実装しているので、 InvalidationListener と ChangeListener のどちらのリスナーも登録できるようになっている。
Binding
ObservableValue を継承したインターフェースとして Binding が存在する。
Binding は、複数の値をまとめた計算結果を表現している。
まとめられた値は Binding によって監視されており、値が変更されたときに自動的に結果が再計算されるようになっている。
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(1.0);
        DoubleProperty b = new SimpleDoubleProperty(2.0);
        DoubleBinding sum = a.add(b);
        System.out.println("sum=" + sum.get());
        
        a.set(3.0);
        System.out.println("sum=" + sum.get());
    }
}
sum=3.0
sum=5.0
この例では、2つの DoubleProperty の合計を表す DoubleBinding を作成している。
片方の DoubleProperty の値を変更し DoubleBinding の値を再取得すると、合計は再計算された値になっている。
このように、 Binding は複数の値をまとめ、各値の変更を監視して計算結果を自動更新する仕組みを提供している。
Binding がまとめている値のことをソース、または依存性と呼ぶ。
Binding 自体はソースの型に制限を設けていないが、実際はたいてい Observable や ObservableValue になる。
遅延計算
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(1.0);
        DoubleProperty b = new SimpleDoubleProperty(2.0);
        DoubleBinding sum = a.add(b);
        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
        
        a.set(3.0);
        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
    }
}
sum=DoubleBinding [invalid]
sum.get()=3.0
sum=DoubleBinding [value: 3.0]
sum=DoubleBinding [invalid]
sum.get()=5.0
sum=DoubleBinding [value: 5.0]
- 標準ライブラリ内に存在する Bindingを実装したクラスは全て、計算が遅延されるようになっている
- つまり、ソースの値が変更された直後は再計算を行わず、計算結果の取得が試みられたときに再計算を行う
- 再計算が行われるまで、 Bindingは無効となっている
ChangeListener を登録すると遅延計算されなくなる
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(1.0);
        DoubleProperty b = new SimpleDoubleProperty(2.0);
        DoubleBinding sum = a.add(b);
        sum.addListener((observableValue, oldValue, newValue) -> {
            System.out.println("変更されました(oldValue=" + oldValue + ", newValue=" + newValue + ")");
        });
        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
        
        a.set(3.0);
        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
    }
}
sum=DoubleBinding [value: 3.0]
sum.get()=3.0
sum=DoubleBinding [value: 3.0]
変更されました(oldValue=3.0, newValue=5.0)
sum=DoubleBinding [value: 5.0]
sum.get()=5.0
sum=DoubleBinding [value: 5.0]
- 
ObservableValue.addListener(ChangeListener)で変更通知のリスナが登録されている場合、遅延計算は行われなくなる
- つまり、 ChangeListenerを登録している場合は必ず即時計算されるようになる
- これは Bindingに限った話ではなく、ObservableValueを実装している遅延計算する全ての実装に共通した話- 詳しくは Observable の Javadoc や 公式のチュートリアル の説明を参照
 
高レベルAPI
Binding を作成するための API には高レベルAPIと低レベルAPIの2種類が用意されている。
さらに高レベルAPIはFluent APIとBindings クラスのファクトリメソッドの2つに分かれている。
Fluent API
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(2.0);
        DoubleBinding binding = a.add(3).multiply(2).subtract(4.0).divide(3.0);
        System.out.println("((2.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
        
        a.set(5.0);
        System.out.println("((5.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
    }
}
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((4.0 + 3) * 2 - 4.0) / 3.0 = 4.0
- Fluent API では、流れるようなインターフェースで Bindingを構築することができる- 
a.add(3).multiply(2).subtract(4.0).divide(3.0)の部分
 
- 
- これらのメソッドは NumberExpression というインターフェースに定義されている
- プロパティで使用しているクラスはこのインターフェースを実装している
 
- 数値演算以外にも比較演算や論理演算などを行うメソッドも用意されている
- 
NumberExpressionの数値演算系の Fluent API の戻り値の型はNumberBindingになっているが、NumberBindingもまたNumberExpressionのサブインターフェースになっているので、そのまま続けて Fluent API を呼び出すことができるようになっている
- 各インターフェースやクラスの関係は、下図のようになっている(一部だけ)
Bindings クラスのファクトリメソッド
package sample.javafx.property;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(2.0);
        DoubleBinding binding =
                Bindings.divide(
                    Bindings.subtract(
                        Bindings.multiply(
                            Bindings.add(a, 3)
                            ,2
                        )
                        ,4.0
                    )
                    ,3.0
                );  
        System.out.println("((2.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
        
        a.set(5.0);
        System.out.println("((5.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
    }
}
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0
- 
Bindings というクラスには、 Bindingのファクトリメソッドが大量に用意されている
- Fluent API に存在するものもあれば、存在しないものもある
- とりあえず数が多いので、ざっと眺めてどういうのがあるのか知っておくとよさげ
- when()とかおもろそう
 
低レベルAPI
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
public class MyBinding extends DoubleBinding {
    private final DoubleProperty source;
    public MyBinding(DoubleProperty source) {
        this.bind(source);
        this.source = source;
    }
    @Override
    protected double computeValue() {
        return ((this.source.get() + 3) * 2 - 4.0) / 3.0;
    }
}
package sample.javafx.property;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(2.0);
        
        DoubleBinding binding = new MyBinding(a);
        System.out.println("((2.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
        
        a.set(5.0);
        System.out.println("((5.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
    }
}
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0
- 低レベルAPI では、 Bindingの抽象クラスを継承して独自のBindingクラスを作成する- ここでは DoubleBindingを継承したMyBindingを作成している
 
- ここでは 
- 親クラスに存在する bind()メソッドで、監視対象のObservableを登録する- 引数は可変長になっているので、複数の Observableを監視することも可能
 
- 引数は可変長になっているので、複数の 
- 
bind()で登録したObservableの内容が変更されるとcomputeValue()が呼ばれるので、計算結果を返すように実装する
プロパティを Binding と結び付ける
package sample.javafx.property;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty();
        DoubleProperty b = new SimpleDoubleProperty();
        
        System.out.println("a=" + a + ", b=" + b);
        a.bind(b);
        System.out.println("a=" + a + ", b=" + b);
        
        b.set(3.0);
        System.out.println("a=" + a + ", b=" + b);
        System.out.println("a=" + a.get() + ", b=" + b);
    }
}
a=DoubleProperty [value: 0.0], b=DoubleProperty [value: 0.0]
a=DoubleProperty [bound, invalid], b=DoubleProperty [value: 0.0]
a=DoubleProperty [bound, invalid], b=DoubleProperty [value: 3.0]
a=3.0, b=DoubleProperty [value: 3.0]
- 
Property に定義されている bind(ObservalbeValue)メソッドを使用すると、一方向のバインディングが実現できる
- 引数で渡した ObservableValueの値を監視するようになり、値が変更されると自分自身の値を監視対象と同じ値に変更する
- あるプロパティの値を他のプロパティと連動させたい場合に、この bind()を使えばシンプルに実装できる
読み取り専用のプロパティ
package sample.javafx.property;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
public class MyClass {
    private ReadOnlyDoubleWrapper value = new ReadOnlyDoubleWrapper();
    
    public final double getValue() {
        return this.value.getValue();
    }
    
    public final ReadOnlyDoubleProperty valueProperty() {
        return this.value.getReadOnlyProperty();
    }
    
    public void updateValue() {
        this.value.set(99.9);
    }
}
package sample.javafx.property;
public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        System.out.println(myClass.valueProperty());
        
        myClass.updateValue();
        System.out.println(myClass.valueProperty());
    }
}
ReadOnlyDoubleProperty [value: 0.0]
ReadOnlyDoubleProperty [value: 99.9]
- 
DoublePropertyをそのまま公開すると、公開先で値を自由に書き換えることができてしまう
- 公開先で値を変更できないようにするなら、 setter は公開せず、内部で使用するプロパティの型を ReadOnly**Wrapper型にする
- 
ReadOnly**Wrapper型は、それ自体は普通に値を変更できるが、getReadOnlyProperty()で取得したオブジェクトは読み取り専用になる
双方向のバインディング
package sample.javafx;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private TextField textField;
    
    private StringProperty value = new SimpleStringProperty();
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.textField.textProperty().bindBidirectional(this.value);
    }
    
    @FXML
    public void checkValue() {
        System.out.println("value=" + value.getValue());
    }
    
    @FXML
    public void resetValue() {
        this.value.set("reset!!");
    }
}
実行結果
- 
Property に定義された bindBidirectional() メソッドを使用すると、2つの Propertyを双方向にバインドできる
StringProperty の双方向バインディング
Property<T> に用意されている双方向のバインディングメソッドは、同じ Property<T> 型のプロパティとだけ双方向のバインディングができる。
一方、 StringProperty には、次の2つの双方向バインディング用のメソッドが追加されている。
- フォーマット指定
- コンバーター指定
フォーマット指定
package sample.javafx;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.Format;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private TextField textField;
    
    private DoubleProperty value = new SimpleDoubleProperty();
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Format format = new DecimalFormat("#,##0.00");
        this.textField.textProperty().bindBidirectional(this.value, format);
    }
    
    @FXML
    public void checkValue() {
        System.out.println("value=" + value.getValue());
    }
    
    @FXML
    public void resetValue() {
        this.value.set(9876.54);
    }
}
実行結果
- 第二引数に Formatを指定できる
- 
StringProperty側には、Formatでフォーマットされた文字列が設定され、他方のプロパティにはパースされた結果が設定されるようになる
- 不正な書式の入力があると、 WARINING レベルのログがスタックトレースとともに出力される
コンバーター指定
package sample.javafx;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
import java.net.URL;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private TextField textField;
    
    private ObjectProperty<LocalDate> value = new SimpleObjectProperty<>(LocalDate.of(2017, 1, 1));
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu/MM/dd");
        
        this.textField.textProperty().bindBidirectional(this.value, new StringConverter<LocalDate>() {
            @Override
            public String toString(LocalDate date) {
                if (date == null) {
                    return "";
                }
                
                try {
                    return formatter.format(date);
                } catch (DateTimeException e) {
                    return "";
                }
            }
            @Override
            public LocalDate fromString(String text) {
                try {
                    return LocalDate.parse(text, formatter);
                } catch (DateTimeParseException e) {
                    return null;
                }
            }
        });
    }
    
    @FXML
    public void checkValue() {
        System.out.println("value=" + value.getValue());
    }
    
    @FXML
    public void resetValue() {
        this.value.set(LocalDate.of(2017, 1, 1));
    }
}
実行結果
- 
StringConverter を指定することで、任意の値と StringPropertyとを双方向にバインドできる
- 
StringConverterには2つの抽象メソッドが定義されている- 
String toString(T): オブジェクトTをStringに変換する
- 
T fromString(String):Stringを、オブジェクトTに変換する
 
- 
- 一応、標準で LocalDateStringConverter とかが用意されている
- しかし、パースやフォーマットができないとスタックトレースを含めた WARNING ログを出力してくるので、ちょっと使いづらい印象
- フリー入力でないところとかなら、使えるのかもしれない
 
コレクション
JavaFX では、標準のコレクション(List とか Map)を拡張した、独自のコレクションクラスが用意されている。
監視可能な List
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("fizz", "buzz");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("list が変更されました change=" + change);
        });
        
        list.add("foo");
        list.remove("buzz");
        FXCollections.sort(list);
    }
}
list が変更されました change={ [foo] added at 2 }
list が変更されました change={ [buzz] removed at 1 }
list が変更されました change={ permutated by [0, 1] }
- 
ObservableList は、 java.util.Listを継承したインターフェースで、監視機能を実現するためのメソッドが追加されている
- インスタンスの生成は FXCollectionsのファクトリメソッドを使用する- 
FXCollectionsはjava.util.Collectionsと似たようなメソッドを持つ、 JavaFX のコレクション用のユーティリティクラス
 
- 
- 
addListener(ListChangeListener)で、変更の通知を受け取るリスナーを登録できる- 
ObservableListはObservableも継承している
- そして、 ObservableにもaddListener(InvalidationListener)という名前が同じで引数が異なるメソッドが定義されている
- 両者が引数で受け取るリスナーは、どちらも関数型インターフェースで、しかもメソッドの引数が1つしかない
- つまり、引数の型を省略した最もシンプルなラムダ式を書くと、コンパイラはどちらの addListener()が使用されているのか判別できなくなり、コンパイルエラーになる
- 仕方ないので、ラムダ式を書く場合でも最低限引数の型は省略せずに書かなければならない
 
- 
変更内容を確認する
リストが変更されると、リスナーには Change オブジェクトが渡される。
Change オブジェクトにはリストがどのように変更されたのかについての情報が記録されている。
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            while (change.next()) {
                System.out.println("list=" + change.getList());
                if (change.wasAdded()) {
                    System.out.println("追加 : " + change);
                }
                if (change.wasRemoved()) {
                    System.out.println("削除 : " + change);
                }
                if (change.wasPermutated()) {
                    System.out.println("順序変更 : " + change);
                }
                if (change.wasReplaced()) {
                    System.out.println("置換 : " + change);
                }
            }
        });
        list.add("FOO");
        list.remove("one");
        FXCollections.sort(list);
        list.set(1, "hoge");
    }
}
==========================================
list=[one, two, three, FOO]
追加 : { [FOO] added at 3 }
==========================================
list=[two, three, FOO]
削除 : { [one] removed at 0 }
==========================================
list=[FOO, three, two]
順序変更 : { permutated by [2, 1, 0] }
==========================================
list=[FOO, hoge, two]
追加 : { [three] replaced by [hoge] at 1 }
削除 : { [three] replaced by [hoge] at 1 }
置換 : { [three] replaced by [hoge] at 1 }
- 
Changeオブジェクトから変更内容を取得する前に、必ずnext()メソッドを実行しなければならない- 
next()を呼ばずに変更内容を取得しようとすると、IllegalStateExceptionがスローされる
- 一度の通知で複数の変更があった場合は、 next()を呼ぶことで次の変更に移ることができる
- 後述する removeAll()を使うと複数の変更が一度に通知されることがあるので、whileで回すようにしておけば全ての変更を確認できる
 
- 
- 変更には「追加」や「削除」などの種類がある
- どの種類の変更が発生したかは、 wasAdded()やwasRemoved()などのwas***()メソッドで確認できる
 
- どの種類の変更が発生したかは、 
- リスナーの実装には次の注意点がある
- Changeオブジェクトを別スレッドで使ってはいけない
- リスナー内でリストの状態を変更してはいけない
- Change オブジェクトの Javadoc に記載されている
 
追加された要素を確認する
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import java.util.Arrays;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            while (change.next()) {
                System.out.println("list=" + change.getList());
                System.out.println("added=" + change.wasAdded());
                System.out.println("from=" + change.getFrom());
                System.out.println("to=" + change.getTo());
                System.out.println("addedSubList=" + change.getAddedSubList());
                System.out.println("addedSize=" + change.getAddedSize());
            }
        });
        list.add("FOO");
        list.addAll(Arrays.asList("hoge", "fuga"));
    }
}
==========================================
list=[one, two, three, FOO]
added=true
from=3
to=4
addedSubList=[FOO]
addedSize=1
==========================================
list=[one, two, three, FOO, hoge, fuga]
added=true
from=4
to=6
addedSubList=[hoge, fuga]
addedSize=2
- 要素が追加されると、 wasAdded()がtrueを返す
- 
getFrom(),getTo()で、要素が追加された箇所を指すインデックスを取得できる- 
getFrom()は、要素が追加された先頭のインデックス(このインデックスを含む)
- 
getTo()は、要素が追加された末尾のインデックス(このインデックスは含まない)
 
- 
- 
getAddedSubList()で、追加された要素だけを含むListを取得できる
- 
getAddedSize()で、追加された要素数を取得できる
削除された要素を確認する
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list
            = FXCollections.observableArrayList(
                    "one", "two", "three", "four", "five", "six", "seven");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            System.out.println("list=" + change.getList());
            while (change.next()) {
                System.out.println("----------------------------------");
                System.out.println("removed=" + change.wasRemoved());
                System.out.println("from=" + change.getFrom());
                System.out.println("to=" + change.getTo());
                System.out.println("removed=" + change.getRemoved());
                System.out.println("removedSize=" + change.getRemovedSize());
            }
        });
        list.remove("three");
        list.removeAll("two", "seven");
        list.retainAll("one", "six");
    }
}
==========================================
list=[one, two, four, five, six, seven]
----------------------------------
removed=true
from=2
to=2
removed=[three]
removedSize=1
==========================================
list=[one, four, five, six]
----------------------------------
removed=true
from=1
to=1
removed=[two]
removedSize=1
----------------------------------
removed=true
from=4
to=4
removed=[seven]
removedSize=1
==========================================
list=[one, six]
----------------------------------
removed=true
from=1
to=1
removed=[four, five]
removedSize=2
- 要素が削除されると、 wasRemoved()がtrueを返す
- 削除の場合、 getFrom()は削除された要素の、元のListでの開始インデックスを返してる- また getTo()はgetFrom()と同じ値を返している(Javadoc の記述的にそれでいいのか若干あやしい気がするが)
- 
Changeの Javadoc をよく見ると、削除時のgetFrom()とgetTo()については言及していないように読めなくもない気がするがどうなんだろう(実は未確定とか?)
 
- また 
- 
getRemoved()で、削除された要素をListで取得できる
- 
getRemovedSize()は削除された要素数を返す
並び順の変更を確認する
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list
            = FXCollections.observableArrayList("aaa", "ccc", "bbb", "eee", "ddd", "fff");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            System.out.println("new list=" + change.getList());
            while (change.next()) {
                System.out.println("permutated=" + change.wasPermutated());
                for (int oldIndex=change.getFrom(); oldIndex<change.getTo(); oldIndex++) {
                    int newIndex = change.getPermutation(oldIndex);
                    System.out.println("old(" + oldIndex + ") -> new(" + newIndex + ")");
                }
            }
        });
        System.out.println("old list=" + list);
        list.sort(String::compareTo);
    }
}
old list=[aaa, ccc, bbb, eee, ddd, fff]
==========================================
new list=[aaa, bbb, ccc, ddd, eee, fff]
permutated=true
old(0) -> new(0)
old(1) -> new(2)
old(2) -> new(1)
old(3) -> new(4)
old(4) -> new(3)
old(5) -> new(5)
- リストの順序が変更されると、 wasPermutated()がtrueを返す
- 
getPermutated(int)に変更前のインデックスを渡すと、順序変更後のインデックスを返す
置換された要素を確認する
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            while (change.next()) {
                System.out.println("list=" + change.getList());
                
                System.out.println("replaced=" + change.wasReplaced());
                System.out.println("added=" + change.wasAdded());
                System.out.println("removed=" + change.wasRemoved());
                
                System.out.println("from=" + change.getFrom());
                System.out.println("to=" + change.getTo());
                System.out.println("addedSubList=" + change.getAddedSubList());
                System.out.println("addedSize=" + change.getAddedSize());
                System.out.println("removed=" + change.getRemoved());
                System.out.println("removedSize=" + change.getRemovedSize());
            }
        });
        list.set(1, "FOO");
    }
}
==========================================
list=[one, FOO, three]
replaced=true
added=true
removed=true
from=1
to=2
addedSubList=[FOO]
addedSize=1
removed=[two]
removedSize=1
- 要素が別の要素に置き換えられると、 wasReplaced()がtrueを返す- 厳密には wasReplaced()はwasAddedd() && wasRemoved()のときにtrueとなる
- つまり、 wasReplaced()だけがtrueになることはない
- 
wasReplaced()がtrueとなるときは、wasAddedd()とwasRemoved()も必ずtrueとなる
 
- 厳密には 
ソートされたリスト
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import java.util.Comparator;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        SortedList<String> sortedList = list.sorted();
        System.out.println("list=" + list);
        System.out.println("sortedList=" + sortedList);
        list.addAll("four", "five", "six");
        System.out.println("list=" + list);
        System.out.println("sortedList=" + sortedList);
        
        sortedList.setComparator(Comparator.reverseOrder());
        System.out.println("list=" + list);
        System.out.println("sortedList=" + sortedList);
    }
}
list=[one, two, three]
sortedList=[one, three, two]
list=[one, two, three, four, five, six]
sortedList=[five, four, one, six, three, two]
list=[one, two, three, four, five, six]
sortedList=[two, three, six, one, four, five]
- 
sorted()メソッドを実行すると、元のObservableListと連動したソートされたリスト(SortedList)を取得できる
- 元の ObservalbeListを変更すると、SortedListの状態もソートされた状態で更新される
- 
SortedListはObservableListを実装している
- 引数に Comparatorを渡せるsorted(Comparator)メソッドもある
- 
setComparator(Comparator)で後からソート方法を変更することもできる
フィルターされたリスト
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        FilteredList<String> filteredList = list.filtered(e -> e.contains("e"));
        System.out.println("list=" + list);
        System.out.println("filteredList=" + filteredList);
        
        list.addAll("four", "five");
        System.out.println("list=" + list);
        System.out.println("filteredList=" + filteredList);
        filteredList.setPredicate(e -> e.contains("f"));
        System.out.println("list=" + list);
        System.out.println("filteredList=" + filteredList);
    }
}
list=[one, two, three]
filteredList=[one, three]
list=[one, two, three, four, five]
filteredList=[one, three, five]
list=[one, two, three, four, five]
filteredList=[four, five]
- 
filtered(Predicate)メソッドを実行すると、元のObservableListと連動したフィルターされたリスト(FilteredList)を取得できる
- 元の ObservalbeListを変更すると、FilteredListの状態もフィルターされた状態で更新される
- 
setPredicate()で後から絞り込み条件を変更することもできる
- 
FilteredListはObservableListを実装している
監視可能な Map
package sample.javafx.property;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener.Change;
import javafx.collections.ObservableMap;
public class Main {
    public static void main(String[] args) {
        ObservableMap<String, String> map = FXCollections.observableHashMap();
        map.put("foo", "FOO");
        map.put("bar", "BAR");
        
        map.addListener((Change<? extends String, ? extends String> change) -> {
            System.out.println("==============================");
            System.out.println("map=" + change.getMap());
            System.out.println("key=" + change.getKey());
            System.out.println("added=" + change.wasAdded());
            System.out.println("valueAdded=" + change.getValueAdded());
            System.out.println("removed=" + change.wasRemoved());
            System.out.println("valueRemoved=" + change.getValueRemoved());
        });
        
        map.put("fizz", "FIZZ");
        map.put("bar", "BARBAR");
        map.remove("foo");
    }
}
==============================
map={bar=BAR, foo=FOO, fizz=FIZZ}
key=fizz
added=true
valueAdded=FIZZ
removed=false
valueRemoved=null
==============================
map={bar=BARBAR, foo=FOO, fizz=FIZZ}
key=bar
added=true
valueAdded=BARBAR
removed=true
valueRemoved=BAR
==============================
map={bar=BARBAR, fizz=FIZZ}
key=foo
added=false
valueAdded=null
removed=true
valueRemoved=FOO
- 監視可能な Mapとして、ObservableMapが用意されている
- インスタンスの生成には、 FXCollections.observableHashMap()を使用する
- 
MapのChangeは、Listほど複雑ではない- 
Listとは違い、next()は不要
- 変更の種類は「追加(wasAdded())」と「削除(wasRemoved())」の2つ
- 
getValueAdded()で追加された値を、
 getValueRemoved()で削除された値を取得できる
- 既に存在するキーに別の値を設定した場合は「追加」と「削除」の変更が同時に発生する
 
- 
スタイルシート
基本
`-src/main/
  |-java/sample/javafx/
  | |-Main.java
  | `-MainController.java
  |
  `-resources/
    |-main.fxml
    `-my-style.css
main.fxml
.label {
    -fx-background-color: skyblue;
}
package sample.javafx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.net.URL;
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add("my-style.css");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
実行結果
スタイルシートの読み込み
        Scene scene = new Scene(root);
        scene.getStylesheets().add("my-style.css");
- スタイルシートのファイルは、 Scene.getStylesheets()でスタイルシートの URL を持つリストを取得し、そこにadd()することで追加できる- このシーンで使用するスタイルシートを複数登録できる
 
- URL は [scheme:][//authority][path]という形式で記述する- 
[scheme:]が省略された場合は[path]のみとみなされる
- 
[path]は、クラスパスのルートからの相対パスとして処理される
- 詳しくは Scene.getStylesheets() の Javadoc を参照
 
- 
CSS の書き方
.label {
    -fx-background-color: skyblue;
}
- CSS の書き方は、基本的に HTML で使用する CSS と同じ
- セレクタ {プロパティ: 値;}
 
- ただし、プロパティ名がほぼ例外なく -fxという接頭辞で始まるようになっている
class セレクタ
- HTML の CSS で .xxxxとした場合、class="xxxx"が設定されているタグにマッチする
- 一方で、 JavaFX の CSS の場合は .xxxxはstyleClass="xxxx"にマッチする
ノードに設定されているデフォルトの styleClass
- いくつかのノード(Label,ListViewなど)には、デフォルトで特定の変換ルールに従って命名されたstyleClass属性が設定されている
- 例えば、 Labelクラスの場合はlabelというstyleClassが、ListViewクラスにはlist-viewというstyleClassがデフォルトで設定されている
- 変換ルールは単純で、クラス名の各単語を -で区切るようにして、全てを小文字にしている
- ただし、この変換は何かしらの自動変換ロジックがあるわけではなく、上記ルールに従って各クラスで設定しているだけにすぎない
- どういうことかというと、 Labelクラスの実装を見ればわかる
...
public class Label extends Labeled {
    public Label() {
        initialize();
    }
...
    private void initialize() {
        getStyleClass().setAll("label");
        ...
    }
- コンストラクタで initialzie()が呼ばれ、styleClassプロパティにlabelという文字列が渡されている(クラス名から自動変換されたstyleClassが設定されているわけではない)
- ということで、 LabelクラスのインスタンスにはデフォルトでlabelというstyleClassが設定されているので、 CSS ファイルではそれを.を使用したセレクタを使って指定している(.label)
- どのノードにどういうデフォルトのクラスが設定されているかは、 こちら に載っている
- 「スタイル・クラス: デフォルトでは空」とか
 「スタイル・クラス: button」って書いているところ
 
- 「スタイル・クラス: デフォルトでは空」とか
プロパティの実体
.my-class {
    -fx-background-color: pink;
    -fx-padding: 10px;
    
    -fx-border-color: black;
    -fx-border-width: 1px;
    -fx-border-style: solid;
    
    -fx-underline: true;
}
実行結果
- CSS で指定するプロパティ(-fx-background-colorとか-fx-paddingとか)は、一見すると HTML の CSS で指定できるプロパティの頭に-fxがついているだけに見える
- なので、使えるプロパティがよくわからない場合は、 HTML の CSS で使っているプロパティに -fxをつければ何とかなりそうな気がする
- しかし、実際は -fx-underline: true;のように、 HTML の CSS には存在しないプロパティもある- HTML の CSS なら text-decoration: underline;
 
- HTML の CSS なら 
- これは、実際は対象のノードが持つプロパティを指している
- サンプルでは Labelクラスに対して CSS を指定しているが、underlineはLabelクラスの親クラスであるLabeledクラスの underline プロパティ を指定していることになっている
- ただし、ノードのプロパティと CSS で指定するプロパティが常に完全に一致しているかというと、そうでもない
- 例えば borderやbackgroundは、-fx-border,-fx-backgroundではなく-fx-border-styleや-fx-background-colorのように細かいプロパティに分けられている
- どのノードに対してどのプロパティが指定できるのかについては、公式のドキュメントで詳しく説明されている
任意の styleClass 属性をセットする
Scene Builder でセットする
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.text.Font?>
<Label fx:id="label" alignment="CENTER" prefHeight="200.0" prefWidth="300.0"
       styleClass="my-class"
       text="CSS Test" xmlns="http://javafx.com/javafx/8.0.151" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.javafx.MainController">
   <font>
      <Font size="30.0" />
   </font>
</Label>
.my-class {
    -fx-font-weight: bold;
    -fx-underline: true;
    -fx-text-fill: red;
}
実行結果
- Scene Builder の Style Classで要素に対して任意のstyleClass属性を設定できる
プログラムから設定する
package sample.javafx;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private Label label;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        ObservableList<String> styleClass = label.getStyleClass();
        styleClass.add("my-class");
        System.out.println("styleClass=" + styleClass);
    }
}
- ノードから getStyleClass()でstyleClassのObservableListを取得する
- そのリストに add()することで任意の属性を追加できる
ローカルのスタイルシートファイルを読み込む
|-my-styles.css
`-src/main/java/
  `-sample/javafx/
    |-Main.java
    :
.my-class {
    -fx-background-color: green;
}
package sample.javafx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.net.URL;
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add("file:./my-style.css"); // ★ローカルファイルを指定
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
実行結果
- ローカルの CSS ファイルを指定する場合は file:【パス】と指定する
- 
PathやFileで読み込んでいる場合は、toURI(),toUri()メソッドでURIオブジェクトを取得して、そのtoString()を渡せばいい
背景画像の設定
-fx-background-image の設定方法について
|-image.jpg
`-src/main/
  |-resources/
  | |-img/
  | | `-image.jpg
  | |-my-style.css
  | `-main.fxml
  :
クラスパス内のファイルを指定する
.my-class {
    -fx-background-image: url('/img/image.jpg');
}
- 
urlで指定する
- 書式は CSS ファイルの読み込みと同じで、 schemaを省略した場合はクラスパス以下のファイルになる
ローカルのファイルを指定する
.my-class {
    -fx-background-image: url('file:./image.jpg');
}
- ローカルのファイルを指定する場合は file:【パス】とする
イベント・キャプチャ、イベント・バブリング
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private Pane bluePane;
    @FXML
    private Pane yellowPane;
    @FXML
    private Button button;
    @FXML
    private TextField textField;
    @FXML
    private Label label;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.registerHandlers(this.bluePane, "bluePane");
        this.registerHandlers(this.yellowPane, "yellowPane");
        this.registerHandlers(this.button, "button");
        this.registerHandlers(this.textField, "textField");
        this.registerHandlers(this.label, "label");
    }
    
    private void registerHandlers(Node node, String name) {
        this.registerEventFilter(node, name);
        this.registerEventHandler(node, name);
    }
    private void registerEventFilter(Node node, String name) {
        node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("filter " + name));
    }
    private void registerEventHandler(Node node, String name) {
        node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("handler " + name));
    }
}
実行結果
filter bluePane
handler bluePane
filter bluePane
filter yellowPane
handler yellowPane
handler bluePane
filter bluePane
filter yellowPane
filter button
handler button
filter bluePane
filter yellowPane
filter textField
handler textField
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane
説明
イベントが発生すると、次の順序で処理が実行される
- ターゲットの選択
- 経路1の決定
- イベント・キャプチャ・フェーズ
- イベント・バブリング・フェーズ
ターゲットの選択
- まず最初にイベントが発生したノード(ターゲット)が決定される
- 例えば、クリックイベントならクリックしたノードが、キーイベントならフォーカスが当たっているノードがターゲットになる
経路の決定
- 次に、ターゲットからシーングラフを親に向かって辿って行き、ルート(root)ノードまでの経路(route)を決定する
- この経路に沿って、次のイベント・キャプチャ・フェーズとイベント・バブリング・フェーズが実行される
イベント・キャプチャ・フェーズ
filter bluePane
filter yellowPane
filter textField
...
- イベント・キャプチャ・フェーズでは、決定された経路をルートからターゲットに向かって各ノードに登録されているイベント・フィルタが実行される
    private void registerEventFilter(Node node, String name) {
        node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("filter " + name));
    }
- イベント・フィルタは addEventFilter(EventType, EventHandler)でノードに登録することができる
イベント・バブリング・フェーズ
...
handler label
handler yellowPane
handler bluePane
- イベント・バブリング・フェーズでは、逆にターゲットからルートノードに向かって各ノードに登録されているイベント・ハンドラが実行される
    private void registerEventHandler(Node node, String name) {
        node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("handler " + name));
    }
- イベント・ハンドラは、 addEventHandler(EventType, EventHandler)で登録できる
- なおマウスクリックなどよく利用するイベント・ハンドラについては、専用の設定メソッドが用意されているので、そちらを使うこともできる
- 
setOnMouseClicked(EventHandler)など
 
- 
- FXML の onMouseClicked属性などでイベント処理を登録した場合も、このイベント・ハンドラが登録されている
イベントを消費する
package sample.javafx;
...
public class MainController implements Initializable {
    ...
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.registerHandlers(this.bluePane, "bluePane");
        
        this.registerEventFilter(this.yellowPane, "yellowPane");
        this.yellowPane.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
            System.out.println("handler yellowPane");
            e.consume();
        });
        
        this.registerHandlers(this.button, "button");
        this.registerHandlers(this.textField, "textField");
        this.registerHandlers(this.label, "label");
    }
    
    ...
}
- 黄色背景の領域(yellowPane)のイベント・ハンドラで、MouseEventのconsume()メソッドを実行している
実行結果
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
- 
Event.consume()メソッドを実行することで、イベントの連鎖を中断できる
- これを、イベントの消費と表現する
- イベント・キャプチャの段階でイベントを消費すると、イベント・バブリングも行われなくなる
UIコントロールのなかにはイベント・バブリングが中断されるものがある
filter bluePane
filter yellowPane
filter button
handler button
filter bluePane
filter yellowPane
filter textField
handler textField
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane
- 
Labelは普通にイベント・バブリングが実行されているが、Button,TextFieldは中断されている
- イベント・キャプチャ・フェーズはどれも実行されている
一応公式ドキュメントには、以下のように書かれている。
JavaFX UIコントロールのデフォルト・ハンドラでは一般にほとんどの入力イベントが消費されます。
1 イベントの処理(リリース8) | JavaFX: イベントの処理
ただ、具体的にどのクラスがイベントを消費するかをどこで確認すればいいかはよく分かってない。
ドラッグ&ドロップ
ドロップ
基本
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
public class MainController {
    
    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        String string = dragboard.getString();
        System.out.println("dropped string = " + string);
        e.consume();
    }
}
実行結果
説明
ドロップの受け入れ
    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }
- ドロップ先のノードに対して DRAG_OVERイベントのハンドラを登録する
- そこで、 DragEvent.acceptTransferModes()メソッドを実行することで、ドロップの受け入れができるようになる- 引数にはどの転送モード(COPY,MOVE,LINKのいずれか)を受け入れるかをTransferModeの定数で指定する
- 
ANYは3つの転送モードの全てを表す(つまり、なんでもOK)。
 
- 引数にはどの転送モード(
- ドロップを受け入れないと、マウスカーソールがドロップ不可能を表すアイコンになる
- 最後に consume()メソッドを実行してイベントを消費する- 消費しなくても一応動くは動くが、公式のドキュメントには必ず consume()が記載されている
- たぶん、親ノードにもドラッグ&ドロップの処理が存在した場合に、間違ってそちらが実行されないようにしているのかもしれない
 
- 消費しなくても一応動くは動くが、公式のドキュメントには必ず 
- 今回は常に acceptTransferModes()を実行しているが、条件によって呼び出しを制御することで、ドロップしようとしている内容によってドロップを許可するかどうかを制御したりできる
ドロップ時の処理
    @FXML
    public void onDragDropped(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        String string = dragboard.getString();
        System.out.println("dropped string = " + string);
        e.consume();
    }
- ドロップ時の処理は、対象のノードの DRAG_DROPPEDイベントにハンドラを登録する
- 
DragEvent.getDragboard()で取得したDragboardからドロップ内容にアクセスできる
ファイルのドロップ
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import java.io.File;
import java.util.List;
public class MainController {
    
    @FXML
    public void onDragOver(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        
        if (dragboard.hasFiles()) {
            e.acceptTransferModes(TransferMode.ANY);
        }
        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();
        List<File> files = dragboard.getFiles();
        files.forEach(file -> System.out.println(file.getName()));
        e.consume();
    }
}
実行結果
説明
    @FXML
    public void onDragOver(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        
        if (dragboard.hasFiles()) {
            e.acceptTransferModes(TransferMode.ANY);
        }
        e.consume();
    }
- 
DragEvent.getDragboard()で取得し、Dragboard.hasFiles()でドラッグされているものがファイルかどうかを確認できる
- ここでは hasFiles()がtrueのときだけacceptTransferModes()を実行しているので、ファイル以外のドロップはできないようになる
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();
        List<File> files = dragboard.getFiles();
        files.forEach(file -> System.out.println(file.getName()));
        e.consume();
    }
- 
Dragboard.getFiles()で、ドラッグ&ドロップされたファイルを取得できる
HTML のドロップ
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
public class MainController {
    
    @FXML
    public void onDragOver(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        
        if (dragboard.hasHtml()) {
            e.acceptTransferModes(TransferMode.ANY);
        }
        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();
        System.out.println("[html]   = " + dragboard.getHtml());
        System.out.println("[string] = " + dragboard.getString());
        System.out.println("[files]  = " + dragboard.getFiles());
        e.consume();
    }
}
実行結果
説明
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();
        System.out.println("[html]   = " + dragboard.getHtml());
        System.out.println("[string] = " + dragboard.getString());
        System.out.println("[files]  = " + dragboard.getFiles());
        e.consume();
    }
- ブラウザのテキストなどをドロップすると、そのテキストを再現する HTML を getHtml()で取得できる
- 
getString()なら素のテキストでも取得できる
ドロップ時のスタイルを変更する
.drag-over {
    -fx-text-fill: red;
}
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
public class MainController {
    
    @FXML
    private Label dropLabel;
    
    @FXML
    public void onDragEntered() {
        this.dropLabel.getStyleClass().add("drag-over");
    }
    
    @FXML
    public void onDragExited() {
        this.dropLabel.getStyleClass().remove("drag-over");
    }
    
    ...
}
実行結果
説明
- ドロップ対象のノードに DRAG_ENTEREDとDRAG_EXITEDのイベントハンドラを登録する
- 
DRAG_ENTEREDでノードにスタイルを追加し、DRAG_EXITEDでスタイルを外すように実装する
- こうすることで、ドロップ待機中をより分かりやすく表現することができる
ドラッグ
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
public class MainController {
    
    @FXML
    private Label dragLabel;
    
    @FXML
    public void onDragDetected(MouseEvent e) {
        Dragboard dragboard = this.dragLabel.startDragAndDrop(TransferMode.ANY);
        
        ClipboardContent content = new ClipboardContent();
        content.putString("Drag Test");
        dragboard.setContent(content);
        e.consume();
    }
    
    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();
        System.out.println(dragboard.getString());
        
        e.consume();
    }
}
実行結果
説明
    @FXML
    private Label dragLabel;
    
    @FXML
    public void onDragDetected(MouseEvent e) {
        Dragboard dragboard = this.dragLabel.startDragAndDrop(TransferMode.ANY);
        
        ClipboardContent content = new ClipboardContent();
        content.putString("Drag Test");
        dragboard.setContent(content);
        e.consume();
    }
- ドラッグの開始は、 DRAG_DETECTEDイベントにハンドラを登録することで開始する
- ドラッグを開始するノードの startDragAndDrop()メソッドを実行する- 引数には、ドラッグでサポートする転送モードを TransferModeで指定する
- ドロップ側は、ここで設定した転送モードと一致する転送モードを acceptTransferModes()で指定しているときだけ、マウスカーソルがドロップ可能なアイコンになる
 
- 引数には、ドラッグでサポートする転送モードを 
- ドラッグ内容を ClipboardContentにセットして、そのClipboardContentをDragboardに設定する- 
putString()以外にも、putHtml()やputFiles()などがある
- 
ClipboardContent自体はHashMap<DataFormat, Object>を継承しているマップなので、任意の値を保存できる
 
- 
参考
ダイアログ
※Alert などのクラスは JDK8u40 以上でないと使えない(それ以前の場合は自力でダイアログウィンドウを作る必要がある)。
準備
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
public class MainController {
    
    @FXML
    public void showDialog() {
        ...
    }
}
ボタンをクリックしたら showDialog() メソッドが呼ばれるようにしておく。
基本
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.showAndWait();
実行結果
- 
Alertクラスを使うことでダイアログを表示できる
- コンストラクタにダイアログの種類を指定する
- 種類は AlertType列挙型の定数を指定する
- 
CONFIRMATIONは確認ダイアログ
 
- 種類は 
- 
showAndWait()メソッドでダイアログを表示する- 
AndWaitとあるように、このメソッドでダイアログを表示すると、ダイアログが閉じられるまで処理がブロックされる
 
- 
- ダイアログはモーダルになっており、表示されている間は親ウィンドウを触れない
選択されたボタンを確認する
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
Optional<ButtonType> buttonType = alert.showAndWait();
buttonType.ifPresent(System.out::println);
実行結果
「OK」「取消」ボタンをそれぞれクリック。
ButtonType [text=OK, buttonData=OK_DONE]
ButtonType [text=取消, buttonData=CANCEL_CLOSE]
- 
showAndWait()の戻り値はButtonTypeのOptionalになっており、閉じるときに選択されたボタンを確認できる
- キャンセル系のボタン2以外しかないダイアログ(「はい」ボタンしかないダイアログとか)が ESCやウィンドウの閉じるボタンで閉じられたときに、showAndWait()の戻り値はOptional.empty()になる- 厳密には resultConverterが設定されているケースもあるが、その辺は Javadoc を参照のこと
 
- 厳密には 
メッセージを設定する
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ");
alert.showAndWait();
- コンストラクタ引数の第二引数で、ダイアログにセットするメッセージを指定できる
ダイアログの種類
INFORMATION
CONFIRMATION
WARNING
ERROR
ボタンを指定する
Alert alert = new Alert(
        Alert.AlertType.INFORMATION, 
        "めっせーじ",
        ButtonType.APPLY,
        ButtonType.CANCEL,
        ButtonType.CLOSE,
        ButtonType.FINISH,
        ButtonType.NEXT,
        ButtonType.NO,
        ButtonType.OK,
        ButtonType.PREVIOUS,
        ButtonType.YES);
alert.showAndWait();
- コンストラクタの第三引数以降に、 ButtonTypeを指定できる- 
ButtonTypeにはよく使うボタンが定数として定義されているので、それで済むならそれを利用する
- 自作することもできる(後述)
 
- 
- 可変長引数なので、複数の ButtonTypeを指定できる
ボタンの配置順序
- 可変長引数の順序がそのままボタンの配置順序になるわけではない
- ボタンの配置には ButtonBarというクラスが使用されている
- 
ButtonBarは、ボタン配置のためのクラスで、 OS ごとに固有のボタン配置を再現できる- 各 OS ごとのボタン配置の順序は ButtonBar の Javadoc に例のイメージが掲載されている
 
- ボタンは種類ごとに**ボタン順序(button order)**が決められており、ボタン順序によって ButtonBarのどこにボタンが配置されるかが決まる- 例えば、「はい」「いいえ」のボタンは、 Windows なら 「はい」→「いいえ」 の順序で配置されるが、 Mac や Linux では「いいえ」→「はい」の順序で配置される
 
- このボタンの種類を表現するクラスとして、 ButtonData 列挙型が用意されている
- 
Alertのコンストラクタで指定していたButtonTypeクラスの定数にも、このButtonDataが設定されていて、その値に従ってボタンが配置されている
ButtonType の定数に設定されている ButtonData
| ButtonType | 設定されているButtonData | 
|---|---|
| APPLY | APPLY | 
| CANCEL | CANCEL_CLOSE | 
| CLOSE | CANCEL_CLOSE | 
| FINISH | FINISH | 
| NEXT | NEXT_FORWARD | 
| NO | NO | 
| OK | OK_DONE | 
| PREVIOUS | BACK_PREVIOUS | 
| YES | YES | 
- 各 OS ごとの、 ButtonDataの詳細な配置順序は Javadoc に記載されている
OS ごとの ButtonData の配置順序
| OS | 配置順序 | 
|---|---|
| Windows | L_E+U+FBXI_YNOCAH_R | 
| Mac OS | L_HE+U+FBIX_NCYOA_R | 
| Linux | L_HE+UNYACBXIO_R | 
- この謎の文字列は、1文字1文字が ButtonDataの定数に割り当てられているボタン順序コードに対応している
- 例えば、 ButtonData.LEFTのボタン順序コードはLで、ButtonData.HELP_2はEといった具合- 定数に割り当てられているボタン順序コードは、各定数の Javadoc に記載されている
 
- もうちょい見やすくするために、 ButtonDataの定数との関係を表にすると下のような感じ
- (Fは対応する定数が存在しないのでよくわかってない)
- つまり、 ButtonType.CANCELをAlertのコンストラクタで指定した場合、
 ButtonType.CANCELのButtonDataはCANCEL_CLOSEなので、ButtonBarの順序でCとなっている部分にキャンセルボタンが挿入されることになる
任意のボタンを配置する
ButtonType myButton = new ButtonType("ぼたん");
Alert alert = new Alert(Alert.AlertType.INFORMATION, "めっせーじ", myButton);
alert.showAndWait().ifPresent(System.out::println);
実行結果
ButtonType [text=ぼたん, buttonData=OTHER]
- 
ButtonTypeクラスを生成してAlertのコンストラクタに渡せば、任意のボタンを配置できる
- 
ButtonTypeのコンストラクタでは、ボタンのラベル文字列を指定できる
- 何も指定していない場合、 ButtonDataはOTHERになる
- 
ButtonTypeにはButtonDataを受け取るコンストラクタも存在するので、明示的に指定することも可能
ボタン以外で閉じられたときの判定
- ダイアログは ESCやAlt + F4, ウィンドウの閉じるボタンなどでも閉じることができる(Windows の場合)
- その場合、 showAndWait()の戻り値は配置されたボタンによってはやや複雑な動きをする
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ", ButtonType.YES);
Optional<ButtonType> buttonType = alert.showAndWait();
String result = buttonType.map(ButtonType::getText).orElse("選択無し");
System.out.println(result);
実行結果
ESC で閉じたときの結果
選択無し
- ボタンの中にはキャンセル系のボタン2が無いので、戻り値は Optional.empty()になる
- つぎに、キャンセル系のボタンが複数ある場合
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
Optional<ButtonType> buttonType = alert.showAndWait();
String result = buttonType.map(ButtonType::getText).orElse("選択無し");
System.out.println(result);
実行結果
- 「はい」
- 「いいえ」
- 「取消」
- ESC
- Alt + F4
- ウィンドウの閉じる
の順序でダイアログを閉じてみる。
はい
いいえ
取消
取消
取消
取消
- 各ボタンが押されたときは、そのボタンが showAndWait()の戻り値となる
- ボタン以外の方法でダイアログが閉じた場合は、「取消」ボタンが押されたことになっている
- これは、キャンセル系のボタンが複数あった場合、
- 
ButtonDataがCANCEL_CLOSEになっているButtonType
- 
ButtonData.isCancelButton()がtrueを返すButtonType
 
- 
- の優先順序で戻り値が決定するようになっているため、こういう動きになっている
 
- これは、キャンセル系のボタンが複数あった場合、
- つまり、キャンセル系のボタンが ButtonType.NOだけの場合は、ボタン以外の方法でダイアログを閉じたときの戻り値はButtonType.NOになる
ボタン以外の方法でダイアログが閉じられたときの showAndWait() の戻り値をまとめると、
- キャンセル系のボタン2が1つも存在しない場合は Optional.empty()
- キャンセル系のボタンが1つある場合は、その ButtonType
- キャンセル系のボタンが2つ以上ある場合は、次の優先順序で戻り値の ButtonTypeが決まる- 
ButtonDataがCANCEL_CLOSEであるButtonType
- 
ButtonData.isCancelButton()がtrueを返すButtonType
 
- 
各種テキストの設定
コンストラクタ引数だと contentText しか設定できないが、 Alert を生成したあとならタイトルなども変更できる。
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("たいとる");
alert.setHeaderText("へっだーてきすと");
alert.setContentText("こんてんつてきすと");
alert.showAndWait();
それぞれは null を設定することで消すこともできる。
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(null);
alert.setHeaderText(null);
alert.setContentText("こんてんつてきすと");
alert.showAndWait();
スレッド
JavaFX でスレッドを使うときの基本的な話
UIスレッド
JavaFX の GUI 処理は、 JavaFX Application Thread という専用のスレッド(UIスレッド)上で実行される。
package sample.javafx;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        System.out.println("thread = " + Thread.currentThread().getName());
    }
}
thread = JavaFX Application Thread
普通に実装していると、この UI スレッド上で処理が実行されることになる。
しかし、時間のかかる処理をこの UI スレッド上で実行していると、 UI の更新やマウス操作などのイベント処理が待たされることになる。
すると、アプリケーションはユーザの操作を一切受け付けられなくなり、見た目も変化しない、いわゆる固まった状態になってしまう。
これを回避するには、時間のかかる処理を UIスレッドとは別のスレッドで実行する必要がある。
非 UI スレッドから UI を触る
JavaFX の UI コンポーネントは、 UI スレッドからしかアクセスできない。
UI スレッド以外から UI コンポーネントにアクセスしようとすると、例外がスローされるようになっている。
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class MainController {
    @FXML
    private Button button;
    
    @FXML
    public void click() {
        new Thread(() -> {
            System.out.println("thread = " + Thread.currentThread().getName());
            button.setText("hoge");
        }).start();
    }
}
thread = Thread-4
Exception in thread "Thread-4" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4
	at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:279)
	at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
	at javafx.scene.Parent$2.onProposedChange(Parent.java:367)
        ...
このような制限は JavaFX だけでなく、 Swing など他の GUI に関係するフレームワーク全般でも存在することが多い。
GUI はマルチスレッドで動いたときにスレッドセーフを実現するのが非常に困難らしい。
そのため、たいていの GUI フレームワークは UI 操作のための専用スレッドを用意し、 UI はその専用スレッドからのみアクセスできるようにしている(「GUI, シングルスレッド」で検索するとその辺の話が色々出てくる)。
JavaFX では、 UI スレッド以外から UI コンポーネントを操作するための手段として、 Platform.runLater()というメソッドが用意されている。
package sample.javafx;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class MainController {
    @FXML
    private Button button;
    
    @FXML
    public void click() {
        new Thread(() -> {
            System.out.println("thread = " + Thread.currentThread().getName());
            
            Platform.runLater(() -> {
                System.out.println("runLater thread = " + Thread.currentThread().getName());
                button.setText("hoge");
            });
        }).start();
    }
}
thread = Thread-4
runLater thread = JavaFX Application Thread
runLater() に渡したラムダ式(型は Runnable)は、 UIスレッド(JavaFx Application Thread)上で実行されるようになる。
そのため、この runLater() に渡した処理の中からなら、安全に UIスレッドを触ることができる。
JavaFX でスレッドを簡単に扱うための仕組み
JavaFX では、スレッドを使った処理をより簡単に実装できるようにするための仕組みが用意されている。
Worker
JavaFX で使用するスレッド処理は、 Worker として抽象化されている。
Worker には、通常の Thread にはない便利な API が定義されている。
(これ自体はインターフェースで、具体的な実装は Task または Service によって実現されている)
Worker のライフサイクル
Worker にはライフサイクルがあり、次のように状態が遷移する。
READY
- 
Workerが作成されると、まず READY 状態となる
- 開始状態 と呼ばれる状態は、この READY 状態のみ
SCHEDULED
- 続いて Workerの開始がスケジュールされると、 SCHEDULED 状態になる- スレッドプールを利用している場合に、プールが空くのを待つために SCHEDULED 状態で待機することがありえる
 
- 
Workerの実装によってはスケジュールされずに即座に起動される場合もある
- しかし、その場合も必ず SCHEDULED を経由してから RUNNING 状態になる
- 実行中を表す runningプロパティはtrueになる
RUNNING
- 
Workerが実際に実行されている状態
- 実行中を表す runningプロパティはtrueになる
SUCCEEDED
- 正常終了した場合にこの状態になる
- 結果が valueプロパティにセットされる
FAILED
- 実行中に例外がスローされた場合にこの状態になる
- スローされた例外は exceptionプロパティにセットされる
CANCELLED
- 終了状態になる前にキャンセルが実行されると、この状態になる
Worker には、これら状態についての情報を取得するための stateProperty() や runningProperty() といったプロパティが用意されている。
Task
Worker を実装したクラスとして Task というクラスが用意されている。
package sample.javafx;
import javafx.concurrent.Task;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Runnable task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                System.out.println("task.call()");
                System.out.println("thread = " + Thread.currentThread().getName());
                return null;
            }
        };
        new Thread(task).start();
    }
}
task.call()
thread = Thread-4
- 
Taskクラス自体は抽象クラスなので、継承して自作のクラスを作成する
- 
call()メソッドが抽象メソッドになっている
- 自作クラスで call()メソッドを実装し、この中でバックグラウンドで動かす処理を記述する
- 
TaskクラスはRunnableインターフェースを実装しているため、ThreadやExecutorに渡して実行できる
キャンセル
package sample.javafx;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    private Worker worker;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                System.out.println("start...");
                final long max = 10000000000000L;
                
                for (long i=0; i<max; i++) {
                    if (this.isCancelled()) {
                        System.out.println("cancelled!!");
                        return null;
                    }
                }
                System.out.println("finished");
                return null;
            }
        };
        
        this.worker = task;
        new Thread(task).start();
    }
    
    @FXML
    public void stop() {
        this.worker.cancel();
    }
}
initialize() でタスクを起動して、ボタンのイベントハンドラに登録している stop() メソッドで Worker の cancel() メソッドを呼んでいる。
start...
cancelled!!
- 
Workerのcancel()メソッドを呼ぶと、Workerの実行をキャンセルできる- キャンセルできるのは Workerの状態が「終了状態」以外のときに限られる
- 「終了状態」で cancel()を呼んだ場合は何も行われない
 
- キャンセルできるのは 
- キャンセルが実行されると、 TaskのisCancelled()がtrueを返すようになる- これを利用して、 call()メソッドの中でキャンセルされたかどうかが判定できる
- 
Thread.sleep()などでブロック中にキャンセルされた場合はInterruptedExceptionをキャッチしたうえでisCancelled()の判定を入れる
- 詳しくは Taskクラスの Javadoc を参照
 
- これを利用して、 
進捗
package sample.javafx;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ProgressBar;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private ProgressBar progressBar;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                final long max = 100000000L;
                
                for (long i=0; i<=max; i++) {
                    this.updateProgress(i, max);
                }
                
                return null;
            }
        };
        
        this.progressBar.progressProperty().bind(task.progressProperty());
        new Thread(task).start();
    }
}
実行結果
説明
        this.progressBar.progressProperty().bind(task.progressProperty());
- 
WorkerはprogressProperty()というプロパティを公開している
- これを ProgressBarのprogressProperty()にバインドすることで、TaskのprogressとProgressBarのprogressを連動させることができる
                for (long i=0; i<=max; i++) {
                    this.updateProgress(i, max);
                }
- 
Taskの進捗を更新するにはupdateProgress()メソッドを使用する
- 第一引数には処理済みの件数、第二引数には全体の件数を渡す(doubleを渡すメソッドもオーバーロードされている)
- この updateProgress()は非 UI スレッドから実行しても問題ないようになっている- 中で runLater()が使用されている
 
- 中で 
任意の値を公開する
package sample.javafx;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import java.io.IOException;
public class MainController {
    @FXML
    private Label label;
    @FXML
    public void start() throws IOException {
        MyTask task = new MyTask();
        this.label.textProperty().bind(task.valueProperty());
        
        new Thread(task).start();
    }
    
    private static class MyTask extends Task<String> {
        @Override
        protected String call() throws Exception {
            for (int i=0; i<100000000; i++) {
                this.updateValue("i=" + i);
            }
            
            return "finished!!";
        }
    }
}
実行結果
説明
    private static class MyTask extends Task<String> {
        @Override
        protected String call() throws Exception {
            for (int i=0; i<100000000; i++) {
                this.updateValue("i=" + i);
            }
            
            return "finished!!";
        }
    }
- 
Taskにはvalueというプロパティが用意されている
- この valueの型は、Taskの型引数で指定する
- 
valueの値はupdateValue()メソッドで更新できる- こちらも、非 UI スレッドから安全に実行できるようになっている
 
- 
Taskの実行が正常終了した場合、call()メソッドの戻り値が最終的にvalueプロパティにセットされる
イベントリスナー
package sample.javafx;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import java.io.IOException;
public class MainController {
    @FXML
    public void start() throws IOException {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                return null;
            }
        };
        
        task.setOnScheduled(event -> System.out.println("scheduled thread=" + Thread.currentThread().getName()));
        task.setOnRunning(event -> System.out.println("running thread=" + Thread.currentThread().getName()));
        task.setOnSucceeded(event -> System.out.println("succeeded thread=" + Thread.currentThread().getName()));
        
        new Thread(task).start();
    }
}
実行結果
scheduled thread=JavaFX Application Thread
running thread=JavaFX Application Thread
succeeded thread=JavaFX Application Thread
- 
setOn****メソッドで、状態変化を監視できるリスナーを登録できる
- 登録したリスナーは UI スレッドで実行される
例外プロパティ
package sample.javafx;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import java.io.IOException;
public class MainController {
    @FXML
    public void start() throws IOException {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                throw new RuntimeException("test");
            }
        };
        
        task.setOnFailed(event -> {
            Throwable exception = task.getException();
            System.out.println("exception=" + exception);
        });
        
        new Thread(task).start();
    }
}
exception=java.lang.RuntimeException: test
- 
call()メソッドの中で例外がスローされると、exceptionプロパティにスローされた例外がセットされる
- 例外はスタックの上位に伝播しないので、ほっといたら例外がスローされたことに気付かない気がする
ObservableList を公開する
package sample.javafx;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import java.util.concurrent.TimeUnit;
public class MainController {
    
    @FXML
    private Label label;
    
    @FXML
    public void start() {
        MyTask myTask = new MyTask();
        this.label.textProperty().bind(myTask.listProperty().asString());
        Thread thread = new Thread(myTask);
        thread.setDaemon(true);
        thread.start();
    }
    
    private static class MyTask extends Task<Void> {
        private ReadOnlyListWrapper<String> list = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());
        public final ObservableList<String> getList() {
            return list.get();
        }
        public ReadOnlyListProperty<String> listProperty() {
            return list.getReadOnlyProperty();
        }
        @Override
        protected Void call() throws Exception {
            for (int i=0; i<10; i++) {
                String value = String.valueOf(i);
                Platform.runLater(() -> this.list.add(value));
                
                TimeUnit.SECONDS.sleep(1);
            }
            return null;
        }
    }
}
- 
TaskにObservableListを持たせ、その状態を外部に公開する場合、リストの更新はPlatform.runLater()を使うように注意しないといけない
Service
package sample.javafx;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import java.io.IOException;
public class MainController {
    private Service<Void> service = new Service<Void>() {
        @Override
        protected Task<Void> createTask() {
            System.out.println("Service.createTask()");
            
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    System.out.println("Task.call()");
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        if (!this.service.getState().equals(Worker.State.READY)) {
            System.out.println("reset");
            this.service.reset();
        }
        System.out.println("start");
        this.service.start();
    }
}
start() メソッドをボタンのアクションイベントに割り当てて、何回か実行する。
start
Service.createTask()
Task.call()
reset
start
Service.createTask()
Task.call()
reset
start
Service.createTask()
Task.call()
- 
Taskは使い捨てが前提となっており、同じインスタンスをもう一度使いまわすような使い方は想定されていない
- 一方、 Serviceは何度もスレッド処理を実行することを想定して作られている
- 
ServiceはTaskをラップしており、スレッド処理を実行するたびにTaskのインスタンスを新規に作成してスレッドを起動するようになっている
- 
Serviceは抽象クラスで、このクラスを継承した独自クラスを作成して利用する
- 抽象メソッドとして createTask()が存在するので、これを実装する必要がある
- このメソッドでは、 Taskのインスタンスを新規作成する処理を実装する
- 
Serviceのstart()メソッドを実行すると、スレッドが起動してcreateTask()メソッドが生成したTaskが実行される
再実行
    @FXML
    public void start() throws IOException {
        if (!this.service.getState().equals(Worker.State.READY)) {
            System.out.println("reset");
            this.service.reset();
        }
        System.out.println("start");
        this.service.start();
    }
- 
ServiceはWorkerインターフェースを実装しているため、Serviceの状態はWorkerが定義するルールに従って変化する
- つまり、 READYから始まり、最終的にはSUCCEEDEDやFAILED,CANCELLEDになる
- 
READY以外の状態でServiceのstart()を実行するとIllegalStateExceptionがスローされる
- 状態を READYに戻す方法はいくつかある- 終了状態(SUCCEEDED,FAILED,CANCELLED)の場合はreset()メソッドを実行する
- 
SCHEDULED,RUNNINGの場合は、一度cancel()でキャンセルしてから、reset()メソッドを実行する
 
- 終了状態(
- いちいち状態を見て戻すのが面倒な場合は、 restart()メソッドを実行すればいい感じに処理してくれる- 状態が READYならそのまま処理を起動する
- 実行中なら処理を一旦キャンセルしてから再実行する
- 終了状態なら、一度 READYに戻してから再実行する
 
- 状態が 
    @FXML
    public void start() throws IOException {
        System.out.println("restart");
        this.service.restart();
    }
- 
start(),reset(),restart()メソッドは、どれも UI スレッドから実行しなければならない
使用するスレッドを指定する
まずは、デフォルトの動作を確認する。
package sample.javafx;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import java.io.IOException;
public class MainController {
    private Service<Void> service = new Service<Void>() {
        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    Thread thread = Thread.currentThread();
                    System.out.println("thread.name = " + thread.getName() + ", daemon = " + thread.isDaemon());
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        this.service.restart();
    }
}
何度か restart() を実行する
thread.name = Thread-4, daemon = true
thread.name = Thread-5, daemon = true
thread.name = Thread-6, daemon = true
thread.name = Thread-7, daemon = true
- デフォルトでは、 ServiceはTaskを起動するたびにデーモンスレッドを新規に作成するようになっている
- 使用するスレッドを指定する(たとえば、スレッドプールのスレッドを使用する)場合は、次のように実装する
package sample.javafx;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import java.io.IOException;
import java.util.concurrent.Executors;
public class MainController {
    private Service<Void> service = new Service<Void>() {
        {
            this.setExecutor(Executors.newFixedThreadPool(3));
        }
        
        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    Thread thread = Thread.currentThread();
                    System.out.println("thread.name = " + thread.getName() + ", daemon = " + thread.isDaemon());
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        this.service.restart();
    }
}
thread.name = pool-2-thread-1, daemon = false
thread.name = pool-2-thread-2, daemon = false
thread.name = pool-2-thread-3, daemon = false
thread.name = pool-2-thread-1, daemon = false
thread.name = pool-2-thread-2, daemon = false
- 
Serviceのexecutorプロパティに、 Executor を指定する
- すると、 ServiceはTaskを実行するためのスレッドをexecutorから取得するようになる
各種プロパティ
package sample.javafx;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.ProgressBar;
import java.io.IOException;
public class MainController {
    @FXML
    private ProgressBar progressBar;
    private Service<Void> service = new Service<Void>() {
        
        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    int max = 10000000;
                    for (int i=0; i<max; i++) {
                        this.updateProgress(i, max);
                    }
                    
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        this.service.restart();
        this.progressBar.progressProperty().bind(this.service.progressProperty());
    }
}
- 
ServiceはWorkerを実装しているので、Taskと同じようにprogressなどのプロパティを提供している
- 各プロパティは、 Serviceが実行しているTaskのプロパティと連動するようになっている
- つまり、 Serviceのプロパティを参照することは、実行中のTaskのプロパティを参照するのと同じということになる
参考
視覚効果
ライティングやドロップシャドウなどの視覚効果を適用する方法。
仕組みとしては、一旦シーングラフをピクセルイメージとしてレンダリングして、それに対して影を落としたり光を当てたりする画像処理をするようになっているっぽい。
次のようなシーングラフに各種視覚効果を適用してみて、どうなるか確認する。
ブルーム効果
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.Bloom;
import javafx.scene.layout.Pane;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Bloom bloom = new Bloom();
        this.pane.setEffect(bloom);
    }
}
- Bloom クラスを使用すると、ブルーム(画像が輝いているように見える)効果を適用できる
- 
Bloomのインスタンスを生成し、効果を適用するシーングラフ上のノードにsetEffect()で設定することで、そのノード以下に視覚効果が適用される
ぼかし効果
ボックスぼかし
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.BoxBlur;
import javafx.scene.layout.Pane;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        BoxBlur boxBlur = new BoxBlur();
        boxBlur.setWidth(5);
        boxBlur.setHeight(5);
        
        this.pane.setEffect(boxBlur);
    }
}
- BoxBlur を使用すると、ボックスぼかし効果を適用できる
- 
width,heightで、縦横方向のぼかし距離を指定できる
- 
iterationsを指定することで、ぼかし効果の適用回数を指定できる(デフォルトは3)
モーションぼかし
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.MotionBlur;
import javafx.scene.layout.Pane;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        MotionBlur motionBlur = new MotionBlur();
        motionBlur.setAngle(45.0);
        motionBlur.setRadius(40.0);
        
        this.pane.setEffect(motionBlur);
    }
}
- MotionBlur を使用すると、モーションぼかし(物体が動いているときのようなぼかし)を適用できる
- 
angleでぼかしの向きを指定できる- 度数法(角度を 0.0~360.0で表現するやつ)で指定する
- 水平方向が 0.0で、そこから時計回りに回転していく感じ- 
―:0.0
- 
\:45.0
- 
|:90.0
- 
/:135.0
- 
―:180.0
 
- 
 
- 度数法(角度を 
- 
radiusでぼかしの半径を指定する
ガウスぼかし
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.layout.Pane;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        GaussianBlur gaussianBlur = new GaussianBlur();
        gaussianBlur.setRadius(10.0);
        
        this.pane.setEffect(gaussianBlur);
    }
}
- GaussianBlur を使用すると、ガウスぼかしを適用できる(ボックスぼかしとの違いはよく分かってない)
- 
radiusでぼかしの半径を指定できる
ドロップ・シャドウ効果
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        DropShadow dropShadow = new DropShadow();
        dropShadow.setOffsetX(5.0);
        dropShadow.setOffsetY(5.0);
        dropShadow.setWidth(20.0);
        dropShadow.setHeight(20.0);
        dropShadow.setColor(Color.valueOf("CACA00"));
        this.pane.setEffect(dropShadow);
    }
}
- DropShadow を使用すると、ドロップシャドウ(影を落としたような)効果を適用できる
- 
offsetX,offsetYで、影の相対的な位置(オフセット)を指定する
- 
width,heightで、影の縦横方向のぼかし具合を指定する
- 
colorで、影の色を指定する
- 
spreadで、影の拡散具合(0.0~1.0)を指定する
インナー・シャドウ効果
確認しやすくするため、シーングラフをちょっと変更する。
※視覚効果適用前
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.InnerShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        InnerShadow innerShadow = new InnerShadow();
        innerShadow.setOffsetX(7.0);
        innerShadow.setOffsetY(7.0);
        innerShadow.setColor(Color.valueOf("500"));
        innerShadow.setRadius(10.0);
        
        this.pane.setEffect(innerShadow);
    }
}
- InnerShadow を使用すると、インナーシャドウ(内側に窪んだような影の)効果を適用できる
- 
offsetX,offsetYで、影の相対的な位置(オフセット)を指定する
- 
width,heightで、影の縦横方向の長さを指定する
- 
colorで、影の色を指定する
- 
radiusで、影のぼかし半径を指定する
リフレクション
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.effect.Reflection;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Label label;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Reflection reflection = new Reflection();
        reflection.setTopOffset(-25.0);
        reflection.setTopOpacity(0.8);
        reflection.setFraction(0.8);
        this.label.setEffect(reflection);
    }
}
- Reflection を使用すると、リフレクション(物体が床に反射しているような)効果を適用できる
- 
topOffsetで、反射表示されている部分の上部の相対的な位置(オフセット)を指定する
- 
topOpacityで、反射表示されている部分の上部の透明度を指定する
- 
fractionで、反射表示する部分の割合を指定する(0.0~1.0で、デフォルトは0.75、つまり 75% だけが反射表示される)
- 
bottomOpacityで、反射表示されている部分の下部の透明度を指定する
視覚効果を組み合わせる
実行結果
実装
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Reflection;
import javafx.scene.paint.Color;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    
    @FXML
    private Label label;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Reflection reflection = new Reflection();
        reflection.setTopOffset(-40.0);
        reflection.setTopOpacity(0.8);
        reflection.setFraction(0.8);
        DropShadow dropShadow = new DropShadow();
        dropShadow.setOffsetX(5.0);
        dropShadow.setOffsetY(5.0);
        dropShadow.setColor(Color.valueOf("AA0"));
        reflection.setInput(dropShadow);
        this.label.setEffect(reflection);
    }
}
- 
setInput()で他のEffectを指定することで、複数のEffectを組み合わせることができる
- 上記実装の場合、ドロップシャドウが先に適用されて、次にリフレクションが適用される
- つまり、 setInput()で渡した方が先に適用される
参考
メディア
※以下、音声や音楽、動画・映像を総称してメディアと表現する(例:メディアファイル→音声ファイルや動画ファイルのこと)
メディアファイルの仕組み
コーデック
- メディアデータは、そのままだと数分の内容でも数百MBや数GBといった大容量になってしまう
- そのため、通常は何らかの方法で圧縮されている
- この圧縮のことをエンコードと呼ぶ
- エンコードされたメディアデータは、再生するときに元の状態に戻す(解凍する)必要がある
- この解凍のことをデコードと呼ぶ
- メディアデータをエンコード・デコードするプログラムのことを、両方の言葉を合わせてコーデックと呼ぶ
- モノによってはエンコード(デコード)しかできないものもあるらしい
 
- メディアデータを再生するためには、メディアデータをデコードできるコーデックが存在しなければならない
- エンコードの形式には多くの種類が存在する
- 音声データなら、 MP3, AAC, WMA, etc...
- 動画データなら、 H.264, VP9, DivX, Xvid, etc..
 
コンテナ
- メディアファイルの中には、複数のメディアデータが組み合わされた状態で格納されている
- 例えば、単純な動画ファイルの場合は映像データと音声データが1つのファイルの中に格納されている
- メディアデータを入れるファイルのことをコンテナと呼ぶ
- コンテナにも複数の種類が存在する
- 音声ファイルなら、 WAV, AIFF, MP3, etc...
- MP3 は、エンコード形式としての MP3 もあれば、それを格納できるコンテナとしての MP3 もある(ややこしい)
 
- 動画ファイルなら、 MP4, FLV, AVI, MOV, etc...
 
- 音声ファイルなら、 WAV, AIFF, MP3, etc...
- コンテナの中には、様々なエンコード形式のメディアデータを格納できる
- 1つのコンテナに入れられるエンコード形式の種類は1つとは限らない
- 例えば MP4 というコンテナの場合
- 映像データには H.264 や H.265 でエンコードされたデータを入れられる
- 音声データには AAC や MP3 でエンコードされたデータを入れられる
 
- つまり、同じコンテナのファイルであっても、中に入っているメディアデータのエンコード形式次第では再生できない可能性がある(対応するコーデックがないと再生できない)
- コンテナによって格納可能なエンコード形式は異なる
- コンテナに入っているメディアデータの実際のエンコード形式は、 MediaInfo などのツールを利用することで確認できる
 
- コンテナの種類とファイルの拡張子は、だいたい一致していることが多い(MP4 なら .mp4、 FLV なら.flv,.f4v, etc...)
JavaFX がサポートしているエンコード形式・コンテナ
- JavaFX がサポートしているメディアのエンコード形式やコンテナは、Javadoc に記載されている
エンコード形式
| エンコード形式 | 音声/映像 | 
|---|---|
| PCM | 音声 | 
| MP3 | 音声 | 
| AAC | 音声 | 
| H.264/AVC | 映像 | 
| VP6 | 映像 | 
PCM
- パルス符号変調(pulse code modulation) の略
- 非圧縮データのこと
- エンコード形式と呼ぶのは不適切かも?
MP3 (エンコード形式)
- MPEG-1 Audio Layer-3 の略
- 動画コンテナの MPEG-1 で使用されていた音声用のエンコード形式
- 圧縮率が高く、非常に普及している
AAC
- Advanced Audio Coding の略
- MP3 の後継として作られた音声用のエンコード形式
- MP3 より圧縮率が高い
- MP4 コンテナの音声データとして広く採用されている
H.264/AVC
- MP4 コンテナの映像データによく利用される映像用のエンコード形式
- H.264 と MPEG-4 AVC という2つの名前があり、くっつけて「H.264/MPEG-4 AVC」と表記されることが多い
- JavaFX の Javadoc は「H.264/AVC」って書いている
 
- 非常に高い圧縮率で、広く普及している
- 後継として、さらに圧縮率の上がった H.265 がある
VP6
- FLV コンテナ(Flash ムービー)で利用される映像用のエンコード形式
- H.264 より圧縮率がいいらしい
- 後継として、さらに圧縮率の上がった VP9 がある(Youtube で採用されてるらしい)
- ただし、 Java9 以降は非推奨らしい
コンテナ形式
| コンテナ | エンコード形式(映像) | エンコード形式(音声) | 
|---|---|---|
| AIFF | - | PCM | 
| WAV | - | PCM | 
| MP3 | - | MP3 | 
| FLV | VP6 | MP3 | 
| MP4 | H.264/AVC | AAC | 
AIFF
- Audio Interchange File Format の略
- MAC オリジナルの音声ファイル用のコンテナ形式
- 非圧縮
WAV
- Waveform Audio Format の略
- Windows オリジナルの音声ファイル用のコンテナ形式
- 非圧縮
MP3 (コンテナ)
- MP3 (コーデック)の音声データが入った音声ファイル用のコンテナ形式
- タグや歌詞、画像などメタ情報を入れられたりする
FLV
- Flash プレイヤーで再生することを目的とした動画ファイル用のコンテナ形式
- FLV はバージョンによっては動画コーデックに H.264 を使用したり音声コーデックに ACC を使用することも可能だが、 JavaFX がサポートしているのはあくまで VP6-MP3 の組み合わせだけ
MP4
- 動画ファイル用のコンテナ形式
- 携帯電話などの低速回線環境で動画を配信することを考慮して考えられた規格
- 2017 現在、非常に普及している
広く普及しているエンコード形式(MP3, ACC, H.264)は最低限サポートしている、という感じ。
サポートされていないエンコード・コンテナのメディアファイルを再生する
- 標準でサポートされていない形式のメディアファイルを再生するにはどうするか
- コーデックを別途追加するような方法があればできそうだが、無理っぽい?
- WebView を使えば再生できそうなことが書いているように読めたが、実際やってみても無理だった
 
- そうなると、メディアファイルの形式を JavaFX がサポートする形式に変換せざるを得ない気がする
- エンコーディングを変換できるフリーのツールは、音声・動画ともに色々とあるので、それで JavaFX がサポートする形式に変換すれば再生できるようになる
- 「音声 動画 エンコード 変換」とかで検索すれば色々ヒットする
 
参考
- 動画形式の種類と違い(AVI・MP4・MOV・MPEG・MKV・WMV・FLV・ASF等)【コンテナ】
- 音声コーデックの種類と違い(MP3・AAC・WMA・WAV・Vorbis・AC3・FLAC等)【フォーマット】
- 音声ファイルの主な形式と特徴|コーデック・MP3・AAC・Ogg
- 動画ファイルの主な形式と特徴|コーデック・AVI・MPEG
- コンテナフォーマット - Wikipedia
メディアファイルを再生する
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MainController {
    
    private MediaPlayer player;
    
    @FXML
    public void start() {
        Path music = Paths.get("./media/music.m4a");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        this.player.play();
    }
}
- メディアファイルは、音声/動画に関わらず、 Media,MediaPlayerクラスを使って再生できる- 
Mediaクラスが、再生するメディアを表す
- 
MediaPlayerが、メディアを操作するための API を提供する
 
- 
- 映像を表示する場合は、さらに MediaViewを使用する(詳細は後述)
- 
MediaPlayerのplay()メソッドでメディアファイルを再生できる
MediaPlayer の状態遷移
※play(), pause(), stop() は MediaPlayer のメソッド
| 状態 | 説明 | 
|---|---|
| UNKNOWN | 作成直後で読み込みが完了していない状態 | 
| READY | 読み込みが完了し、再生の準備ができた状態 | 
| PLAYING | 再生中の状態 | 
| PAUSED | 一時停止中の状態 PLAYINGに戻ると停止していたところから再開する | 
| STOPPED | 停止中の状態 PLAYINGに戻ると最初から再生する | 
| STALLED | ストリーミング再生でプレイヤーの再生を続けるのに十分な情報が取得できていない状態 | 
| HALTED | エラーが発生してプレイヤーが再生できない状態 | 
| DIPOSED | プレイヤーが破棄されリソースが解放された状態 | 
- 
MediaPlayerは上図のような状態遷移をする- 
DISPOSED,HALTEDは特殊な状態なので省略している
 
- 
- メディアファイルの読み込みは非同期で行われ、 Mediaインスタンスを生成した直後はまだ読み込みが完了していない可能性がある
- 
Mediaインスタンスが関連付けられたMediaPlayerの状態がREADYになったら、メタ情報の読み込みは確実に完了している
- 
UNKNOWN状態の場合は再生などがまだできないが、play()メソッドなどの命令はUNKNOWNの状態で呼んでいるとバッファされ、READYになった時点で実行されるようになっている(明示的にREADYを待たなくていい)
- 詳しくは MediaPlayer.Status の Javadoc を参照
リソースファイルを再生する
`-src/main/
  |-java/
  | `-sample/javafx/
  |   :
  `-resources/
    |-...
    `-media/
      `-music.mp3
package sample.javafx;
import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        URL url = MainController.class.getResource("/media/music.mp3");
        Media media = new Media(url.toString());
        MediaPlayer player = new MediaPlayer(media);
        
        player.play();
    }
}
- 
URLでリソースファイルを取得してtoString()すれば、リソースファイルを再生できる
繰り返し再生する
package sample.javafx;
...
public class MainController implements Initializable {
    private MediaPlayer player;
    
    ...
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        this.player.setCycleCount(3);
    }
}
- 
setCycleCount(int)で、メディアファイルを連続で再生する回数を指定できる
- 
MediaPlayer.INDEFINITEを指定すると、無制限に連続再生されるようになる
再生時間
package sample.javafx;
...
import javafx.util.Duration;
public class MainController implements Initializable {
    private MediaPlayer player;
    
    ...
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        
        this.player.setCycleCount(3);
        Duration cycleDuration = this.player.getCycleDuration();
        System.out.println("cycleDuration=" + cycleDuration);
        Duration totalDuration = this.player.getTotalDuration();
        System.out.println("totalDuration=" + totalDuration);
        this.player.setOnReady(() -> {
            System.out.println("[onReady] cycleDuration=" + this.player.getCycleDuration());
            System.out.println("[onReady] totalDuration=" + this.player.getTotalDuration());
        });
    }
}
cycleDuration=UNKNOWN
totalDuration=UNKNOWN
[onReady] cycleDuration=9541.0 ms
[onReady] totalDuration=28623.0 ms
- 
cycleDurationとtotalDurationで再生時間を取得できる- 
cycleDurationは、1回分の再生時間を表す
- 
totalDurationは、繰り返しも含めた総再生時間を表す- 再生回数が INDEFINITEの場合はDuration.INDEFINITE
 
- 再生回数が 
 
- 
- ただし、 READYになる前に取得するとUNKNOWNになってしまう
- 再生時間は Durationというクラスのインスタンスになっている
Duration クラス
package sample.javafx.property;
import javafx.util.Duration;
public class Main {
    public static void main(String[] args) {
        Duration duration = new Duration(90000.0);
        System.out.println("duration=" + duration);
        System.out.println("duration.seconds=" + duration.toSeconds());
        System.out.println("duration.minutes=" + duration.toMinutes());
        Duration oneMinute = Duration.seconds(60);
        System.out.println("oneMinute=" + oneMinute);
        System.out.println("oneMinute.minutes=" + oneMinute.toMinutes());
    }
}
duration=90000.0 ms
duration.seconds=90.0
duration.minutes=1.5
oneMinute=60000.0 ms
oneMinute.minutes=1.0
- 
Duration は期間を表すクラス
- 期間とは、何分間・何秒間、といった長さをもった時間のこと(特定の時点を表すものではない)
 
- ミリ秒を基本の単位として期間を保持している
- コンストラクタで生成する場合はミリ秒を指定する
- ファクトリメソッドを使えば、時間・分・秒指定などでもインスタンスを生成できる
- このクラスは不変で、 add() や multiply() といったメソッドは計算結果を別の新規インスタンスに格納して返すようになっている
文字列フォーマット
- 
Durationの情報を文字列にフォーマット(02:00:12とかに)したい場合、直接的にフォーマットできるモノは標準 API の中に見当たらない
- Date and Time API の Duration なら良い感じのフォーマッターがあるかと思ったが、なさそう...
- 仕方ないので、↑の StackOverflow の実装例を参考に JavaFX の Duration版を書いてみると↓のような感じか
package sample.javafx.property;
import javafx.util.Duration;
public class Main {
    public static void main(String[] args) {
        Duration duration = new Duration(12294027.0);
        String text = format(duration);
        System.out.println("text=" + text);
    }
    
    public static String format(Duration duration) {
        long millis = (long) duration.toMillis();
        long absMillis = Math.abs(millis);
        long hours = absMillis / 3_600_000;
        long minutes = (absMillis / 60_000) % 60;
        long seconds = (absMillis / 1_000) % 60;
        long milliseconds = absMillis % 1_000;
        return (millis < 0 ? "-" : "") + String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds);
    }
}
text=03:24:54.027
再生する範囲を指定する
package sample.javafx;
...
public class MainController implements Initializable {
    private MediaPlayer player;
    
    ...
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        
        this.player.setCycleCount(3);
        
        this.player.setStartTime(Duration.seconds(1));
        this.player.setStopTime(Duration.seconds(3));
        this.player.setOnReady(() -> {
            System.out.println("cycleDuration=" + this.player.getCycleDuration());
            System.out.println("totalDuration=" + this.player.getTotalDuration());
        });
    }
}
cycleDuration=2000.0 ms
totalDuration=6000.0 ms
- 
startTime,stopTimeにそれぞれ開始時間、終了時間を設定すると、その範囲内でのみメディアデータが再生される
- 
cycleDurationはstartTime~stopTimeまでの時間を返す
- 
totalDurationは、cycleDuration * 再生回数
メタデータ
MP3 ファイルには、歌手やアルバム名、歌詞、アルバムアートなど様々なメタデータを付加することができる。
JavaFX から、それらのメタデータにアクセスすることができる(FLV のメタデータもとれるっぽいが、ここでは省略)。
package sample.javafx;
import javafx.collections.ObservableMap;
import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
...
public class MainController implements Initializable {
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music2.mp3");
        Media media = new Media(music.toUri().toString());
        MediaPlayer player = new MediaPlayer(media);
        
        player.setOnReady(() -> {
            ObservableMap<String, Object> metadata = media.getMetadata();
            metadata.forEach((key, value) -> {
                System.out.println("key=" + key + ", value=" + value);
            });
        });
    }
}
key=comment-1, value=iTunPGAP[eng]=0  
key=album artist, value=ClariS 
key=comment-0, value=[eng]=  
key=image, value=javafx.scene.image.Image@1ca0c8fb
key=artist, value=ClariS 
key=raw metadata, value={ID3=java.nio.HeapByteBufferR[pos=779441 lim=789675 cap=789675]}
key=composer, value=ryo 
key=album, value=SHIORI 
key=comment-3, value=iTunSMPB[eng]= 00000000 00000210 0000096C 0000000000E5EE04 00000000 007D1469 00000000 00000000 00000000 00000000 00000000 00000000 
key=comment-2, value=iTunNORM[eng]= 000022CF 000021D8 0000C4E5 0000FF84 0002901F 0004CBF5 0000823D 0000832E 0004A049 00049D87 
key=genre, value=  
key=title, value=君の知らない物語 
- 
MediaのgetMetadata()で、メタデータを保持したObservableMapが取得できる- メタデータがなかったり、メタデータの取得がサポートされていないコンテナ形式の場合は空のマップが返る
 
- サポートされているメタデータタグの一覧は、Javadoc に記載されているのでそちらを参照。
- 残念ながら AAC はメタデータの取得がサポートされていない
- 一応 Java9 も確認したが、変わらず AAC はサポート対象外
 
再生位置を変更する
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.util.Duration;
...
public class MainController implements Initializable {
    
    private MediaPlayer player;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music2.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        this.player.play();
    }
    
    @FXML
    public void seek() {
        this.player.seek(Duration.minutes(2));
    }
}
- 再生位置を変更するには seek(Duration seekTime)メソッドを使用する
- 引数で指定する seekTimeは、メディアデータの先頭からの期間を指定する- 
startTimeを指定していても、startTimeからの期間ではない(たぶん)
 
- 
- 図にすると↓のような感じ
- 
startTime,stopTimeを指定していて、その範囲の外に出るseekTimeを指定した場合は、startTimeかstopTimeのいずれかに納まるように制限される
- 詳しくは Javadoc を参照
映像を表示する
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private MediaView mediaView;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/movie.mp4");
        Media media = new Media(music.toUri().toString());
        MediaPlayer player = new MediaPlayer(media);
        
        this.mediaView.setMediaPlayer(player);
        player.play();
    }
}
- 映像を表示するには MediaViewを使用する
- 
MediaViewのsetMediaPlayer()に再生するメディアを読み込んだMediaPlayerを渡す
- 幅や高さは fitWidth,fitHeightプロパティで調整できる
AudioClip
再生時間の短いちょっとした効果音などは、 Media よりも AudioClip を使ったほうが効率がいい。
package sample.javafx;
import javafx.fxml.Initializable;
import javafx.scene.media.AudioClip;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music2.mp3");
        AudioClip audioClip = new AudioClip(music.toUri().toString());
        audioClip.play();
    }
}
- インスタンスの生成は Mediaと同様 URL 文字列をコンストラクタ引数に渡す
- 
play()メソッドで再生を開始できる
Media との違いは次のような感じ。
- 最小の遅延で再生できる(すぐに再生できる)
- インスタンス生成後、すぐに使い始められる
- 一度再生すると、以後は stop()以外のメソッドは効果がない
- 同一インスタンスを同時に複数回 play()で再生できる- 同時に複数再生中の状態で stop()を呼ぶと、再生中の全ての音声が停止する
 
- 同時に複数再生中の状態で 
詳細は Javadoc を参照。
簡単な音楽プレイヤーを作成してみる
ここまでの内容を活用しつつ、簡単な音楽プレイヤーを作成してみる。
package sample.javafx;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaPlayer.Status;
import javafx.util.Duration;
import java.net.URL;
import java.nio.file.Paths;
import java.util.ResourceBundle;
import java.util.stream.Stream;
public class SimpleMusicPlayerController implements Initializable {
    private MediaPlayer player;
    
    @FXML
    private Button playButton;
    @FXML
    private Button stopButton;
    @FXML
    private Button pauseButton;
    @FXML
    private Label volumeLabel;
    @FXML
    private Label totalTimeLabel;
    @FXML
    private Label currentTimeLabel;
    
    @FXML
    private Slider volumeSlider;
    @FXML
    private Slider seekSlider;
    @FXML
    public void play() {
        this.player.play();
    }
    @FXML
    public void stop() {
        this.player.stop();
    }
    @FXML
    public void pause() {
        this.player.pause();
    }
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Media media = new Media(Paths.get("./media/music.m4a").toUri().toString());
        this.player = new MediaPlayer(media);
        // 音量ラベル
        this.volumeLabel.textProperty().bind(this.player.volumeProperty().asString("%.2f"));
        // 合計再生時間ラベル
        this.player.setOnReady(() -> {
            Duration total = this.player.getTotalDuration();
            this.totalTimeLabel.setText(this.format(total));
        });
        // 現在の再生時間ラベル
        this.currentTimeLabel.textProperty().bind(new StringBinding() {
            {bind(player.currentTimeProperty());}
            
            @Override
            protected String computeValue() {
                Duration currentTime = player.getCurrentTime();
                return format(currentTime);
            }
        });
        
        // Playボタン
        this.playButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            
            @Override
            protected boolean computeValue() {
                boolean playable = playerStatusIsAnyOf(Status.READY, Status.PAUSED, Status.STOPPED);
                return !playable;
            }
        });
        // Stopボタン
        this.stopButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            
            @Override
            protected boolean computeValue() {
                boolean stoppable = playerStatusIsAnyOf(Status.PLAYING, Status.PAUSED, Status.STALLED);
                return !stoppable;
            }
        });
        // Pauseボタン
        this.pauseButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            @Override
            protected boolean computeValue() {
                boolean pausable = playerStatusIsAnyOf(Status.PLAYING, Status.STALLED);
                return !pausable;
            }
        });
        
        // 音量スライダー
        this.player.volumeProperty().bind(this.volumeSlider.valueProperty());
        this.volumeSlider.setValue(1);
        
        // シークバー
        this.seekSlider.valueProperty().addListener((value, oldValue, newValue) -> {
            if (seekSliderIsChanging()) {
                Duration seekTime = this.player.getTotalDuration().multiply((Double) newValue);
                this.player.seek(seekTime);
            }
        });
        this.player.currentTimeProperty().addListener((value, oldValue, newValue) -> {
            if (!seekSliderIsChanging()) {
                double totalDuration = this.player.getTotalDuration().toMillis();
                double currentTime = newValue.toMillis();
                this.seekSlider.setValue(currentTime / totalDuration);
            }
        });
        // 再生が終了したときの後処理
        this.player.setOnEndOfMedia(() -> {
            this.player.seek(this.player.getStartTime());
            this.player.stop();
        });
    }
    
    private boolean playerStatusIsAnyOf(Status... statuses) {
        Status status = this.player.getStatus();
        return Stream.of(statuses).anyMatch(candidate -> candidate.equals(status));
    }
    private boolean seekSliderIsChanging() {
        return this.seekSlider.isValueChanging() || this.seekSlider.isPressed();
    }
    
    private String format(Duration duration) {
        long millis = (long) duration.toMillis();
        long minutes = (millis / 60_000) % 60;
        long seconds = (millis / 1_000) % 60;
        return String.format("%02d:%02d", minutes, seconds);
    }
}
現在の再生時間の表示
        // 現在の再生時間ラベル
        this.currentTimeLabel.textProperty().bind(new StringBinding() {
            {bind(player.currentTimeProperty());}
            
            @Override
            protected String computeValue() {
                Duration currentTime = player.getCurrentTime();
                return format(currentTime);
            }
        });
    ...
    
    private String format(Duration duration) {
        long millis = (long) duration.toMillis();
        long minutes = (millis / 60_000) % 60;
        long seconds = (millis / 1_000) % 60;
        return String.format("%02d:%02d", minutes, seconds);
    }
- 
MediaPlayerのcurrentTimeプロパティにバインドして、時間をフォーマットした値をラベルにセットする
ボタンの制御
        // Playボタン
        this.playButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            
            @Override
            protected boolean computeValue() {
                boolean playable = playerStatusIsAnyOf(Status.READY, Status.PAUSED, Status.STOPPED);
                return !playable;
            }
        });
        // Stopボタン
        this.stopButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            
            @Override
            protected boolean computeValue() {
                boolean stoppable = playerStatusIsAnyOf(Status.PLAYING, Status.PAUSED, Status.STALLED);
                return !stoppable;
            }
        });
        // Pauseボタン
        this.pauseButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            @Override
            protected boolean computeValue() {
                boolean pausable = playerStatusIsAnyOf(Status.PLAYING, Status.STALLED);
                return !pausable;
            }
        });
    ...
    private boolean playerStatusIsAnyOf(Status... statuses) {
        Status status = this.player.getStatus();
        return Stream.of(statuses).anyMatch(candidate -> candidate.equals(status));
    }
- ボタンの disabledの制御に、MediaPlayerのstatusを利用する
- 各ボタンをクリックできる状態を決めて、その否定を disabledプロパティにセットするようにしている
音量スライダー
        // 音量ラベル
        this.volumeLabel.textProperty().bind(this.player.volumeProperty().asString("%.2f"));
        ...
        // 音量スライダー
        this.player.volumeProperty().bind(this.volumeSlider.valueProperty());
        this.volumeSlider.setValue(1);
- 音量は volumeプロパティで調整できる
- 
Sliderのvalueプロパティにバインドすることで、スライドの調整と音量の調整を連動させられる
- ただし、 volumeは0.0~1.0までの値で指定しなければならない
- そのため、 Sliderの最小値(Min)、最大値(Max)、増減算量(Block Increment) をvolumeの範囲に合わせて調整しておく
- 音量用のラベルは、 MediaPlayerのvolumeプロパティと連動するようにしておく
シークバー
- シークバーの実装は少し複雑になる
- 再生位置を書き込みできるプロパティはないので、リスナーを登録して MediaPlayer.seek()を呼ばなければならない
- また、次のように双方向で状態を連動させなければならない
- 再生時間にあわせてスライダーの位置を調整する
- ユーザーがスライダーを操作したら、再生時間を調整する
 
 
- 再生位置を書き込みできるプロパティはないので、リスナーを登録して 
- 
Sliderは0.0~1.0までの間で指定できるようにしておき、合計時間に対する割合として扱うようにしている
再生時間→シークバー
        this.player.currentTimeProperty().addListener((value, oldValue, newValue) -> {
            if (!seekSliderIsChanging()) {
                double totalDuration = this.player.getTotalDuration().toMillis();
                double currentTime = newValue.toMillis();
                this.seekSlider.setValue(currentTime / totalDuration);
            }
        });
    ...
    private boolean seekSliderIsChanging() {
        return this.seekSlider.isValueChanging() || this.seekSlider.isPressed();
    }
- 
currentTimeにリスナーを登録する
- 合計時間に対する現在の時間の割合を計算し、スライダーの valueに設定する
- ここで注意なのが、スライダーをユーザが操作していないときにだけ値を変更するようにしなければならない
- そうしないと、ユーザがスライダーを移動させようとしても、強制的に再生時間に戻されてしまうようになる
- ユーザがスライダーを触っているかどうかについては、 Sliderの次のメソッドを使用する- 
isValueChanging()- スライダーをスライドさせている最中の場合は trueを返す
 
- スライダーをスライドさせている最中の場合は 
- 
isPressed()- スライダーをクリックしている場合は trueを返す
 
- スライダーをクリックしている場合は 
 
- 
シークバー→再生時間
        this.seekSlider.valueProperty().addListener((value, oldValue, newValue) -> {
            if (seekSliderIsChanging()) {
                Duration seekTime = this.player.getTotalDuration().multiply((Double) newValue);
                this.player.seek(seekTime);
            }
        });
- スライダーの valueプロパティにリスナーを登録する
- スライダーの新しい値(0.0~1.0)と合計再生時間を掛けて、移動先の時間(seekTime)を求める
- そして、その値を使って MediaPlayerのseek()を実行する
- ここで注意なのが、スライダーをユーザが操作しているときにだけ値を変更するようにしなければならない
- そうしないと、再生時間に合わせてスライダーの値を変更したときにもこのリスナーが呼ばれるので、再生位置が無駄に更新されてしまい、音がカクカク再生されるような感じになってしまう
再生時間が終了したときの処理
        // 再生が終了したときの後処理
        this.player.setOnEndOfMedia(() -> {
            this.player.seek(this.player.getStartTime());
            this.player.stop();
        });
- 再生時間が stopTimeに到達しても、statusはPLAYINGのままになっている
- つまり、そのままだと statusと紐づけているボタンなどは状態が変わらない
- 再生が終了したときに statusを明示的に変更するため、setOnEndOfMedia()でリスナーを登録する
- ここで seek()で再生位置をstartTimeに戻して、stop()で状態をSTOPPEDに変更している- 
stop()だけでもいい気がするけど、それだとなぜかgetCurrentTime()がstopTimeしか返さなくなってしまう
- 
MediaPlayerは、再生が終了したかどうかをbooleanのフラグとして内部で持っているが、そのフラグがtrueだとgetCurrentTime()はstopTimeを返すようになっているっぽい
- そして、再生終了後 stop()だけで状態を変更させると、このフラグがtrueのまま残ってしまい、以後再生中なのにcurrentTimeがstopTimeしか返さなくなっている気がする
- 「バグじゃね?」という気がするが、仕様を熟知しているわけでもないので、なんとも言えない
 
- 
参考
メニュー
メニューバー
実行結果
説明
- メニューバーを作成するには、 MenuBarを使用する
- 配置できる場所に制限はないが、普通はウィンドウの上部に配置する
- 
MenuBarにカテゴリ(「ファイル」となっているところ)を追加するにはMenuを使用する
- 
MenuにさらにMenuItemを追加することで、個々のメニュー項目を定義できる
メニューが選択されたときの処理を実装する
package sample.javafx;
import javafx.fxml.FXML;
public class MainController {
    
    @FXML
    public void open() {
        System.out.println("「開く」が選択された");
    }
}
- メニューの On Actionイベントとコントローラのメソッドを紐づけることで、メニューが選択されたときの処理を実装できる
セパレータ
実行結果
- 
SeparatorMenuItemを差し込むことで、メニューに区切り線を入れることができる
サブメニュー
実行結果
- 
Menuの下にさらに別のMenuを追加することでサブメニューを実現できる
ショートカットキーを割り当てる
実行結果
- 
MenuItemのacceleratorプロパティでショートカットキーを割り当てることができる
- ショートカットキーは、修飾子キーと主要なキーの組み合わせで定義する
- 修飾子キーは、 Ctrl,Shift,Alt,Meta,Shortcutのいずれか- さらにキーの状態を示す DOWN,UP,ANYのいずれかを指定する
 
- さらにキーの状態を示す 
- 主要なキーとは、 A,Bなどの通常のキーのこと
 
- 修飾子キーは、 
- 修飾子キーの Shortcutとは、ショートカットでよく利用されるキーのことで、 OS に依存しない定義を実現できる- Windows の場合は Ctrlキーのことを指し、
 Mac の場合はMetaキーのことを指す
 
- Windows の場合は 
ニーモニック
package sample.javafx;
import javafx.fxml.FXML;
public class MainController {
    
    @FXML
    public void open() {
        System.out.println("open");
    }
    
    @FXML
    public void close() {
        System.out.println("close");
    }
    
    @FXML
    public void fullScreen() {
        System.out.println("full screen");
    }
}
各メニューとコントローラのメソッドを関連付けている。
実行結果
- ちょっと分かりづらいが、マウスを使用せずにキーボードのみでメニューを操作している
- 実際触ってみるとわかるが、正直動きが怪しい
 
- Windows の場合は Alt でメニューにフォーカスが当たる
- そのあとは各メニュー項目のアンダーバーがついている文字を入力することで、キーボードのみでメニューを操作できる
- このメニューに対して割り当てられるキーのことをニーモニックと呼ぶ
- メニューにニーモニックを割り当てるには、次の2つの手順が必要になる
- 
Mnemonic Parsingのチェックをオンにする
- 
Textの中でニーモニックを割り当てたいアルファベットの前にアンダーバー (_) を入れる
 
- 
- すると、アンダーバーの次の文字がニーモニックに割り当てる文字と判断される
日本語メニューにニーモニックを割り当てる
実行結果
- ニーモニックはアルファベットで割り当てるので、日本語メニューはそのままだとニーモニックを割り当てられない(たぶん)
- その場合は、メニューテキストの後ろに (_F)のようにニーモニック用の注記を追加すれば日本語メニューでもニーモニックを割り当てられる(というか、 iTunes とか他の一般的なアプリケーションがそうしているので、それを参考にした)
アイコンを追加する
`-src/main/
  |-java/
  | :
  `-resources/
    `-img/
      `-open.png
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.MenuItem;
import javafx.scene.image.ImageView;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private MenuItem openMenuItem;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        ImageView image = new ImageView("/img/open.png");
        this.openMenuItem.setGraphic(image);
    }
}
実行結果
- 
MenuItemのgraphicプロパティにImageViewを設定することで、メニュー項目に任意の画像を表示させられる
チェックメニュー
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.CheckMenuItem;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private CheckMenuItem checkMenuItem;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.checkMenuItem.selectedProperty().addListener((value, oldValue, newValue) -> {
            System.out.println(newValue ? "チェックされた" : "チェックが外れた");
        });
    }
}
実行結果
- 
CheckMenuItemを使用すると、チェックのオン・オフを切り替えるメニュー項目を定義できる
- メニューが選択されているかどうかは selectedプロパティで確認できる
ラジオボタンメニュー
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.ToggleGroup;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private ToggleGroup group1;
    @FXML
    private RadioMenuItem hogeRadioMenuItem;
    @FXML
    private RadioMenuItem fugaRadioMenuItem;
    @FXML
    private RadioMenuItem piyoRadioMenuItem;
    
    @FXML
    private ToggleGroup group2;
    @FXML
    private RadioMenuItem fizzRadioMenuItem;
    @FXML
    private RadioMenuItem buzzRadioMenuItem;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.hogeRadioMenuItem.setUserData("hoge");
        this.fugaRadioMenuItem.setUserData("fuga");
        this.piyoRadioMenuItem.setUserData("piyo");
        this.group1.selectedToggleProperty().addListener((toggle, oldValue, newValue) -> {
            if (newValue != null) {
                System.out.println("[group1] " + newValue.getUserData());
            }
        });
        
        this.fizzRadioMenuItem.setUserData("fizz");
        this.buzzRadioMenuItem.setUserData("buzz");
        this.group2.selectedToggleProperty().addListener((toggle, oldValue, newValue) -> {
            if (newValue != null) {
                System.out.println("[group2] " + newValue.getUserData());
            }
        });
    }
}
実行結果
- 
RadioMenuItemを使用すると、排他的に選択できるメニュー項目を定義できる
- 
RadioMenuItemのToggle Groupに、その項目が属するグループの識別子(任意の文字列)を設定する- 同じグループの識別子を持つ項目は同じグループに属するものとして識別されるようになり、選択が排他的に制御されるようになる
 
- 
Toggle Groupは、 fxml 上では次のように定義される
  <RadioMenuItem fx:id="hogeRadioMenuItem" text="ほげ">
     <toggleGroup>
        <ToggleGroup fx:id="group1" />
     </toggleGroup>
  </RadioMenuItem>
- 
fx:idがToggle Groupで指定した識別子になっている
- なので、コントローラクラスには指定した Toggle Groupの識別子でToggleGroupインスタンスをインジェクションできるようになる
    @FXML
    private ToggleGroup group1;
- 
ToggleGroupのselectedToggleプロパティにリスナを登録することで、選択されている項目が変わったときのイベントを捕捉できる
- このとき、1回の変更でイベントが2度呼ばれ、最初の1度目は newValueがnullになるという点に注意
- これはバグらしく、 ver 10 で治る予定っぽい
        this.hogeRadioMenuItem.setUserData("hoge");
        this.fugaRadioMenuItem.setUserData("fuga");
        this.piyoRadioMenuItem.setUserData("piyo");
        this.group1.selectedToggleProperty().addListener((toggle, oldValue, newValue) -> {
            if (newValue != null) {
                System.out.println("[group1] " + newValue.getUserData());
            }
        });
- どのメニューが選択されたかどうかは、メニュー項目インスタンスの userDataに登録しておいた値で識別する- 
userDataはObject型なので、何でも値を入れることができる
- 普段はあまり使わないが、今回のようにグループの中で選択されたものを識別するような場面で利用できる
- 残念ながら Scene Builder から userDataを指定することはできないっぽいので、initialize()メソッドなどで設定するしかない
 
- 
コンテキストメニュー
実行結果
- コンテキストメニュー(右クリックとかで出るメニュー)は、対象のノードに ContextMenuを追加することで実現できる
- メニュー項目の定義方法などはメニューバーと同じ
参考
ウィンドウの最大化
package sample.javafx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.net.URL;
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        
        primaryStage.setMaximized(true); // ★最大化
        primaryStage.show();
    }
}
- 
setMaximized()でtrueを設定すれば、ウィンドウを最大化できる
別のウィンドウを開く
基本
`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-MainController.java
  |   `-OtherController.java
  |
  `-resources/
    |-main.fxml
    `-other.fxml
other.fxml
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
public class MainController {
    
    @FXML
    public void openOtherWindow() throws IOException {
        // fxml をロードして、
        URL fxml = this.getClass().getResource("/other.fxml");
        FXMLLoader loader = new FXMLLoader(fxml);
        Pane pane = loader.load();
        
        // シーンを作成
        Scene scene = new Scene(pane);
        
        // ステージにシーンを登録して
        Stage stage = new Stage();
        stage.setScene(scene);
        
        // 表示
        stage.show();
    }
}
- fxml のロードには FXMLLoaderを使用する
- fxml ファイルの指定方法にはいろいろあるが、ここではリソースファイルとして Class.getResource(String)を利用して読み込んでいる- 他の読み込み方は、 FXMLLoader の Javadoc を参照
 
- 
load()メソッドで fxml 内に定義した一番上のノードが取得できる
- あとは、シーン・ステージを作って show()もしくはshowAndWait()メソッドで起動する- JavaFX アプリケーションは、ステージ・シーン・シーングラフから成る
- 
ステージは、 JavaFX アプリケーションの最上位コンテナで、ウィンドウに対応する
- 上の実装だと、Stageが対応する
 
- 上の実装だと、
- 
シーンとは、シーングラフを保持するコンテナで、ステージの表示領域部分に対応する
- 上の実装だと、 Sceneが対応する
 
- 上の実装だと、 
- 
シーングラフとは、シーン上に表示する全てのノードを保持するツリー構造で、シーングラフを書き換えることで表示を変更することができる
- 上の実装だと、 fxml からロードした Paneが、このシーングラフのルートノードとなる
 
- 上の実装だと、 fxml からロードした 
 
コントローラを取得する
package sample.javafx;
public class MainController {
    
    public void hello() {
        System.out.println("Hello MainController!!");
    }
}
package sample.javafx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.net.URL;
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        
        MainController controller = loader.getController();
        controller.hello();
        
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
実行結果
Hello MainController!!
- fxml を FXMLLoader.load()でロードしたあとなら、FXMLLoader.getController()メソッドで fxml にマッピングしているコントローラのインスタンスを取得することができる- 
load()前だとnullが返るので注意
 
- 
所有者の設定
所有者を設定していない場合の挙動
- 
Stageに所有者(owner)を設定していない場合、最初に開いていたウィンドウを閉じても後から開いているウィンドウは閉じられない
所有者を設定する
package sample.javafx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.net.URL;
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        MainController controller = loader.getController();
        controller.setStage(primaryStage); // ★コントローラに、そのコントローラのステージを設定する
        
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
public class MainController {
    
    private Stage stage; // ★このコントローラのステージが設定されている
    public void setStage(Stage stage) {
        this.stage = stage;
    }
    @FXML
    public void openOtherWindow() throws IOException {
        URL fxml = this.getClass().getResource("/other.fxml");
        FXMLLoader loader = new FXMLLoader(fxml);
        Pane pane = loader.load();
        Scene scene = new Scene(pane);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.initOwner(this.stage); // ★オーナーを設定
        stage.showAndWait();
    }
}
実行結果
- 所有者を設定すると、親のウィンドウが閉じると子のウィンドウも一緒に閉じられるようになる
- シーングラフを fxml からロードしている場合、対応するコントローラ内で自分自身の Stageを取得する方法はいくつかあるが、FXMLLoaderからコントローラのインスタンスを取得してセッターなどを介して設定するのが、なんだかんだスマートな気がする
- 所有者の設定には initOwner()メソッドを使用する
- ちなみに、所有者を設定してもモーダルになっているわけではない
モーダル
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
public class MainController {
    private Stage stage;
    public void setStage(Stage stage) {
        this.stage = stage;
    }
    @FXML
    public void openOtherWindow() throws IOException {
        URL fxml = this.getClass().getResource("/other.fxml");
        FXMLLoader loader = new FXMLLoader(fxml);
        Pane pane = loader.load();
        Scene scene = new Scene(pane);
        Stage stage = new Stage();
        stage.setScene(scene);
        stage.initOwner(this.stage);
        stage.initModality(Modality.WINDOW_MODAL); // ★Modality を設定
        stage.showAndWait();
    }
}
実行結果
- 
initModality()で、そのステージをモーダルにすることができる
- 
initModality()には、Modality列挙型の定数を渡す
- 
ModalityにはNONE,WINDOW_MODAL,APPLICATION_MODALの3つが定義されている
| 定数 | 意味 | 
|---|---|
| NONE | 他のウィンドウをブロックしない | 
| WINDOW_MODAL | 所有者のウィンドウをブロックする (所有者以外のウィンドウはブロックしない) | 
| APPLICATION_MODAL | すべてのウィンドウをブロックする | 
参考
- JavaFXの基本構造|軽Lab
- 1 JavaFXシーン・グラフの操作(リリース8)
- Stage (JavaFX 8)
- FXML Controller で Stage を使うためのアレコレ - Java開発のんびり日記
別の fxml を埋め込む
- 複雑な画面を単一の fxml で作ると、コントローラクラスも含めて実装がごちゃごちゃになってしまう
- また、同じ部品を複数の箇所で使いまわす場合なども、単一の fxml で作っていると重複が発生するなどして具合が悪い
- fxml には、他の fxml を埋め込むための <fx:include>というタグが用意されている
- これを使えば、上述のような問題を回避できる
基本
フォルダ構成
`-src/main/resources/
  |-embedded.fxml
  `-main.fxml
埋め込むfxml (embedded.fxml)
埋め込み先のfxml (main.fxml)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.text.Font?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.151" xmlns:fx="http://javafx.com/fxml/1">
   <top>
      <Label text="Main" BorderPane.alignment="CENTER">
         <font>
            <Font size="40.0" />
         </font>
      </Label>
   </top>
   <center>
      <fx:include source="embedded.fxml" />
   </center>
</BorderPane>
説明
- 
<fx:include>のsource属性に、埋め込む対象の fxml ファイルを指定する- この値は、 /から始まっている場合はクラスパス上のパスになるっぽい- リファレンスには「クラスパスに相対的であるとみなされます」と書いてあって、意味が分かりづらい。。。
 
- 
/で始まらない場合は、埋め込み先の fxml ファイルのある場所からの相対パスになるっぽい- こちらもリファレンスが「現在のドキュメントのパスに相対的であるとみなされます」と書いてあって意味が分かりづらい。。。
 
- SceneBuilder を使っている場合は、 /無しのパスで書いておかないとプレビューできない- SceneBuilder 自体のクラスパスに対象の fxml が置いてあるフォルダも追加したら動くが、まぁ普通はそんな起動の仕方はしないだろう...
 
 
- この値は、 
SceneBuilder で include しようとすると失敗する
- SceneBuilder には一応 fxml を include するためのメニューが用意されているっぽい(File -> Include -> FXML)
- しかし、これを使って fxml を include しようとしても、 Failed to include '***.fxml'と出て include に失敗する- たまに成功するときもあるが、よくわからない
 
- SceneBuilder が include をサポートしていないわけでもないらしく、手で直接 fxml を修正して <fx:include>を書けばちゃんとプレビューもしてくれるようになる
- 原因は分からないが、とりあえず <fx:include>だけは手書きしておくのが無難そう
- 手書きで <fx:include>を埋め込んだ後なら、普通に SceneBuilder から操作できるようになる
埋め込んだ fxml のコントローラを取得する
`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-Main.java
  |   |-MainController.java
  |   `-EmbeddedController.java
  `-resources/
    |-main.fxml
    `-embedded.fxml
embedded.fxml
package sample.javafx;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class EmbeddedController {
    @FXML
    private Label label;
    
    public void setMessage(String message) {
        this.label.setText(message);
    }
}
main.fxml
...
<BorderPane fx:id="pane" ... fx:controller="sample.javafx.MainController">
   <top>
      <Label text="Main" BorderPane.alignment="CENTER">
         ...
      </Label>
   </top>
   <center>
      <fx:include fx:id="embedded" source="embedded.fxml" />
   </center>
</BorderPane>
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private EmbeddedController embeddedController;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.embeddedController.setMessage("Hello!!");
    }
}
実行結果
説明
- 
<fx:include>で埋め込んだ FXML (embedded.fxml)に紐づけられたコントローラ(EmbeddedController)は、埋め込み先の
 FXML (main.fxml)のコントローラ(MainController)に@FXMLでインジェクションできる
- ただし、 <fx:include>にfx:id属性が設定されている必要がある- 
fx:idが設定されていないと、コントローラのインスタンスはインジェクションされずnullになる
 
- 
埋め込んだ fxml を別ウィンドウとして表示する
embedded.fxml
package sample.javafx;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import java.net.URL;
import java.util.ResourceBundle;
public class EmbeddedController implements Initializable {
    
    private IntegerProperty count = new SimpleIntegerProperty(0);
    
    @FXML
    private Label label;
    
    @FXML
    public void countUp() {
        int now = this.count.get();
        this.count.set(now + 1);
    }
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        StringBinding output = this.count.asString("count = %d");
        this.label.textProperty().bind(output);
    }
}
main.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.text.Font?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.151" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.javafx.MainController">
   <fx:define>
      <fx:include fx:id="embeddedPane" source="embedded.fxml" />
   </fx:define>
   <top>
      <Label text="Main" BorderPane.alignment="CENTER">
         <font>
            <Font size="40.0" />
         </font>
      </Label>
   </top>
   <center>
      <Button mnemonicParsing="false" onAction="#openWindow" text="Open Window" BorderPane.alignment="CENTER">
         <font>
            <Font size="25.0" />
         </font>
      </Button>
   </center>
</BorderPane>
package sample.javafx;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.net.URL;
import java.util.ResourceBundle;
public class MainController implements Initializable {
    @FXML
    private Pane embeddedPane;
    
    private Stage embeddedWindow;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Scene scene = new Scene(this.embeddedPane);
        this.embeddedWindow = new Stage();
        this.embeddedWindow.setScene(scene);
        this.embeddedWindow.initModality(Modality.APPLICATION_MODAL);
    }
    
    @FXML
    public void openWindow() {
        this.embeddedWindow.showAndWait();
    }
}
実行結果
説明
...
<BorderPane ... fx:controller="sample.javafx.MainController">
   <fx:define>
      <fx:include fx:id="embeddedPane" source="embedded.fxml" />
   </fx:define>
   ...
</BorderPane>
- 埋め込み先の FXML(main.fxml)に、<fx:include>を使って埋め込み対象の FXML (embedded.fxml)を埋め込む
- このとき、普通に埋め込むとシーン上で見えてしまうので、 <fx:define>の中に埋め込むようにする- こうすると、見た目上は埋め込んだタグは表示されずに済む
 
- 
<fx:include>にfx:id属性を設定しておく(コントローラに DI するのに必要になる)
package sample.javafx;
...
public class MainController implements Initializable {
    @FXML
    private Pane embeddedPane;
    
    private Stage embeddedWindow;
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Scene scene = new Scene(this.embeddedPane);
        this.embeddedWindow = new Stage();
        this.embeddedWindow.setScene(scene);
        this.embeddedWindow.initModality(Modality.APPLICATION_MODAL);
    }
    
    @FXML
    public void openWindow() {
        this.embeddedWindow.showAndWait();
    }
}
- 埋め込み先のコントローラ(MainController.java)に、埋め込む FXML (embedded.fxml) を@FXMLで DI する(embeddedPane)
- 
embeddedPaneを使ってStageを作っておけば、あとはshow()またはshowAndWait()でウィンドウを表示できる
- 埋め込む FXML のコントローラ (EmbeddedController) のライフサイクルは、埋め込み先のコントローラと一緒になる- 表示のたびに新規にインスタンスを生成したい場合は、 FXMLLoaderで都度読み込むことになると思う
 
- 表示のたびに新規にインスタンスを生成したい場合は、 
参考
ファイル選択ダイアログ
- ボタンをクリックしたら、ファイル選択ダイアログが表示されるようにする
package sample.javafx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.net.URL;
public class Main extends Application {
    public static void main(String[] args) {
        Application.launch(args);
    }
    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        MainController controller = loader.getController();
        controller.setStage(primaryStage);
        
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
- 
MainControllerのインスタンスをFXMLLoaderから取得してprimaryStageをセットする
package sample.javafx;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
public class MainController {
    
    private Stage stage;
    
    @FXML
    public void openFileDialog() {
        FileChooser chooser = new FileChooser();
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
    }
    public void setStage(Stage stage) {
        this.stage = stage;
    }
}
実行結果
表示されたダイアログ
ファイルを選択して「開く」をクリック
file=C:\Program Files\Java\jdk-9.0.1\README.html
説明
        FileChooser chooser = new FileChooser();
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
- ファイルを選択するダイアログを表示するには、 FileChooser クラスを使用する
- インスタンスを生成して showOpenDialog(Stage)メソッドを実行すると、単一ファイルを選択するためのファイル選択ダイアログが開く- 引数の Stageには、オーナーとなるStageを指定する
- オーナーが指定されている場合、ファイル選択ダイアログが開いている間、オーナーへの操作はブロックされる(モーダルになる)
- 
nullを渡すこともでき、その場合ファイル選択ダイアログはモーダルにはならない
 
- 引数の 
- ダイアログが表示されている間、 showOpenDialog()メソッドはブロックされる
- ダイアログでの選択結果が File型で返される
- ファイルが選択された場合は、そのファイルを指す Fileオブジェクトが返る
- ファイルを選択せずにダイアログが閉じられた場合は nullが返る
タイトルを設定する
        FileChooser chooser = new FileChooser();
        chooser.setTitle("ふぁいるせんたく");
        File file = chooser.showOpenDialog(this.stage);
実行結果
- 
setTitile(String)でダイアログのタイトルを指定できる
初期表示するディレクトリを指定する
        FileChooser chooser = new FileChooser();
        chooser.setInitialDirectory(new File("C:/Program Files/java/jdk-9.0.1"));
        File file = chooser.showOpenDialog(this.stage);
- 
setInitialDirectory(File)で、ダイアログを開いたときに表示するディレクトリを指定できる
- 何も指定しない場合は、環境(OS)ごとにデフォルトのディレクトリが表示される
- 
FileChooserクラス自体には、前回開いたディレクトリ(ファイル)を記録しておく、みたいな機能はないっぽい
- なので、前回開いたディレクトリをもう一度開きたい場合は、別途開いたディレクトリを記憶しておいて、もう一度ダイアログを開くときに setInitialDirectory()でディレクトリを指定することで実現できる
選択できるファイルを拡張子で絞る
package sample.javafx;
import javafx.collections.ObservableList;
import javafx.stage.FileChooser.ExtensionFilter;
...
public class MainController {
    
    private Stage stage;
    
    @FXML
    public void openFileDialog() {
        FileChooser chooser = new FileChooser();
        
        ObservableList<ExtensionFilter> extensionFilters = chooser.getExtensionFilters();
        extensionFilters.add(new ExtensionFilter("何でもあり", "*.*"));
        extensionFilters.add(new ExtensionFilter("画像だけやで", "*.jpg", "*.jpeg", "*.png", "*.gif"));
        extensionFilters.add(new ExtensionFilter("まさかの動画", "*.mp4"));
        
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
    }
    ...
}
実行結果
- 
getExtensionFilters()で、拡張子による絞り込みを定義したObservableListを取得できる
- このリストに ExtensionFilterを追加することで、拡張子により絞り込みを指定できる
- 
ExtensionFilterのコンストラクタには、「説明」と「拡張子の定義」を渡す- 「拡張子の定義」は *.<拡張子>の形式で指定する
- 何でもありの場合は *.*にする
- 「拡張子の定義」は複数指定することが可能
 
- 「拡張子の定義」は 
複数のファイルを選択できるダイアログを開く
        FileChooser chooser = new FileChooser();
        List<File> files = chooser.showOpenMultipleDialog(this.stage);
        if (files == null) {
            System.out.println("files=" + files);
        } else {
            files.forEach(System.out::println);
        }
実行結果
C:\Program Files\Java\jdk-9.0.1\COPYRIGHT
C:\Program Files\Java\jdk-9.0.1\README.html
C:\Program Files\Java\jdk-9.0.1\release
- 
showOpenMultipleDialog(Stage)でダイアログを開くと、複数のファイルを選択できるダイアログが開く
- 選択結果は List<File>で返される
- 何も選択されなかった場合は nullを返す(空のリストじゃないのか...)
ファイルの保存先を選択するダイアログ
        FileChooser chooser = new FileChooser();
        File file = chooser.showSaveDialog(this.stage);
        System.out.println("file=" + file);
実行結果
file=C:\Users\Public\hoge
- 
showSaveDialog(Stage)を使用すると、ファイルの保存先を選択するダイアログを開くことができる
- 戻り値は、ユーザが指定した保存先のファイルを指す Fileオブジェクトになる
- 何も選択せずにダイアログが閉じられた場合は、 nullが返される
ディレクトリを選択するダイアログ
package sample.javafx;
import javafx.stage.DirectoryChooser;
...
public class MainController {
    
    private Stage stage;
    
    @FXML
    public void openFileDialog() {
        DirectoryChooser chooser = new DirectoryChooser();
        File directory = chooser.showDialog(this.stage);
        System.out.println("directory=" + directory);
    }
    ...
}
実行結果
directory=C:\Users\Public\Downloads
- ディレクトリを選択するダイアログを開くには、 DirectoryChooser を使用する
- 選択対象がディレクトリになること以外は、基本 FileChooserと同じ使い方になる(initialDirectoryとか)
参考
ボタンやラベルを同じ幅(高さ)に揃えて縦(横)に並べる
見た目
構造
縦並びの各要素のサイズ設定
- 同じ幅で縦に並べるなら、 VBoxを使う- 並べる各要素は、最大幅 (Max Width) をMAX_VALUEにして最大幅まで伸ばす
 
- 並べる各要素は、最大幅 (
- 同じ高さで横に並べるなら、 HBoxを使う- 並べる各要素は、最大高さ (Max Height) をMAX_VALUEにして最大高さまで伸ばす
 
- 並べる各要素は、最大高さ (






































































































