JavaFX 勉強メモ

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](http://gluonhq.com/products/scene-builder/) が開発してるやつ



# JavaFX プロパティ
JavaFX では、オブジェクトのプロパティを表現する方法として JavaBeans を拡張した **JavaFX プロパティ** というものが使用されている。

JavaFX プロパティでは、オブジェクトのプロパティを次のように実装する。

```java
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;
    }
}
```

1. プロパティ（`value`）を `private` で定義する
    - プロパティの型はプリミティブ型ではなく、 `DoubleProperty` のように JavaFX が用意しているラッパー型を使用する
    - ラッパー型は [javafx.beans.property](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/property/package-summary.html) パッケージ以下に用意されている
    - `DoubleProperty` 自体はインターフェースで、実装クラスとして `SimpleDoubleProperty` が用意されている
2. Getter, Setter を定義する
    - メソッド名は JavaBeans と同じ規則で命名する
    - 型はラッパー型ではなく、中身の値を出し入れするようにする
    - メソッドは `final` で定義する
3. `プロパティ名Property` というメソッドを定義する
    - これはラッパー型をそのまま返す

JavaFX に用意されている多くのクラスは、この JavaFX プロパティを実装している。
例えば、 [Label](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Label.html) クラスの `text` プロパティに対応する各メソッドは以下になる（実際に定義されているのは [Labeled](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Labeled.html) クラス）。

- [getText()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Labeled.html#getText--)
- [setText()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Labeled.html#setText-java.lang.String-)
- [textProperty()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Labeled.html#textProperty--)

## Observable
JavaFX プロパティは単にメソッドの宣言を決めているだけでなく、 JavaBeans には無い特別な機能をいくつか提供している。
そのうちの１つが、 [Observable](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/Observable.html) になる。

`Observable` を実装したクラスは、内容が無効になったことを監視できる機能を提供している。

前述の JavaFX プロパティで使用した `DoubleProperty` も `Observable` を継承している。

この `Observable` には `addListener(InvalidationListener)` というメソッドが定義されており、内容が無効になったときに通知を受け取るリスナーを登録できる。

```java
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);
    }
}
```

```text:実行結果
observable=DoubleProperty [value: 1.0]
observable=DoubleProperty [value: 2.0]
```

リスナーには、変更された `Observable` 自身が引数として渡される。

### 無効と変更
`Observable` を実装したクラスの中には、内容の変更が要求されても（例えば Setter メソッドが呼ばれても）すぐには計算が行われず、次に内容が要求されたときに初めて計算をするようなものがある（遅延計算）。

つまり、内容の変更が要求されてから実際に計算されるまでの間、内容が確定していない期間がありえることになる。

この内容が確定していないことを **無効** と表現する。
そして、実際に計算が行われて内容が確定したときを **変更** と表現する。

`InvalidationListener` は、内容が無効になったときにコールバックされるようになっている。

なお、実装クラスによっては内容が即座に計算されるものもあるので、その場合は無効と変更は同時に発生することになる（無効になったあと、すぐ内容が確定（変更）する）。

## ObservableValue
```java
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);
    }
}
```

```text:実行結果
observableValue=DoubleProperty [value: 1.0], oldValue=0.0, newValue=1.0
observableValue=DoubleProperty [value: 2.0], oldValue=1.0, newValue=2.0
```

`Observable` を継承したインターフェースとして、 [ObservableValue](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/value/ObservableValue.html) インターフェースが存在する。
これには `addListener(ChangeListener)` というメソッドが用意されており、値が変更されたときに通知を受け取るリスナーを登録できる。
リスナーには、変更された `ObservableValue` 自身と、変更の前後の生の値が渡される。

`DoubleProperty` を含め JavaFX で用意されているプロパティ用のクラスは `ObservableValue` を実装しているので、 `InvalidationListener` と `ChangeListener` のどちらのリスナーも登録できるようになっている。

## Binding
`ObservableValue` を継承したインターフェースとして [Binding](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/binding/Binding.html) が存在する。

`Binding` は、複数の値をまとめた計算結果を表現している。
まとめられた値は `Binding` によって監視されており、値が変更されたときに自動的に結果が再計算されるようになっている。

```java
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());
    }
}
```

```text:実行結果
sum=3.0
sum=5.0
```

この例では、２つの `DoubleProperty` の合計を表す `DoubleBinding` を作成している。
片方の `DoubleProperty` の値を変更し `DoubleBinding` の値を再取得すると、合計は再計算された値になっている。

このように、 `Binding` は複数の値をまとめ、各値の変更を監視して計算結果を自動更新する仕組みを提供している。

`Binding` がまとめている値のことを**ソース**、または**依存性**と呼ぶ。
`Binding` 自体はソースの型に制限を設けていないが、実際はたいてい `Observable` や `ObservableValue` になる。

### 遅延計算
```java
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);
    }
}
```

```text:実行結果
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 を登録すると遅延計算されなくなる
```java
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);
    }
}
```

```text:実行結果
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/value/ObservableValue.html) や [公式のチュートリアル](https://docs.oracle.com/javase/jp/8/javafx/properties-binding-tutorial/binding.htm#sthref12) の説明を参照

### 高レベルAPI
`Binding` を作成するための API には**高レベルAPI**と**低レベルAPI**の2種類が用意されている。

さらに高レベルAPIは**Fluent API**と**Bindings クラスのファクトリメソッド**の２つに分かれている。

#### Fluent API
```java
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());
    }
}
```

```text:実行結果
((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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/binding/NumberExpression.html) というインターフェースに定義されている
    - プロパティで使用しているクラスはこのインターフェースを実装している
- 数値演算以外にも比較演算や論理演算などを行うメソッドも用意されている
- `NumberExpression` の数値演算系の Fluent API の戻り値の型は `NumberBinding` になっているが、 `NumberBinding` もまた `NumberExpression` のサブインターフェースになっているので、そのまま続けて Fluent API を呼び出すことができるようになっている
- 各インターフェースやクラスの関係は、下図のようになっている（一部だけ）

![javafx.png](https://qiita-image-store.s3.amazonaws.com/0/28302/22ac02bb-2fa4-e87a-2429-9caa7509eff3.png)

#### Bindings クラスのファクトリメソッド
```java
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());
    }
}
```

```text:実行結果
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0
```

- [Bindings](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/binding/Bindings.html) というクラスには、 `Binding` のファクトリメソッドが大量に用意されている
- Fluent API に存在するものもあれば、存在しないものもある
- とりあえず数が多いので、ざっと眺めてどういうのがあるのか知っておくとよさげ
    - [when()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/binding/Bindings.html#when-javafx.beans.value.ObservableBooleanValue-)とかおもろそう

### 低レベルAPI
```java:MyBinding.java
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;
    }
}
```

```java
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());
    }
}
```

```text:実行結果
((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 と結び付ける
```java
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);
    }
}
```

```text:実行結果
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/property/Property.html) に定義されている `bind(ObservalbeValue)` メソッドを使用すると、一方向のバインディングが実現できる
- 引数で渡した `ObservableValue` の値を監視するようになり、値が変更されると自分自身の値を監視対象と同じ値に変更する
- あるプロパティの値を他のプロパティと連動させたい場合に、この `bind()` を使えばシンプルに実装できる

## 読み取り専用のプロパティ
```java:MyClass.java
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);
    }
}
```

```java:Main.java
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());
    }
}
```

```text:実行結果
ReadOnlyDoubleProperty [value: 0.0]
ReadOnlyDoubleProperty [value: 99.9]
```

- `DoubleProperty` をそのまま公開すると、公開先で値を自由に書き換えることができてしまう
- 公開先で値を変更できないようにするなら、 setter は公開せず、内部で使用するプロパティの型を `ReadOnly**Wrapper` 型にする
- `ReadOnly**Wrapper` 型は、それ自体は普通に値を変更できるが、 `getReadOnlyProperty()` で取得したオブジェクトは読み取り専用になる


# 双方向のバインディング
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/080469b9-e7c6-809e-243d-2e967270c68a.jpeg)

```java
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!!");
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/d2569fb3-bd8c-c6ff-28f9-59596691b742.gif)

- [Property](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/property/Property.html) に定義された [bindBidirectional()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/property/Property.html#bindBidirectional-javafx.beans.property.Property-) メソッドを使用すると、２つの `Property` を双方向にバインドできる

## StringProperty の双方向バインディング
`Property<T>` に用意されている双方向のバインディングメソッドは、同じ `Property<T>` 型のプロパティとだけ双方向のバインディングができる。

一方、 [StringProperty](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/property/StringProperty.html) には、次の２つの双方向バインディング用のメソッドが追加されている。

- フォーマット指定
- コンバーター指定

### フォーマット指定
```java
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);
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/14723cb7-f3c9-e1a9-8c99-fea6068a412d.gif)

- 第二引数に `Format` を指定できる
- `StringProperty` 側には、 `Format` でフォーマットされた文字列が設定され、他方のプロパティにはパースされた結果が設定されるようになる
- 不正な書式の入力があると、 WARINING レベルのログがスタックトレースとともに出力される

### コンバーター指定
```java
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));
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/a2245f2f-61a5-99a1-3578-ae33f5eafe88.gif)

- [StringConverter](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/util/StringConverter.html) を指定することで、任意の値と `StringProperty` とを双方向にバインドできる
- `StringConverter` には２つの抽象メソッドが定義されている
    - `String toString(T)`: オブジェクト `T` を `String` に変換する
    - `T fromString(String)`: `String` を、オブジェクト `T` に変換する 
- 一応、標準で [LocalDateStringConverter](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/util/converter/LocalDateStringConverter.html) とかが用意されている
- しかし、パースやフォーマットができないとスタックトレースを含めた WARNING ログを出力してくるので、ちょっと使いづらい印象
    - フリー入力でないところとかなら、使えるのかもしれない

# コレクション
JavaFX では、標準のコレクション（`List` とか `Map`）を拡張した、独自のコレクションクラスが用意されている。

## 監視可能な List
```java
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);
    }
}
```

```text:実行結果
list が変更されました change={ [foo] added at 2 }
list が変更されました change={ [buzz] removed at 1 }
list が変更されました change={ permutated by [0, 1] }
```

- [ObservableList](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/collections/ObservableList.html) は、 `java.util.List` を継承したインターフェースで、監視機能を実現するためのメソッドが追加されている
- インスタンスの生成は `FXCollections` のファクトリメソッドを使用する
    - `FXCollections` は `java.util.Collections` と似たようなメソッドを持つ、 JavaFX のコレクション用のユーティリティクラス
- `addListener(ListChangeListener)` で、変更の通知を受け取るリスナーを登録できる
    - `ObservableList` は `Observable` も継承している
    - そして、 `Observable` にも `addListener(InvalidationListener)` という名前が同じで引数が異なるメソッドが定義されている
    - 両者が引数で受け取るリスナーは、どちらも関数型インターフェースで、しかもメソッドの引数が１つしかない
    - つまり、引数の型を省略した最もシンプルなラムダ式を書くと、コンパイラはどちらの `addListener()` が使用されているのか判別できなくなり、コンパイルエラーになる
    - 仕方ないので、ラムダ式を書く場合でも最低限引数の型は省略せずに書かなければならない

### 変更内容を確認する
リストが変更されると、リスナーには [Change](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/collections/ListChangeListener.Change.html) オブジェクトが渡される。

`Change` オブジェクトにはリストがどのように変更されたのかについての情報が記録されている。

```java
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");
    }
}
```

```text:実行結果
==========================================
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/collections/ListChangeListener.Change.html) に記載されている


#### 追加された要素を確認する
```java
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"));
    }
}
```

```text:実行結果
==========================================
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()` で、追加された要素数を取得できる

#### 削除された要素を確認する
```java
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");
    }
}
```

```text:実行結果
==========================================
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()` は削除された要素数を返す

#### 並び順の変更を確認する
```java
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);
    }
}
```

```text:実行結果
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)` に変更前のインデックスを渡すと、順序変更後のインデックスを返す

#### 置換された要素を確認する
```java
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");
    }
}
```

```text:実行結果
==========================================
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` となる

### ソートされたリスト
```java
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);
    }
}
```

```text:実行結果
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)` で後からソート方法を変更することもできる

### フィルターされたリスト
```java
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);
    }
}
```

```text:実行結果
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
```java
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");
    }
}
```

```text:実行結果
==============================
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()`)」の２つ
    - `getValueAdded()` で追加された値を、  
      `getValueRemoved()` で削除された値を取得できる
    - 既に存在するキーに別の値を設定した場合は「追加」と「削除」の変更が同時に発生する


# スタイルシート
## 基本
```text:フォルダ構成
`-src/main/
  |-java/sample/javafx/
  | |-Main.java
  | `-MainController.java
  |
  `-resources/
    |-main.fxml
    `-my-style.css
```

**main.fxml**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/fa1e7422-1a85-9716-88a4-62d8aab24322.jpeg)


```css:my-style.css
.label {
    -fx-background-color: skyblue;
}
```

```java:Main.java
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();
    }
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/edc96d02-1d18-8f3b-aa18-b97cf282045d.jpeg)

### スタイルシートの読み込み
```java:Main.java
        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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/Scene.html#getStylesheets--) を参照

### CSS の書き方
```css:my-style.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` クラスの実装を見ればわかる


```java:Label.java
...
public class Label extends Labeled {

    public Label() {
        initialize();
    }

...

    private void initialize() {
        getStyleClass().setAll("label");
        ...
    }
```

- コンストラクタで `initialzie()` が呼ばれ、 `styleClass` プロパティに `label` という文字列が渡されている（クラス名から自動変換された `styleClass` が設定されているわけではない）
- ということで、 `Label` クラスのインスタンスにはデフォルトで `label` という `styleClass` が設定されているので、 CSS ファイルではそれを `.` を使用したセレクタを使って指定している（`.label`）
- どのノードにどういうデフォルトのクラスが設定されているかは、 [こちら](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/doc-files/cssref.html#nodes) に載っている
    - 「スタイル・クラス: デフォルトでは空」とか  
      「スタイル・クラス: button」って書いているところ

## プロパティの実体
```css:my-style.css
.my-class {
    -fx-background-color: pink;
    -fx-padding: 10px;
    
    -fx-border-color: black;
    -fx-border-width: 1px;
    -fx-border-style: solid;
    
    -fx-underline: true;
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/ee086897-1d70-8341-55aa-f9981bd75f17.jpeg)

- CSS で指定するプロパティ（`-fx-background-color` とか `-fx-padding` とか）は、一見すると HTML の CSS で指定できるプロパティの頭に `-fx` がついているだけに見える
- なので、使えるプロパティがよくわからない場合は、 HTML の CSS で使っているプロパティに `-fx` をつければ何とかなりそうな気がする
- しかし、実際は `-fx-underline: true;` のように、 HTML の CSS には存在しないプロパティもある
    - HTML の CSS なら `text-decoration: underline;`
- これは、実際は対象のノードが持つプロパティを指している
- サンプルでは `Label` クラスに対して CSS を指定しているが、 `underline` は `Label` クラスの親クラスである `Labeled` クラスの [underline プロパティ](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Labeled.html#fontProperty) を指定していることになっている
- ただし、ノードのプロパティと CSS で指定するプロパティが常に完全に一致しているかというと、そうでもない
- 例えば `border` や `background` は、 `-fx-border`, `-fx-background` ではなく `-fx-border-style` や `-fx-background-color` のように細かいプロパティに分けられている
- どのノードに対してどのプロパティが指定できるのかについては、[公式のドキュメント](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/doc-files/cssref.html)で詳しく説明されている




## 任意の styleClass 属性をセットする
### Scene Builder でセットする
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/966f3226-da95-2f23-0b38-94bb953515a5.jpeg)

```xml:main.fxml
<?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>
```

```css:my-style.css
.my-class {
    -fx-font-weight: bold;
    -fx-underline: true;
    -fx-text-fill: red;
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/dce07e5d-f069-515b-27f6-668efa114049.jpeg)

- Scene Builder の `Style Class` で要素に対して任意の `styleClass` 属性を設定できる

### プログラムから設定する
```java:MainContoller.java
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()` することで任意の属性を追加できる

## ローカルのスタイルシートファイルを読み込む
```text:フォルダ構成
|-my-styles.css
`-src/main/java/
  `-sample/javafx/
    |-Main.java
    :
```

```css:my-style.css
.my-class {
    -fx-background-color: green;
}
```

```java:Main.java
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();
    }
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/679c8e9e-2e5c-991a-08eb-13c7a16712a3.jpeg)

- ローカルの CSS ファイルを指定する場合は `file:【パス】` と指定する
- `Path` や `File` で読み込んでいる場合は、 `toURI()`, `toUri()` メソッドで `URI` オブジェクトを取得して、その `toString()` を渡せばいい

## 背景画像の設定
`-fx-background-image` の設定方法について

```text:フォルダ構成
|-image.jpg
`-src/main/
  |-resources/
  | |-img/
  | | `-image.jpg
  | |-my-style.css
  | `-main.fxml
  :
```

### クラスパス内のファイルを指定する
```css:my-style.css
.my-class {
    -fx-background-image: url('/img/image.jpg');
}
```

- `url` で指定する
- 書式は CSS ファイルの読み込みと同じで、 `schema` を省略した場合はクラスパス以下のファイルになる

### ローカルのファイルを指定する
```css:my-style.css
.my-class {
    -fx-background-image: url('file:./image.jpg');
}
```

- ローカルのファイルを指定する場合は `file:【パス】` とする


# イベント・キャプチャ、イベント・バブリング
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/bf2fa9a7-f826-6926-c36b-b387603ca0f0.jpeg)


```java:MainController.java
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));
    }
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/bf2fa9a7-f826-6926-c36b-b387603ca0f0.jpeg)

```text:青い領域をクリック
filter bluePane
handler bluePane
```

```text:黄色い領域をクリック
filter bluePane
filter yellowPane
handler yellowPane
handler bluePane
```

```text:ボタンをクリック
filter bluePane
filter yellowPane
filter button
handler button
```

```text:テキストフィールドをクリック
filter bluePane
filter yellowPane
filter textField
handler textField
```

```text:ラベルをクリック
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane
```

## 説明
イベントが発生すると、次の順序で処理が実行される

1. ターゲットの選択
2. 経路[^2]の決定
3. イベント・キャプチャ・フェーズ
4. イベント・バブリング・フェーズ

[^2]: 公式の日本語ドキュメントだと「ルート」と記載されているが、「根っこ(root)」と勘違いしそうなので「経路(route)」と表記 

### ターゲットの選択
- まず最初にイベントが発生したノード（ターゲット）が決定される
- 例えば、クリックイベントならクリックしたノードが、キーイベントならフォーカスが当たっているノードがターゲットになる

### 経路の決定
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/4418c026-f133-a8ef-9ddf-dbf4cfbd92de.jpeg)

- 次に、ターゲットからシーングラフを親に向かって辿って行き、ルート（root）ノードまでの経路（route）を決定する
- この経路に沿って、次のイベント・キャプチャ・フェーズとイベント・バブリング・フェーズが実行される

### イベント・キャプチャ・フェーズ
```text:テキストフィールドをクリック
filter bluePane
filter yellowPane
filter textField
...
```

- イベント・キャプチャ・フェーズでは、決定された経路をルートからターゲットに向かって各ノードに登録されているイベント・フィルタが実行される

```java:MainController.java
    private void registerEventFilter(Node node, String name) {
        node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("filter " + name));
    }
```

- イベント・フィルタは `addEventFilter(EventType, EventHandler)` でノードに登録することができる

### イベント・バブリング・フェーズ
```text:ラベルをクリック
...
handler label
handler yellowPane
handler bluePane
```

- イベント・バブリング・フェーズでは、逆にターゲットからルートノードに向かって各ノードに登録されているイベント・ハンドラが実行される

```java:MainController.java
    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` 属性などでイベント処理を登録した場合も、このイベント・ハンドラが登録されている

### イベントを消費する
```java:MainController.java
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()` メソッドを実行している

**実行結果**

```text:黄色い領域をクリック
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
```

- `Event.consume()` メソッドを実行することで、イベントの連鎖を中断できる
- これを、**イベントの消費**と表現する
- イベント・キャプチャの段階でイベントを消費すると、イベント・バブリングも行われなくなる


### UIコントロールのなかにはイベント・バブリングが中断されるものがある
```text:ボタンをクリック
filter bluePane
filter yellowPane
filter button
handler button
```

```text:テキストフィールドをクリック
filter bluePane
filter yellowPane
filter textField
handler textField
```

```text:ラベルをクリック
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane
```

- `Label` は普通にイベント・バブリングが実行されているが、`Button`, `TextField` は中断されている
- イベント・キャプチャ・フェーズはどれも実行されている

一応公式ドキュメントには、以下のように書かれている。

> JavaFX UIコントロールのデフォルト・ハンドラでは一般にほとんどの入力イベントが消費されます。

[1 イベントの処理(リリース8) | JavaFX: イベントの処理](https://docs.oracle.com/javase/jp/8/javafx/events-tutorial/processing.htm)

ただ、具体的にどのクラスがイベントを消費するかをどこで確認すればいいかはよく分かってない。


# ドラッグ＆ドロップ
## ドロップ
### 基本
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/05152c23-0a43-e28d-9ca9-eaad51e8b5e6.jpeg)

```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/478ad12e-d79f-5d09-5f0c-50f0716f5f1d.gif)

**説明**

#### ドロップの受け入れ
```java:MainController.java
    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }
```

- ドロップ先のノードに対して `DRAG_OVER` イベントのハンドラを登録する
- そこで、 `DragEvent.acceptTransferModes()` メソッドを実行することで、ドロップの受け入れができるようになる
    - 引数にはどの転送モード（`COPY`, `MOVE`, `LINK` のいずれか）を受け入れるかを `TransferMode` の定数で指定する
    - `ANY` は３つの転送モードの全てを表す（つまり、なんでもOK）。
- ドロップを受け入れないと、マウスカーソールがドロップ不可能を表すアイコンになる
- 最後に `consume()` メソッドを実行してイベントを消費する
    - 消費しなくても一応動くは動くが、公式のドキュメントには必ず `consume()` が記載されている
    - たぶん、親ノードにもドラッグ＆ドロップの処理が存在した場合に、間違ってそちらが実行されないようにしているのかもしれない
- 今回は常に `acceptTransferModes()` を実行しているが、条件によって呼び出しを制御することで、ドロップしようとしている内容によってドロップを許可するかどうかを制御したりできる

#### ドロップ時の処理
```java:MainController.java
    @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` からドロップ内容にアクセスできる

### ファイルのドロップ
```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/f7a792a9-943d-4444-a45c-58869f33743e.gif)

**説明**

```java:MainController.java
    @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()` を実行しているので、ファイル以外のドロップはできないようになる

```java:MainController.java
    @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 のドロップ
```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/96b93e90-97da-7c44-3e95-ba4200c74131.gif)

**説明**

```java:MainController.java
    @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()` なら素のテキストでも取得できる

### ドロップ時のスタイルを変更する
```css:my-style.css
.drag-over {
    -fx-text-fill: red;
}
```

```java:MainController.java
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");
    }
    
    ...
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/bb1f7bf7-eb64-17f9-342b-a71a67a7c83f.gif)

**説明**

- ドロップ対象のノードに `DRAG_ENTERED` と `DRAG_EXITED` のイベントハンドラを登録する
- `DRAG_ENTERED` でノードにスタイルを追加し、 `DRAG_EXITED` でスタイルを外すように実装する
- こうすることで、ドロップ待機中をより分かりやすく表現することができる

## ドラッグ
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/c5424388-2579-1a39-de20-0d3bb6abd8a5.jpeg)

```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/cbf9decf-112e-2236-6b7e-ab8bebbd857b.gif)

**説明**

```java:MainController.java
    @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>` を継承しているマップなので、任意の値を保存できる



**参考**

- [JavaFX(SB) ドロップメモ(Hishidama's JavaFX2 Scene Builder drop Memo)](http://www.ne.jp/asahi/hishidama/home/tech/java/fx/sb/drop.html)
- [7 ドラッグ・アンド・ドロップ操作(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/events-tutorial/drag-drop.htm#CHDJFJDH)


# ダイアログ
※`Alert` などのクラスは JDK8u40 以上でないと使えない（それ以前の場合は自力でダイアログウィンドウを作る必要がある）。

## 準備
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/09c6c226-9e8c-4f24-3ff8-f816cc278626.jpeg)

```java
package sample.javafx;

import javafx.fxml.FXML;
import javafx.scene.control.Alert;

public class MainController {
    
    @FXML
    public void showDialog() {
        ...
    }
}
```

ボタンをクリックしたら `showDialog()` メソッドが呼ばれるようにしておく。

## 基本
```java
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.showAndWait();
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/32170f95-57b4-7cc2-b8c3-42dfa913cfc5.jpeg)

- `Alert` クラスを使うことでダイアログを表示できる
- コンストラクタにダイアログの種類を指定する
    - 種類は `AlertType` 列挙型の定数を指定する
    - `CONFIRMATION` は確認ダイアログ
- `showAndWait()` メソッドでダイアログを表示する
    - `AndWait` とあるように、このメソッドでダイアログを表示すると、ダイアログが閉じられるまで処理がブロックされる
- ダイアログはモーダルになっており、表示されている間は親ウィンドウを触れない

## 選択されたボタンを確認する
```java
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` になっており、閉じるときに選択されたボタンを確認できる
- キャンセル系のボタン[^1]**以外しかない**ダイアログ（「はい」ボタンしかないダイアログとか）が `ESC` やウィンドウの閉じるボタンで閉じられたときに、`showAndWait()` の戻り値は `Optional.empty()` になる
    - 厳密には `resultConverter` が設定されているケースもあるが、その辺は [Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/Dialog.html) を参照のこと

[^1]: `ButtonData` が `NO` または `CANCEL_CLOSE`


## メッセージを設定する
```java
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ");
alert.showAndWait();
```

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5ca4af8b-6cdd-fc67-bbac-a9844b157b23.jpeg)

- コンストラクタ引数の第二引数で、ダイアログにセットするメッセージを指定できる

## ダイアログの種類
**INFORMATION**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/6eef1414-98a6-d8ec-acf2-cd2f075b267c.jpeg)

**CONFIRMATION**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/e75be284-bab0-c8dc-c477-7fd9fba5846c.jpeg)

**WARNING**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/f5499ef9-1cf3-3361-3882-0a6a0a91ed6f.jpeg)

**ERROR**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/ad4e15ab-df78-e288-9b4f-84d3736c3512.jpeg)

## ボタンを指定する
```java
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();
```

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/563b10ca-d71c-e5f9-a653-bdac6090e9d9.jpeg)

- コンストラクタの第三引数以降に、 `ButtonType` を指定できる
    - `ButtonType` にはよく使うボタンが定数として定義されているので、それで済むならそれを利用する
    - 自作することもできる（後述）
- 可変長引数なので、複数の `ButtonType` を指定できる

### ボタンの配置順序
- 可変長引数の順序がそのままボタンの配置順序に**なるわけではない**
- ボタンの配置には `ButtonBar` というクラスが使用されている
- `ButtonBar` は、ボタン配置のためのクラスで、 OS ごとに固有のボタン配置を再現できる
    - 各 OS ごとのボタン配置の順序は [ButtonBar の Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/ButtonBar.html) に例のイメージが掲載されている
- ボタンは種類ごとに**ボタン順序（button order）**が決められており、ボタン順序によって `ButtonBar` のどこにボタンが配置されるかが決まる
    - 例えば、「はい」「いいえ」のボタンは、 Windows なら 「はい」→「いいえ」 の順序で配置されるが、 Mac や Linux では「いいえ」→「はい」の順序で配置される
- このボタンの種類を表現するクラスとして、 [ButtonData](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/ButtonBar.ButtonData.html) 列挙型が用意されている
- `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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/ButtonBar.html) に記載されている

**OS ごとの `ButtonData` の配置順序**

|OS     |配置順序              |
|-------|---------------------|
|Windows|`L_E+U+FBXI_YNOCAH_R`|
|Mac OS |`L_HE+U+FBIX_NCYOA_R`|
|Linux  |`L_HE+UNYACBXIO_R`   |

- この謎の文字列は、１文字１文字が `ButtonData` の定数に割り当てられている**ボタン順序コード**に対応している
- 例えば、 `ButtonData.LEFT` のボタン順序コードは `L` で、 `ButtonData.HELP_2` は `E` といった具合
    - 定数に割り当てられているボタン順序コードは、各定数の Javadoc に記載されている
- もうちょい見やすくするために、 `ButtonData` の定数との関係を表にすると下のような感じ

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/0aae6424-6258-3a7e-4292-3aa51578069c.jpeg)

- （`F` は対応する定数が存在しないのでよくわかってない）
- つまり、 `ButtonType.CANCEL` を `Alert` のコンストラクタで指定した場合、  
  `ButtonType.CANCEL` の `ButtonData` は `CANCEL_CLOSE` なので、 `ButtonBar` の順序で `C` となっている部分にキャンセルボタンが挿入されることになる

### 任意のボタンを配置する
```java
ButtonType myButton = new ButtonType("ぼたん");
Alert alert = new Alert(Alert.AlertType.INFORMATION, "めっせーじ", myButton);
alert.showAndWait().ifPresent(System.out::println);
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/929fcc45-80db-f9e6-218e-4b350adddac1.jpeg)

```text:「ぼたん」ボタンをクリックしたときのコンソール出力
ButtonType [text=ぼたん, buttonData=OTHER]
```

- `ButtonType` クラスを生成して `Alert` のコンストラクタに渡せば、任意のボタンを配置できる
- `ButtonType` のコンストラクタでは、ボタンのラベル文字列を指定できる
- 何も指定していない場合、 `ButtonData` は `OTHER` になる
- `ButtonType` には `ButtonData` を受け取るコンストラクタも存在するので、明示的に指定することも可能

## ボタン以外で閉じられたときの判定
- ダイアログは `ESC` や `Alt + F4`, ウィンドウの閉じるボタンなどでも閉じることができる（Windows の場合）
- その場合、 `showAndWait()` の戻り値は配置されたボタンによってはやや複雑な動きをする

```java
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);
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/ba790cfe-2b82-cda0-1a32-c2cfd0dfb63f.jpeg)

`ESC` で閉じたときの結果

```text
選択無し
```

- ボタンの中にはキャンセル系のボタン[^1]が無いので、戻り値は `Optional.empty()` になる

---
- つぎに、キャンセル系のボタンが複数ある場合

```java
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);
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/60621097-5dbf-9df6-2bdd-90aadd7a11d0.jpeg)

1. 「はい」
2. 「いいえ」
3. 「取消」
4. `ESC`
5. `Alt + F4`
6. ウィンドウの閉じる

の順序でダイアログを閉じてみる。

```
はい
いいえ
取消
取消
取消
取消
```

- 各ボタンが押されたときは、そのボタンが `showAndWait()` の戻り値となる
- ボタン以外の方法でダイアログが閉じた場合は、「取消」ボタンが押されたことになっている
    - これは、キャンセル系のボタンが複数あった場合、
        1. `ButtonData` が `CANCEL_CLOSE` になっている `ButtonType`
        2. `ButtonData.isCancelButton()` が `true` を返す `ButtonType`
    - の優先順序で戻り値が決定するようになっているため、こういう動きになっている
- つまり、キャンセル系のボタンが `ButtonType.NO` だけの場合は、ボタン以外の方法でダイアログを閉じたときの戻り値は `ButtonType.NO` になる

---
ボタン以外の方法でダイアログが閉じられたときの `showAndWait()` の戻り値をまとめると、

1. キャンセル系のボタン[^1]が１つも存在しない場合は `Optional.empty()`
2. キャンセル系のボタンが１つある場合は、その `ButtonType`
3. キャンセル系のボタンが２つ以上ある場合は、次の優先順序で戻り値の `ButtonType` が決まる
    1. `ButtonData` が `CANCEL_CLOSE` である `ButtonType`
    2. `ButtonData.isCancelButton()` が `true` を返す `ButtonType`

## 各種テキストの設定
コンストラクタ引数だと `contentText` しか設定できないが、 `Alert` を生成したあとならタイトルなども変更できる。

```java
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("たいとる");
alert.setHeaderText("へっだーてきすと");
alert.setContentText("こんてんつてきすと");
alert.showAndWait();
```

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5a544923-b46c-a346-db83-cbf4745d3c2c.jpeg)

それぞれは `null` を設定することで消すこともできる。

```java
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(null);
alert.setHeaderText(null);
alert.setContentText("こんてんつてきすと");
alert.showAndWait();
```

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/4e6766dc-4cc2-eb70-8251-31caffcaa86f.jpeg)


# スレッド
## JavaFX でスレッドを使うときの基本的な話
### UIスレッド
JavaFX の GUI 処理は、 `JavaFX Application Thread` という専用のスレッド（**UIスレッド**）上で実行される。

```java:MainController.java
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());
    }
}
```

```text:実行結果
thread = JavaFX Application Thread
```

普通に実装していると、この UI スレッド上で処理が実行されることになる。

しかし、時間のかかる処理をこの UI スレッド上で実行していると、 UI の更新やマウス操作などのイベント処理が待たされることになる。
すると、アプリケーションはユーザの操作を一切受け付けられなくなり、見た目も変化しない、いわゆる固まった状態になってしまう。

これを回避するには、時間のかかる処理を UIスレッドとは別のスレッドで実行する必要がある。

### 非 UI スレッドから UI を触る
JavaFX の UI コンポーネントは、 UI スレッドからしかアクセスできない。
UI スレッド以外から UI コンポーネントにアクセスしようとすると、例外がスローされるようになっている。

```java:MainController.java
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();
    }
}
```

```text:実行結果
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()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/application/Platform.html#runLater-java.lang.Runnable-)というメソッドが用意されている。

```java:MainController.java
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();
    }
}
```

```text:実行結果
thread = Thread-4
runLater thread = JavaFX Application Thread
```

`runLater()` に渡したラムダ式（型は `Runnable`）は、 UIスレッド（`JavaFx Application Thread`）上で実行されるようになる。
そのため、この `runLater()` に渡した処理の中からなら、安全に UIスレッドを触ることができる。

## JavaFX でスレッドを簡単に扱うための仕組み
JavaFX では、スレッドを使った処理をより簡単に実装できるようにするための仕組みが用意されている。

### Worker
JavaFX で使用するスレッド処理は、 [Worker](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Worker.html) として抽象化されている。

`Worker` には、通常の `Thread` にはない便利な API が定義されている。
（これ自体はインターフェースで、具体的な実装は [Task](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Task.html) または [Service](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Service.html) によって実現されている）

#### Worker のライフサイクル
Worker にはライフサイクルがあり、次のように状態が遷移する。

![javafx.png](https://qiita-image-store.s3.amazonaws.com/0/28302/5c902a4a-4702-4cfd-af7c-38969b869cc1.png)

**READY**

- `Worker` が作成されると、まず READY 状態となる
- **開始状態** と呼ばれる状態は、この READY 状態のみ

**SCHEDULED**

- 続いて `Worker` の開始がスケジュールされると、 SCHEDULED 状態になる
    - スレッドプールを利用している場合に、プールが空くのを待つために SCHEDULED 状態で待機することがありえる
- `Worker` の実装によってはスケジュールされずに即座に起動される場合もある
- しかし、その場合も必ず SCHEDULED を経由してから RUNNING 状態になる
- 実行中を表す `running` プロパティは `true` になる

**RUNNING**

- `Worker` が実際に実行されている状態
- 実行中を表す `running` プロパティは `true` になる

**SUCCEEDED**

- 正常終了した場合にこの状態になる
- 結果が `value` プロパティにセットされる

**FAILED**

- 実行中に例外がスローされた場合にこの状態になる
- スローされた例外は `exception` プロパティにセットされる

**CANCELLED**

- 終了状態になる前にキャンセルが実行されると、この状態になる

`Worker` には、これら状態についての情報を取得するための [stateProperty()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Worker.html#stateProperty--) や [runningProperty()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Worker.html#runningProperty--) といったプロパティが用意されている。

### Task
`Worker` を実装したクラスとして [Task](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Task.html) というクラスが用意されている。

```java
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();
    }
}
```

```text:実行結果
task.call()
thread = Thread-4
```

- `Task` クラス自体は抽象クラスなので、継承して自作のクラスを作成する
- `call()` メソッドが抽象メソッドになっている
- 自作クラスで `call()` メソッドを実装し、この中でバックグラウンドで動かす処理を記述する
- `Task` クラスは `Runnable` インターフェースを実装しているため、 `Thread` や `Executor` に渡して実行できる

#### キャンセル
```java
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()` メソッドを呼んでいる。

```text:実行結果
start...
cancelled!!
```

- `Worker` の `cancel()` メソッドを呼ぶと、 `Worker` の実行をキャンセルできる
    - キャンセルできるのは `Worker` の状態が「終了状態」以外のときに限られる
    - 「終了状態」で `cancel()` を呼んだ場合は何も行われない
- キャンセルが実行されると、 `Task` の `isCancelled()` が `true` を返すようになる
    - これを利用して、 `call()` メソッドの中でキャンセルされたかどうかが判定できる
    - `Thread.sleep()` などでブロック中にキャンセルされた場合は `InterruptedException` をキャッチしたうえで `isCancelled()` の判定を入れる
    - 詳しくは `Task` クラスの [Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Task.html) を参照

#### 進捗
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/f6e5fc3e-f1bf-dfa8-3836-14b8e89d5d2d.jpeg)

```java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/722a77c7-31af-f5d7-e6ec-36b0889212b1.gif)

**説明**

```java
        this.progressBar.progressProperty().bind(task.progressProperty());
```

- `Worker` は `progressProperty()` というプロパティを公開している
- これを `ProgressBar` の `progressProperty()` にバインドすることで、 `Task` の `progress` と `ProgressBar` の `progress` を連動させることができる

```java
                for (long i=0; i<=max; i++) {
                    this.updateProgress(i, max);
                }
```

- `Task` の進捗を更新するには `updateProgress()` メソッドを使用する
- 第一引数には処理済みの件数、第二引数には全体の件数を渡す（`double` を渡すメソッドもオーバーロードされている）
- この `updateProgress()` は非 UI スレッドから実行しても問題ないようになっている
    - 中で `runLater()` が使用されている

#### 任意の値を公開する
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/911e8c5b-9ee5-6cf0-4a51-be081d3a4892.jpeg)

```java
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!!";
        }
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/8520fc07-6bf7-7d86-077e-98430b423268.gif)

**説明**

```java
    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` プロパティにセットされる

#### イベントリスナー
```java
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 スレッドで実行される

#### 例外プロパティ
```java
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();
    }
}
```

```text:実行結果
exception=java.lang.RuntimeException: test
```

- `call()` メソッドの中で例外がスローされると、 `exception` プロパティにスローされた例外がセットされる
- 例外はスタックの上位に伝播しないので、ほっといたら例外がスローされたことに気付かない気がする

#### ObservableList を公開する
```java
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
```java
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()` メソッドをボタンのアクションイベントに割り当てて、何回か実行する。

```text:実行結果
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` が実行される

#### 再実行
```java:状態をみて再実行する場合
    @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` に戻してから再実行する

```java:restart()で書き換えた場合
    @FXML
    public void start() throws IOException {
        System.out.println("restart");
        this.service.restart();
    }
```

- `start()`, `reset()`, `restart()` メソッドは、どれも UI スレッドから実行しなければならない

#### 使用するスレッドを指定する
まずは、デフォルトの動作を確認する。

```java
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()` を実行する

```text:実行結果
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` を起動するたびにデーモンスレッドを新規に作成するようになっている
- 使用するスレッドを指定する（たとえば、スレッドプールのスレッドを使用する）場合は、次のように実装する

```java:Serviceが使用するスレッドを指定する
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();
    }
}
```

```text:実行結果
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](https://docs.oracle.com/javase/jp/8/docs/api/java/util/concurrent/Executor.html) を指定する
- すると、 `Service` は `Task` を実行するためのスレッドを `executor` から取得するようになる


#### 各種プロパティ
```java
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` のプロパティを参照するのと同じということになる





**参考**

- [1 JavaFXでの同時実行(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/interoperability-tutorial/concurrency.htm)
- [javafx.concurrent (JavaFX 8)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/package-summary.html)

# 視覚効果
ライティングやドロップシャドウなどの視覚効果を適用する方法。

仕組みとしては、一旦シーングラフをピクセルイメージとしてレンダリングして、それに対して影を落としたり光を当てたりする画像処理をするようになっているっぽい。

次のようなシーングラフに各種視覚効果を適用してみて、どうなるか確認する。

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/04320c8d-34fc-0cdd-30ef-774981ed0a96.jpeg)

## ブルーム効果

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/7fe3f201-9267-b399-af69-de4c749f1af4.jpeg)

**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/Bloom.html) クラスを使用すると、ブルーム（画像が輝いているように見える）効果を適用できる
- `Bloom` のインスタンスを生成し、効果を適用するシーングラフ上のノードに `setEffect()` で設定することで、そのノード以下に視覚効果が適用される

## ぼかし効果
### ボックスぼかし

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/92108a65-ff19-48b3-e908-2bed4fea8145.jpeg)

**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/BoxBlur.html) を使用すると、ボックスぼかし効果を適用できる
- `width`, `height` で、縦横方向のぼかし距離を指定できる
- `iterations` を指定することで、ぼかし効果の適用回数を指定できる（デフォルトは `3`）

### モーションぼかし

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/720964af-12ac-0a72-da05-2fdd0211ae95.jpeg)

**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/MotionBlur.html) を使用すると、モーションぼかし（物体が動いているときのようなぼかし）を適用できる
- `angle` でぼかしの向きを指定できる
    - 度数法（角度を `0.0` ～ `360.0` で表現するやつ）で指定する
    - 水平方向が `0.0` で、そこから時計回りに回転していく感じ
        - `―` : `0.0`
        - `＼` : `45.0`
        - `｜` : `90.0`
        - `／` : `135.0`
        - `―` : `180.0`
- `radius` でぼかしの半径を指定する

### ガウスぼかし

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/eecb3d83-401d-4842-36b3-9472c0ca2950.jpeg)

**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/GaussianBlur.html) を使用すると、ガウスぼかしを適用できる（ボックスぼかしとの違いはよく分かってない）
- `radius` でぼかしの半径を指定できる

## ドロップ・シャドウ効果

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5c1b2279-2e25-3a0c-8f11-9103582d6a56.jpeg)

**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/DropShadow.html) を使用すると、ドロップシャドウ（影を落としたような）効果を適用できる
- `offsetX`, `offsetY` で、影の相対的な位置（オフセット）を指定する
- `width`, `height` で、影の縦横方向のぼかし具合を指定する
- `color` で、影の色を指定する
- `spread` で、影の拡散具合（`0.0` ～ `1.0`）を指定する


## インナー・シャドウ効果
確認しやすくするため、シーングラフをちょっと変更する。

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d21130a6-92a9-11cc-1d43-f1aafd71e954.jpeg)

※視覚効果適用前

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d4f94a86-5595-0f06-8c94-39e47e38b66a.jpeg)

**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/InnerShadow.html) を使用すると、インナーシャドウ（内側に窪んだような影の）効果を適用できる
- `offsetX`, `offsetY` で、影の相対的な位置（オフセット）を指定する
- `width`, `height` で、影の縦横方向の長さを指定する
- `color` で、影の色を指定する
- `radius` で、影のぼかし半径を指定する

## リフレクション

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/49c81865-5290-0220-d2bb-4919573b5a04.jpeg)


**実装**

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/effect/Reflection.html) を使用すると、リフレクション（物体が床に反射しているような）効果を適用できる
- `topOffset` で、反射表示されている部分の上部の相対的な位置（オフセット）を指定する
- `topOpacity` で、反射表示されている部分の上部の透明度を指定する
- `fraction` で、反射表示する部分の割合を指定する（`0.0` ～ `1.0` で、デフォルトは `0.75`、つまり 75% だけが反射表示される）
- `bottomOpacity` で、反射表示されている部分の下部の透明度を指定する

## 視覚効果を組み合わせる

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1fc910d1-0640-9434-5415-dcebe3f969df.jpeg)

**実装**

```java
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()` で渡した方が先に適用される

**参考**

- [ビジュアル効果 | 2 JavaFXアーキテクチャの理解(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/get-started-tutorial/jfx-architecture.htm#A1106693)
- [5 効果の適用(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/visual-effects-tutorial/effect-types.htm)

# メディア
※以下、音声や音楽、動画・映像を総称して**メディア**と表現する（例：メディアファイル→音声ファイルや動画ファイルのこと）

## メディアファイルの仕組み
### コーデック
- メディアデータは、そのままだと数分の内容でも数百MBや数GBといった大容量になってしまう
- そのため、通常は何らかの方法で圧縮されている
- この圧縮のことを**エンコード**と呼ぶ
- エンコードされたメディアデータは、再生するときに元の状態に戻す（解凍する）必要がある
- この解凍のことを**デコード**と呼ぶ
- メディアデータをエンコード・デコードするプログラムのことを、両方の言葉を合わせて**コーデック**と呼ぶ
    - モノによってはエンコード（デコード）しかできないものもあるらしい
- メディアデータを再生するためには、メディアデータをデコードできるコーデックが存在しなければならない
- エンコードの形式には多くの種類が存在する
    - 音声データなら、 MP3, AAC, WMA, etc...
    - 動画データなら、 H.264, VP9, DivX, Xvid, etc..

### コンテナ
- メディアファイルの中には、複数のメディアデータが組み合わされた状態で格納されている
- 例えば、単純な動画ファイルの場合は映像データと音声データが１つのファイルの中に格納されている
- メディアデータを入れるファイルのことを**コンテナ**と呼ぶ
- コンテナにも複数の種類が存在する
    - 音声ファイルなら、 WAV, AIFF, MP3, etc...
        - MP3 は、エンコード形式としての MP3 もあれば、それを格納できるコンテナとしての MP3 もある（ややこしい）
    - 動画ファイルなら、 MP4, FLV, AVI, MOV, etc...
- コンテナの中には、様々なエンコード形式のメディアデータを格納できる
    - １つのコンテナに入れられるエンコード形式の種類は１つとは限らない
    - 例えば MP4 というコンテナの場合
        - 映像データには H.264 や H.265 でエンコードされたデータを入れられる
        - 音声データには AAC や MP3 でエンコードされたデータを入れられる
    - つまり、同じコンテナのファイルであっても、中に入っているメディアデータのエンコード形式次第では再生できない可能性がある（対応するコーデックがないと再生できない）
    - コンテナによって格納可能なエンコード形式は異なる
    - コンテナに入っているメディアデータの実際のエンコード形式は、 [MediaInfo](http://aviutl.info/mediainfo/) などのツールを利用することで確認できる
- コンテナの種類とファイルの拡張子は、だいたい一致していることが多い（MP4 なら `.mp4`、 FLV なら `.flv`, `.f4v`, etc...）

### JavaFX がサポートしているエンコード形式・コンテナ
- JavaFX がサポートしているメディアのエンコード形式やコンテナは、[Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/media/package-summary.html) に記載されている

#### エンコード形式
|エンコード形式|音声/映像|
|-------------|--------|
|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 という２つの名前があり、くっつけて「H.264/MPEG-4 AVC」と表記されることが多い
    - JavaFX の Javadoc は「H.264/AVC」って書いている
- 非常に高い圧縮率で、広く普及している
- 後継として、さらに圧縮率の上がった H.265 がある

**VP6**

- FLV コンテナ（Flash ムービー）で利用される映像用のエンコード形式
- H.264 より圧縮率がいいらしい
- 後継として、さらに圧縮率の上がった VP9 がある（Youtube で採用されてるらしい）
- ただし、 [Java9 以降は非推奨](https://docs.oracle.com/javase/jp/9/docs/api/javafx/scene/media/package-summary.html#package-summary.3siPA6)らしい

#### コンテナ形式
|コンテナ|エンコード形式(映像)|エンコード形式(音声)|
|-------|-------------------|------------------|
|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)は最低限サポートしている、という感じ。

#### サポートされていないエンコード・コンテナのメディアファイルを再生する
- 標準でサポートされていない形式のメディアファイルを再生するにはどうするか
- コーデックを別途追加するような方法があればできそうだが、[無理っぽい？](https://stackoverflow.com/questions/8153227/adding-other-video-codecs-dvd-support-to-javafx-2-2)
    - WebView を使えば再生できそうなことが書いているように読めたが、実際やってみても無理だった
- そうなると、メディアファイルの形式を JavaFX がサポートする形式に変換せざるを得ない気がする
    - エンコーディングを変換できるフリーのツールは、音声・動画ともに色々とあるので、それで JavaFX がサポートする形式に変換すれば再生できるようになる
    - 「音声　動画　エンコード　変換」とかで検索すれば色々ヒットする


**参考**

- [動画形式の種類と違い（AVI･MP4･MOV･MPEG･MKV･WMV･FLV･ASF等）【コンテナ】](http://aviutl.info/dougakeisiki-konntena/)
- [音声コーデックの種類と違い(MP3･AAC･WMA･WAV･Vorbis･AC3･FLAC等)【フォーマット】](http://aviutl.info/onnsei-codec/)
- [音声ファイルの主な形式と特徴｜コーデック・MP3・AAC・Ogg](http://yamanjo.net/knowledge/others/others_12.html)
- [動画ファイルの主な形式と特徴｜コーデック・AVI・MPEG](http://yamanjo.net/knowledge/others/others_13.html)
- [コンテナフォーマット - Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88#%E9%9F%B3%E5%A3%B0%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A)

## メディアファイルを再生する
```java
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 の状態遷移
![javafx.png](https://qiita-image-store.s3.amazonaws.com/0/28302/6360e4ee-e033-fde3-fb1a-0a3be1fcc763.png)

※`play()`, `pause()`, `stop()` は `MediaPlayer` のメソッド

|状態|説明|
|---|---|
|`UNKNOWN`|作成直後で読み込みが完了していない状態  |
|`READY`  |読み込みが完了し、再生の準備ができた状態|
|`PLAYING`|再生中の状態                         |
|`PAUSED` |一時停止中の状態<br>`PLAYING` に戻ると停止していたところから再開する|
|`STOPPED`|停止中の状態<br>`PLAYING` に戻ると最初から再生する|
|`STALLED`|ストリーミング再生でプレイヤーの再生を続けるのに十分な情報が取得できていない状態|
|`HALTED` |エラーが発生してプレイヤーが再生できない状態|
|`DIPOSED`|プレイヤーが破棄されリソースが解放された状態|

- `MediaPlayer` は上図のような状態遷移をする
    - `DISPOSED`, `HALTED` は特殊な状態なので省略している
- メディアファイルの読み込みは非同期で行われ、 `Media` インスタンスを生成した直後はまだ読み込みが完了していない可能性がある
- `Media` インスタンスが関連付けられた `MediaPlayer` の状態が `READY` になったら、メタ情報の読み込みは確実に完了している
- `UNKNOWN` 状態の場合は再生などがまだできないが、 `play()` メソッドなどの命令は `UNKNOWN` の状態で呼んでいるとバッファされ、 `READY` になった時点で実行されるようになっている（明示的に `READY` を待たなくていい）
- 詳しくは [MediaPlayer.Status の Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/media/MediaPlayer.Status.html) を参照

## リソースファイルを再生する
```text:フォルダ構成
`-src/main/
  |-java/
  | `-sample/javafx/
  |   :
  `-resources/
    |-...
    `-media/
      `-music.mp3
```

```java
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()` すれば、リソースファイルを再生できる


## 繰り返し再生する
```java
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` を指定すると、無制限に連続再生されるようになる

## 再生時間
```java
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());
        });
    }
}
```

```text:実行結果
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 クラス
```java
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());
    }
}
```

```text:実行結果
duration=90000.0 ms
duration.seconds=90.0
duration.minutes=1.5

oneMinute=60000.0 ms
oneMinute.minutes=1.0
```

- [Duration](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/util/Duration.html) は期間を表すクラス
    - 期間とは、何分間・何秒間、といった長さをもった時間のこと（特定の時点を表すもの**ではない**）
- ミリ秒を基本の単位として期間を保持している
- コンストラクタで生成する場合はミリ秒を指定する
- ファクトリメソッドを使えば、時間・分・秒指定などでもインスタンスを生成できる
- このクラスは不変で、 [add()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/util/Duration.html#add-javafx.util.Duration-) や [multiply()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/util/Duration.html#divide-double-) といったメソッドは計算結果を別の新規インスタンスに格納して返すようになっている

#### 文字列フォーマット
- `Duration` の情報を文字列にフォーマット（`02:00:12` とかに）したい場合、直接的にフォーマットできるモノは標準 API の中に見当たらない
- Date and Time API の [Duration](https://docs.oracle.com/javase/jp/8/docs/api/java/time/Duration.html) なら良い感じのフォーマッターがあるかと思ったが、[なさそう](https://stackoverflow.com/questions/266825/how-to-format-a-duration-in-java-e-g-format-hmmss)...
- 仕方ないので、↑の StackOverflow の実装例を参考に JavaFX の `Duration` 版を書いてみると↓のような感じか

```java
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:実行結果
text=03:24:54.027
```

### 再生する範囲を指定する
```java
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());
        });
    }
}
```

```text:実行結果
cycleDuration=2000.0 ms
totalDuration=6000.0 ms
```

- `startTime`, `stopTime` にそれぞれ開始時間、終了時間を設定すると、その範囲内でのみメディアデータが再生される
- `cycleDuration` は `startTime` ～ `stopTime` までの時間を返す
- `totalDuration` は、 `cycleDuration * 再生回数`

## メタデータ
MP3 ファイルには、歌手やアルバム名、歌詞、アルバムアートなど様々なメタデータを付加することができる。
JavaFX から、それらのメタデータにアクセスすることができる(FLV のメタデータもとれるっぽいが、ここでは省略)。

```java
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);
            });
        });
    }
}
```

```text:実行結果
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/media/package-summary.html#SupportedMetadataTags) に記載されているのでそちらを参照。
- 残念ながら AAC はメタデータの取得がサポートされていない
    - 一応 [Java9](https://docs.oracle.com/javase/jp/9/docs/api/javafx/scene/media/package-summary.html#SupportedMetadataTags) も確認したが、変わらず AAC はサポート対象外

## 再生位置を変更する
```java
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` からの期間ではない（たぶん）
- 図にすると↓のような感じ

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b60f9ec9-f20d-3df1-929b-0f52566a475d.jpeg)

- `startTime`, `stopTime` を指定していて、その範囲の外に出る `seekTime` を指定した場合は、 `startTime` か `stopTime` のいずれかに納まるように制限される
- 詳しくは [Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/media/MediaPlayer.html#seek-javafx.util.Duration-) を参照

## 映像を表示する
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/ef3f7cd3-27c1-9ada-5aa7-fe870909f0fb.jpeg)

```java
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` を使ったほうが効率がいい。

```java
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](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/media/AudioClip.html) を参照。

## 簡単な音楽プレイヤーを作成してみる
ここまでの内容を活用しつつ、簡単な音楽プレイヤーを作成してみる。

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d8f53595-5dfa-fd76-cd00-3384b222f998.jpeg)

```java
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);
    }
}
```

### 現在の再生時間の表示
```java
        // 現在の再生時間ラベル
        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` プロパティにバインドして、時間をフォーマットした値をラベルにセットする

### ボタンの制御
```java
        // 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` プロパティにセットするようにしている

### 音量スライダー
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/55ea7e79-e1cd-3cd5-2e7f-f703d169c972.jpeg)

```java
        // 音量ラベル
        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()` を呼ばなければならない
    - また、次のように双方向で状態を連動させなければならない
        1. 再生時間にあわせてスライダーの位置を調整する
        2. ユーザーがスライダーを操作したら、再生時間を調整する


![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/8c385546-5341-44a8-a619-f0c7a7b09f78.jpeg)

- `Slider` は `0.0` ～ `1.0` までの間で指定できるようにしておき、合計時間に対する割合として扱うようにしている

#### 再生時間→シークバー
```java
        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` を返す

#### シークバー→再生時間
```java
        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()` を実行する
- ここで注意なのが、スライダーをユーザが操作**しているとき**にだけ値を変更するようにしなければならない
- そうしないと、再生時間に合わせてスライダーの値を変更したときにもこのリスナーが呼ばれるので、再生位置が無駄に更新されてしまい、音がカクカク再生されるような感じになってしまう

#### 再生時間が終了したときの処理
```java
        // 再生が終了したときの後処理
        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` しか返さなくなっている気がする
    - 「バグじゃね？」という気がするが、仕様を熟知しているわけでもないので、なんとも言えない



**参考**

- [javafx.scene.media (JavaFX 8)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/media/package-summary.html)
- [3 メディア再生の制御(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/media-tutorial/playercontrol.htm)
- [1 JavaFXメディアの概要(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/media-tutorial/overview.htm)


# メニュー
## メニューバー
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d0fddfaa-4412-9134-1bab-17f0b1dca2ad.jpeg)

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1fa3710d-b3dc-7a85-5f1a-8453e1d9ecd0.jpeg)

**説明**

- メニューバーを作成するには、 `MenuBar` を使用する
- 配置できる場所に制限はないが、普通はウィンドウの上部に配置する
- `MenuBar` にカテゴリ（「ファイル」となっているところ）を追加するには `Menu` を使用する
- `Menu` にさらに `MenuItem` を追加することで、個々のメニュー項目を定義できる

### メニューが選択されたときの処理を実装する
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1ddc1161-0eb4-0638-46da-45ad34929dc1.jpeg)

```java
package sample.javafx;

import javafx.fxml.FXML;

public class MainController {
    
    @FXML
    public void open() {
        System.out.println("「開く」が選択された");
    }
}
```

- メニューの `On Action` イベントとコントローラのメソッドを紐づけることで、メニューが選択されたときの処理を実装できる

### セパレータ
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/51e57af1-150c-97f4-7992-72660935eeb5.jpeg)

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/c6119341-e7b6-05e8-bb24-d66a88e3a074.jpeg)

- `SeparatorMenuItem` を差し込むことで、メニューに区切り線を入れることができる

### サブメニュー
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/2ea14e06-fe73-02e4-883f-f67f00cf5aa2.jpeg)

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/70d0a93c-4c4b-61c8-e3f2-3b3bd344d102.jpeg)

- `Menu` の下にさらに別の `Menu` を追加することでサブメニューを実現できる

### ショートカットキーを割り当てる
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/29731e34-b11a-1ef5-d39b-041616ffb52f.jpeg)

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/23c17ad0-6d6a-382d-26d7-9ae6b7cf22ae.jpeg)

- `MenuItem` の `accelerator` プロパティでショートカットキーを割り当てることができる
- ショートカットキーは、修飾子キーと主要なキーの組み合わせで定義する
    - 修飾子キーは、 `Ctrl`, `Shift`, `Alt`, `Meta`, `Shortcut` のいずれか
        - さらにキーの状態を示す `DOWN`, `UP`, `ANY` のいずれかを指定する
    - 主要なキーとは、 `A`, `B` などの通常のキーのこと
- 修飾子キーの `Shortcut` とは、ショートカットでよく利用されるキーのことで、 OS に依存しない定義を実現できる
    - Windows の場合は `Ctrl` キーのことを指し、  
      Mac の場合は `Meta` キーのことを指す

### ニーモニック
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/2c7a4ad2-3ea4-c1aa-49ba-85fef290ca9d.jpeg)

```java
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");
    }
}
```

各メニューとコントローラのメソッドを関連付けている。

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/abddc490-2d12-1082-4554-2c98081e8580.gif)

- ちょっと分かりづらいが、マウスを使用せずにキーボードのみでメニューを操作している
    - 実際触ってみるとわかるが、正直動きが怪しい
- Windows の場合は Alt でメニューにフォーカスが当たる
- そのあとは各メニュー項目のアンダーバーがついている文字を入力することで、キーボードのみでメニューを操作できる
- このメニューに対して割り当てられるキーのことを**ニーモニック**と呼ぶ

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1fd219d8-fadc-6bae-9416-5bb9fae086dd.jpeg)

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/cc424b5b-719f-35b6-7ac2-c0631dd8bf33.jpeg)

- メニューにニーモニックを割り当てるには、次の２つの手順が必要になる
    1. `Mnemonic Parsing` のチェックをオンにする
    2. `Text` の中でニーモニックを割り当てたいアルファベットの前にアンダーバー (`_`) を入れる
- すると、アンダーバーの次の文字がニーモニックに割り当てる文字と判断される

#### 日本語メニューにニーモニックを割り当てる
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/9cd9e7f5-ee0f-603a-1321-a986d5028731.jpeg)

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b6b562ec-036e-c35c-8efe-712682a3b847.jpeg)

- ニーモニックはアルファベットで割り当てるので、日本語メニューはそのままだとニーモニックを割り当てられない（たぶん）
- その場合は、メニューテキストの後ろに `(_F)` のようにニーモニック用の注記を追加すれば日本語メニューでもニーモニックを割り当てられる（というか、 iTunes とか他の一般的なアプリケーションがそうしているので、それを参考にした）


### アイコンを追加する
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/26d6f773-91ce-b887-f8f1-6963a1c8808d.jpeg)

```text:フォルダ構成
`-src/main/
  |-java/
  | :
  `-resources/
    `-img/
      `-open.png
```

```java
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);
    }
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/df5cf4ad-4ccb-e47e-b406-7ac59f4a3c83.jpeg)

- `MenuItem` の `graphic` プロパティに `ImageView` を設定することで、メニュー項目に任意の画像を表示させられる

### チェックメニュー
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/aa338707-5c99-1904-7302-6981005bff09.jpeg)

```java
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 ? "チェックされた" : "チェックが外れた");
        });
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/4daab0fe-823a-94a8-a88b-ffa1721795c2.gif)

- `CheckMenuItem` を使用すると、チェックのオン・オフを切り替えるメニュー項目を定義できる
- メニューが選択されているかどうかは `selected` プロパティで確認できる

### ラジオボタンメニュー
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/8773a422-f0d9-69a5-941d-cb3ab9cf942d.jpeg)

```java
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());
            }
        });
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/6a598513-e5ab-55d7-8411-633cfd3db1cf.gif)

- `RadioMenuItem` を使用すると、排他的に選択できるメニュー項目を定義できる
- `RadioMenuItem` の `Toggle Group` に、その項目が属するグループの識別子（任意の文字列）を設定する
    - 同じグループの識別子を持つ項目は同じグループに属するものとして識別されるようになり、選択が排他的に制御されるようになる
- `Toggle Group` は、 fxml 上では次のように定義される

```xml:fxml上のToggleGroup
  <RadioMenuItem fx:id="hogeRadioMenuItem" text="ほげ">
     <toggleGroup>
        <ToggleGroup fx:id="group1" />
     </toggleGroup>
  </RadioMenuItem>
```

- `fx:id` が `Toggle Group` で指定した識別子になっている
- なので、コントローラクラスには指定した `Toggle Group` の識別子で `ToggleGroup` インスタンスをインジェクションできるようになる

```java:ToggleGroupのインジェクション
    @FXML
    private ToggleGroup group1;
```

- `ToggleGroup` の `selectedToggle` プロパティにリスナを登録することで、選択されている項目が変わったときのイベントを捕捉できる
- このとき、**1回の変更でイベントが2度呼ばれ、最初の1度目は `newValue` が `null` になる**という点に注意
- これはバグらしく、 ver 10 で治る予定っぽい
    - [[JDK-8189677] RadioMenuItem fires extra NULL value in property - Java Bug System](https://bugs.openjdk.java.net/browse/JDK-8189677)

```java
        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()` メソッドなどで設定するしかない

## コンテキストメニュー
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/4467f191-945d-4015-ff39-d32f1b10d90c.jpeg)

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/14700875-2933-4fa9-c3ec-99a6fb0ac77d.jpeg)

- コンテキストメニュー（右クリックとかで出るメニュー）は、対象のノードに `ContextMenu` を追加することで実現できる
- メニュー項目の定義方法などはメニューバーと同じ


**参考**

- [24 メニュー(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/user-interface-tutorial/menu_controls.htm#BABGHADI)


# ウィンドウの最大化
```java:Main.java
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` を設定すれば、ウィンドウを最大化できる


# 別のウィンドウを開く
## 基本
```text:フォルダ構成
`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-MainController.java
  |   `-OtherController.java
  |
  `-resources/
    |-main.fxml
    `-other.fxml
```

**other.fxml**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/8bee7bfd-9b31-bdf6-788b-d15a713db47c.jpeg)


```java:MainController.java
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 を参照](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/fxml/FXMLLoader.html)
- `load()` メソッドで fxml 内に定義した一番上のノードが取得できる
- あとは、シーン・ステージを作って `show()` もしくは `showAndWait()` メソッドで起動する
    - JavaFX アプリケーションは、**ステージ**・**シーン**・**シーングラフ**から成る
    - **ステージ**は、 JavaFX アプリケーションの最上位コンテナで、ウィンドウに対応する
        - 上の実装だと、`Stage` が対応する
    - **シーン**とは、シーングラフを保持するコンテナで、ステージの表示領域部分に対応する
        - 上の実装だと、 `Scene` が対応する
    - **シーングラフ**とは、シーン上に表示する全てのノードを保持するツリー構造で、シーングラフを書き換えることで表示を変更することができる
        - 上の実装だと、 fxml からロードした `Pane` が、このシーングラフのルートノードとなる

## コントローラを取得する
```java:MainController.java
package sample.javafx;

public class MainController {
    
    public void hello() {
        System.out.println("Hello MainController!!");
    }
}
```

```java:Main.java
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` が返るので注意


## 所有者の設定
**所有者を設定していない場合の挙動**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/2241c444-e8a3-bf8e-ebb4-5f92a65c2639.gif)

- `Stage` に所有者（`owner`）を設定していない場合、最初に開いていたウィンドウを閉じても後から開いているウィンドウは閉じられない

**所有者を設定する**

```java:Main.java
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();
    }
}
```

```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/ce59121a-46d6-5e0b-973f-7a9889f285ae.gif)

- 所有者を設定すると、親のウィンドウが閉じると子のウィンドウも一緒に閉じられるようになる
- シーングラフを fxml からロードしている場合、対応するコントローラ内で自分自身の `Stage` を取得する方法はいくつかあるが、 `FXMLLoader` からコントローラのインスタンスを取得してセッターなどを介して設定するのが、なんだかんだスマートな気がする
- 所有者の設定には `initOwner()` メソッドを使用する
- ちなみに、所有者を設定してもモーダルになっているわけではない

## モーダル
```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/caa985ad-9912-bbcd-6835-cf8eac4e4f55.gif)


- `initModality()` で、そのステージをモーダルにすることができる
- `initModality()` には、 `Modality` 列挙型の定数を渡す
- `Modality` には `NONE`, `WINDOW_MODAL`, `APPLICATION_MODAL` の３つが定義されている

|定数|意味|
|----|----|
|`NONE`        |他のウィンドウをブロックしない|
|`WINDOW_MODAL`|所有者のウィンドウをブロックする<br>（所有者以外のウィンドウはブロックしない）|
|`APPLICATION_MODAL`|すべてのウィンドウをブロックする|


**参考**

- [JavaFXの基本構造｜軽Lab](http://krr.blog.shinobi.jp/javafx/javafx%E3%81%AE%E5%9F%BA%E6%9C%AC%E6%A7%8B%E9%80%A0)
- [1 JavaFXシーン・グラフの操作(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/scene-graph-tutorial/scenegraph.htm)
- [Stage (JavaFX 8)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/stage/Stage.html#initOwner-javafx.stage.Window-)
- [FXML Controller で Stage を使うためのアレコレ - Java開発のんびり日記](http://hideoku.hatenablog.jp/entry/2013/06/07/205016)


# 別の fxml を埋め込む
- 複雑な画面を単一の fxml で作ると、コントローラクラスも含めて実装がごちゃごちゃになってしまう
- また、同じ部品を複数の箇所で使いまわす場合なども、単一の fxml で作っていると重複が発生するなどして具合が悪い
- fxml には、他の fxml を埋め込むための `<fx:include>` というタグが用意されている
- これを使えば、上述のような問題を回避できる

## 基本

**フォルダ構成**

```text
`-src/main/resources/
  |-embedded.fxml
  `-main.fxml
```

**埋め込むfxml (embedded.fxml)**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/051e49bb-66eb-89e0-ad87-580918cd9eba.jpeg)

**埋め込み先のfxml (main.fxml)**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5f600116-8f56-2ce7-58f3-402e339787ca.jpeg)

```xml: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 しようとすると失敗する**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/a3b0b1a7-201c-fd0d-f753-c5b29d488570.jpeg)

- SceneBuilder には一応 fxml を include するためのメニューが用意されているっぽい（File -> Include -> FXML）
- しかし、これを使って fxml を include しようとしても、 `Failed to include '***.fxml'` と出て include に失敗する
    - たまに成功するときもあるが、よくわからない
- SceneBuilder が include をサポートしていないわけでもないらしく、手で直接 fxml を修正して `<fx:include>` を書けばちゃんとプレビューもしてくれるようになる
- 原因は分からないが、とりあえず `<fx:include>` だけは手書きしておくのが無難そう
- 手書きで `<fx:include>` を埋め込んだ後なら、普通に SceneBuilder から操作できるようになる

## 埋め込んだ fxml のコントローラを取得する
```text:ファイル構成
`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-Main.java
  |   |-MainController.java
  |   `-EmbeddedController.java
  `-resources/
    |-main.fxml
    `-embedded.fxml
```

**embedded.fxml**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/d970aff7-7693-a7d9-7c99-be6c31abe46f.jpeg)

```java:EmbeddedController.java
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**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/601e3a86-55ae-df65-b9fd-8d32185c57f5.jpeg)

```xml: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>
```

```java:MainController.java
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!!");
    }
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/f6b48a18-40ab-64fe-d146-31cec665574b.jpeg)

**説明**

- `<fx:include>` で埋め込んだ FXML (`embedded.fxml`)に紐づけられたコントローラ(`EmbeddedController`)は、埋め込み先の
 FXML (`main.fxml`)のコントローラ(`MainController`)に `@FXML` でインジェクションできる
- ただし、 `<fx:include>` に `fx:id` 属性が設定されている必要がある
    - `fx:id` が設定されていないと、コントローラのインスタンスはインジェクションされず `null` になる

## 埋め込んだ fxml を別ウィンドウとして表示する

**embedded.fxml**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5175b491-c1a6-4bbf-7b95-52c6a02e52d0.jpeg)

```java:EmbeddedController.java
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**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b1853dd7-fae6-46ff-a189-c680e6944be2.jpeg)

```xml: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>
```

```java:MainController.java
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();
    }
}
```

**実行結果**

![javafx.gif](https://qiita-image-store.s3.amazonaws.com/0/28302/f2e476a8-c92c-0e45-a592-62f51c763e9e.gif)

**説明**

```xml:main.fxml
...

<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 するのに必要になる）

```java:MainController.java
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` で都度読み込むことになると思う



**参考**

- [&lt;fx:include&gt; | FXMLの概要 | JavaFX 8.0](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/fxml/doc-files/introduction_to_fxml.html#include_elements)


# ファイル選択ダイアログ
![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/f2abe756-a5e0-b35b-f050-14a165d75c2b.jpeg)

- ボタンをクリックしたら、ファイル選択ダイアログが表示されるようにする

```java:Main.java
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` をセットする

```java:MainController.java
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;
    }
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/3555af53-278e-ebf6-1b18-5aee38318d2c.jpeg)

表示されたダイアログ

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/f5285560-a091-bfd4-268b-c5f3178d2cc0.jpeg)

ファイルを選択して「開く」をクリック

```text:コンソール出力
file=C:\Program Files\Java\jdk-9.0.1\README.html
```

**説明**

```java
        FileChooser chooser = new FileChooser();
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
```

- ファイルを選択するダイアログを表示するには、 [FileChooser](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/stage/FileChooser.html) クラスを使用する
- インスタンスを生成して `showOpenDialog(Stage)` メソッドを実行すると、単一ファイルを選択するためのファイル選択ダイアログが開く
    - 引数の `Stage` には、オーナーとなる `Stage` を指定する
    - オーナーが指定されている場合、ファイル選択ダイアログが開いている間、オーナーへの操作はブロックされる（モーダルになる）
    - `null` を渡すこともでき、その場合ファイル選択ダイアログはモーダルにはならない
- ダイアログが表示されている間、 `showOpenDialog()` メソッドはブロックされる
- ダイアログでの選択結果が `File` 型で返される
- ファイルが選択された場合は、そのファイルを指す `File` オブジェクトが返る
- ファイルを選択せずにダイアログが閉じられた場合は `null` が返る

## タイトルを設定する
```java
        FileChooser chooser = new FileChooser();
        chooser.setTitle("ふぁいるせんたく");
        File file = chooser.showOpenDialog(this.stage);
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/96996c49-1c57-6eb9-a4b2-16bacfa3115e.jpeg)

- `setTitile(String)` でダイアログのタイトルを指定できる

## 初期表示するディレクトリを指定する
```java
        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()` でディレクトリを指定することで実現できる

## 選択できるファイルを拡張子で絞る
```java
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);
    }

    ...
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/9d34e35d-18fe-6c0b-44d2-197c6b0c99f2.jpeg)

- `getExtensionFilters()` で、拡張子による絞り込みを定義した `ObservableList` を取得できる
- このリストに `ExtensionFilter` を追加することで、拡張子により絞り込みを指定できる
- `ExtensionFilter` のコンストラクタには、「説明」と「拡張子の定義」を渡す
    - 「拡張子の定義」は `*.<拡張子>` の形式で指定する
    - 何でもありの場合は `*.*` にする
    - 「拡張子の定義」は複数指定することが可能

## 複数のファイルを選択できるダイアログを開く
```java
        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);
        }
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/c0c1adb1-ad91-939a-09c7-bba457209399.jpeg)

```text:コンソール出力
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` を返す（空のリストじゃないのか...）

## ファイルの保存先を選択するダイアログ
```java
        FileChooser chooser = new FileChooser();
        File file = chooser.showSaveDialog(this.stage);
        System.out.println("file=" + file);
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/6ec3f2f5-7208-aabc-19f7-a76ecb9a8793.jpeg)

```text:コンソール出力
file=C:\Users\Public\hoge
```

- `showSaveDialog(Stage)` を使用すると、ファイルの保存先を選択するダイアログを開くことができる
- 戻り値は、ユーザが指定した保存先のファイルを指す `File` オブジェクトになる
- 何も選択せずにダイアログが閉じられた場合は、 `null` が返される


## ディレクトリを選択するダイアログ
```java
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);
    }

    ...
}
```

**実行結果**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/8ebe3793-6fdf-dae5-5c43-1681e06f095a.jpeg)

```text:コンソール出力
directory=C:\Users\Public\Downloads
```

- ディレクトリを選択するダイアログを開くには、 [DirectoryChooser](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/stage/DirectoryChooser.html) を使用する
- 選択対象がディレクトリになること以外は、基本 `FileChooser` と同じ使い方になる（`initialDirectory` とか）


**参考**

- [28 ファイル・チューザ(リリース8)](https://docs.oracle.com/javase/jp/8/javafx/user-interface-tutorial/file-chooser.htm)
- [FileChooser (JavaFX 8)](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/stage/FileChooser.html)


# ボタンやラベルを同じ幅（高さ）に揃えて縦（横）に並べる
**見た目**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b512f275-8055-7442-8fab-f116f382af41.jpeg)

**構造**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/5be5aba8-62b4-027a-3a22-2ede383c54e1.jpeg)

**縦並びの各要素のサイズ設定**

![javafx.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/6181ac38-c562-e461-2fa0-82515ca41ff5.jpeg)

- 同じ幅で縦に並べるなら、 `VBox` を使う
    - 並べる各要素は、最大幅 (`Max Width`) を `MAX_VALUE` にして最大幅まで伸ばす
- 同じ高さで横に並べるなら、 `HBox` を使う
    - 並べる各要素は、最大高さ (`Max Height`) を `MAX_VALUE` にして最大高さまで伸ばす