JavaFX study notes

Notes on studying JavaFX

environment

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

The one developed by Gluon

JavaFX properties

JavaFX uses ** JavaFX Properties **, which is an extension of JavaBeans, as a way to represent the properties of an object.

JavaFX properties implement object properties as follows:

package sample.javafx.property;

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

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

    public DoubleProperty valueProperty() {
        return this.value;
    }
}
  1. Define the property (value) with private --The property type is not a primitive type, but a wrapper type provided by JavaFX, such as DoubleProperty. --The wrapper type is prepared under the javafx.beans.property package. Are --DoubleProperty itself is an interface, and SimpleDoubleProperty is provided as an implementation class.
  2. Define Getter and Setter --Method names are named according to the same rules as JavaBeans --The type is not a wrapper type, but the value of the contents should be put in and out. --Methods are defined with final
  3. Define a method called property name Property --This returns the wrapper type as is

Many of the classes provided by JavaFX implement this JavaFX property. For example, the methods corresponding to the text property of the Label class are listed below. (The actual definition is the Labeled class).

Observable JavaFX properties do more than just determine method declarations, they provide some special features that JavaBeans doesn't have. One of them is Observable.

Classes that implement ʻObservable` provide the ability to monitor for invalid content.

The DoubleProperty used in the JavaFX property above also inherits from ʻObservable`.

This ʻObservable has a method called ʻaddListener (InvalidationListener) that allows you to register a listener to be notified when the content becomes invalid.

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

Execution result


observable=DoubleProperty [value: 1.0]
observable=DoubleProperty [value: 2.0]

The modified ʻObservable` itself is passed to the listener as an argument.

Disable and change

Some classes that implement ʻObservable` do not calculate immediately when a change is requested (for example, when the Setter method is called), and only when the content is requested next time. There is something like (delay calculation).

In other words, there may be a period in which the content is not finalized between the time when the content change is requested and the time when the content is actually calculated.

The fact that this content is not finalized is expressed as ** invalid **. Then, when the calculation is actually performed and the content is confirmed, it is expressed as ** change **.

ʻInvalidationListener` is now called back when the content becomes invalid.

Depending on the implementation class, the content is calculated immediately, so in that case, invalidation and change will occur at the same time (after invalidation, the content will be confirmed (changed) immediately).

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

Execution result


observableValue=DoubleProperty [value: 1.0], oldValue=0.0, newValue=1.0
observableValue=DoubleProperty [value: 2.0], oldValue=1.0, newValue=2.0

ʻObservableValue](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/value/ObservableValue.html) interface exists as an interface that inherits Observable. It provides a method called ʻaddListener (ChangeListener) , which allows you to register a listener to be notified when a value changes. The listener is passed the changed ʻObservableValue` itself and the raw values before and after the change.

The property classes provided by JavaFX, including DoubleProperty, implement ʻObservableValue, so both ʻInvalidationListener and ChangeListener listeners can be registered.

Binding ʻThere is Binding as an interface that inherits ObservableValue`.

Binding represents the calculation result of aggregating multiple values. The aggregated values are monitored by Binding so that the results are automatically recalculated when the values change.

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

Execution result


sum=3.0
sum=5.0

In this example, we are creating a DoubleBinding that represents the sum of two DoublePropertys. If you change the value of one of the DoubleProperty and get the value of DoubleBinding again, the sum is the recalculated value.

In this way, Binding provides a mechanism to combine multiple values, monitor changes in each value, and automatically update the calculation results.

The values collected by Binding are called ** source ** or ** dependency **. The Binding itself does not limit the type of the source, but in reality it is usually ʻObservable or ʻObservableValue.

Delay calculation

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

Execution result


sum=DoubleBinding [invalid]
sum.get()=3.0
sum=DoubleBinding [value: 3.0]

sum=DoubleBinding [invalid]
sum.get()=5.0
sum=DoubleBinding [value: 5.0]

--All classes that implement Binding that exist in the standard library are delayed in calculation. --In other words, it does not recalculate immediately after the source value is changed, but recalculates when an attempt is made to obtain the calculation result. --Binding is invalid until recalculation is done

If you register ChangeListener, the delay will not be calculated.

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("Changed(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);
    }
}

Execution result


sum=DoubleBinding [value: 3.0]
sum.get()=3.0
sum=DoubleBinding [value: 3.0]

Changed(oldValue=3.0, newValue=5.0)

sum=DoubleBinding [value: 5.0]
sum.get()=5.0
sum=DoubleBinding [value: 5.0]

--If a change notification listener is registered with ʻObservableValue.addListener (ChangeListener) , the delay calculation will not be performed. --In other words, if you have registered ChangeListener, it will always be calculated immediately. --This is not limited to Binding, but is common to all implementations of delay calculation that implement ʻObservableValue. --For more information, see Observable Javadoc and [Official Tutorial](https: // See the description in docs.oracle.com/javase/jp/8/javafx/properties-binding-tutorial/binding.htm#sthref12)

High level API

There are two types of APIs for creating Binding: ** high-level API ** and ** low-level API **.

The higher level API is divided into ** Fluent API ** and ** Bindings class factory method **.

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

Execution result


((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((4.0 + 3) * 2 - 4.0) / 3.0 = 4.0

--The Fluent API allows you to build Binding with a fluent interface. --ʻA.add (3) .multiply (2) .subtract (4.0) .divide (3.0) part --These methods are defined in the interface [NumberExpression](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/beans/binding/NumberExpression.html) --The class used in the property implements this interface --In addition to numerical operations, there are also methods for performing comparison operations and logical operations. --The return type of the Fluent API of the numeric operation system of NumberExpression is NumberBinding, but since NumberBinding is also a subinterface of NumberExpression`, call Fluent API as it is. Be able to --The relationship between each interface and class is as shown in the figure below (only a part).

javafx.png

Factory method of Bindings class

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

Execution result


((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0

--The class Bindings has a large number of Binding factory methods. Has been --Some exist in the Fluent API, others do not ――There are a lot of them for the time being, so it's a good idea to take a quick look and know what's going on. -when () so

Low level API

MyBinding.java


package sample.javafx.property;

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

public class MyBinding extends DoubleBinding {
    private final DoubleProperty source;

    public MyBinding(DoubleProperty source) {
        this.bind(source);
        this.source = source;
    }

    @Override
    protected double computeValue() {
        return ((this.source.get() + 3) * 2 - 4.0) / 3.0;
    }
}
package sample.javafx.property;

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

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

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

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

Execution result


((2.0 + 3) * 2 - 4.0) / 3.0 = 2.0
((5.0 + 3) * 2 - 4.0) / 3.0 = 4.0

--The low-level API inherits from the Binding abstract class and creates its ownBinding class. --Here, we are creating MyBinding that inherits DoubleBinding. --Register the monitored ʻObservablewith thebind () method that exists in the parent class. --The arguments are variable length, so you can monitor multiple ʻObservables. --When the contents of ʻObservableregistered withbind ()are changed,computeValue ()` is called, so implement it so that the calculation result is returned.

Bind a property with a 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);
    }
}

Execution result


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]

--If you use the bind (ObservalbeValue) method defined in Property , One-way binding can be realized --It now monitors the value of ʻObservableValuepassed as an argument, and when the value changes, it changes its own value to the same value as the monitored target. --If you want to link the value of one property with another, you can use thisbind ()` to implement it simply.

Read-only properties

MyClass.java


package sample.javafx.property;

import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;

public class MyClass {
    private ReadOnlyDoubleWrapper value = new ReadOnlyDoubleWrapper();
    
    public final double getValue() {
        return this.value.getValue();
    }
    
    public final ReadOnlyDoubleProperty valueProperty() {
        return this.value.getReadOnlyProperty();
    }
    
    public void updateValue() {
        this.value.set(99.9);
    }
}

Main.java


package sample.javafx.property;

public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();

        System.out.println(myClass.valueProperty());
        
        myClass.updateValue();

        System.out.println(myClass.valueProperty());
    }
}

Execution result


ReadOnlyDoubleProperty [value: 0.0]
ReadOnlyDoubleProperty [value: 99.9]

--If you publish DoubleProperty as it is, you can freely rewrite the value at the publishing destination. --If you want to prevent the value from being changed at the publishing destination, do not publish setter and set the type of the property used internally to ReadOnly ** Wrapper type. --The ReadOnly ** Wrapper type can change its value normally, but the object obtained bygetReadOnlyProperty ()is read-only.

Bidirectional binding

javafx.jpg

package sample.javafx;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;

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

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

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.textField.textProperty().bindBidirectional(this.value);
    }
    
    @FXML
    public void checkValue() {
        System.out.println("value=" + value.getValue());
    }
    
    @FXML
    public void resetValue() {
        this.value.set("reset!!");
    }
}

Execution result

javafx.gif

-BindBidirectional () defined in Property .oracle.com/javase/jp/8/javafx/api/javafx/beans/property/Property.html#bindBidirectional-javafx.beans.property.Property-) When you use the method, you can make two Property bidirectional. Can be bound

Bidirectional binding of StringProperty

The bidirectional binding method provided for Property <T> can only be bidirectionally bound to a property of the same Property <T> type.

On the other hand, StringProperty has the following two methods for bidirectional binding. Has been added.

--Format specification --Converter designation

Format specification

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

Execution result

javafx.gif

--You can specify Format as the second argument --The StringProperty side will be set to a string formatted with Format, and the other property will be set to the parsed result. --WARINING level log is output with stack trace if there is malformed input

Converter designation

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

Execution result

javafx.gif

-By specifying StringConverter, any value and StringProperty are bidirectional. Can be bound to --Two abstract methods are defined in StringConverter --String toString (T): Convert object T to String --T fromString (String): Converts String to object T --For the time being, LocalDateStringConverter is prepared as standard. ――However, if you cannot parse or format it, the WARNING log including the stack trace will be output, so it seems a little difficult to use. --If it is not free input, it may be usable

collection

JavaFX provides its own collection classes that extend the standard collections (List and Map).

Monitorable 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 changed change=" + change);
        });
        
        list.add("foo");
        list.remove("buzz");
        FXCollections.sort(list);
    }
}

Execution result


list changed change={ [foo] added at 2 }
list changed change={ [buzz] removed at 1 }
list changed change={ permutated by [0, 1] }

--ObservableList is an interface that inherits java.util.List and has a monitoring function. Has been added to realize --Instantiation uses the factory method of FXCollections --FXCollections is a utility class for JavaFX collections that has methods similar to java.util.Collections. --ʻAddListener (ListChangeListener) can register listeners to be notified of changes --ʻObservableList also inherits ʻObservable --And ʻObservable also defines a method with the same name ʻaddListener (InvalidationListener)but different arguments. --The listeners that both receive as arguments are both functional interfaces and have only one method argument. --In other words, if you write the simplest lambda expression with the argument types omitted, the compiler will not be able to determine which ʻaddListener ()is being used, resulting in a compilation error. --There is no choice, so even when writing a lambda expression, you must write at least the argument type without omitting it.

Check the changes

When the list changes, the listener is passed a Change object.

The Change object contains information about how the list was changed.

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("add to: " + change);
                }
                if (change.wasRemoved()) {
                    System.out.println("Delete: " + change);
                }
                if (change.wasPermutated()) {
                    System.out.println("Change order: " + change);
                }
                if (change.wasReplaced()) {
                    System.out.println("Replacement: " + change);
                }
            }
        });

        list.add("FOO");
        list.remove("one");
        FXCollections.sort(list);
        list.set(1, "hoge");
    }
}

Execution result


==========================================
list=[one, two, three, FOO]
add to: { [FOO] added at 3 }
==========================================
list=[two, three, FOO]
Delete: { [one] removed at 0 }
==========================================
list=[FOO, three, two]
Change order: { permutated by [2, 1, 0] }
==========================================
list=[FOO, hoge, two]
add to: { [three] replaced by [hoge] at 1 }
Delete: { [three] replaced by [hoge] at 1 }
Replacement: { [three] replaced by [hoge] at 1 }

--You must ** always execute the next () method before you can get the changes from the Change object ** --When trying to get changes without calling next (), ʻIllegalStateExceptionis thrown --If there are multiple changes in one notification, you can move to the next change by callingnext (). --If you use removeAll ()described later, multiple changes may be notified at once, so if you turn it withwhile, you can check all the changes. --There are types of changes such as "add" and "delete" --You can see what kind of changes have occurred with the was *** ()methods such aswasAdded ()andwasRemoved (). --There are the following notes on the listener implementation -** Change` object must not be used in another thread ** -** Do not change the state of the list in the listener ** -It is described in Javadoc of Change object.

Check the added elements

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

Execution result


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

--When an element is added, wasAdded () returns true --You can get an index pointing to where the element was added with getFrom (), getTo () --getFrom () is the first index (including this index) to which the element was added. --getTo () is the last index to which the element was added (this index is not included) --You can get a List containing only the added elements withgetAddedSubList () --You can get the number of added elements with getAddedSize ()

Check for deleted elements

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

Execution result


==========================================
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 () returns true when the element is removed --In case of deletion, getFrom () returns the ** starting index ** of the deleted element in the original List. --Also, getTo () returns the same value asgetFrom ()(although I'm a little skeptical about the Javadoc description). --If you look closely at the Javadoc for Change, it seems like you can't readgetFrom ()andgetTo ()at the time of deletion, but what about (actually unconfirmed)? ?) --You can get the removed elements with List withgetRemoved () --getRemovedSize () returns the number of removed elements

Check for changes in sort order

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

Execution result


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 () returns true when the list is reordered --If you pass the index before the change to getPermutated (int), the index after the change is returned.

Check the replaced element

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

Execution result


==========================================
list=[one, FOO, three]
replaced=true
added=true
removed=true
from=1
to=2
addedSubList=[FOO]
addedSize=1
removed=[two]
removedSize=1

--wasReplaced () returns true when an element is replaced by another element --Strictly speaking, wasReplaced () becomes true when wasAddedd () && wasRemoved () --That is, only wasReplaced () can never be true --When wasReplaced () is true, wasAddedd () and wasRemoved () are always true.

Sorted list

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

Execution result


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]

--You can get the sorted list (SortedList) linked with the original ʻObservableList by executing the sorted () method. --If you change the original ʻObservalbeList, the state of SortedList will also be updated in a sorted state. --SortedList implements ʻObservableList --There is also asorted (Comparator)method that allows you to pass aComparatoras an argument --You can also change the sorting method later withsetComparator (Comparator)`

Filtered list

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

Execution result


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]

--You can get the filtered list (FilteredList) linked with the original ʻObservableList by executing the filtered (Predicate) method. --If you change the original ʻObservalbeList, the state of FilteredList will also be updated in a filtered state. --You can also change the filtering conditions later with setPredicate () --FilteredList implements ʻObservableList`

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

Execution result


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

--ʻObservableMapis provided as a monitorableMap --UseFXCollections.observableHashMap () to create an instance --Change in Mapis not as complicated asList --UnlikeList, next () is unnecessary --There are two types of changes: "added ( wasAdded () )" and "deleted ( wasRemoved () )". --The value added by getValueAdded (), You can get the removed value with getValueRemoved ()` --If you set a different value for an existing key, the "add" and "delete" changes will occur at the same time.

Style sheet

Basic

Folder structure


`-src/main/
  |-java/sample/javafx/
  | |-Main.java
  | `-MainController.java
  |
  `-resources/
    |-main.fxml
    `-my-style.css

main.fxml

javafx.jpg

my-style.css


.label {
    -fx-background-color: skyblue;
}

Main.java


package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add("my-style.css");
        primaryStage.setScene(scene);

        primaryStage.show();
    }
}

Execution result

javafx.jpg

Loading stylesheets

Main.java


        Scene scene = new Scene(root);
        scene.getStylesheets().add("my-style.css");

--Stylesheet files can be added by using Scene.getStylesheets () to get a list of stylesheet URLs and ʻadd () to it. --You can register multiple style sheets to be used in this scene. --URL is described in the format [scheme:] [// authority] [path] --If[scheme:]is omitted, only[path] is considered. --[path]` is treated as ** relative to the root of the classpath ** --For details, refer to Javadoc of Scene.getStylesheets ().

How to write CSS

my-style.css


.label {
    -fx-background-color: skyblue;
}

--The way to write CSS is basically the same as the CSS used in HTML. --Selector {property: value;} --However, property names almost always start with the prefix -fx.

class selector

--If you set .xxxx in HTML CSS, it matches the tag withclass = "xxxx"set. --On the other hand, for JavaFX CSS, .xxxx matchesstyleClass = "xxxx"

The default styleClass set on the node

--Some nodes (Label, ListView, etc.) have the styleClass attribute named according to certain conversion rules by default. --For example, the Label class defaults to label styleClass, and the ListView class defaults to list-view`` styleClass. --The conversion rule is simple, each word in the class name is separated by - and all are in lowercase. ――However, this conversion does not have any automatic conversion logic, it is just set in each class according to the above rules. --What that means is that you can see it by looking at the implementation of the Label class.

Label.java


...
public class Label extends Labeled {

    public Label() {
        initialize();
    }

...

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

--ʻInitialzie () is called in the constructor, and the string labelis passed to thestyleClass property (it does not mean that styleClass, which is automatically converted from the class name, is set). --So, the instance of the Label class has a styleClassoflabel by default, which is specified in the CSS file using a selector using . (. .label`) --Click here to see what default class is set for which node [https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/doc-files/cssref.html #nodes) --"Style class: empty by default" Where I write "style class: button"

Property entity

my-style.css


.my-class {
    -fx-background-color: pink;
    -fx-padding: 10px;
    
    -fx-border-color: black;
    -fx-border-width: 1px;
    -fx-border-style: solid;
    
    -fx-underline: true;
}

Execution result

javafx.jpg

--At first glance, properties specified by CSS (such as -fx-background-color and -fx-padding) appear to have -fx at the beginning of the properties that can be specified by HTML CSS. --So, if you're not sure which properties you can use, you can add -fx to the properties used in HTML CSS. --But there are actually some properties that don't exist in HTML CSS, like -fx-underline: true; --HTML CSS text-decoration: underline; --This actually refers to the properties of the target node --In the sample, CSS is specified for the Label class, but ʻunderline is the [underline property](https://docs.oracle.com) of the Labeledclass, which is the parent class of theLabelclass. /javase/jp/8/javafx/api/javafx/scene/control/Labeled.html#fontProperty) is supposed to be specified. --However, if the properties of the node and the properties specified by CSS always match exactly, that is not the case. --For example,border and backgroundare divided into small properties such as-fx-border-style and -fx-background-colorinstead of-fx-border and -fx-background`. Have been --For information on which properties can be specified for which node, see [Official Documents](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/doc-files/cssref] .html) explained in detail

Set any styleClass attribute

Set in Scene Builder

javafx.jpg

main.fxml


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.text.Font?>

<Label fx:id="label" alignment="CENTER" prefHeight="200.0" prefWidth="300.0"
       styleClass="my-class"
       text="CSS Test" xmlns="http://javafx.com/javafx/8.0.151" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.javafx.MainController">
   <font>
      <Font size="30.0" />
   </font>
</Label>

my-style.css


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

Execution result

javafx.jpg

--Scene Builder's Style Class allows you to set arbitrary styleClass attributes for elements

Set from the program

MainContoller.java


package sample.javafx;

import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;

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

public class MainController implements Initializable {

    @FXML
    private Label label;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        ObservableList<String> styleClass = label.getStyleClass();
        styleClass.add("my-class");
        System.out.println("styleClass=" + styleClass);
    }
}

--Get ʻObservableList of styleClasswithgetStyleClass ()from the node --Any attribute can be added to the list by ʻadd ()

Load a local stylesheet file

Folder structure


|-my-styles.css
`-src/main/java/
  `-sample/javafx/
    |-Main.java
    :

my-style.css


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

Main.java


package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add("file:./my-style.css"); //★ Specify a local file
        primaryStage.setScene(scene);

        primaryStage.show();
    }
}

Execution result

javafx.jpg

--Specify file: [path] to specify a local CSS file --If you are reading with Path or File, you can get the ʻURIobject with thetoURI (), toUri ()method and pass thetoString ()`.

Background image setting

How to set -fx-background-image

Folder structure


|-image.jpg
`-src/main/
  |-resources/
  | |-img/
  | | `-image.jpg
  | |-my-style.css
  | `-main.fxml
  :

Specify a file in the classpath

my-style.css


.my-class {
    -fx-background-image: url('/img/image.jpg');
}

--Specify with ʻurl --The format is the same as reading a CSS file, and ifschema` is omitted, the file will be under the classpath.

Specify a local file

my-style.css


.my-class {
    -fx-background-image: url('file:./image.jpg');
}

--When specifying a local file, use file: [path]

Event capture, event bubbling

javafx.jpg

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

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

public class MainController implements Initializable {
    @FXML
    private Pane bluePane;
    @FXML
    private Pane yellowPane;
    @FXML
    private Button button;
    @FXML
    private TextField textField;
    @FXML
    private Label label;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.registerHandlers(this.bluePane, "bluePane");
        this.registerHandlers(this.yellowPane, "yellowPane");
        this.registerHandlers(this.button, "button");
        this.registerHandlers(this.textField, "textField");
        this.registerHandlers(this.label, "label");
    }
    
    private void registerHandlers(Node node, String name) {
        this.registerEventFilter(node, name);
        this.registerEventHandler(node, name);
    }

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

    private void registerEventHandler(Node node, String name) {
        node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("handler " + name));
    }
}

Execution result

javafx.jpg

Click on the blue area


filter bluePane
handler bluePane

Click on the yellow area


filter bluePane
filter yellowPane
handler yellowPane
handler bluePane

Click the button


filter bluePane
filter yellowPane
filter button
handler button

Click on the text field


filter bluePane
filter yellowPane
filter textField
handler textField

Click on the label


filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane

Description

When an event occurs, processing is executed in the following order

  1. Target selection
  2. Determining the route [^ 2]
  3. Event capture phase
  4. Event bubbling phase

[^ 2]: In the official Japanese document, it is described as "root", but it is written as "route" because it seems to be mistaken for "root".

Target selection

--First, the node (target) where the event occurred is determined. --For example, if it is a click event, the clicked node will be targeted, and if it is a key event, the focused node will be targeted.

Route determination

javafx.jpg

――Next, follow the scene graph from the target toward the parent and determine the route to the root node. --The next event capture phase and event bubbling phase will be executed along this path.

Event capture phase

Click on the text field


filter bluePane
filter yellowPane
filter textField
...

--In the event capture phase, the event filter registered in each node is executed from the root to the target on the determined route.

MainController.java


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

--Event filters can be registered with nodes with ʻaddEventFilter (EventType, EventHandler) `

Event bubbling phase

Click on the label


...
handler label
handler yellowPane
handler bluePane

--In the event bubbling phase, conversely, the event handler registered in each node is executed from the target to the root node.

MainController.java


    private void registerEventHandler(Node node, String name) {
        node.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("handler " + name));
    }

--Event handlers can be registered with ʻaddEventHandler (EventType, EventHandler) --For event handlers that are often used such as mouse clicks, there is a dedicated setting method, so you can use that as well. --setOnMouseClicked (EventHandler) etc. --This event handler is also registered when event processing is registered with the ʻonMouseClicked attribute of FXML.

Consume the event

MainController.java


package sample.javafx;

...

public class MainController implements Initializable {
    ...

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.registerHandlers(this.bluePane, "bluePane");
        
        this.registerEventFilter(this.yellowPane, "yellowPane");
        this.yellowPane.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
            System.out.println("handler yellowPane");
            e.consume();
        });
        
        this.registerHandlers(this.button, "button");
        this.registerHandlers(this.textField, "textField");
        this.registerHandlers(this.label, "label");
    }
    
    ...
}

--The event handler in the area with a yellow background (yellowPane) is executing theconsume ()method of MouseEvent.

Execution result

Click on the yellow area


filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane

--You can break the chain of events by executing the ʻEvent.consume ()` method. --This is expressed as ** event consumption ** --If you consume an event during the event capture stage, event bubbling will no longer occur.

Some UI controls interrupt event bubbling

Click the button


filter bluePane
filter yellowPane
filter button
handler button

Click on the text field


filter bluePane
filter yellowPane
filter textField
handler textField

Click on the label


filter bluePane
filter yellowPane
filter label
handler label
handler yellowPane
handler bluePane

--Label is performing event bubbling normally, but Button and TextField are interrupted. --All event capture phases are running

For the time being, the official document says as follows.

The JavaFX UI control's default handler generally consumes most input events.

1 Event Processing (Release 8) | JavaFX: Event Processing

However, I'm not sure exactly where to check which class consumes the event.

Drag and Drop

drop

Basic

javafx.jpg

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;

public class MainController {
    
    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        String string = dragboard.getString();
        System.out.println("dropped string = " + string);
        e.consume();
    }
}

Execution result

javafx.gif

Description

Accepting drops

MainController.java


    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }

--Register a handler for the DRAG_OVER event for the drop destination node --So, by executing the DragEvent.acceptTransferModes () method, you can accept the drop. --In the argument, specify which transfer mode (COPY, MOVE, or LINK) is accepted by the constant of TransferMode. ―― ʻANYrepresents all three transfer modes (that is, anything is OK). --If you do not accept the drop, the mouse cursor will become an icon indicating that it cannot be dropped. --Finally, execute theconsume ()method to consume the event ――It works even if you don't consume it, but the official documentation always mentionsconsume (). --Maybe if the parent node also has a drag-and-drop process, it will not be executed by mistake. --This time, ʻacceptTransferModes () is always executed, but by controlling the call by the condition, you can control whether to allow the drop depending on what you are trying to drop.

Processing at the time of drop

MainController.java


    @FXML
    public void onDragDropped(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        String string = dragboard.getString();
        System.out.println("dropped string = " + string);
        e.consume();
    }

--Register a handler in the DRAG_DROPPED event of the target node for the process at the time of drop. --You can access the drop contents from Dragboard obtained byDragEvent.getDragboard ().

File drop

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;

import java.io.File;
import java.util.List;

public class MainController {
    
    @FXML
    public void onDragOver(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        
        if (dragboard.hasFiles()) {
            e.acceptTransferModes(TransferMode.ANY);
        }

        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();

        List<File> files = dragboard.getFiles();
        files.forEach(file -> System.out.println(file.getName()));

        e.consume();
    }
}

Execution result

javafx.gif

Description

MainController.java


    @FXML
    public void onDragOver(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        
        if (dragboard.hasFiles()) {
            e.acceptTransferModes(TransferMode.ANY);
        }

        e.consume();
    }

--You can get it with DragEvent.getDragboard () and check if the one being dragged with Dragboard.hasFiles () is a file. --Here, ʻacceptTransferModes ()is executed only whenhasFiles ()istrue`, so you can only drop files.

MainController.java


    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();

        List<File> files = dragboard.getFiles();
        files.forEach(file -> System.out.println(file.getName()));

        e.consume();
    }

--You can get the dragged and dropped files with Dragboard.getFiles ()

HTML drop

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;

public class MainController {
    
    @FXML
    public void onDragOver(DragEvent e) {
        Dragboard dragboard = e.getDragboard();
        
        if (dragboard.hasHtml()) {
            e.acceptTransferModes(TransferMode.ANY);
        }

        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();

        System.out.println("[html]   = " + dragboard.getHtml());
        System.out.println("[string] = " + dragboard.getString());
        System.out.println("[files]  = " + dragboard.getFiles());

        e.consume();
    }
}

Execution result

javafx.gif

Description

MainController.java


    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();

        System.out.println("[html]   = " + dragboard.getHtml());
        System.out.println("[string] = " + dragboard.getString());
        System.out.println("[files]  = " + dragboard.getFiles());

        e.consume();
    }

--If you drop the text of the browser, you can get the HTML that reproduces the text with getHtml (). --You can get even plain text with getString ()

Change the style when dropping

my-style.css


.drag-over {
    -fx-text-fill: red;
}

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;

public class MainController {
    
    @FXML
    private Label dropLabel;
    
    @FXML
    public void onDragEntered() {
        this.dropLabel.getStyleClass().add("drag-over");
    }
    
    @FXML
    public void onDragExited() {
        this.dropLabel.getStyleClass().remove("drag-over");
    }
    
    ...
}

Execution result

javafx.gif

Description

--Register the event handlers for DRAG_ENTERED and DRAG_EXITED on the node to be dropped. --Implement to add a style to the node with DRAG_ENTERED and remove the style with DRAG_EXITED ――By doing this, you can express the waiting for drop more clearly.

drag

javafx.jpg

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;

public class MainController {
    
    @FXML
    private Label dragLabel;
    
    @FXML
    public void onDragDetected(MouseEvent e) {
        Dragboard dragboard = this.dragLabel.startDragAndDrop(TransferMode.ANY);
        
        ClipboardContent content = new ClipboardContent();
        content.putString("Drag Test");
        dragboard.setContent(content);

        e.consume();
    }
    
    @FXML
    public void onDragOver(DragEvent e) {
        e.acceptTransferModes(TransferMode.ANY);
        e.consume();
    }
    
    @FXML
    public void onDragDropped(DragEvent e) {
        System.out.println("onDragDropped()");
        Dragboard dragboard = e.getDragboard();
        System.out.println(dragboard.getString());
        
        e.consume();
    }
}

Execution result

javafx.gif

Description

MainController.java


    @FXML
    private Label dragLabel;
    
    @FXML
    public void onDragDetected(MouseEvent e) {
        Dragboard dragboard = this.dragLabel.startDragAndDrop(TransferMode.ANY);
        
        ClipboardContent content = new ClipboardContent();
        content.putString("Drag Test");
        dragboard.setContent(content);

        e.consume();
    }

--Drag start by registering a handler in the DRAG_DETECTED event --Execute the startDragAndDrop () method of the node to start dragging --In the argument, specify the transfer mode supported by dragging with TransferMode. --On the drop side, the mouse cursor becomes a droppable icon only when the transfer mode that matches the transfer mode set here is specified by ʻacceptTransferModes (). --Set the drag content to ClipboardContent and set that ClipboardContenttoDragboard. --In addition to putString (), there are putHtml ()andputFiles ()etc. --SinceClipboardContent itself is a map that inherits HashMap <DataFormat, Object> `, you can save any value.

reference

-JavaFX (SB) Drop Memo (Hishidama's JavaFX2 Scene Builder drop Memo) -7 Drag and drop operation (Release 8)

dialog

Preparation

javafx.jpg

package sample.javafx;

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

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

Make sure the showDialog () method is called when you click the button.

Basic

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

Execution result

javafx.jpg

--A dialog can be displayed by using the ʻAlert class --Specify the dialog type in the constructor --Type specifies a constant of ʻAlertType enumeration --CONFIRMATION is a confirmation dialog --Show dialog with showAndWait () method --When displaying a dialog with this method, as in ʻAndWait`, processing is blocked until the dialog is closed. --Dialog is modal and does not touch the parent window while it is displayed

Confirm the selected button

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

Execution result

Click the "OK" and "Cancel" buttons respectively.

ButtonType [text=OK, buttonData=OK_DONE]
ButtonType [text=cancel, buttonData=CANCEL_CLOSE]

--The return value of showAndWait () is ʻOptional of ButtonType, so you can see which button was selected when closing. --Return of showAndWait () when a dialog (such as a dialog with only a" yes "button) that has only a cancel button [^ 1] ** is closed by ʻESC or the window close button. The value will be ʻOptional.empty () --Strictly speaking, there are cases whereresultConverter` is set, but that area is [Javadoc](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/scene/control/ See Dialog.html)

[^ 1]: ButtonData is NO or CANCEL_CLOSE

Set a message

Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Messeji");
alert.showAndWait();

javafx.jpg

--You can specify the message to be set in the dialog with the second argument of the constructor argument.

Dialog type

INFORMATION

javafx.jpg

CONFIRMATION

javafx.jpg

WARNING

javafx.jpg

ERROR

javafx.jpg

Specify the button

Alert alert = new Alert(
        Alert.AlertType.INFORMATION, 
        "Messeji",
        ButtonType.APPLY,
        ButtonType.CANCEL,
        ButtonType.CLOSE,
        ButtonType.FINISH,
        ButtonType.NEXT,
        ButtonType.NO,
        ButtonType.OK,
        ButtonType.PREVIOUS,
        ButtonType.YES);
alert.showAndWait();

javafx.jpg

--ButtonType can be specified after the third argument of the constructor --Frequently used buttons are defined as constants in ButtonType, so if that's all you need, use it. --You can also make your own (described later) --Variadic arguments allow multiple ButtonTypes to be specified

Button placement order

--The order of variadic arguments does not become ** the order of button placement ** --A class called ButtonBar is used for button placement. --ButtonBar is a class for button layout, and can reproduce the button layout unique to each OS. --The order of button arrangement for each OS is ButtonBar Javadoc. Is posted --The button order ** is determined for each type of button, and the button order determines where the button is placed in ButtonBar. --For example, "Yes" and "No" buttons are arranged in the order of "Yes" → "No" on Windows, but in the order of "No" → "Yes" on Mac and Linux. --As a class that expresses this button type, ButtonData enumeration type Prepared --This ButtonData is also set in the constant of the ButtonType class specified in the constructor of ʻAlert`, and the buttons are arranged according to that value.

** ButtonData ** set to ButtonType constant

ButtonType ButtonData that is set
APPLY APPLY
CANCEL CANCEL_CLOSE
CLOSE CANCEL_CLOSE
FINISH FINISH
NEXT NEXT_FORWARD
NO NO
OK OK_DONE
PREVIOUS BACK_PREVIOUS
YES YES

--The detailed placement order of ButtonData for each OS is in Javadoc. Are listed

** Arrangement order of ButtonData for each OS **

OS Placement order
Windows L_E+U+FBXI_YNOCAH_R
Mac OS L_HE+U+FBIX_NCYOA_R
Linux L_HE+UNYACBXIO_R

--This mysterious string corresponds to the ** button sequence code **, where each character is assigned to the ButtonData constant. --For example, the button sequence code for ButtonData.LEFT is L, ButtonData.HELP_2 is ʻE, and so on. --The button sequence code assigned to each constant can be found in the Javadoc for each constant. --To make it a little easier to see, the table below shows the relationship between the ButtonData` and the constants.

javafx.jpg

-(F is not well understood because there is no corresponding constant) --That is, if you specify ButtonType.CANCEL in the constructor of ʻAlert, Since ButtonData in ButtonType.CANCELisCANCEL_CLOSE, the cancel button will be inserted in the part where Cis in the order ofButtonBar`.

Place any button

ButtonType myButton = new ButtonType("Button");
Alert alert = new Alert(Alert.AlertType.INFORMATION, "Messeji", myButton);
alert.showAndWait().ifPresent(System.out::println);

Execution result

javafx.jpg

Console output when clicking the "Botan" button


ButtonType [text=Button, buttonData=OTHER]

--You can place any button by creating a ButtonType class and passing it to the ʻAlertconstructor. --The button label string can be specified in theButtonTypeconstructor. --If nothing is specified,ButtonData becomes ʻOTHER --ButtonType also has a constructor that receives ButtonData, so you can specify it explicitly.

Judgment when closed by other than the button

--The dialog can also be closed with ʻESC, ʻAlt + F4, the window close button, etc. (for Windows) --In that case, the return value of showAndWait () behaves a little complicated depending on the placed button.

Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Messeji", ButtonType.YES);
Optional<ButtonType> buttonType = alert.showAndWait();
String result = buttonType.map(ButtonType::getText).orElse("No selection");
System.out.println(result);

Execution result

javafx.jpg

Result when closed with ʻESC`

No selection

--Since there is no cancel button [^ 1] in the buttons, the return value will be ʻOptional.empty () `.


--Next, if there are multiple cancel buttons

Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Messeji", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
Optional<ButtonType> buttonType = alert.showAndWait();
String result = buttonType.map(ButtonType::getText).orElse("No selection");
System.out.println(result);

Execution result

javafx.jpg

1 cup" 2. "No" 3. "Cancel" 4. ESC 5. Alt + F4 6. Close the window

Try closing the dialogs in the order of.

Yes
No
cancel
cancel
cancel
cancel

--When each button is pressed, that button is the return value of showAndWait () --If the dialog is closed by a method other than the button, it means that the "Cancel" button has been pressed. --This is when there are multiple cancel buttons.

  1. ButtonData is CANCEL_CLOSE ButtonType
  2. ButtonData.isCancelButton () returns true ButtonType This is because the return value is determined in the order of priority of --. --In other words, if the only cancel button is ButtonType.NO, the return value when the dialog is closed by a method other than the button will be ButtonType.NO.

To summarize the return value of showAndWait () when the dialog is closed by a method other than a button,

  1. If there is no cancel button [^ 1], ʻOptional.empty ()`
  2. If there is one cancel button, that ButtonType
  3. If there are two or more cancel buttons, the return value ButtonType is determined in the following priority order.
  4. ButtonType where ButtonData is CANCEL_CLOSE
  5. ButtonData.isCancelButton () returns true ButtonType

Various text settings

Only contentText can be set in the constructor argument, but the title etc. can be changed after ʻAlert` is generated.

Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle("Take it");
alert.setHeaderText("Header text");
alert.setContentText("Content");
alert.showAndWait();

javafx.jpg

Each can also be turned off by setting null.

Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(null);
alert.setHeaderText(null);
alert.setContentText("Content");
alert.showAndWait();

javafx.jpg

thread

The basic story of using threads in JavaFX

UI thread

JavaFX GUI processing is executed on a dedicated thread (** UI thread **) called JavaFX Application Thread.

MainController.java


package sample.javafx;

import javafx.fxml.Initializable;

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

public class MainController implements Initializable {

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        System.out.println("thread = " + Thread.currentThread().getName());
    }
}

Execution result


thread = JavaFX Application Thread

If implemented normally, the process will be executed on this UI thread.

However, if time-consuming processing is executed on this UI thread, event processing such as UI update and mouse operation will be waited for. Then, the application cannot accept any user's operation, and the appearance does not change, that is, a so-called solid state.

To avoid this, the time-consuming process must be executed in a thread separate from the UI thread.

Touch the UI from a non-UI thread

JavaFX UI components can only be accessed from UI threads. Exceptions are now thrown when trying to access a UI component from outside the UI thread.

MainController.java


package sample.javafx;

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

public class MainController {

    @FXML
    private Button button;
    
    @FXML
    public void click() {
        new Thread(() -> {
            System.out.println("thread = " + Thread.currentThread().getName());
            button.setText("hoge");
        }).start();
    }
}

Execution result


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

Such restrictions often exist not only in JavaFX, but also in other GUI-related frameworks such as Swing.

The GUI seems to be very difficult to achieve thread safety when running multithreaded. Therefore, most GUI frameworks have a dedicated thread for UI operations, and the UI can only be accessed from that dedicated thread (searching for "GUI, single thread" reveals a lot of stories about that. come).

In JavaFX, as a means to operate UI components from other than UI threads, Platform.runLater () A method called .html # runLater-java.lang.Runnable-) is provided.

MainController.java


package sample.javafx;

import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Button;

public class MainController {

    @FXML
    private Button button;
    
    @FXML
    public void click() {
        new Thread(() -> {
            System.out.println("thread = " + Thread.currentThread().getName());
            
            Platform.runLater(() -> {
                System.out.println("runLater thread = " + Thread.currentThread().getName());
                button.setText("hoge");
            });
        }).start();
    }
}

Execution result


thread = Thread-4
runLater thread = JavaFX Application Thread

The lambda expression (type Runnable) passed torunLater ()will be executed on the UI thread ( JavaFx Application Thread). Therefore, you can safely touch the UI thread from the process passed to this runLater ().

How to handle threads easily in JavaFX

JavaFX provides a mechanism to make it easier to implement threaded processing.

Worker The thread processing used in JavaFX is abstracted as Worker.

Worker defines a convenient API not found in regular Thread. (This is an interface in itself, and the specific implementation is Task or Service (It is realized by: //docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Service.html)

Worker life cycle

Workers have a life cycle, and the state transitions as follows.

javafx.png

READY

--When Worker is created, it first goes into the READY state. -The only state called ** start state ** is this READY state.

SCHEDULED

--Then, when the start of Worker is scheduled, it will be in the SCHEDULED state. --If you are using a thread pool, you may wait in the SCHEDULED state to wait for the pool to become free. --Depending on the implementation of Worker, it may be started immediately without being scheduled. ――However, even in that case, it will always be in the RUNNING state after going through SCHEDULED. --The running property that represents running becomes true

RUNNING

--The state where Worker is actually running --The running property that represents running becomes true

SUCCEEDED

--This state occurs when the process ends normally. --The result is set to the value property

FAILED

--This state occurs when an exception is thrown during execution --Thrown exceptions are set to the ʻexception` property

CANCELLED

--If cancellation is executed before the end state, this state will be reached.

Worker has [stateProperty ()](https://docs.oracle.com/javase/jp/8/javafx/api/javafx/concurrent/Worker.html#" to get information about these states. Properties such as stateProperty--) and runningProperty () are available. ..

Task A class called Task is prepared as a class that implements Worker.

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

Execution result


task.call()
thread = Thread-4

--The Task class itself is an abstract class, so inherit it and create your own class. --The call () method is an abstract method --Implement the call () method in your own class and describe the process that runs in the background. --The Task class implements the Runnable interface, so you can pass it to Thread or ʻExecutor` to execute it.

Cancel

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 ()starts the task, and thestop ()method registered in the button's event handler calls thecancel () method of Worker`.

Execution result


start...
cancelled!!

--You can cancel the execution of Worker by calling thecancel ()method of Worker. --Cancellation is possible only when the state of Worker is other than" finished state ". --Nothing happens when you call cancel () in the "finished state" --When cancel is executed, isCancelled () of Task will return true. --You can use this to determine if it was canceled in the call () method. --If canceled during blocking by Thread.sleep () etc., catch ʻInterruptedException and enter ʻisCancelled () judgment. --For details, refer to Javadoc of the Task class.

progress

javafx.jpg

package sample.javafx;

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

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

public class MainController implements Initializable {
    @FXML
    private ProgressBar progressBar;

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

            @Override
            protected Void call() throws Exception {
                final long max = 100000000L;
                
                for (long i=0; i<=max; i++) {
                    this.updateProgress(i, max);
                }
                
                return null;
            }
        };
        
        this.progressBar.progressProperty().bind(task.progressProperty());
        new Thread(task).start();
    }
}

Execution result

javafx.gif

Description

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

--Worker exposes a property calledprogressProperty () --By binding this to progressProperty () of ProgressBar, you can make progress of Task and progress of ProgressBar work together.

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

--Use the ʻupdateProgress ()method to update the progress ofTask --Pass the number of processed items in the first argument and the total number in the second argument (the method that passesdoubleis also overloaded) --This ʻupdateProgress ()is safe to run from a non-UI thread --In which runLater () is used

Publish arbitrary values

javafx.jpg

package sample.javafx;

import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

import java.io.IOException;

public class MainController {

    @FXML
    private Label label;

    @FXML
    public void start() throws IOException {
        MyTask task = new MyTask();
        this.label.textProperty().bind(task.valueProperty());
        
        new Thread(task).start();
    }
    
    private static class MyTask extends Task<String> {

        @Override
        protected String call() throws Exception {
            for (int i=0; i<100000000; i++) {
                this.updateValue("i=" + i);
            }
            
            return "finished!!";
        }
    }
}

Execution result

javafx.gif

Description

    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 has a property called value --The type of this value is specified by the type argument of Task. --The value of value can be updated with the ʻupdateValue ()method. --This is also safe to run from non-UI threads --If the execution ofTask is successful, the return value of the call () method is finally set to the value` property.

Event listener

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

Execution result

scheduled thread=JavaFX Application Thread
running thread=JavaFX Application Thread
succeeded thread=JavaFX Application Thread

--You can register a listener that can monitor the state change with the setOn **** method. --The registered listener is executed in the UI thread

Exception property

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

Execution result


exception=java.lang.RuntimeException: test

--When an exception is thrown in the call () method, the thrown exception is set in the ʻexception` property. ――The exception doesn't propagate to the top of the stack, so if you leave it alone, you won't notice that the exception was thrown.

Publish 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;
        }
    }
}

--If you want Task to have ʻObservableList and expose its state to the outside world, you have to be careful to use Platform.runLater () `to update the list.

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

Assign the start () method to the button's action event and execute it several times.

Execution result


start
Service.createTask()
Task.call()

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

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

--Task is supposed to be disposable, and it is not supposed to be used to reuse the same instance again. --On the other hand, Service is designed on the assumption that thread processing will be executed many times. --Service wraps Task so that it creates a new instance of Task and starts a thread each time it executes a thread process. --Service is an abstract class, and you can create and use your own class that inherits this class. --There is createTask () as an abstract method, so you need to implement it --In this method, implement the process to create a new instance of Task. --When you execute the start () method of Service, the thread is started and the Task generated by the createTask () method is executed.

Re-execute

When checking the status and re-executing


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

--Since Service implements the Worker interface, the state of Service changes according to the rules defined by Worker. --That is, it starts with READY and ends up with SUCCEEDED, FAILED, CANCELLED. --Execute start () of Service in a state other than READY will throw ʻIllegalStateException. --There are several ways to return the state to READY --Execute thereset () method in the finished state (SUCCEEDED, FAILED, CANCELLED). --For SCHEDULED, RUNNING, cancel with cancel ()and then execute thereset ()method. ――If you don't want to look back at each state, just execute therestart ()method. --If the status isREADY, start the process as it is --If it is running, cancel the process and then re-execute. --If it is finished, return it to READY` and then re-execute.

restart()When rewritten with


    @FXML
    public void start() throws IOException {
        System.out.println("restart");
        this.service.restart();
    }

--The start (), reset (), and restart () methods must all be executed from the UI thread.

Specify the thread to use

First, check the default behavior.

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

Execute restart () several times

Execution result


thread.name = Thread-4, daemon = true
thread.name = Thread-5, daemon = true
thread.name = Thread-6, daemon = true
thread.name = Thread-7, daemon = true

--By default, Service will create a new daemon thread each time you start Task. --If you want to specify the thread to use (for example, use the thread of the thread pool), implement as follows

Specify the thread used by the Service


package sample.javafx;

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

import java.io.IOException;
import java.util.concurrent.Executors;

public class MainController {

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

Execution result


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

--Specify Executor in the ʻexecutor property of Service. --Then Service will get the thread to execute Task from ʻexecutor.

Various properties

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 implements Worker, so it provides properties such as progress like Task. --Each property is linked to the property of Task that Service is executing. --In other words, referencing the properties of Service is the same as referencing the properties of the running Task.

reference

-1 Concurrency in JavaFX (Release 8)

Visual effects

How to apply visual effects such as lighting and drop shadows.

As a mechanism, it seems that the scene graph is once rendered as a pixel image, and image processing is performed to cast shadows or shine light on it.

Try applying various visual effects to the following scene graph and see what happens.

javafx.jpg

Bloom effect

Execution result

javafx.jpg

Implementation

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 (image looks shining) when using class ) Effect can be applied --By creating an instance of Bloom and setting it withsetEffect ()to the node on the scene graph to which the effect is applied, the visual effect will be applied below that node.

Blur effect

Box blur

Execution result

javafx.jpg

Implementation

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 can be used to apply the box blur effect. --You can specify the blur distance in the vertical and horizontal directions with width and height. --By specifying ʻiterations, you can specify the number of times the blur effect is applied (default is 3`).

Motion blur

Execution result

javafx.jpg

Implementation

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

-When using MotionBlur, motion blur (like when an object is moving) Blur) can be applied --You can specify the direction of blur with ʻangle --Specify by the degree method (the one whose angle is expressed by0.0 to 360.0) ――The horizontal direction is 0.0, and it feels like it rotates clockwise from there. - :0.0 -:45.0 -:90.0 -:135.0 -:180.0 --Specify the blur radius withradius`

Gaussian blur

Execution result

javafx.jpg

Implementation

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 can be used to apply Gaussian blur (difference from box blur). I'm not sure) --You can specify the radius of the blur with radius

Drop shadow effect

Execution result

javafx.jpg

Implementation

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

-Using DropShadow has a drop shadow effect. Can be applied --Specify the relative position (offset) of the shadow with ʻoffsetX, ʻoffsetY --Use width and height to specify the degree of blurring of shadows in the vertical and horizontal directions. --Specify the shadow color with color --Use spread to specify the degree of shadow diffusion ( 0.0 to 1.0).

Inner shadow effect

Make some changes to the scenegraph to make it easier to see.

javafx.jpg

Execution result

javafx.jpg

Implementation

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

-If you use InnerShadow, you can use the inner shadow (shadow that looks like a hollow inside). ) Effect can be applied --Specify the relative position (offset) of the shadow with ʻoffsetX, ʻoffsetY --Specify the length of the shadow in the vertical and horizontal directions with width and height. --Specify the shadow color with color --Specify the shadow blur radius with radius

reflection

Execution result

javafx.jpg

Implementation

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

-If you use Reflection, it seems that the object is reflected on the floor. ) Effect can be applied --With topOffset, specify the relative position (offset) of the upper part of the reflected part. --With topOpacity, specify the transparency of the upper part of the reflected part. --Use fraction to specify the percentage of the part to be reflected ( 0.0 to 1.0, the default is 0.75, that is, only 75% is reflected) --With bottomOpacity, specify the transparency at the bottom of the reflected part.

Combine visual effects

Execution result

javafx.jpg

Implementation

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

--Multiple ʻEffects can be combined by specifying another ʻEffect withsetInput (). --In the above implementation, the drop shadow is applied first, then the reflection. --In other words, the one passed by setInput () is applied first.

reference

-Visual Effects | 2 Understanding JavaFX Architecture (Release 8) -5 Applying Effects (Release 8)

media

How media files work

Codec

――If the media data is left as it is, even a few minutes of content will have a large capacity of several hundred MB or several GB. ――Therefore, it is usually compressed in some way --This compression is called ** encoding ** --Encoded media data needs to be restored (decompressed) to its original state when played. --This decompression is called ** decoding ** --A program that encodes and decodes media data is collectively called a ** codec **. ――It seems that some things can only be encoded (decoded). --In order to play media data, there must be a codec that can decode the media data. --There are many types of encoding formats --For audio data, MP3, AAC, WMA, etc ... --For video data, H.264, VP9, DivX, Xvid, etc ..

container

--In the media file, multiple media data are stored in a combined state. --For example, in the case of a simple video file, video data and audio data are stored in one file. --The file that contains the media data is called a ** container **. --There are multiple types of containers --For audio files, WAV, AIFF, MP3, etc ... --MP3 has MP3 as an encoding format and MP3 as a container that can store it (confusing) --For video files, MP4, FLV, AVI, MOV, etc ... --The container can store media data in various encoding formats. --There is not always one type of encoding format that can be stored in one container. --For example, in the case of a container called MP4 --H.264 or H.265 encoded data can be included in the video data. --AAC or MP3 encoded data can be included in the audio data. --In other words, even files in the same container may not be playable depending on the encoding format of the media data contained in them (cannot be played without the corresponding codec). --The encoding format that can be stored differs depending on the container. --The actual encoding format of the media data contained in the container can be confirmed by using a tool such as MediaInfo. --The container type and file extension often match ( .mp4 for MP4, .flv, .f4v, etc ... for FLV).

Encoding formats and containers supported by JavaFX

--The media encoding formats and containers supported by JavaFX are Javadoc. ) It is described in

Encode format

Encode format voice/Video
PCM voice
MP3 voice
AAC voice
H.264/AVC Video
VP6 Video

PCM

--Abbreviation for pulse code modulation --Uncompressed data ――Is it inappropriate to call it an encoded format?

** MP3 (encode format) **

--MPEG-1 Abbreviation for Audio Layer-3 --Encoding format for audio used in MPEG-1 of video container --High compression rate and very popular

AAC

--Abbreviation for Advanced Audio Coding --Encoding format for audio created as a successor to MP3 --Higher compression ratio than MP3 --Widely used as audio data for MP4 containers

H.264/AVC

--Encoding format for video that is often used for video data in MP4 containers --There are two names, H.264 and MPEG-4 AVC, which are often referred to as "H.264 / MPEG-4 AVC". --JavaFX's Javadoc says "H.264 / AVC" --Very high compression ratio, widely used --As a successor, there is H.265 with a higher compression ratio.

VP6

--Encoding format for video used in FLV container (Flash movie) --It seems that the compression rate is better than H.264 --As a successor, there is VP9 with a higher compression ratio (it seems to be adopted by Youtube) --However, it seems that Java 9 or later is not recommended

Container format

container Encode format(Video) Encode format(voice)
AIFF - PCM
WAV - PCM
MP3 - MP3
FLV VP6 MP3
MP4 H.264/AVC AAC

AIFF

--Abbreviation for Audio Interchange File Format --Container format for MAC original audio files

WAV

--Abbreviation for Waveform Audio Format --Container format for Windows original audio files

** MP3 (container) **

--Container format for audio files containing MP3 (codec) audio data -You can enter meta information such as tags, lyrics, and images.

FLV

--Container format for video files intended for playback on Flash players --FLV can use H.264 as the video codec or ACC as the audio codec depending on the version, but JavaFX only supports the combination of VP6-MP3.

MP4

--Container format for video files --A standard designed in consideration of delivering video in a low-speed line environment such as a mobile phone. --As of 2017, it is very popular

It seems that the widely used encoding formats (MP3, ACC, H.264) are at least supported.

Play media files in unsupported encoding containers

--How to play media files in formats that are not supported by default ――It seems that you can do it if there is a way to add a codec separately, but Is it impossible? ――I read it as if I could play it using WebView, but I couldn't even try it. ――In that case, I feel that I have to convert the format of the media file to the format supported by JavaFX. --There are various free tools that can convert the encoding, both audio and video, so if you convert it to a format supported by JavaFX, you will be able to play it. ――If you search for "audio / video encoding / conversion", you will get various hits.

reference

-Differences from video format types (AVI, MP4, MOV, MPEG, MKV, WMV, FLV, ASF, etc.) [Container] -Differences from voice codec types (MP3, AAC, WMA, WAV, Vorbis, AC3, FLAC, etc.) [Format] -Main formats and features of audio files | Codec / MP3 / AAC / Ogg -Main formats and features of video files | Codec / AVI / MPEG -[Container format-Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%95 % E3% 82% A9% E3% 83% BC% E3% 83% 9E% E3% 83% 83% E3% 83% 88 #% E9% 9F% B3% E5% A3% B0% E3% 82% B3% E3% 83% B3% E3% 83% 86% E3% 83% 8A)

Play media file

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 files can be played using the Media, MediaPlayer classes, regardless of audio / video. --The Media class represents the media to play --MediaPlayer provides API for manipulating media --If you want to display the video, use MediaView (details will be described later). --You can play media files with the play () method of MediaPlayer

MediaPlayer state transition

javafx.png

Status Description
UNKNOWN Immediately after creation and reading is not completed
READY Ready to load and play
PLAYING Playing state
PAUSED Paused state
PLAYINGWhen you return to, it will resume from where it was stopped.
STOPPED Stopped state
PLAYINGPlay from the beginning when returning to
STALLED Streaming playback does not have enough information to keep the player playing
HALTED A state in which the player cannot play due to an error
DIPOSED Players are destroyed and resources are released

--MediaPlayer makes a state transition as shown above. -- DISPOSED and HALTED are omitted because they are in a special state. --Media files are read asynchronously and may not have been read immediately after the Media instance was created. --When the state of MediaPlayer with which the Media instance is associated is READY, the meta information loading is definitely complete. --In the state of ʻUNKNOWN, playback etc. cannot be performed yet, but instructions such as the play () method are buffered when called in the state of ʻUNKNOWN and will be executed when READY is reached. (You don't have to explicitly wait for READY) --For details, refer to Javadoc of MediaPlayer.Status.

Play resource file

Folder structure


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

--You can play the resource file by getting the resource file with ʻURLand doingtoString ()`.

Play repeatedly

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) can be used to specify the number of times media files should be played continuously. --If you specify MediaPlayer.INDEFINITE, it will be played continuously indefinitely.

Playback time

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

Execution result


cycleDuration=UNKNOWN
totalDuration=UNKNOWN
[onReady] cycleDuration=9541.0 ms
[onReady] totalDuration=28623.0 ms

--You can get the playback time with cycleDuration and totalDuration --cycleDuration represents the playback time for one time --totalDuration represents the total playback time including repetition --If the number of views is ʻINDEFINITE, Duration.INDEFINITE --However, if you get it before it becomesREADY, it will become ʻUNKNOWN. --Playback time is an instance of a class called Duration

Duration class

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

Execution result


duration=90000.0 ms
duration.seconds=90.0
duration.minutes=1.5

oneMinute=60000.0 ms
oneMinute.minutes=1.0

-Duration is a class that represents the period --A period is a period of time such as minutes or seconds (not ** that represents a specific point in time **). --The period is held with milliseconds as the basic unit. --Specify milliseconds when generating with the constructor --If you use the factory method, you can create an instance by specifying hours, minutes, seconds, etc. --This class is immutable, add () Methods such as multiply () will give the calculation result to another new instance. It is designed to be stored and returned in

String format

--If you want to format the Duration information into a string (such as 02: 00: 12), there is nothing in the standard API that can be formatted directly. --I thought there was a good formatter for Duration of Date and Time API. Not likely ... --There is no help for it, so if you try to write the Duration version of JavaFX with reference to the implementation example of StackOverflow in ↑, it looks like ↓

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

Execution result


text=03:24:54.027

Specify the range to play

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

Execution result


cycleDuration=2000.0 ms
totalDuration=6000.0 ms

--If you set the start time and end time for startTime and stopTime, respectively, the media data will be played only within that range. --cycleDuration returns the time between startTime and stopTime --totalDuration is cycleDuration * views

Metadata

Various metadata such as singer, album name, lyrics, and album art can be added to MP3 files. You can access those metadata from JavaFX (FLV metadata seems to be available, but omitted here).

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

Execution result


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=A story that you do not know

--You can get ʻObservableMap that holds the metadata with getMetadata () of Media`. --Empty map is returned if there is no metadata or if the container format does not support getting metadata --The list of supported metadata tags is Javadoc Please refer to that because it is described in. --Unfortunately AAC does not support retrieving metadata --I also checked Java9, but AAC is still supported. Not applicable

Change the playback position

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

--Use the seek (Duration seekTime) method to change the playback position --The seekTime specified in the argument specifies the period from the beginning of the media data. --Even if you specify startTime, it is not the period from startTime (probably) ――It looks like ↓ in the figure

javafx.jpg

--If you specify startTime, stopTime and specify seekTime that goes out of that range, you are restricted to staying in either startTime or stopTime. --For details, refer to Javadoc.

Display video

javafx.jpg

package sample.javafx;

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

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

public class MainController implements Initializable {
    @FXML
    private MediaView mediaView;

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

--Use MediaView to display the video --Pass the loaded MediaPlayer tosetMediaPlayer ()of MediaView --Width and height can be adjusted with the fitWidth and fitHeight properties

AudioClip For small sound effects with a short playback time, it is more efficient to use ʻAudioClip than Media`.

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

--Instance generation passes URL string as constructor argument like Media --Playback can be started with the play () method

The difference from Media is as follows.

--Playback with minimum delay (playback immediately) --You can start using it immediately after instantiation --Once played, methods other than stop () have no effect after that. --The same instance can be played multiple times at the same time with play () --If you call stop () while playing multiple sounds at the same time, all the sounds being played will stop.

See Javadoc for details.

Try to create a simple music player

Let's create a simple music player while utilizing the contents so far.

javafx.jpg

package sample.javafx;

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

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

public class SimpleMusicPlayerController implements Initializable {

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

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

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

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

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

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

        //Volume label
        this.volumeLabel.textProperty().bind(this.player.volumeProperty().asString("%.2f"));
        //Total play time label
        this.player.setOnReady(() -> {
            Duration total = this.player.getTotalDuration();
            this.totalTimeLabel.setText(this.format(total));
        });
        //Current play time label
        this.currentTimeLabel.textProperty().bind(new StringBinding() {
            {bind(player.currentTimeProperty());}
            
            @Override
            protected String computeValue() {
                Duration currentTime = player.getCurrentTime();
                return format(currentTime);
            }
        });
        
        //Play button
        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 button
        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 button
        this.pauseButton.disableProperty().bind(new BooleanBinding() {
            {bind(player.statusProperty());}

            @Override
            protected boolean computeValue() {
                boolean pausable = playerStatusIsAnyOf(Status.PLAYING, Status.STALLED);
                return !pausable;
            }
        });
        
        //Volume slider
        this.player.volumeProperty().bind(this.volumeSlider.valueProperty());
        this.volumeSlider.setValue(1);
        
        //Seek bar
        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);
            }
        });

        //Post-processing when playback ends
        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);
    }
}

Display of current playback time

        //Current play time label
        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);
    }

--Bind to the currentTime property of MediaPlayer and set the time-formatted value to the label

Button control

        //Play button
        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 button
        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 button
        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));
    }

--Use status of MediaPlayer to control disabled of the button --Determine the state in which each button can be clicked, and set its negation to the disabled property.

Volume slider

javafx.jpg

        //Volume label
        this.volumeLabel.textProperty().bind(this.player.volumeProperty().asString("%.2f"));

        ...

        //Volume slider
        this.player.volumeProperty().bind(this.volumeSlider.valueProperty());
        this.volumeSlider.setValue(1);

--Volume can be adjusted with the volume property --By binding to the value property of Slider, slide adjustment and volume adjustment can be linked. --However, volume must be specified as a value between 0.0 and 1.0. --Therefore, adjust the minimum value (Min), maximum value (Max), and increment / decrement amount (Block Increment) of Slider according to the range of volume. --Volume labels should work with the volume property of MediaPlayer

Seek bar

--Seekbar implementation is a bit complicated --There is no property to write the playback position, so you have to register a listener and call MediaPlayer.seek () --In addition, the states must be linked in both directions as follows.

  1. Adjust the slider position according to the playback time
  2. Adjust the playback time when the user operates the slider

javafx.jpg

--Slider can be specified between 0.0 and 1.0, and it is treated as a ratio to the total time.

Playback time → seek bar

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

--Register listener in currentTime --Calculate the ratio of the current time to the total time and set it to the slider value --The note here is that you have to change the value only when the slider is not ** operated by the user **. --Otherwise, even if the user tries to move the slider, it will be forced back to the playback time. --Use the following method in Slider to determine if the user is touching the slider: - isValueChanging() --Returns true if the slider is being slid - isPressed() --Returns true if the slider is clicked

Seek bar → Playback time

        this.seekSlider.valueProperty().addListener((value, oldValue, newValue) -> {
            if (seekSliderIsChanging()) {
                Duration seekTime = this.player.getTotalDuration().multiply((Double) newValue);
                this.player.seek(seekTime);
            }
        });

--Register a listener in the value property of the slider --Multiply the new value of the slider (0.0 to 1.0) by the total playback time to find the destination time (seekTime). --And use that value to run seek () on MediaPlayer --The note here is that you should only change the value ** when the user is manipulating the slider **. --Otherwise, this listener will be called even when the slider value is changed according to the playback time, so the playback position will be updated unnecessarily, and the sound will be played jerky.

Processing when the playback time ends

        //Post-processing when playback ends
        this.player.setOnEndOfMedia(() -> {
            this.player.seek(this.player.getStartTime());
            this.player.stop();
        });

--status remains PLAYING when the playback time reaches stopTime --In other words, if it is left as it is, the status of the button associated with status will not change. --Register a listener with setOnEndOfMedia () to explicitly change status when playback ends --Here, seek () is used to return the playback position to startTime, andstop ()is used to change the state to STOPPED. --I think it's okay to just use stop (), but for some reason getCurrentTime () only returns stopTime. --MediaPlayer internally has a flag for boolean to indicate whether playback has ended, but if that flag is true,getCurrentTime ()will return stopTime. Like --And if you change the state with just stop () after the end of playback, this flag remains as true, and I feel that currentTime returns only stopTime even though it is playing after that. ――I think it's a bug, but I'm not familiar with the specifications, so I can't say anything.

reference

menu

menu bar

javafx.jpg

Execution result

javafx.jpg

Description

--Use MenuBar to create a menu bar --There is no limit to where it can be placed, but it is usually placed at the top of the window. --Use Menu to add a category (where it says" File ") to MenuBar --Individual menu items can be defined by adding MenuItem to Menu.

Implement the process when a menu is selected

javafx.jpg

package sample.javafx;

import javafx.fxml.FXML;

public class MainController {
    
    @FXML
    public void open() {
        System.out.println(""Open" is selected");
    }
}

--By associating the ʻOn Action` event of the menu with the method of the controller, you can implement the processing when the menu is selected.

Separator

javafx.jpg

Execution result

javafx.jpg

--You can insert a separator line in the menu by inserting SeparatorMenuItem.

Submenu

javafx.jpg

Execution result

javafx.jpg

--Submenus can be realized by adding another Menu under Menu.

Assign shortcut keys

javafx.jpg

Execution result

javafx.jpg

--Shortcut keys can be assigned with the ʻaccelerator property of MenuItem --Shortcut keys are defined by a combination of modifier keys and major keys. --The modifier key is one ofCtrl, Shift, ʻAlt, Meta, Shortcut. --Furthermore, specify one of DOWN, ʻUP, ʻANY indicating the state of the key. --The main keys are ordinary keys such as ʻAandB. --The modifier key Shortcutis a key that is often used in shortcuts, and can realize an OS-independent definition. --For Windows, it refers to theCtrlkey. For Mac, it refers to theMeta` key.

Memonic

javafx.jpg

package sample.javafx;

import javafx.fxml.FXML;

public class MainController {
    
    @FXML
    public void open() {
        System.out.println("open");
    }
    
    @FXML
    public void close() {
        System.out.println("close");
    }
    
    @FXML
    public void fullScreen() {
        System.out.println("full screen");
    }
}

Each menu is associated with a controller method.

Execution result

javafx.gif

――It's a little difficult to understand, but you can operate the menu only with the keyboard without using the mouse. ――As you can see by actually touching it, honestly the movement is suspicious --For Windows, Alt focuses on the menu --After that, you can operate the menu only with the keyboard by entering the characters with underscores for each menu item. --The key assigned to this menu is called ** mnemonic **.

javafx.jpg

javafx.jpg

--Two steps are required to assign a mnemonic to a menu

  1. Check Mnemonic Parsing
  2. In Text, put an underscore ( _) before the alphabet to which you want to assign a mnemonic. --Then, the character next to the underscore is judged to be the character to be assigned to the mnemonic.

Assign mnemonics to Japanese menu

javafx.jpg

Execution result

javafx.jpg

--Since mnemonics are assigned in alphabets, you cannot assign mnemonics if the Japanese menu is left as it is (probably) --In that case, you can add a mnemonic note after the menu text, such as (_F), to assign the mnemonic to the Japanese menu as well (or iTunes or other common applications). I referred to it)

Add icon

javafx.jpg

Folder structure


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

Execution result

javafx.jpg

--By setting the graphic property of MenuItem to ʻImageView`, you can display any image in the menu item.

Check menu

javafx.jpg

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.CheckMenuItem;

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

public class MainController implements Initializable {
    @FXML
    private CheckMenuItem checkMenuItem;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.checkMenuItem.selectedProperty().addListener((value, oldValue, newValue) -> {
            System.out.println(newValue ? "Checked" : "Unchecked");
        });
    }
}

Execution result

javafx.gif

--You can use CheckMenuItem to define a menu item to turn the check on and off. --You can check if the menu is selected with the selected property

Radio button menu

javafx.jpg

package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.ToggleGroup;

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

public class MainController implements Initializable {
    @FXML
    private ToggleGroup group1;
    @FXML
    private RadioMenuItem hogeRadioMenuItem;
    @FXML
    private RadioMenuItem fugaRadioMenuItem;
    @FXML
    private RadioMenuItem piyoRadioMenuItem;
    
    @FXML
    private ToggleGroup group2;
    @FXML
    private RadioMenuItem fizzRadioMenuItem;
    @FXML
    private RadioMenuItem buzzRadioMenuItem;
    

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.hogeRadioMenuItem.setUserData("hoge");
        this.fugaRadioMenuItem.setUserData("fuga");
        this.piyoRadioMenuItem.setUserData("piyo");
        this.group1.selectedToggleProperty().addListener((toggle, oldValue, newValue) -> {
            if (newValue != null) {
                System.out.println("[group1] " + newValue.getUserData());
            }
        });
        
        this.fizzRadioMenuItem.setUserData("fizz");
        this.buzzRadioMenuItem.setUserData("buzz");
        this.group2.selectedToggleProperty().addListener((toggle, oldValue, newValue) -> {
            if (newValue != null) {
                System.out.println("[group2] " + newValue.getUserData());
            }
        });
    }
}

Execution result

javafx.gif

--RadioMenuItem allows you to define menu items that can be selected exclusively --Set the identifier (arbitrary character string) of the group to which the item belongs in Toggle Group of RadioMenuItem. --Items with the same group identifier will now be identified as belonging to the same group, and selection will be controlled exclusively. --Toggle Group is defined as follows on fxml

Toggle Group on fxml


  <RadioMenuItem fx:id="hogeRadioMenuItem" text="Hoge">
     <toggleGroup>
        <ToggleGroup fx:id="group1" />
     </toggleGroup>
  </RadioMenuItem>

--fx: id is the identifier specified by Toggle Group --So you can inject a ToggleGroup instance into the controller class with the specified Toggle Group identifier.

Toggle Group injection


    @FXML
    private ToggleGroup group1;

--By registering a listener in the selectedToggle property of ToggleGroup, you can catch the event when the selected item changes. --At this time, note that ** one change calls the event twice, and the first time newValue becomes null **. ――This seems to be a bug, and it seems that it will be fixed in ver 10. - [JDK-8189677] RadioMenuItem fires extra NULL value in property - Java Bug System

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

--Which menu is selected is identified by the value registered in ʻuserData of the menu item instance. --ʻUserData is of type ʻObject, so you can enter any value ――I don't use it often, but it can be used in situations like this time to identify selected items in a group. --Unfortunately, it seems that you cannot specify ʻuserData from Scene Builder, so you have to set it with the ʻinitialize ()` method etc.

Context menu

javafx.jpg

Execution result

javafx.jpg

--The context menu (menu that appears by right-clicking) can be realized by adding ContextMenu to the target node. --The definition method of menu items is the same as the menu bar.

reference

-24 Menu (Release 8)

Maximize the window

Main.java


package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        
        primaryStage.setMaximized(true); //★ Maximize

        primaryStage.show();
    }
}

--You can maximize the window by setting true withsetMaximized ()

Open another window

Basic

Folder structure


`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-MainController.java
  |   `-OtherController.java
  |
  `-resources/
    |-main.fxml
    `-other.fxml

other.fxml

javafx.jpg

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.URL;

public class MainController {
    
    @FXML
    public void openOtherWindow() throws IOException {
        //Load fxml and
        URL fxml = this.getClass().getResource("/other.fxml");
        FXMLLoader loader = new FXMLLoader(fxml);
        Pane pane = loader.load();
        
        //Create a scene
        Scene scene = new Scene(pane);
        
        //Register the scene on the stage
        Stage stage = new Stage();
        stage.setScene(scene);
        
        //display
        stage.show();
    }
}

--Use FXMLLoader to load fxml --There are various ways to specify the fxml file, but here it is read using Class.getResource (String) as the resource file. --For other loading methods, refer to FXMLLoader Javadoc --You can get the top node defined in fxml with the load () method. --After that, create a scene stage and start it with the show () or showAndWait () method. --JavaFX application consists of ** Stage , ** Scene , ** Scene Graph ** - Stage ** is the top-level container for JavaFX applications and corresponds to windows --In the above implementation, Stage corresponds - Scene ** is a container that holds the scene graph and corresponds to the display area of the stage. --In the above implementation, Scene corresponds -** Scene graph ** is a tree structure that holds all the nodes displayed on the scene, and the display can be changed by rewriting the scene graph. --In the above implementation, the Pane loaded from fxml will be the root node for this scenegraph.

Get the controller

MainController.java


package sample.javafx;

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

Main.java


package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();
        
        MainController controller = loader.getController();
        controller.hello();
        
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);

        primaryStage.show();
    }
}

Execution result

Hello MainController!!

--After loading fxml with FXMLLoader.load (), you can get an instance of the controller mapped to fxml with the FXMLLoader.getController () method. --Note that null is returned beforeload ()

Owner settings

** Behavior when no owner is set **

javafx.gif

--If you have not set the owner (ʻowner) to Stage`, closing the first open window will not close the later open window.

** Set owner **

Main.java


package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);
        Parent root = loader.load();

        MainController controller = loader.getController();
        controller.setStage(primaryStage); //★ Set the stage of the controller on the controller
        
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);

        primaryStage.show();
    }
}

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.URL;

public class MainController {
    
    private Stage stage; //★ The stage of this controller is set

    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); //★ Set owner

        stage.showAndWait();
    }
}

Execution result

javafx.gif

--If you set the owner, when the parent window is closed, the child window will be closed as well. --If you are loading the scenegraph from fxml, there are several ways to get your own Stage in the corresponding controller, but get an instance of the controller from FXMLLoader and set it via a setter etc. It feels kind of smart to do --Use the ʻinitOwner () `method to set the owner ――By the way, even if you set the owner, it is not modal.

modal

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Modality;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.URL;

public class MainController {

    private Stage stage;

    public void setStage(Stage stage) {
        this.stage = stage;
    }

    @FXML
    public void openOtherWindow() throws IOException {
        URL fxml = this.getClass().getResource("/other.fxml");
        FXMLLoader loader = new FXMLLoader(fxml);
        Pane pane = loader.load();

        Scene scene = new Scene(pane);

        Stage stage = new Stage();
        stage.setScene(scene);
        stage.initOwner(this.stage);
        stage.initModality(Modality.WINDOW_MODAL); //★ Set Modality

        stage.showAndWait();
    }
}

Execution result

javafx.gif

--ʻInitModality () can make the stage modal --Pass a constant of Modality enumeration to ʻinitModality () --NONE, WINDOW_MODAL, ʻAPPLICATION_MODALare defined inModality`.

constant meaning
NONE Do not block other windows
WINDOW_MODAL Block the owner's window
(Do not block windows other than the owner)
APPLICATION_MODAL Block all windows

reference

-[Basic structure of JavaFX | Light Lab](http://krr.blog.shinobi.jp/javafx/javafx%E3%81%AE%E5%9F%BA%E6%9C%AC%E6%A7%8B % E9% 80% A0) -1 JavaFX Scene Graph Operation (Release 8)

Embed another fxml

--If you create a complicated screen with a single fxml, the implementation will be messed up including the controller class. ――Also, when the same part is reused in multiple places, it is not good because duplication occurs if it is made with a single fxml. --fxml provides a tag called <fx: include> to embed other fxml --If you use this, you can avoid the above problems.

Basic

** Folder structure **

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

** Embed fxml (embedded.fxml) **

javafx.jpg

** Embed destination fxml (main.fxml) **

javafx.jpg

main.fxml


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.text.Font?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.151" xmlns:fx="http://javafx.com/fxml/1">
   <top>
      <Label text="Main" BorderPane.alignment="CENTER">
         <font>
            <Font size="40.0" />
         </font>
      </Label>
   </top>
   <center>
      <fx:include source="embedded.fxml" />
   </center>
</BorderPane>

Description

--Specify the fxml file to be embedded in the source attribute of<fx: include> --This value seems to be a path on the classpath if it starts with / -The reference says "It is considered relative to the classpath" and the meaning is difficult to understand. .. .. --If it doesn't start with /, it seems to be a relative path from the location of the embedded fxml file. ――This is also difficult to understand because the reference says "It is considered to be relative to the path of the current document". .. .. --If you are using SceneBuilder, you must write it with a path without / to preview it. --If you add the folder where the target fxml is placed to the classpath of SceneBuilder itself, it will work, but normally you will not start it like that ...

** Attempts to include in SceneBuilder fail **

javafx.jpg

--SceneBuilder seems to have a menu to include fxml (File-> Include-> FXML) --But when I try to include fxml using this, it fails to include with Failed to include'***. Fxml' ――Sometimes you succeed, but I'm not sure --It doesn't seem that SceneBuilder doesn't support include, so if you modify fxml directly by hand and write <fx: include>, it will preview properly. --I don't know the cause, but for the time being, it seems safe to handwrite only <fx: include>. --After embedding <fx: include> by hand, you can operate from SceneBuilder normally.

Get the embedded fxml controller

file organization


`-src/main/
  |-java/
  | `-sample/javafx/
  |   |-Main.java
  |   |-MainController.java
  |   `-EmbeddedController.java
  `-resources/
    |-main.fxml
    `-embedded.fxml

embedded.fxml

javafx.jpg

EmbeddedController.java


package sample.javafx;

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

public class EmbeddedController {
    @FXML
    private Label label;
    
    public void setMessage(String message) {
        this.label.setText(message);
    }
}

main.fxml

javafx.jpg

main.fxml


...

<BorderPane fx:id="pane" ... fx:controller="sample.javafx.MainController">
   <top>
      <Label text="Main" BorderPane.alignment="CENTER">
         ...
      </Label>
   </top>
   <center>
      <fx:include fx:id="embedded" source="embedded.fxml" />
   </center>
</BorderPane>

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;

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

public class MainController implements Initializable {
    @FXML
    private EmbeddedController embeddedController;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        this.embeddedController.setMessage("Hello!!");
    }
}

Execution result

javafx.jpg

Description

--The controller (ʻEmbeddedController) associated with the FXML (ʻembedded.fxml) embedded with<fx: include>is the embedding destination. You can inject into the controller (MainController) of FXML (main.fxml) with @FXML --However, the fx: id attribute must be set in<fx: include>. --If fx: id is not set, the controller instance will not be injected and will be null.

Display the embedded fxml as a separate window

embedded.fxml

javafx.jpg

EmbeddedController.java


package sample.javafx;

import javafx.beans.binding.StringBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;

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

public class EmbeddedController implements Initializable {
    
    private IntegerProperty count = new SimpleIntegerProperty(0);
    
    @FXML
    private Label label;
    
    @FXML
    public void countUp() {
        int now = this.count.get();
        this.count.set(now + 1);
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        StringBinding output = this.count.asString("count = %d");
        this.label.textProperty().bind(output);
    }
}

main.fxml

javafx.jpg

main.fxml


<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.text.Font?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/8.0.151" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.javafx.MainController">
   <fx:define>
      <fx:include fx:id="embeddedPane" source="embedded.fxml" />
   </fx:define>
   <top>
      <Label text="Main" BorderPane.alignment="CENTER">
         <font>
            <Font size="40.0" />
         </font>
      </Label>
   </top>
   <center>
      <Button mnemonicParsing="false" onAction="#openWindow" text="Open Window" BorderPane.alignment="CENTER">
         <font>
            <Font size="25.0" />
         </font>
      </Button>
   </center>
</BorderPane>

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Modality;
import javafx.stage.Stage;

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

public class MainController implements Initializable {
    @FXML
    private Pane embeddedPane;
    
    private Stage embeddedWindow;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Scene scene = new Scene(this.embeddedPane);
        this.embeddedWindow = new Stage();
        this.embeddedWindow.setScene(scene);
        this.embeddedWindow.initModality(Modality.APPLICATION_MODAL);
    }
    
    @FXML
    public void openWindow() {
        this.embeddedWindow.showAndWait();
    }
}

Execution result

javafx.gif

Description

main.fxml


...

<BorderPane ... fx:controller="sample.javafx.MainController">
   <fx:define>
      <fx:include fx:id="embeddedPane" source="embedded.fxml" />
   </fx:define>
   ...
</BorderPane>

--Embed the FXML (ʻembedded.fxml) to be embedded using <fx: include> in the FXML (main.fxml) to be embedded. --At this time, if you embed it normally, it will be visible in the scene, so embed it in <fx: define>. ――This way, the embedded tags will not be displayed visually. --Set fx: id attribute to <fx: include> `(required to DI to controller)

MainController.java


package sample.javafx;

...

public class MainController implements Initializable {
    @FXML
    private Pane embeddedPane;
    
    private Stage embeddedWindow;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Scene scene = new Scene(this.embeddedPane);
        this.embeddedWindow = new Stage();
        this.embeddedWindow.setScene(scene);
        this.embeddedWindow.initModality(Modality.APPLICATION_MODAL);
    }
    
    @FXML
    public void openWindow() {
        this.embeddedWindow.showAndWait();
    }
}

--DI embed FXML (ʻembedded.fxml) with @FXML in the embed destination controller (MainController.java) (ʻembeddedPane) --If you create a Stage using ʻembeddedPane, you can display the window with show () or showAndWait () . --The life cycle of the embedded FXML controller (ʻEmbeddedController) is the same as the embedded controller. --If you want to create a new instance each time it is displayed, I think that you will load it with FXMLLoader each time.

reference

File selection dialog

javafx.jpg

--Click the button to display the file selection dialog

Main.java


package sample.javafx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.net.URL;

public class Main extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        URL url = this.getClass().getResource("/main.fxml");
        FXMLLoader loader = new FXMLLoader(url);

        Parent root = loader.load();
        MainController controller = loader.getController();
        controller.setStage(primaryStage);
        
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);

        primaryStage.show();
    }
}

--Get an instance of MainController from FXMLLoader and set primaryStage

MainController.java


package sample.javafx;

import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

import java.io.File;

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

    public void setStage(Stage stage) {
        this.stage = stage;
    }
}

Execution result

javafx.jpg

Displayed dialog

javafx.jpg

Select a file and click "Open"

Console output


file=C:\Program Files\Java\jdk-9.0.1\README.html

Description

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

--Use the FileChooser class to display the file selection dialog. --Create an instance and execute the showOpenDialog (Stage) method to open a file selection dialog for selecting a single file. --Specify the owner Stage as the argumentStage. --If an owner is specified, operations on the owner are blocked (become modal) while the file selection dialog is open. --You can also pass null, in which case the file selection dialog will not be modal --The showOpenDialog () method is blocked while the dialog is displayed --Dialog selection results are returned as File type --If a file is selected, a File object pointing to that file is returned. --null is returned if the dialog is closed without selecting a file

Set the title

        FileChooser chooser = new FileChooser();
        chooser.setTitle("File laundry");
        File file = chooser.showOpenDialog(this.stage);

Execution result

javafx.jpg

--You can specify the dialog title with setTitile (String)

Specify the directory to be initially displayed

        FileChooser chooser = new FileChooser();
        chooser.setInitialDirectory(new File("C:/Program Files/java/jdk-9.0.1"));
        File file = chooser.showOpenDialog(this.stage);

--setInitialDirectory (File) allows you to specify the directory to display when the dialog is opened. --If nothing is specified, the default directory will be displayed for each environment (OS). ――It seems that the FileChooser class itself does not have a function to record the directory (file) opened last time. --So, if you want to reopen the directory you opened last time, you can do it by remembering the directory you opened separately and specifying the directory with setInitialDirectory () when opening the dialog again.

Narrow down the selectable files by extension

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("Anything", "*.*"));
        extensionFilters.add(new ExtensionFilter("Only with images", "*.jpg ", "*.jpeg ", "*.png ", "*.gif"));
        extensionFilters.add(new ExtensionFilter("Rainy day video", "*.mp4"));
        
        File file = chooser.showOpenDialog(this.stage);
        System.out.println("file=" + file);
    }

    ...
}

Execution result

javafx.jpg

--You can get ʻObservableList which defines the extension by extension with getExtensionFilters () . --By adding ʻExtensionFilter to this list, you can specify the refinement by extension. --- Pass "description" and "extension definition" to the constructor of ʻExtensionFilter --"Extension definition" is specified in the format. --If there is anything, set it to. *` --Multiple "extension definitions" can be specified

Opens a dialog where you can select multiple files

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

Execution result

javafx.jpg

Console output


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

--When you open a dialog with showOpenMultipleDialog (Stage), a dialog where you can select multiple files opens. --Selection result is returned by List <File> --Returns null if nothing is selected (isn't it an empty list ...)

Dialog to select where to save the file

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

Execution result

javafx.jpg

Console output


file=C:\Users\Public\hoge

--You can use showSaveDialog (Stage) to open a dialog to select where to save the file. --The return value will be a File object that points to the user-specified destination file. --If the dialog is closed without selecting anything, null is returned.

Dialog to select a directory

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

    ...
}

Execution result

javafx.jpg

Console output


directory=C:\Users\Public\Downloads

--Use DirectoryChooser to open the directory selection dialog. --The usage is the same as the basic FileChooser except that the selection target is a directory (such as ʻinitialDirectory`).

reference

-28 File Chooser (Release 8)

Align buttons and labels to the same width (height) and arrange them vertically (horizontally)

Appearance

javafx.jpg

Construction

javafx.jpg

** Size setting of each vertical element **

javafx.jpg

--Use VBox if you want to line up vertically with the same width --Each element to be arranged is extended to the maximum width with the maximum width (Max Width) set to MAX_VALUE. --Use HBox if you want to line up side by side at the same height --Each element to be lined up is extended to the maximum height with the maximum height (Max Height) set to MAX_VALUE.

Recommended Posts

JavaFX study notes
Jigsaw study notes
Docker study notes
[Java] Study notes
Ruby study notes (puts)
Maven basic study notes
Ruby study notes (variables & functions)
Internal class and lambda study notes
JUnit 4 notes
Study policy
java notes
Spring Framework study notes [Part 1] DI container
JavaFx memorandum
synchronized notes