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