52
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaFX 勉強メモ

Last updated at Posted at 2018-01-02

JavaFX を勉強したときのメモ

環境

OS

Windows 10

Java

>java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)

Scene Builder

JavaFX Scene Builder 8.4.1

Gluon が開発してるやつ

JavaFX プロパティ

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

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

package sample.javafx.property;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class MyClass {
    private DoubleProperty value = new SimpleDoubleProperty();
    
    public final double getValue() {
        return this.value.get();
    }
    
    public final void setValue(double value) {
        this.value.set(value);
    }

    public DoubleProperty valueProperty() {
        return this.value;
    }
}
  1. プロパティ(value)を private で定義する
    • プロパティの型はプリミティブ型ではなく、 DoubleProperty のように JavaFX が用意しているラッパー型を使用する
    • ラッパー型は javafx.beans.property パッケージ以下に用意されている
    • DoubleProperty 自体はインターフェースで、実装クラスとして SimpleDoubleProperty が用意されている
  2. Getter, Setter を定義する
    • メソッド名は JavaBeans と同じ規則で命名する
    • 型はラッパー型ではなく、中身の値を出し入れするようにする
    • メソッドは final で定義する
  3. プロパティ名Property というメソッドを定義する
    • これはラッパー型をそのまま返す

JavaFX に用意されている多くのクラスは、この JavaFX プロパティを実装している。
例えば、 Label クラスの text プロパティに対応する各メソッドは以下になる(実際に定義されているのは Labeled クラス)。

Observable

JavaFX プロパティは単にメソッドの宣言を決めているだけでなく、 JavaBeans には無い特別な機能をいくつか提供している。
そのうちの1つが、 Observable になる。

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

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

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

package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty();

        a.addListener(observable -> {
            System.out.println("observable=" + observable);
        });

        a.set(1.0);
        a.set(2.0);
    }
}
実行結果
observable=DoubleProperty [value: 1.0]
observable=DoubleProperty [value: 2.0]

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

無効と変更

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

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

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

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

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

ObservableValue

package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty();

        a.addListener((observableValue, oldValue, newValue) -> {
            System.out.println("observableValue=" + observableValue + ", oldValue=" + oldValue + ", newValue=" + newValue);
        });
        
        a.set(1.0);
        a.set(2.0);
    }
}
実行結果
observableValue=DoubleProperty [value: 1.0], oldValue=0.0, newValue=1.0
observableValue=DoubleProperty [value: 2.0], oldValue=1.0, newValue=2.0

Observable を継承したインターフェースとして、 ObservableValue インターフェースが存在する。
これには addListener(ChangeListener) というメソッドが用意されており、値が変更されたときに通知を受け取るリスナーを登録できる。
リスナーには、変更された ObservableValue 自身と、変更の前後の生の値が渡される。

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

Binding

ObservableValue を継承したインターフェースとして Binding が存在する。

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

package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(1.0);
        DoubleProperty b = new SimpleDoubleProperty(2.0);

        DoubleBinding sum = a.add(b);

        System.out.println("sum=" + sum.get());
        
        a.set(3.0);

        System.out.println("sum=" + sum.get());
    }
}
実行結果
sum=3.0
sum=5.0

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

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

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

遅延計算

package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(1.0);
        DoubleProperty b = new SimpleDoubleProperty(2.0);

        DoubleBinding sum = a.add(b);

        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
        
        a.set(3.0);

        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
    }
}
実行結果
sum=DoubleBinding [invalid]
sum.get()=3.0
sum=DoubleBinding [value: 3.0]

sum=DoubleBinding [invalid]
sum.get()=5.0
sum=DoubleBinding [value: 5.0]
  • 標準ライブラリ内に存在する Binding を実装したクラスは全て、計算が遅延されるようになっている
  • つまり、ソースの値が変更された直後は再計算を行わず、計算結果の取得が試みられたときに再計算を行う
  • 再計算が行われるまで、 Binding は無効となっている

ChangeListener を登録すると遅延計算されなくなる

package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(1.0);
        DoubleProperty b = new SimpleDoubleProperty(2.0);

        DoubleBinding sum = a.add(b);
        sum.addListener((observableValue, oldValue, newValue) -> {
            System.out.println("変更されました(oldValue=" + oldValue + ", newValue=" + newValue + ")");
        });

        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
        
        a.set(3.0);

        System.out.println("sum=" + sum);
        System.out.println("sum.get()=" + sum.get());
        System.out.println("sum=" + sum);
    }
}
実行結果
sum=DoubleBinding [value: 3.0]
sum.get()=3.0
sum=DoubleBinding [value: 3.0]

変更されました(oldValue=3.0, newValue=5.0)

sum=DoubleBinding [value: 5.0]
sum.get()=5.0
sum=DoubleBinding [value: 5.0]
  • ObservableValue.addListener(ChangeListener) で変更通知のリスナが登録されている場合、遅延計算は行われなくなる
  • つまり、 ChangeListener を登録している場合は必ず即時計算されるようになる
  • これは Binding に限った話ではなく、 ObservableValue を実装している遅延計算する全ての実装に共通した話

高レベルAPI

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

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

Fluent API

package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(2.0);

        DoubleBinding binding = a.add(3).multiply(2).subtract(4.0).divide(3.0);

        System.out.println("((2.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
        
        a.set(5.0);

        System.out.println("((5.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
    }
}
実行結果
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((4.0 + 3) * 2 - 4.0) / 3.0 = 4.0
  • Fluent API では、流れるようなインターフェースで Binding を構築することができる
    • a.add(3).multiply(2).subtract(4.0).divide(3.0) の部分
  • これらのメソッドは NumberExpression というインターフェースに定義されている
    • プロパティで使用しているクラスはこのインターフェースを実装している
  • 数値演算以外にも比較演算や論理演算などを行うメソッドも用意されている
  • NumberExpression の数値演算系の Fluent API の戻り値の型は NumberBinding になっているが、 NumberBinding もまた NumberExpression のサブインターフェースになっているので、そのまま続けて Fluent API を呼び出すことができるようになっている
  • 各インターフェースやクラスの関係は、下図のようになっている(一部だけ)

javafx.png

Bindings クラスのファクトリメソッド

package sample.javafx.property;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(2.0);

        DoubleBinding binding =
                Bindings.divide(
                    Bindings.subtract(
                        Bindings.multiply(
                            Bindings.add(a, 3)
                            ,2
                        )
                        ,4.0
                    )
                    ,3.0
                );  

        System.out.println("((2.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
        
        a.set(5.0);

        System.out.println("((5.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
    }
}
実行結果
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0
  • Bindings というクラスには、 Binding のファクトリメソッドが大量に用意されている
  • Fluent API に存在するものもあれば、存在しないものもある
  • とりあえず数が多いので、ざっと眺めてどういうのがあるのか知っておくとよさげ

低レベルAPI

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;
    }
}
package sample.javafx.property;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty(2.0);
        
        DoubleBinding binding = new MyBinding(a);

        System.out.println("((2.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
        
        a.set(5.0);

        System.out.println("((5.0 + 3) * 2 - 4.0) / 3.0 = " + binding.get());
    }
}
実行結果
((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0
  • 低レベルAPI では、 Binding の抽象クラスを継承して独自の Binding クラスを作成する
    • ここでは DoubleBinding を継承した MyBinding を作成している
  • 親クラスに存在する bind() メソッドで、監視対象の Observable を登録する
    • 引数は可変長になっているので、複数の Observable を監視することも可能
  • bind() で登録した Observable の内容が変更されると computeValue() が呼ばれるので、計算結果を返すように実装する

プロパティを Binding と結び付ける

package sample.javafx.property;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;

public class Main {
    public static void main(String[] args) {
        DoubleProperty a = new SimpleDoubleProperty();
        DoubleProperty b = new SimpleDoubleProperty();
        
        System.out.println("a=" + a + ", b=" + b);

        a.bind(b);

        System.out.println("a=" + a + ", b=" + b);
        
        b.set(3.0);

        System.out.println("a=" + a + ", b=" + b);
        System.out.println("a=" + a.get() + ", b=" + b);
    }
}
実行結果
a=DoubleProperty [value: 0.0], b=DoubleProperty [value: 0.0]
a=DoubleProperty [bound, invalid], b=DoubleProperty [value: 0.0]
a=DoubleProperty [bound, invalid], b=DoubleProperty [value: 3.0]
a=3.0, b=DoubleProperty [value: 3.0]
  • Property に定義されている bind(ObservalbeValue) メソッドを使用すると、一方向のバインディングが実現できる
  • 引数で渡した ObservableValue の値を監視するようになり、値が変更されると自分自身の値を監視対象と同じ値に変更する
  • あるプロパティの値を他のプロパティと連動させたい場合に、この bind() を使えばシンプルに実装できる

読み取り専用のプロパティ

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);
    }
}
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());
    }
}
実行結果
ReadOnlyDoubleProperty [value: 0.0]
ReadOnlyDoubleProperty [value: 99.9]
  • DoubleProperty をそのまま公開すると、公開先で値を自由に書き換えることができてしまう
  • 公開先で値を変更できないようにするなら、 setter は公開せず、内部で使用するプロパティの型を ReadOnly**Wrapper 型にする
  • ReadOnly**Wrapper 型は、それ自体は普通に値を変更できるが、 getReadOnlyProperty() で取得したオブジェクトは読み取り専用になる

双方向のバインディング

javafx.jpg

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

  • Property に定義された bindBidirectional() メソッドを使用すると、2つの Property を双方向にバインドできる

StringProperty の双方向バインディング

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

一方、 StringProperty には、次の2つの双方向バインディング用のメソッドが追加されている。

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

フォーマット指定

package sample.javafx;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;

import java.net.URL;
import java.text.DecimalFormat;
import java.text.Format;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    private TextField textField;
    
    private DoubleProperty value = new SimpleDoubleProperty();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Format format = new DecimalFormat("#,##0.00");
        this.textField.textProperty().bindBidirectional(this.value, format);
    }
    
    @FXML
    public void checkValue() {
        System.out.println("value=" + value.getValue());
    }
    
    @FXML
    public void resetValue() {
        this.value.set(9876.54);
    }
}

実行結果

javafx.gif

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

コンバーター指定

package sample.javafx;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;

import java.net.URL;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    private TextField textField;
    
    private ObjectProperty<LocalDate> value = new SimpleObjectProperty<>(LocalDate.of(2017, 1, 1));

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu/MM/dd");
        
        this.textField.textProperty().bindBidirectional(this.value, new StringConverter<LocalDate>() {
            @Override
            public String toString(LocalDate date) {
                if (date == null) {
                    return "";
                }
                
                try {
                    return formatter.format(date);
                } catch (DateTimeException e) {
                    return "";
                }
            }

            @Override
            public LocalDate fromString(String text) {
                try {
                    return LocalDate.parse(text, formatter);
                } catch (DateTimeParseException e) {
                    return null;
                }
            }
        });
    }
    
    @FXML
    public void checkValue() {
        System.out.println("value=" + value.getValue());
    }
    
    @FXML
    public void resetValue() {
        this.value.set(LocalDate.of(2017, 1, 1));
    }
}

実行結果

javafx.gif

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

コレクション

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

監視可能な List

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("fizz", "buzz");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("list が変更されました change=" + change);
        });
        
        list.add("foo");
        list.remove("buzz");
        FXCollections.sort(list);
    }
}
実行結果
list が変更されました change={ [foo] added at 2 }
list が変更されました change={ [buzz] removed at 1 }
list が変更されました change={ permutated by [0, 1] }
  • ObservableList は、 java.util.List を継承したインターフェースで、監視機能を実現するためのメソッドが追加されている
  • インスタンスの生成は FXCollections のファクトリメソッドを使用する
    • FXCollectionsjava.util.Collections と似たようなメソッドを持つ、 JavaFX のコレクション用のユーティリティクラス
  • addListener(ListChangeListener) で、変更の通知を受け取るリスナーを登録できる
    • ObservableListObservable も継承している
    • そして、 Observable にも addListener(InvalidationListener) という名前が同じで引数が異なるメソッドが定義されている
    • 両者が引数で受け取るリスナーは、どちらも関数型インターフェースで、しかもメソッドの引数が1つしかない
    • つまり、引数の型を省略した最もシンプルなラムダ式を書くと、コンパイラはどちらの addListener() が使用されているのか判別できなくなり、コンパイルエラーになる
    • 仕方ないので、ラムダ式を書く場合でも最低限引数の型は省略せずに書かなければならない

変更内容を確認する

リストが変更されると、リスナーには Change オブジェクトが渡される。

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

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            while (change.next()) {
                System.out.println("list=" + change.getList());
                if (change.wasAdded()) {
                    System.out.println("追加 : " + change);
                }
                if (change.wasRemoved()) {
                    System.out.println("削除 : " + change);
                }
                if (change.wasPermutated()) {
                    System.out.println("順序変更 : " + change);
                }
                if (change.wasReplaced()) {
                    System.out.println("置換 : " + change);
                }
            }
        });

        list.add("FOO");
        list.remove("one");
        FXCollections.sort(list);
        list.set(1, "hoge");
    }
}
実行結果
==========================================
list=[one, two, three, FOO]
追加 : { [FOO] added at 3 }
==========================================
list=[two, three, FOO]
削除 : { [one] removed at 0 }
==========================================
list=[FOO, three, two]
順序変更 : { permutated by [2, 1, 0] }
==========================================
list=[FOO, hoge, two]
追加 : { [three] replaced by [hoge] at 1 }
削除 : { [three] replaced by [hoge] at 1 }
置換 : { [three] replaced by [hoge] at 1 }
  • Change オブジェクトから変更内容を取得する前に、必ず next() メソッドを実行しなければならない
    • next() を呼ばずに変更内容を取得しようとすると、 IllegalStateException がスローされる
    • 一度の通知で複数の変更があった場合は、 next() を呼ぶことで次の変更に移ることができる
    • 後述する removeAll() を使うと複数の変更が一度に通知されることがあるので、 while で回すようにしておけば全ての変更を確認できる
  • 変更には「追加」や「削除」などの種類がある
    • どの種類の変更が発生したかは、 wasAdded()wasRemoved()などの was***() メソッドで確認できる
  • リスナーの実装には次の注意点がある
    • Change オブジェクトを別スレッドで使ってはいけない
    • リスナー内でリストの状態を変更してはいけない
    • Change オブジェクトの Javadoc に記載されている

追加された要素を確認する

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");

        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            while (change.next()) {
                System.out.println("list=" + change.getList());
                System.out.println("added=" + change.wasAdded());
                System.out.println("from=" + change.getFrom());
                System.out.println("to=" + change.getTo());
                System.out.println("addedSubList=" + change.getAddedSubList());
                System.out.println("addedSize=" + change.getAddedSize());
            }
        });

        list.add("FOO");
        list.addAll(Arrays.asList("hoge", "fuga"));
    }
}
実行結果
==========================================
list=[one, two, three, FOO]
added=true
from=3
to=4
addedSubList=[FOO]
addedSize=1
==========================================
list=[one, two, three, FOO, hoge, fuga]
added=true
from=4
to=6
addedSubList=[hoge, fuga]
addedSize=2
  • 要素が追加されると、 wasAdded()true を返す
  • getFrom(), getTo() で、要素が追加された箇所を指すインデックスを取得できる
    • getFrom() は、要素が追加された先頭のインデックス(このインデックスを含む)
    • getTo() は、要素が追加された末尾のインデックス(このインデックスは含まない)
  • getAddedSubList() で、追加された要素だけを含む List を取得できる
  • getAddedSize() で、追加された要素数を取得できる

削除された要素を確認する

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list
            = FXCollections.observableArrayList(
                    "one", "two", "three", "four", "five", "six", "seven");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            System.out.println("list=" + change.getList());
            while (change.next()) {
                System.out.println("----------------------------------");
                System.out.println("removed=" + change.wasRemoved());
                System.out.println("from=" + change.getFrom());
                System.out.println("to=" + change.getTo());
                System.out.println("removed=" + change.getRemoved());
                System.out.println("removedSize=" + change.getRemovedSize());
            }
        });

        list.remove("three");
        list.removeAll("two", "seven");
        list.retainAll("one", "six");
    }
}
実行結果
==========================================
list=[one, two, four, five, six, seven]
----------------------------------
removed=true
from=2
to=2
removed=[three]
removedSize=1
==========================================
list=[one, four, five, six]
----------------------------------
removed=true
from=1
to=1
removed=[two]
removedSize=1
----------------------------------
removed=true
from=4
to=4
removed=[seven]
removedSize=1
==========================================
list=[one, six]
----------------------------------
removed=true
from=1
to=1
removed=[four, five]
removedSize=2
  • 要素が削除されると、 wasRemoved()true を返す
  • 削除の場合、 getFrom() は削除された要素の、元の List での開始インデックスを返してる
    • また getTo()getFrom() と同じ値を返している(Javadoc の記述的にそれでいいのか若干あやしい気がするが)
    • Change の Javadoc をよく見ると、削除時の getFrom()getTo() については言及していないように読めなくもない気がするがどうなんだろう(実は未確定とか?)
  • getRemoved() で、削除された要素を List で取得できる
  • getRemovedSize() は削除された要素数を返す

並び順の変更を確認する

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list
            = FXCollections.observableArrayList("aaa", "ccc", "bbb", "eee", "ddd", "fff");
        
        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            System.out.println("new list=" + change.getList());
            while (change.next()) {
                System.out.println("permutated=" + change.wasPermutated());
                for (int oldIndex=change.getFrom(); oldIndex<change.getTo(); oldIndex++) {
                    int newIndex = change.getPermutation(oldIndex);
                    System.out.println("old(" + oldIndex + ") -> new(" + newIndex + ")");
                }
            }
        });

        System.out.println("old list=" + list);
        list.sort(String::compareTo);
    }
}
実行結果
old list=[aaa, ccc, bbb, eee, ddd, fff]
==========================================
new list=[aaa, bbb, ccc, ddd, eee, fff]
permutated=true
old(0) -> new(0)
old(1) -> new(2)
old(2) -> new(1)
old(3) -> new(4)
old(4) -> new(3)
old(5) -> new(5)
  • リストの順序が変更されると、 wasPermutated()true を返す
  • getPermutated(int) に変更前のインデックスを渡すと、順序変更後のインデックスを返す

置換された要素を確認する

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");

        list.addListener((Change<? extends String> change) -> {
            System.out.println("==========================================");
            while (change.next()) {
                System.out.println("list=" + change.getList());
                
                System.out.println("replaced=" + change.wasReplaced());
                System.out.println("added=" + change.wasAdded());
                System.out.println("removed=" + change.wasRemoved());
                
                System.out.println("from=" + change.getFrom());
                System.out.println("to=" + change.getTo());
                System.out.println("addedSubList=" + change.getAddedSubList());
                System.out.println("addedSize=" + change.getAddedSize());
                System.out.println("removed=" + change.getRemoved());
                System.out.println("removedSize=" + change.getRemovedSize());
            }
        });

        list.set(1, "FOO");
    }
}
実行結果
==========================================
list=[one, FOO, three]
replaced=true
added=true
removed=true
from=1
to=2
addedSubList=[FOO]
addedSize=1
removed=[two]
removedSize=1
  • 要素が別の要素に置き換えられると、 wasReplaced()true を返す
    • 厳密には wasReplaced()wasAddedd() && wasRemoved() のときに true となる
    • つまり、 wasReplaced() だけが true になることはない
    • wasReplaced()true となるときは、 wasAddedd()wasRemoved() も必ず true となる

ソートされたリスト

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;

import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        SortedList<String> sortedList = list.sorted();

        System.out.println("list=" + list);
        System.out.println("sortedList=" + sortedList);

        list.addAll("four", "five", "six");

        System.out.println("list=" + list);
        System.out.println("sortedList=" + sortedList);
        
        sortedList.setComparator(Comparator.reverseOrder());

        System.out.println("list=" + list);
        System.out.println("sortedList=" + sortedList);
    }
}
実行結果
list=[one, two, three]
sortedList=[one, three, two]

list=[one, two, three, four, five, six]
sortedList=[five, four, one, six, three, two]

list=[one, two, three, four, five, six]
sortedList=[two, three, six, one, four, five]
  • sorted() メソッドを実行すると、元の ObservableList と連動したソートされたリスト(SortedList)を取得できる
  • 元の ObservalbeList を変更すると、 SortedList の状態もソートされた状態で更新される
  • SortedListObservableList を実装している
  • 引数に Comparator を渡せる sorted(Comparator) メソッドもある
  • setComparator(Comparator) で後からソート方法を変更することもできる

フィルターされたリスト

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;

public class Main {
    public static void main(String[] args) {
        ObservableList<String> list = FXCollections.observableArrayList("one", "two", "three");
        FilteredList<String> filteredList = list.filtered(e -> e.contains("e"));

        System.out.println("list=" + list);
        System.out.println("filteredList=" + filteredList);
        
        list.addAll("four", "five");

        System.out.println("list=" + list);
        System.out.println("filteredList=" + filteredList);

        filteredList.setPredicate(e -> e.contains("f"));

        System.out.println("list=" + list);
        System.out.println("filteredList=" + filteredList);
    }
}
実行結果
list=[one, two, three]
filteredList=[one, three]

list=[one, two, three, four, five]
filteredList=[one, three, five]

list=[one, two, three, four, five]
filteredList=[four, five]
  • filtered(Predicate) メソッドを実行すると、元の ObservableList と連動したフィルターされたリスト(FilteredList)を取得できる
  • 元の ObservalbeList を変更すると、 FilteredList の状態もフィルターされた状態で更新される
  • setPredicate() で後から絞り込み条件を変更することもできる
  • FilteredListObservableList を実装している

監視可能な Map

package sample.javafx.property;

import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener.Change;
import javafx.collections.ObservableMap;

public class Main {
    public static void main(String[] args) {
        ObservableMap<String, String> map = FXCollections.observableHashMap();
        map.put("foo", "FOO");
        map.put("bar", "BAR");
        
        map.addListener((Change<? extends String, ? extends String> change) -> {
            System.out.println("==============================");
            System.out.println("map=" + change.getMap());
            System.out.println("key=" + change.getKey());
            System.out.println("added=" + change.wasAdded());
            System.out.println("valueAdded=" + change.getValueAdded());
            System.out.println("removed=" + change.wasRemoved());
            System.out.println("valueRemoved=" + change.getValueRemoved());
        });
        
        map.put("fizz", "FIZZ");
        map.put("bar", "BARBAR");
        map.remove("foo");
    }
}
実行結果
==============================
map={bar=BAR, foo=FOO, fizz=FIZZ}
key=fizz
added=true
valueAdded=FIZZ
removed=false
valueRemoved=null
==============================
map={bar=BARBAR, foo=FOO, fizz=FIZZ}
key=bar
added=true
valueAdded=BARBAR
removed=true
valueRemoved=BAR
==============================
map={bar=BARBAR, fizz=FIZZ}
key=foo
added=false
valueAdded=null
removed=true
valueRemoved=FOO
  • 監視可能な Map として、 ObservableMap が用意されている
  • インスタンスの生成には、 FXCollections.observableHashMap() を使用する
  • MapChange は、 List ほど複雑ではない
    • List とは違い、 next() は不要
    • 変更の種類は「追加(wasAdded())」と「削除(wasRemoved())」の2つ
    • getValueAdded() で追加された値を、
      getValueRemoved() で削除された値を取得できる
    • 既に存在するキーに別の値を設定した場合は「追加」と「削除」の変更が同時に発生する

スタイルシート

基本

フォルダ構成
`-src/main/
  |-java/sample/javafx/
  | |-Main.java
  | `-MainController.java
  |
  `-resources/
    |-main.fxml
    `-my-style.css

main.fxml

javafx.jpg

my-style.css
.label {
    -fx-background-color: skyblue;
}
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

スタイルシートの読み込み

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 を参照

CSS の書き方

my-style.css
.label {
    -fx-background-color: skyblue;
}
  • CSS の書き方は、基本的に HTML で使用する CSS と同じ
    • セレクタ {プロパティ: 値;}
  • ただし、プロパティ名がほぼ例外なく -fx という接頭辞で始まるようになっている

class セレクタ

  • HTML の CSS で .xxxx とした場合、 class="xxxx" が設定されているタグにマッチする
  • 一方で、 JavaFX の CSS の場合は .xxxxstyleClass="xxxx" にマッチする

ノードに設定されているデフォルトの styleClass

  • いくつかのノード(Label, ListView など)には、デフォルトで特定の変換ルールに従って命名された styleClass 属性が設定されている
  • 例えば、 Label クラスの場合は label という styleClass が、 ListView クラスには list-view という styleClass がデフォルトで設定されている
  • 変換ルールは単純で、クラス名の各単語を - で区切るようにして、全てを小文字にしている
  • ただし、この変換は何かしらの自動変換ロジックがあるわけではなく、上記ルールに従って各クラスで設定しているだけにすぎない
  • どういうことかというと、 Label クラスの実装を見ればわかる
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
  • どのノードにどういうデフォルトのクラスが設定されているかは、 こちら に載っている
    • 「スタイル・クラス: デフォルトでは空」とか
      「スタイル・クラス: button」って書いているところ

プロパティの実体

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

  • CSS で指定するプロパティ(-fx-background-color とか -fx-padding とか)は、一見すると HTML の CSS で指定できるプロパティの頭に -fx がついているだけに見える
  • なので、使えるプロパティがよくわからない場合は、 HTML の CSS で使っているプロパティに -fx をつければ何とかなりそうな気がする
  • しかし、実際は -fx-underline: true; のように、 HTML の CSS には存在しないプロパティもある
    • HTML の CSS なら text-decoration: underline;
  • これは、実際は対象のノードが持つプロパティを指している
  • サンプルでは Label クラスに対して CSS を指定しているが、 underlineLabel クラスの親クラスである Labeled クラスの underline プロパティ を指定していることになっている
  • ただし、ノードのプロパティと CSS で指定するプロパティが常に完全に一致しているかというと、そうでもない
  • 例えば borderbackground は、 -fx-border, -fx-background ではなく -fx-border-style-fx-background-color のように細かいプロパティに分けられている
  • どのノードに対してどのプロパティが指定できるのかについては、公式のドキュメントで詳しく説明されている

任意の styleClass 属性をセットする

Scene Builder でセットする

javafx.jpg

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>
my-style.css
.my-class {
    -fx-font-weight: bold;
    -fx-underline: true;
    -fx-text-fill: red;
}

実行結果

javafx.jpg

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

プログラムから設定する

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()styleClassObservableList を取得する
  • そのリストに add() することで任意の属性を追加できる

ローカルのスタイルシートファイルを読み込む

フォルダ構成
|-my-styles.css
`-src/main/java/
  `-sample/javafx/
    |-Main.java
    :
my-style.css
.my-class {
    -fx-background-color: green;
}
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

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

背景画像の設定

-fx-background-image の設定方法について

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

クラスパス内のファイルを指定する

my-style.css
.my-class {
    -fx-background-image: url('/img/image.jpg');
}
  • url で指定する
  • 書式は CSS ファイルの読み込みと同じで、 schema を省略した場合はクラスパス以下のファイルになる

ローカルのファイルを指定する

my-style.css
.my-class {
    -fx-background-image: url('file:./image.jpg');
}
  • ローカルのファイルを指定する場合は file:【パス】 とする

イベント・キャプチャ、イベント・バブリング

javafx.jpg

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

青い領域をクリック
filter bluePane
handler bluePane
黄色い領域をクリック
filter bluePane
filter yellowPane
handler yellowPane
handler bluePane
ボタンをクリック
filter bluePane
filter yellowPane
filter button
handler button
テキストフィールドをクリック
filter bluePane
filter yellowPane
filter textField
handler textField
ラベルをクリック
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane

説明

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

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

ターゲットの選択

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

経路の決定

javafx.jpg

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

イベント・キャプチャ・フェーズ

テキストフィールドをクリック
filter bluePane
filter yellowPane
filter textField
...
  • イベント・キャプチャ・フェーズでは、決定された経路をルートからターゲットに向かって各ノードに登録されているイベント・フィルタが実行される
MainController.java
    private void registerEventFilter(Node node, String name) {
        node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> System.out.println("filter " + name));
    }
  • イベント・フィルタは addEventFilter(EventType, EventHandler) でノードに登録することができる

イベント・バブリング・フェーズ

ラベルをクリック
...
handler label
handler yellowPane
handler bluePane
  • イベント・バブリング・フェーズでは、逆にターゲットからルートノードに向かって各ノードに登録されているイベント・ハンドラが実行される
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 属性などでイベント処理を登録した場合も、このイベント・ハンドラが登録されている

イベントを消費する

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)のイベント・ハンドラで、 MouseEventconsume() メソッドを実行している

実行結果

黄色い領域をクリック
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
  • Event.consume() メソッドを実行することで、イベントの連鎖を中断できる
  • これを、イベントの消費と表現する
  • イベント・キャプチャの段階でイベントを消費すると、イベント・バブリングも行われなくなる

UIコントロールのなかにはイベント・バブリングが中断されるものがある

ボタンをクリック
filter bluePane
filter yellowPane
filter button
handler button
テキストフィールドをクリック
filter bluePane
filter yellowPane
filter textField
handler textField
ラベルをクリック
filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane
  • Label は普通にイベント・バブリングが実行されているが、Button, TextField は中断されている
  • イベント・キャプチャ・フェーズはどれも実行されている

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

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

1 イベントの処理(リリース8) | JavaFX: イベントの処理

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

ドラッグ&ドロップ

ドロップ

基本

javafx.jpg

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

説明

ドロップの受け入れ

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

ドロップ時の処理

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 からドロップ内容にアクセスできる

ファイルのドロップ

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

説明

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

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

説明

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

ドロップ時のスタイルを変更する

my-style.css
.drag-over {
    -fx-text-fill: red;
}
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

説明

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

ドラッグ

javafx.jpg

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

説明

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 にセットして、その ClipboardContentDragboard に設定する
    • putString() 以外にも、putHtml()putFiles() などがある
    • ClipboardContent 自体は HashMap<DataFormat, Object> を継承しているマップなので、任意の値を保存できる

参考

ダイアログ

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

準備

javafx.jpg

package sample.javafx;

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

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

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

基本

Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.showAndWait();

実行結果

javafx.jpg

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

選択されたボタンを確認する

Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
Optional<ButtonType> buttonType = alert.showAndWait();
buttonType.ifPresent(System.out::println);

実行結果

「OK」「取消」ボタンをそれぞれクリック。

ButtonType [text=OK, buttonData=OK_DONE]
ButtonType [text=取消, buttonData=CANCEL_CLOSE]
  • showAndWait() の戻り値は ButtonTypeOptional になっており、閉じるときに選択されたボタンを確認できる
  • キャンセル系のボタン2以外しかないダイアログ(「はい」ボタンしかないダイアログとか)が ESC やウィンドウの閉じるボタンで閉じられたときに、showAndWait() の戻り値は Optional.empty() になる
    • 厳密には resultConverter が設定されているケースもあるが、その辺は Javadoc を参照のこと

メッセージを設定する

Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ");
alert.showAndWait();

javafx.jpg

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

ダイアログの種類

INFORMATION

javafx.jpg

CONFIRMATION

javafx.jpg

WARNING

javafx.jpg

ERROR

javafx.jpg

ボタンを指定する

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

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

ボタンの配置順序

  • 可変長引数の順序がそのままボタンの配置順序になるわけではない
  • ボタンの配置には ButtonBar というクラスが使用されている
  • ButtonBar は、ボタン配置のためのクラスで、 OS ごとに固有のボタン配置を再現できる
    • 各 OS ごとのボタン配置の順序は ButtonBar の Javadoc に例のイメージが掲載されている
  • ボタンは種類ごとに**ボタン順序(button order)**が決められており、ボタン順序によって ButtonBar のどこにボタンが配置されるかが決まる
    • 例えば、「はい」「いいえ」のボタンは、 Windows なら 「はい」→「いいえ」 の順序で配置されるが、 Mac や Linux では「いいえ」→「はい」の順序で配置される
  • このボタンの種類を表現するクラスとして、 ButtonData 列挙型が用意されている
  • Alert のコンストラクタで指定していた ButtonType クラスの定数にも、この ButtonData が設定されていて、その値に従ってボタンが配置されている

ButtonType の定数に設定されている ButtonData

ButtonType 設定されているButtonData
APPLY APPLY
CANCEL CANCEL_CLOSE
CLOSE CANCEL_CLOSE
FINISH FINISH
NEXT NEXT_FORWARD
NO NO
OK OK_DONE
PREVIOUS BACK_PREVIOUS
YES YES
  • 各 OS ごとの、 ButtonData の詳細な配置順序は Javadoc に記載されている

OS ごとの ButtonData の配置順序

OS 配置順序
Windows L_E+U+FBXI_YNOCAH_R
Mac OS L_HE+U+FBIX_NCYOA_R
Linux L_HE+UNYACBXIO_R
  • この謎の文字列は、1文字1文字が ButtonData の定数に割り当てられているボタン順序コードに対応している
  • 例えば、 ButtonData.LEFT のボタン順序コードは L で、 ButtonData.HELP_2E といった具合
    • 定数に割り当てられているボタン順序コードは、各定数の Javadoc に記載されている
  • もうちょい見やすくするために、 ButtonData の定数との関係を表にすると下のような感じ

javafx.jpg

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

任意のボタンを配置する

ButtonType myButton = new ButtonType("ぼたん");
Alert alert = new Alert(Alert.AlertType.INFORMATION, "めっせーじ", myButton);
alert.showAndWait().ifPresent(System.out::println);

実行結果

javafx.jpg

「ぼたん」ボタンをクリックしたときのコンソール出力
ButtonType [text=ぼたん, buttonData=OTHER]
  • ButtonType クラスを生成して Alert のコンストラクタに渡せば、任意のボタンを配置できる
  • ButtonType のコンストラクタでは、ボタンのラベル文字列を指定できる
  • 何も指定していない場合、 ButtonDataOTHER になる
  • ButtonType には ButtonData を受け取るコンストラクタも存在するので、明示的に指定することも可能

ボタン以外で閉じられたときの判定

  • ダイアログは ESCAlt + F4, ウィンドウの閉じるボタンなどでも閉じることができる(Windows の場合)
  • その場合、 showAndWait() の戻り値は配置されたボタンによってはやや複雑な動きをする
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ", ButtonType.YES);
Optional<ButtonType> buttonType = alert.showAndWait();
String result = buttonType.map(ButtonType::getText).orElse("選択無し");
System.out.println(result);

実行結果

javafx.jpg

ESC で閉じたときの結果

選択無し
  • ボタンの中にはキャンセル系のボタン2が無いので、戻り値は Optional.empty() になる

  • つぎに、キャンセル系のボタンが複数ある場合
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "めっせーじ", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
Optional<ButtonType> buttonType = alert.showAndWait();
String result = buttonType.map(ButtonType::getText).orElse("選択無し");
System.out.println(result);

実行結果

javafx.jpg

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

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

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

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

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

各種テキストの設定

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

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

javafx.jpg

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

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

javafx.jpg

スレッド

JavaFX でスレッドを使うときの基本的な話

UIスレッド

JavaFX の GUI 処理は、 JavaFX Application Thread という専用のスレッド(UIスレッド)上で実行される。

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());
    }
}
実行結果
thread = JavaFX Application Thread

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

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

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

非 UI スレッドから UI を触る

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

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();
    }
}
実行結果
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()というメソッドが用意されている。

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();
    }
}
実行結果
thread = Thread-4
runLater thread = JavaFX Application Thread

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

JavaFX でスレッドを簡単に扱うための仕組み

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

Worker

JavaFX で使用するスレッド処理は、 Worker として抽象化されている。

Worker には、通常の Thread にはない便利な API が定義されている。
(これ自体はインターフェースで、具体的な実装は Task または Service によって実現されている)

Worker のライフサイクル

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

javafx.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()runningProperty() といったプロパティが用意されている。

Task

Worker を実装したクラスとして Task というクラスが用意されている。

package sample.javafx;

import javafx.concurrent.Task;
import javafx.fxml.Initializable;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Runnable task = new Task<Void>() {

            @Override
            protected Void call() throws Exception {
                System.out.println("task.call()");
                System.out.println("thread = " + Thread.currentThread().getName());
                return null;
            }
        };

        new Thread(task).start();
    }
}
実行結果
task.call()
thread = Thread-4
  • Task クラス自体は抽象クラスなので、継承して自作のクラスを作成する
  • call() メソッドが抽象メソッドになっている
  • 自作クラスで call() メソッドを実装し、この中でバックグラウンドで動かす処理を記述する
  • Task クラスは Runnable インターフェースを実装しているため、 ThreadExecutor に渡して実行できる

キャンセル

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() メソッドで Workercancel() メソッドを呼んでいる。

実行結果
start...
cancelled!!
  • Workercancel() メソッドを呼ぶと、 Worker の実行をキャンセルできる
    • キャンセルできるのは Worker の状態が「終了状態」以外のときに限られる
    • 「終了状態」で cancel() を呼んだ場合は何も行われない
  • キャンセルが実行されると、 TaskisCancelled()true を返すようになる
    • これを利用して、 call() メソッドの中でキャンセルされたかどうかが判定できる
    • Thread.sleep() などでブロック中にキャンセルされた場合は InterruptedException をキャッチしたうえで isCancelled() の判定を入れる
    • 詳しくは Task クラスの Javadoc を参照

進捗

javafx.jpg

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

説明

        this.progressBar.progressProperty().bind(task.progressProperty());
  • WorkerprogressProperty() というプロパティを公開している
  • これを ProgressBarprogressProperty() にバインドすることで、 TaskprogressProgressBarprogress を連動させることができる
                for (long i=0; i<=max; i++) {
                    this.updateProgress(i, max);
                }
  • Task の進捗を更新するには updateProgress() メソッドを使用する
  • 第一引数には処理済みの件数、第二引数には全体の件数を渡す(double を渡すメソッドもオーバーロードされている)
  • この updateProgress() は非 UI スレッドから実行しても問題ないようになっている
    • 中で runLater() が使用されている

任意の値を公開する

javafx.jpg

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

説明

    private static class MyTask extends Task<String> {

        @Override
        protected String call() throws Exception {
            for (int i=0; i<100000000; i++) {
                this.updateValue("i=" + i);
            }
            
            return "finished!!";
        }
    }
  • Task には value というプロパティが用意されている
  • この value の型は、 Task の型引数で指定する
  • value の値は updateValue() メソッドで更新できる
    • こちらも、非 UI スレッドから安全に実行できるようになっている
  • Task の実行が正常終了した場合、 call() メソッドの戻り値が最終的に value プロパティにセットされる

イベントリスナー

package sample.javafx;

import javafx.concurrent.Task;
import javafx.fxml.FXML;

import java.io.IOException;

public class MainController {

    @FXML
    public void start() throws IOException {
        Task<Void> task = new Task<Void>() {

            @Override
            protected Void call() throws Exception {
                return null;
            }
        };
        
        task.setOnScheduled(event -> System.out.println("scheduled thread=" + Thread.currentThread().getName()));
        task.setOnRunning(event -> System.out.println("running thread=" + Thread.currentThread().getName()));
        task.setOnSucceeded(event -> System.out.println("succeeded thread=" + Thread.currentThread().getName()));
        
        new Thread(task).start();
    }
}

実行結果

scheduled thread=JavaFX Application Thread
running thread=JavaFX Application Thread
succeeded thread=JavaFX Application Thread
  • setOn**** メソッドで、状態変化を監視できるリスナーを登録できる
  • 登録したリスナーは UI スレッドで実行される

例外プロパティ

package sample.javafx;

import javafx.concurrent.Task;
import javafx.fxml.FXML;

import java.io.IOException;

public class MainController {

    @FXML
    public void start() throws IOException {
        Task<Void> task = new Task<Void>() {

            @Override
            protected Void call() throws Exception {
                throw new RuntimeException("test");
            }
        };
        
        task.setOnFailed(event -> {
            Throwable exception = task.getException();
            System.out.println("exception=" + exception);
        });
        
        new Thread(task).start();
    }
}
実行結果
exception=java.lang.RuntimeException: test
  • call() メソッドの中で例外がスローされると、 exception プロパティにスローされた例外がセットされる
  • 例外はスタックの上位に伝播しないので、ほっといたら例外がスローされたことに気付かない気がする

ObservableList を公開する

package sample.javafx;

import javafx.application.Platform;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

import java.util.concurrent.TimeUnit;

public class MainController {
    
    @FXML
    private Label label;
    
    @FXML
    public void start() {
        MyTask myTask = new MyTask();
        this.label.textProperty().bind(myTask.listProperty().asString());

        Thread thread = new Thread(myTask);
        thread.setDaemon(true);
        thread.start();
    }
    
    private static class MyTask extends Task<Void> {
        private ReadOnlyListWrapper<String> list = new ReadOnlyListWrapper<>(FXCollections.observableArrayList());

        public final ObservableList<String> getList() {
            return list.get();
        }

        public ReadOnlyListProperty<String> listProperty() {
            return list.getReadOnlyProperty();
        }

        @Override
        protected Void call() throws Exception {
            for (int i=0; i<10; i++) {
                String value = String.valueOf(i);
                Platform.runLater(() -> this.list.add(value));
                
                TimeUnit.SECONDS.sleep(1);
            }
            return null;
        }
    }
}
  • TaskObservableList を持たせ、その状態を外部に公開する場合、リストの更新は Platform.runLater() を使うように注意しないといけない

Service

package sample.javafx;

import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;

import java.io.IOException;

public class MainController {

    private Service<Void> service = new Service<Void>() {
        @Override
        protected Task<Void> createTask() {
            System.out.println("Service.createTask()");
            
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    System.out.println("Task.call()");
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        if (!this.service.getState().equals(Worker.State.READY)) {
            System.out.println("reset");
            this.service.reset();
        }
        System.out.println("start");
        this.service.start();
    }
}

start() メソッドをボタンのアクションイベントに割り当てて、何回か実行する。

実行結果
start
Service.createTask()
Task.call()

reset
start
Service.createTask()
Task.call()

reset
start
Service.createTask()
Task.call()
  • Task は使い捨てが前提となっており、同じインスタンスをもう一度使いまわすような使い方は想定されていない
  • 一方、 Service は何度もスレッド処理を実行することを想定して作られている
  • ServiceTask をラップしており、スレッド処理を実行するたびに Task のインスタンスを新規に作成してスレッドを起動するようになっている
  • Service は抽象クラスで、このクラスを継承した独自クラスを作成して利用する
  • 抽象メソッドとして createTask() が存在するので、これを実装する必要がある
  • このメソッドでは、 Task のインスタンスを新規作成する処理を実装する
  • Servicestart() メソッドを実行すると、スレッドが起動して createTask() メソッドが生成した Task が実行される

再実行

状態をみて再実行する場合
    @FXML
    public void start() throws IOException {
        if (!this.service.getState().equals(Worker.State.READY)) {
            System.out.println("reset");
            this.service.reset();
        }
        System.out.println("start");
        this.service.start();
    }
  • ServiceWorker インターフェースを実装しているため、 Service の状態は Worker が定義するルールに従って変化する
  • つまり、 READY から始まり、最終的には SUCCEEDEDFAILED, CANCELLED になる
  • READY 以外の状態で Servicestart() を実行すると IllegalStateException がスローされる
  • 状態を READY に戻す方法はいくつかある
    • 終了状態(SUCCEEDED, FAILED, CANCELLED)の場合は reset() メソッドを実行する
    • SCHEDULED, RUNNING の場合は、一度 cancel() でキャンセルしてから、 reset() メソッドを実行する
  • いちいち状態を見て戻すのが面倒な場合は、 restart() メソッドを実行すればいい感じに処理してくれる
    • 状態が READY ならそのまま処理を起動する
    • 実行中なら処理を一旦キャンセルしてから再実行する
    • 終了状態なら、一度 READY に戻してから再実行する
restart()で書き換えた場合
    @FXML
    public void start() throws IOException {
        System.out.println("restart");
        this.service.restart();
    }
  • start(), reset(), restart() メソッドは、どれも UI スレッドから実行しなければならない

使用するスレッドを指定する

まずは、デフォルトの動作を確認する。

package sample.javafx;

import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.fxml.FXML;

import java.io.IOException;

public class MainController {

    private Service<Void> service = new Service<Void>() {
        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    Thread thread = Thread.currentThread();
                    System.out.println("thread.name = " + thread.getName() + ", daemon = " + thread.isDaemon());
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        this.service.restart();
    }
}

何度か restart() を実行する

実行結果
thread.name = Thread-4, daemon = true
thread.name = Thread-5, daemon = true
thread.name = Thread-6, daemon = true
thread.name = Thread-7, daemon = true
  • デフォルトでは、 ServiceTask を起動するたびにデーモンスレッドを新規に作成するようになっている
  • 使用するスレッドを指定する(たとえば、スレッドプールのスレッドを使用する)場合は、次のように実装する
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();
    }
}
実行結果
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
  • Serviceexecutor プロパティに、 Executor を指定する
  • すると、 ServiceTask を実行するためのスレッドを executor から取得するようになる

各種プロパティ

package sample.javafx;

import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.ProgressBar;

import java.io.IOException;

public class MainController {
    @FXML
    private ProgressBar progressBar;

    private Service<Void> service = new Service<Void>() {
        
        @Override
        protected Task<Void> createTask() {
            return new Task<Void>() {
                @Override
                protected Void call() throws Exception {
                    int max = 10000000;
                    for (int i=0; i<max; i++) {
                        this.updateProgress(i, max);
                    }
                    
                    return null;
                }
            };
        }
    };
    
    @FXML
    public void start() throws IOException {
        this.service.restart();
        this.progressBar.progressProperty().bind(this.service.progressProperty());
    }
}
  • ServiceWorker を実装しているので、 Task と同じように progress などのプロパティを提供している
  • 各プロパティは、 Service が実行している Task のプロパティと連動するようになっている
  • つまり、 Service のプロパティを参照することは、実行中の Task のプロパティを参照するのと同じということになる

参考

視覚効果

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

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

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

javafx.jpg

ブルーム効果

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.Bloom;
import javafx.scene.layout.Pane;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Pane pane;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Bloom bloom = new Bloom();
        this.pane.setEffect(bloom);
    }
}
  • Bloom クラスを使用すると、ブルーム(画像が輝いているように見える)効果を適用できる
  • Bloom のインスタンスを生成し、効果を適用するシーングラフ上のノードに setEffect() で設定することで、そのノード以下に視覚効果が適用される

ぼかし効果

ボックスぼかし

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.BoxBlur;
import javafx.scene.layout.Pane;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Pane pane;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        BoxBlur boxBlur = new BoxBlur();
        boxBlur.setWidth(5);
        boxBlur.setHeight(5);
        
        this.pane.setEffect(boxBlur);
    }
}
  • BoxBlur を使用すると、ボックスぼかし効果を適用できる
  • width, height で、縦横方向のぼかし距離を指定できる
  • iterations を指定することで、ぼかし効果の適用回数を指定できる(デフォルトは 3

モーションぼかし

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.MotionBlur;
import javafx.scene.layout.Pane;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Pane pane;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        MotionBlur motionBlur = new MotionBlur();
        motionBlur.setAngle(45.0);
        motionBlur.setRadius(40.0);
        
        this.pane.setEffect(motionBlur);
    }
}
  • MotionBlur を使用すると、モーションぼかし(物体が動いているときのようなぼかし)を適用できる
  • angle でぼかしの向きを指定できる
    • 度数法(角度を 0.0360.0 で表現するやつ)で指定する
    • 水平方向が 0.0 で、そこから時計回りに回転していく感じ
      • : 0.0
      • : 45.0
      • : 90.0
      • : 135.0
      • : 180.0
  • radius でぼかしの半径を指定する

ガウスぼかし

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.layout.Pane;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Pane pane;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        GaussianBlur gaussianBlur = new GaussianBlur();
        gaussianBlur.setRadius(10.0);
        
        this.pane.setEffect(gaussianBlur);
    }
}
  • GaussianBlur を使用すると、ガウスぼかしを適用できる(ボックスぼかしとの違いはよく分かってない)
  • radius でぼかしの半径を指定できる

ドロップ・シャドウ効果

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Pane pane;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        DropShadow dropShadow = new DropShadow();
        dropShadow.setOffsetX(5.0);
        dropShadow.setOffsetY(5.0);
        dropShadow.setWidth(20.0);
        dropShadow.setHeight(20.0);
        dropShadow.setColor(Color.valueOf("CACA00"));

        this.pane.setEffect(dropShadow);
    }
}
  • DropShadow を使用すると、ドロップシャドウ(影を落としたような)効果を適用できる
  • offsetX, offsetY で、影の相対的な位置(オフセット)を指定する
  • width, height で、影の縦横方向のぼかし具合を指定する
  • color で、影の色を指定する
  • spread で、影の拡散具合(0.01.0)を指定する

インナー・シャドウ効果

確認しやすくするため、シーングラフをちょっと変更する。

javafx.jpg

※視覚効果適用前

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.effect.InnerShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Pane pane;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        InnerShadow innerShadow = new InnerShadow();
        innerShadow.setOffsetX(7.0);
        innerShadow.setOffsetY(7.0);
        innerShadow.setColor(Color.valueOf("500"));
        innerShadow.setRadius(10.0);
        
        this.pane.setEffect(innerShadow);
    }
}
  • InnerShadow を使用すると、インナーシャドウ(内側に窪んだような影の)効果を適用できる
  • offsetX, offsetY で、影の相対的な位置(オフセット)を指定する
  • width, height で、影の縦横方向の長さを指定する
  • color で、影の色を指定する
  • radius で、影のぼかし半径を指定する

リフレクション

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.effect.Reflection;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Label label;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Reflection reflection = new Reflection();
        reflection.setTopOffset(-25.0);
        reflection.setTopOpacity(0.8);
        reflection.setFraction(0.8);

        this.label.setEffect(reflection);
    }
}
  • Reflection を使用すると、リフレクション(物体が床に反射しているような)効果を適用できる
  • topOffset で、反射表示されている部分の上部の相対的な位置(オフセット)を指定する
  • topOpacity で、反射表示されている部分の上部の透明度を指定する
  • fraction で、反射表示する部分の割合を指定する(0.01.0 で、デフォルトは 0.75、つまり 75% だけが反射表示される)
  • bottomOpacity で、反射表示されている部分の下部の透明度を指定する

視覚効果を組み合わせる

実行結果

javafx.jpg

実装

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Reflection;
import javafx.scene.paint.Color;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    
    @FXML
    private Label label;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Reflection reflection = new Reflection();
        reflection.setTopOffset(-40.0);
        reflection.setTopOpacity(0.8);
        reflection.setFraction(0.8);

        DropShadow dropShadow = new DropShadow();
        dropShadow.setOffsetX(5.0);
        dropShadow.setOffsetY(5.0);
        dropShadow.setColor(Color.valueOf("AA0"));

        reflection.setInput(dropShadow);

        this.label.setEffect(reflection);
    }
}
  • setInput() で他の Effect を指定することで、複数の Effect を組み合わせることができる
  • 上記実装の場合、ドロップシャドウが先に適用されて、次にリフレクションが適用される
  • つまり、 setInput() で渡した方が先に適用される

参考

メディア

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

メディアファイルの仕組み

コーデック

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

コンテナ

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

JavaFX がサポートしているエンコード形式・コンテナ

  • JavaFX がサポートしているメディアのエンコード形式やコンテナは、Javadoc に記載されている

エンコード形式

エンコード形式 音声/映像
PCM 音声
MP3 音声
AAC 音声
H.264/AVC 映像
VP6 映像

PCM

  • パルス符号変調(pulse code modulation) の略
  • 非圧縮データのこと
  • エンコード形式と呼ぶのは不適切かも?

MP3 (エンコード形式)

  • MPEG-1 Audio Layer-3 の略
  • 動画コンテナの MPEG-1 で使用されていた音声用のエンコード形式
  • 圧縮率が高く、非常に普及している

AAC

  • Advanced Audio Coding の略
  • MP3 の後継として作られた音声用のエンコード形式
  • MP3 より圧縮率が高い
  • MP4 コンテナの音声データとして広く採用されている

H.264/AVC

  • MP4 コンテナの映像データによく利用される映像用のエンコード形式
  • H.264 と MPEG-4 AVC という2つの名前があり、くっつけて「H.264/MPEG-4 AVC」と表記されることが多い
    • JavaFX の Javadoc は「H.264/AVC」って書いている
  • 非常に高い圧縮率で、広く普及している
  • 後継として、さらに圧縮率の上がった H.265 がある

VP6

  • FLV コンテナ(Flash ムービー)で利用される映像用のエンコード形式
  • H.264 より圧縮率がいいらしい
  • 後継として、さらに圧縮率の上がった VP9 がある(Youtube で採用されてるらしい)
  • ただし、 Java9 以降は非推奨らしい

コンテナ形式

コンテナ エンコード形式(映像) エンコード形式(音声)
AIFF - PCM
WAV - PCM
MP3 - MP3
FLV VP6 MP3
MP4 H.264/AVC AAC

AIFF

  • Audio Interchange File Format の略
  • MAC オリジナルの音声ファイル用のコンテナ形式
  • 非圧縮

WAV

  • Waveform Audio Format の略
  • Windows オリジナルの音声ファイル用のコンテナ形式
  • 非圧縮

MP3 (コンテナ)

  • MP3 (コーデック)の音声データが入った音声ファイル用のコンテナ形式
  • タグや歌詞、画像などメタ情報を入れられたりする

FLV

  • Flash プレイヤーで再生することを目的とした動画ファイル用のコンテナ形式
  • FLV はバージョンによっては動画コーデックに H.264 を使用したり音声コーデックに ACC を使用することも可能だが、 JavaFX がサポートしているのはあくまで VP6-MP3 の組み合わせだけ

MP4

  • 動画ファイル用のコンテナ形式
  • 携帯電話などの低速回線環境で動画を配信することを考慮して考えられた規格
  • 2017 現在、非常に普及している

広く普及しているエンコード形式(MP3, ACC, H.264)は最低限サポートしている、という感じ。

サポートされていないエンコード・コンテナのメディアファイルを再生する

  • 標準でサポートされていない形式のメディアファイルを再生するにはどうするか
  • コーデックを別途追加するような方法があればできそうだが、無理っぽい?
    • WebView を使えば再生できそうなことが書いているように読めたが、実際やってみても無理だった
  • そうなると、メディアファイルの形式を JavaFX がサポートする形式に変換せざるを得ない気がする
    • エンコーディングを変換できるフリーのツールは、音声・動画ともに色々とあるので、それで JavaFX がサポートする形式に変換すれば再生できるようになる
    • 「音声 動画 エンコード 変換」とかで検索すれば色々ヒットする

参考

メディアファイルを再生する

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 を使用する(詳細は後述)
  • MediaPlayerplay() メソッドでメディアファイルを再生できる

MediaPlayer の状態遷移

javafx.png

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

状態 説明
UNKNOWN 作成直後で読み込みが完了していない状態
READY 読み込みが完了し、再生の準備ができた状態
PLAYING 再生中の状態
PAUSED 一時停止中の状態
PLAYING に戻ると停止していたところから再開する
STOPPED 停止中の状態
PLAYING に戻ると最初から再生する
STALLED ストリーミング再生でプレイヤーの再生を続けるのに十分な情報が取得できていない状態
HALTED エラーが発生してプレイヤーが再生できない状態
DIPOSED プレイヤーが破棄されリソースが解放された状態
  • MediaPlayer は上図のような状態遷移をする
    • DISPOSED, HALTED は特殊な状態なので省略している
  • メディアファイルの読み込みは非同期で行われ、 Media インスタンスを生成した直後はまだ読み込みが完了していない可能性がある
  • Media インスタンスが関連付けられた MediaPlayer の状態が READY になったら、メタ情報の読み込みは確実に完了している
  • UNKNOWN 状態の場合は再生などがまだできないが、 play() メソッドなどの命令は UNKNOWN の状態で呼んでいるとバッファされ、 READY になった時点で実行されるようになっている(明示的に READY を待たなくていい)
  • 詳しくは MediaPlayer.Status の Javadoc を参照

リソースファイルを再生する

フォルダ構成
`-src/main/
  |-java/
  | `-sample/javafx/
  |   :
  `-resources/
    |-...
    `-media/
      `-music.mp3
package sample.javafx;

import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        URL url = MainController.class.getResource("/media/music.mp3");
        Media media = new Media(url.toString());
        MediaPlayer player = new MediaPlayer(media);
        
        player.play();
    }
}
  • URL でリソースファイルを取得して toString() すれば、リソースファイルを再生できる

繰り返し再生する

package sample.javafx;

...

public class MainController implements Initializable {
    private MediaPlayer player;
    
    ...

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);

        this.player.setCycleCount(3);
    }
}
  • setCycleCount(int) で、メディアファイルを連続で再生する回数を指定できる
  • MediaPlayer.INDEFINITE を指定すると、無制限に連続再生されるようになる

再生時間

package sample.javafx;

...
import javafx.util.Duration;

public class MainController implements Initializable {
    private MediaPlayer player;
    
    ...

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        
        this.player.setCycleCount(3);

        Duration cycleDuration = this.player.getCycleDuration();
        System.out.println("cycleDuration=" + cycleDuration);
        Duration totalDuration = this.player.getTotalDuration();
        System.out.println("totalDuration=" + totalDuration);

        this.player.setOnReady(() -> {
            System.out.println("[onReady] cycleDuration=" + this.player.getCycleDuration());
            System.out.println("[onReady] totalDuration=" + this.player.getTotalDuration());
        });
    }
}
実行結果
cycleDuration=UNKNOWN
totalDuration=UNKNOWN
[onReady] cycleDuration=9541.0 ms
[onReady] totalDuration=28623.0 ms
  • cycleDurationtotalDuration で再生時間を取得できる
    • cycleDuration は、1回分の再生時間を表す
    • totalDuration は、繰り返しも含めた総再生時間を表す
      • 再生回数が INDEFINITE の場合は Duration.INDEFINITE
  • ただし、 READY になる前に取得すると UNKNOWN になってしまう
  • 再生時間は Duration というクラスのインスタンスになっている

Duration クラス

package sample.javafx.property;

import javafx.util.Duration;

public class Main {
    public static void main(String[] args) {
        Duration duration = new Duration(90000.0);
        System.out.println("duration=" + duration);
        System.out.println("duration.seconds=" + duration.toSeconds());
        System.out.println("duration.minutes=" + duration.toMinutes());

        Duration oneMinute = Duration.seconds(60);
        System.out.println("oneMinute=" + oneMinute);
        System.out.println("oneMinute.minutes=" + oneMinute.toMinutes());
    }
}
実行結果
duration=90000.0 ms
duration.seconds=90.0
duration.minutes=1.5

oneMinute=60000.0 ms
oneMinute.minutes=1.0
  • Duration は期間を表すクラス
    • 期間とは、何分間・何秒間、といった長さをもった時間のこと(特定の時点を表すものではない
  • ミリ秒を基本の単位として期間を保持している
  • コンストラクタで生成する場合はミリ秒を指定する
  • ファクトリメソッドを使えば、時間・分・秒指定などでもインスタンスを生成できる
  • このクラスは不変で、 add()multiply() といったメソッドは計算結果を別の新規インスタンスに格納して返すようになっている

文字列フォーマット

  • Duration の情報を文字列にフォーマット(02:00:12 とかに)したい場合、直接的にフォーマットできるモノは標準 API の中に見当たらない
  • Date and Time API の Duration なら良い感じのフォーマッターがあるかと思ったが、なさそう...
  • 仕方ないので、↑の StackOverflow の実装例を参考に JavaFX の Duration 版を書いてみると↓のような感じか
package sample.javafx.property;

import javafx.util.Duration;

public class Main {
    public static void main(String[] args) {
        Duration duration = new Duration(12294027.0);
        String text = format(duration);
        System.out.println("text=" + text);
    }
    
    public static String format(Duration duration) {
        long millis = (long) duration.toMillis();
        long absMillis = Math.abs(millis);
        long hours = absMillis / 3_600_000;
        long minutes = (absMillis / 60_000) % 60;
        long seconds = (absMillis / 1_000) % 60;
        long milliseconds = absMillis % 1_000;

        return (millis < 0 ? "-" : "") + String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds);
    }
}
実行結果
text=03:24:54.027

再生する範囲を指定する

package sample.javafx;

...

public class MainController implements Initializable {
    private MediaPlayer player;
    
    ...

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        
        this.player.setCycleCount(3);
        
        this.player.setStartTime(Duration.seconds(1));
        this.player.setStopTime(Duration.seconds(3));

        this.player.setOnReady(() -> {
            System.out.println("cycleDuration=" + this.player.getCycleDuration());
            System.out.println("totalDuration=" + this.player.getTotalDuration());
        });
    }
}
実行結果
cycleDuration=2000.0 ms
totalDuration=6000.0 ms
  • startTime, stopTime にそれぞれ開始時間、終了時間を設定すると、その範囲内でのみメディアデータが再生される
  • cycleDurationstartTimestopTime までの時間を返す
  • totalDuration は、 cycleDuration * 再生回数

メタデータ

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

package sample.javafx;

import javafx.collections.ObservableMap;
import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;

...

public class MainController implements Initializable {
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music2.mp3");
        Media media = new Media(music.toUri().toString());
        MediaPlayer player = new MediaPlayer(media);
        
        player.setOnReady(() -> {
            ObservableMap<String, Object> metadata = media.getMetadata();

            metadata.forEach((key, value) -> {
                System.out.println("key=" + key + ", value=" + value);
            });
        });
    }
}
実行結果
key=comment-1, value=iTunPGAP[eng]=0  
key=album artist, value=ClariS 
key=comment-0, value=[eng]=  
key=image, value=javafx.scene.image.Image@1ca0c8fb
key=artist, value=ClariS 
key=raw metadata, value={ID3=java.nio.HeapByteBufferR[pos=779441 lim=789675 cap=789675]}
key=composer, value=ryo 
key=album, value=SHIORI 
key=comment-3, value=iTunSMPB[eng]= 00000000 00000210 0000096C 0000000000E5EE04 00000000 007D1469 00000000 00000000 00000000 00000000 00000000 00000000 
key=comment-2, value=iTunNORM[eng]= 000022CF 000021D8 0000C4E5 0000FF84 0002901F 0004CBF5 0000823D 0000832E 0004A049 00049D87 
key=genre, value=  
key=title, value=君の知らない物語 
  • MediagetMetadata() で、メタデータを保持した ObservableMap が取得できる
    • メタデータがなかったり、メタデータの取得がサポートされていないコンテナ形式の場合は空のマップが返る
  • サポートされているメタデータタグの一覧は、Javadoc に記載されているのでそちらを参照。
  • 残念ながら AAC はメタデータの取得がサポートされていない
    • 一応 Java9 も確認したが、変わらず AAC はサポート対象外

再生位置を変更する

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.util.Duration;

...

public class MainController implements Initializable {
    
    private MediaPlayer player;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music2.mp3");
        Media media = new Media(music.toUri().toString());
        this.player = new MediaPlayer(media);
        this.player.play();
    }
    
    @FXML
    public void seek() {
        this.player.seek(Duration.minutes(2));
    }
}
  • 再生位置を変更するには seek(Duration seekTime) メソッドを使用する
  • 引数で指定する seekTime は、メディアデータの先頭からの期間を指定する
    • startTime を指定していても、 startTime からの期間ではない(たぶん)
  • 図にすると↓のような感じ

javafx.jpg

  • startTime, stopTime を指定していて、その範囲の外に出る seekTime を指定した場合は、 startTimestopTime のいずれかに納まるように制限される
  • 詳しくは Javadoc を参照

映像を表示する

javafx.jpg

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 を使用する
  • MediaViewsetMediaPlayer() に再生するメディアを読み込んだ MediaPlayer を渡す
  • 幅や高さは fitWidth, fitHeight プロパティで調整できる

AudioClip

再生時間の短いちょっとした効果音などは、 Media よりも AudioClip を使ったほうが効率がいい。

package sample.javafx;

import javafx.fxml.Initializable;
import javafx.scene.media.AudioClip;

import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;

public class MainController implements Initializable {

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Path music = Paths.get("./media/music2.mp3");
        AudioClip audioClip = new AudioClip(music.toUri().toString());
        audioClip.play();
    }
}
  • インスタンスの生成は Media と同様 URL 文字列をコンストラクタ引数に渡す
  • play() メソッドで再生を開始できる

Media との違いは次のような感じ。

  • 最小の遅延で再生できる(すぐに再生できる)
  • インスタンス生成後、すぐに使い始められる
  • 一度再生すると、以後は stop() 以外のメソッドは効果がない
  • 同一インスタンスを同時に複数回 play() で再生できる
    • 同時に複数再生中の状態で stop() を呼ぶと、再生中の全ての音声が停止する

詳細は Javadoc を参照。

簡単な音楽プレイヤーを作成してみる

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

javafx.jpg

package sample.javafx;

import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.StringBinding;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaPlayer.Status;
import javafx.util.Duration;

import java.net.URL;
import java.nio.file.Paths;
import java.util.ResourceBundle;
import java.util.stream.Stream;

public class SimpleMusicPlayerController implements Initializable {

    private MediaPlayer player;
    
    @FXML
    private Button playButton;
    @FXML
    private Button stopButton;
    @FXML
    private Button pauseButton;

    @FXML
    private Label volumeLabel;
    @FXML
    private Label totalTimeLabel;
    @FXML
    private Label currentTimeLabel;
    
    @FXML
    private Slider volumeSlider;
    @FXML
    private Slider seekSlider;

    @FXML
    public void play() {
        this.player.play();
    }

    @FXML
    public void stop() {
        this.player.stop();
    }

    @FXML
    public void pause() {
        this.player.pause();
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Media media = new Media(Paths.get("./media/music.m4a").toUri().toString());
        this.player = new MediaPlayer(media);

        // 音量ラベル
        this.volumeLabel.textProperty().bind(this.player.volumeProperty().asString("%.2f"));
        // 合計再生時間ラベル
        this.player.setOnReady(() -> {
            Duration total = this.player.getTotalDuration();
            this.totalTimeLabel.setText(this.format(total));
        });
        // 現在の再生時間ラベル
        this.currentTimeLabel.textProperty().bind(new StringBinding() {
            {bind(player.currentTimeProperty());}
            
            @Override
            protected String computeValue() {
                Duration currentTime = player.getCurrentTime();
                return format(currentTime);
            }
        });
        
        // Playボタン
        this.playButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            
            @Override
            protected boolean computeValue() {
                boolean playable = playerStatusIsAnyOf(Status.READY, Status.PAUSED, Status.STOPPED);
                return !playable;
            }
        });
        // Stopボタン
        this.stopButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}
            
            @Override
            protected boolean computeValue() {
                boolean stoppable = playerStatusIsAnyOf(Status.PLAYING, Status.PAUSED, Status.STALLED);
                return !stoppable;
            }
        });
        // Pauseボタン
        this.pauseButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}

            @Override
            protected boolean computeValue() {
                boolean pausable = playerStatusIsAnyOf(Status.PLAYING, Status.STALLED);
                return !pausable;
            }
        });
        
        // 音量スライダー
        this.player.volumeProperty().bind(this.volumeSlider.valueProperty());
        this.volumeSlider.setValue(1);
        
        // シークバー
        this.seekSlider.valueProperty().addListener((value, oldValue, newValue) -> {
            if (seekSliderIsChanging()) {
                Duration seekTime = this.player.getTotalDuration().multiply((Double) newValue);
                this.player.seek(seekTime);
            }
        });

        this.player.currentTimeProperty().addListener((value, oldValue, newValue) -> {
            if (!seekSliderIsChanging()) {
                double totalDuration = this.player.getTotalDuration().toMillis();
                double currentTime = newValue.toMillis();
                this.seekSlider.setValue(currentTime / totalDuration);
            }
        });

        // 再生が終了したときの後処理
        this.player.setOnEndOfMedia(() -> {
            this.player.seek(this.player.getStartTime());
            this.player.stop();
        });
    }
    
    private boolean playerStatusIsAnyOf(Status... statuses) {
        Status status = this.player.getStatus();
        return Stream.of(statuses).anyMatch(candidate -> candidate.equals(status));
    }

    private boolean seekSliderIsChanging() {
        return this.seekSlider.isValueChanging() || this.seekSlider.isPressed();
    }
    
    private String format(Duration duration) {
        long millis = (long) duration.toMillis();
        long minutes = (millis / 60_000) % 60;
        long seconds = (millis / 1_000) % 60;

        return String.format("%02d:%02d", minutes, seconds);
    }
}

現在の再生時間の表示

        // 現在の再生時間ラベル
        this.currentTimeLabel.textProperty().bind(new StringBinding() {
            {bind(player.currentTimeProperty());}
            
            @Override
            protected String computeValue() {
                Duration currentTime = player.getCurrentTime();
                return format(currentTime);
            }
        });

    ...
    
    private String format(Duration duration) {
        long millis = (long) duration.toMillis();
        long minutes = (millis / 60_000) % 60;
        long seconds = (millis / 1_000) % 60;

        return String.format("%02d:%02d", minutes, seconds);
    }
  • MediaPlayercurrentTime プロパティにバインドして、時間をフォーマットした値をラベルにセットする

ボタンの制御

        // 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 の制御に、 MediaPlayerstatus を利用する
  • 各ボタンをクリックできる状態を決めて、その否定を disabled プロパティにセットするようにしている

音量スライダー

javafx.jpg

        // 音量ラベル
        this.volumeLabel.textProperty().bind(this.player.volumeProperty().asString("%.2f"));

        ...

        // 音量スライダー
        this.player.volumeProperty().bind(this.volumeSlider.valueProperty());
        this.volumeSlider.setValue(1);
  • 音量は volume プロパティで調整できる
  • Slidervalue プロパティにバインドすることで、スライドの調整と音量の調整を連動させられる
  • ただし、 volume0.01.0 までの値で指定しなければならない
  • そのため、 Slider の最小値(Min)、最大値(Max)、増減算量(Block Increment) を volume の範囲に合わせて調整しておく
  • 音量用のラベルは、 MediaPlayervolume プロパティと連動するようにしておく

シークバー

  • シークバーの実装は少し複雑になる
    • 再生位置を書き込みできるプロパティはないので、リスナーを登録して MediaPlayer.seek() を呼ばなければならない
    • また、次のように双方向で状態を連動させなければならない
      1. 再生時間にあわせてスライダーの位置を調整する
      2. ユーザーがスライダーを操作したら、再生時間を調整する

javafx.jpg

  • Slider0.01.0 までの間で指定できるようにしておき、合計時間に対する割合として扱うようにしている

再生時間→シークバー

        this.player.currentTimeProperty().addListener((value, oldValue, newValue) -> {
            if (!seekSliderIsChanging()) {
                double totalDuration = this.player.getTotalDuration().toMillis();
                double currentTime = newValue.toMillis();
                this.seekSlider.setValue(currentTime / totalDuration);
            }
        });

    ...

    private boolean seekSliderIsChanging() {
        return this.seekSlider.isValueChanging() || this.seekSlider.isPressed();
    }
  • currentTime にリスナーを登録する
  • 合計時間に対する現在の時間の割合を計算し、スライダーの value に設定する
  • ここで注意なのが、スライダーをユーザが操作していないときにだけ値を変更するようにしなければならない
  • そうしないと、ユーザがスライダーを移動させようとしても、強制的に再生時間に戻されてしまうようになる
  • ユーザがスライダーを触っているかどうかについては、 Slider の次のメソッドを使用する
    • isValueChanging()
      • スライダーをスライドさせている最中の場合は true を返す
    • isPressed()
      • スライダーをクリックしている場合は true を返す

シークバー→再生時間

        this.seekSlider.valueProperty().addListener((value, oldValue, newValue) -> {
            if (seekSliderIsChanging()) {
                Duration seekTime = this.player.getTotalDuration().multiply((Double) newValue);
                this.player.seek(seekTime);
            }
        });
  • スライダーの value プロパティにリスナーを登録する
  • スライダーの新しい値(0.01.0)と合計再生時間を掛けて、移動先の時間(seekTime)を求める
  • そして、その値を使って MediaPlayerseek() を実行する
  • ここで注意なのが、スライダーをユーザが操作しているときにだけ値を変更するようにしなければならない
  • そうしないと、再生時間に合わせてスライダーの値を変更したときにもこのリスナーが呼ばれるので、再生位置が無駄に更新されてしまい、音がカクカク再生されるような感じになってしまう

再生時間が終了したときの処理

        // 再生が終了したときの後処理
        this.player.setOnEndOfMedia(() -> {
            this.player.seek(this.player.getStartTime());
            this.player.stop();
        });
  • 再生時間が stopTime に到達しても、 statusPLAYING のままになっている
  • つまり、そのままだと status と紐づけているボタンなどは状態が変わらない
  • 再生が終了したときに status を明示的に変更するため、 setOnEndOfMedia() でリスナーを登録する
  • ここで seek() で再生位置を startTime に戻して、 stop() で状態を STOPPED に変更している
    • stop() だけでもいい気がするけど、それだとなぜか getCurrentTime()stopTime しか返さなくなってしまう
    • MediaPlayer は、再生が終了したかどうかを boolean のフラグとして内部で持っているが、そのフラグが true だと getCurrentTime()stopTime を返すようになっているっぽい
    • そして、再生終了後 stop() だけで状態を変更させると、このフラグが true のまま残ってしまい、以後再生中なのに currentTimestopTime しか返さなくなっている気がする
    • 「バグじゃね?」という気がするが、仕様を熟知しているわけでもないので、なんとも言えない

参考

メニュー

メニューバー

javafx.jpg

実行結果

javafx.jpg

説明

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

メニューが選択されたときの処理を実装する

javafx.jpg

package sample.javafx;

import javafx.fxml.FXML;

public class MainController {
    
    @FXML
    public void open() {
        System.out.println("「開く」が選択された");
    }
}
  • メニューの On Action イベントとコントローラのメソッドを紐づけることで、メニューが選択されたときの処理を実装できる

セパレータ

javafx.jpg

実行結果

javafx.jpg

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

サブメニュー

javafx.jpg

実行結果

javafx.jpg

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

ショートカットキーを割り当てる

javafx.jpg

実行結果

javafx.jpg

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

ニーモニック

javafx.jpg

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

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

javafx.jpg

javafx.jpg

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

日本語メニューにニーモニックを割り当てる

javafx.jpg

実行結果

javafx.jpg

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

アイコンを追加する

javafx.jpg

フォルダ構成
`-src/main/
  |-java/
  | :
  `-resources/
    `-img/
      `-open.png
package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.MenuItem;
import javafx.scene.image.ImageView;

import java.net.URL;
import java.util.ResourceBundle;

public class MainController implements Initializable {
    @FXML
    private MenuItem openMenuItem;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        ImageView image = new ImageView("/img/open.png");
        this.openMenuItem.setGraphic(image);
    }
}

実行結果

javafx.jpg

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

チェックメニュー

javafx.jpg

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

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

ラジオボタンメニュー

javafx.jpg

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

  • RadioMenuItem を使用すると、排他的に選択できるメニュー項目を定義できる
  • RadioMenuItemToggle Group に、その項目が属するグループの識別子(任意の文字列)を設定する
    • 同じグループの識別子を持つ項目は同じグループに属するものとして識別されるようになり、選択が排他的に制御されるようになる
  • Toggle Group は、 fxml 上では次のように定義される
fxml上のToggleGroup
  <RadioMenuItem fx:id="hogeRadioMenuItem" text="ほげ">
     <toggleGroup>
        <ToggleGroup fx:id="group1" />
     </toggleGroup>
  </RadioMenuItem>
  • fx:idToggle Group で指定した識別子になっている
  • なので、コントローラクラスには指定した Toggle Group の識別子で ToggleGroup インスタンスをインジェクションできるようになる
ToggleGroupのインジェクション
    @FXML
    private ToggleGroup group1;
  • ToggleGroupselectedToggle プロパティにリスナを登録することで、選択されている項目が変わったときのイベントを捕捉できる
  • このとき、1回の変更でイベントが2度呼ばれ、最初の1度目は newValuenull になるという点に注意
  • これはバグらしく、 ver 10 で治る予定っぽい
        this.hogeRadioMenuItem.setUserData("hoge");
        this.fugaRadioMenuItem.setUserData("fuga");
        this.piyoRadioMenuItem.setUserData("piyo");
        this.group1.selectedToggleProperty().addListener((toggle, oldValue, newValue) -> {
            if (newValue != null) {
                System.out.println("[group1] " + newValue.getUserData());
            }
        });
  • どのメニューが選択されたかどうかは、メニュー項目インスタンスの userData に登録しておいた値で識別する
    • userDataObject 型なので、何でも値を入れることができる
    • 普段はあまり使わないが、今回のようにグループの中で選択されたものを識別するような場面で利用できる
    • 残念ながら Scene Builder から userData を指定することはできないっぽいので、 initialize() メソッドなどで設定するしかない

コンテキストメニュー

javafx.jpg

実行結果

javafx.jpg

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

参考

ウィンドウの最大化

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 を設定すれば、ウィンドウを最大化できる

別のウィンドウを開く

基本

フォルダ構成
`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-MainController.java
  |   `-OtherController.java
  |
  `-resources/
    |-main.fxml
    `-other.fxml

other.fxml

javafx.jpg

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) を利用して読み込んでいる
  • load() メソッドで fxml 内に定義した一番上のノードが取得できる
  • あとは、シーン・ステージを作って show() もしくは showAndWait() メソッドで起動する
    • JavaFX アプリケーションは、ステージシーンシーングラフから成る
    • ステージは、 JavaFX アプリケーションの最上位コンテナで、ウィンドウに対応する
      • 上の実装だと、Stage が対応する
    • シーンとは、シーングラフを保持するコンテナで、ステージの表示領域部分に対応する
      • 上の実装だと、 Scene が対応する
    • シーングラフとは、シーン上に表示する全てのノードを保持するツリー構造で、シーングラフを書き換えることで表示を変更することができる
      • 上の実装だと、 fxml からロードした Pane が、このシーングラフのルートノードとなる

コントローラを取得する

MainController.java
package sample.javafx;

public class MainController {
    
    public void hello() {
        System.out.println("Hello MainController!!");
    }
}
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

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

所有者を設定する

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.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

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

モーダル

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

  • initModality() で、そのステージをモーダルにすることができる
  • initModality() には、 Modality 列挙型の定数を渡す
  • Modality には NONE, WINDOW_MODAL, APPLICATION_MODAL の3つが定義されている
定数 意味
NONE 他のウィンドウをブロックしない
WINDOW_MODAL 所有者のウィンドウをブロックする
(所有者以外のウィンドウはブロックしない)
APPLICATION_MODAL すべてのウィンドウをブロックする

参考

別の fxml を埋め込む

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

基本

フォルダ構成

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

埋め込むfxml (embedded.fxml)

javafx.jpg

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

javafx.jpg

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

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

埋め込んだ fxml のコントローラを取得する

ファイル構成
`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-Main.java
  |   |-MainController.java
  |   `-EmbeddedController.java
  `-resources/
    |-main.fxml
    `-embedded.fxml

embedded.fxml

javafx.jpg

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

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>
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

説明

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

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

embedded.fxml

javafx.jpg

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

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>
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

説明

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 するのに必要になる)
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 で都度読み込むことになると思う

参考

ファイル選択ダイアログ

javafx.jpg

  • ボタンをクリックしたら、ファイル選択ダイアログが表示されるようにする
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 をセットする
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

表示されたダイアログ

javafx.jpg

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

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

説明

        FileChooser chooser = new FileChooser();
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
  • ファイルを選択するダイアログを表示するには、 FileChooser クラスを使用する
  • インスタンスを生成して showOpenDialog(Stage) メソッドを実行すると、単一ファイルを選択するためのファイル選択ダイアログが開く
    • 引数の Stage には、オーナーとなる Stage を指定する
    • オーナーが指定されている場合、ファイル選択ダイアログが開いている間、オーナーへの操作はブロックされる(モーダルになる)
    • null を渡すこともでき、その場合ファイル選択ダイアログはモーダルにはならない
  • ダイアログが表示されている間、 showOpenDialog() メソッドはブロックされる
  • ダイアログでの選択結果が File 型で返される
  • ファイルが選択された場合は、そのファイルを指す File オブジェクトが返る
  • ファイルを選択せずにダイアログが閉じられた場合は null が返る

タイトルを設定する

        FileChooser chooser = new FileChooser();
        chooser.setTitle("ふぁいるせんたく");
        File file = chooser.showOpenDialog(this.stage);

実行結果

javafx.jpg

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

初期表示するディレクトリを指定する

        FileChooser chooser = new FileChooser();
        chooser.setInitialDirectory(new File("C:/Program Files/java/jdk-9.0.1"));
        File file = chooser.showOpenDialog(this.stage);
  • setInitialDirectory(File) で、ダイアログを開いたときに表示するディレクトリを指定できる
  • 何も指定しない場合は、環境(OS)ごとにデフォルトのディレクトリが表示される
  • FileChooser クラス自体には、前回開いたディレクトリ(ファイル)を記録しておく、みたいな機能はないっぽい
  • なので、前回開いたディレクトリをもう一度開きたい場合は、別途開いたディレクトリを記憶しておいて、もう一度ダイアログを開くときに setInitialDirectory() でディレクトリを指定することで実現できる

選択できるファイルを拡張子で絞る

package sample.javafx;

import javafx.collections.ObservableList;
import javafx.stage.FileChooser.ExtensionFilter;
...

public class MainController {
    
    private Stage stage;
    
    @FXML
    public void openFileDialog() {
        FileChooser chooser = new FileChooser();
        
        ObservableList<ExtensionFilter> extensionFilters = chooser.getExtensionFilters();
        extensionFilters.add(new ExtensionFilter("何でもあり", "*.*"));
        extensionFilters.add(new ExtensionFilter("画像だけやで", "*.jpg", "*.jpeg", "*.png", "*.gif"));
        extensionFilters.add(new ExtensionFilter("まさかの動画", "*.mp4"));
        
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
    }

    ...
}

実行結果

javafx.jpg

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

複数のファイルを選択できるダイアログを開く

        FileChooser chooser = new FileChooser();
        List<File> files = chooser.showOpenMultipleDialog(this.stage);
        if (files == null) {
            System.out.println("files=" + files);
        } else {
            files.forEach(System.out::println);
        }

実行結果

javafx.jpg

コンソール出力
C:\Program Files\Java\jdk-9.0.1\COPYRIGHT
C:\Program Files\Java\jdk-9.0.1\README.html
C:\Program Files\Java\jdk-9.0.1\release
  • showOpenMultipleDialog(Stage) でダイアログを開くと、複数のファイルを選択できるダイアログが開く
  • 選択結果は List<File> で返される
  • 何も選択されなかった場合は null を返す(空のリストじゃないのか...)

ファイルの保存先を選択するダイアログ

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

実行結果

javafx.jpg

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

ディレクトリを選択するダイアログ

package sample.javafx;

import javafx.stage.DirectoryChooser;
...

public class MainController {
    
    private Stage stage;
    
    @FXML
    public void openFileDialog() {
        DirectoryChooser chooser = new DirectoryChooser();
        File directory = chooser.showDialog(this.stage);
        System.out.println("directory=" + directory);
    }

    ...
}

実行結果

javafx.jpg

コンソール出力
directory=C:\Users\Public\Downloads
  • ディレクトリを選択するダイアログを開くには、 DirectoryChooser を使用する
  • 選択対象がディレクトリになること以外は、基本 FileChooser と同じ使い方になる(initialDirectory とか)

参考

ボタンやラベルを同じ幅(高さ)に揃えて縦(横)に並べる

見た目

javafx.jpg

構造

javafx.jpg

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

javafx.jpg

  • 同じ幅で縦に並べるなら、 VBox を使う
    • 並べる各要素は、最大幅 (Max Width) を MAX_VALUE にして最大幅まで伸ばす
  • 同じ高さで横に並べるなら、 HBox を使う
    • 並べる各要素は、最大高さ (Max Height) を MAX_VALUE にして最大高さまで伸ばす
  1. 公式の日本語ドキュメントだと「ルート」と記載されているが、「根っこ(root)」と勘違いしそうなので「経路(route)」と表記

  2. ButtonDataNO または CANCEL_CLOSE 2 3

52
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?