JavaFX SimpleStringProperty Prism error - javafx

I have a more complicated application in which I'm rendering text, in single characters to the screen, as if it were being written. In my real app, things are more complicated, so I've simplified for this test.
My issue is: I have a model containing a SimpleStringProperty. I'm binding to this a Text element. Then by changing the SimpleStringProperty in the background thread, I'm expecting my Text element to change.
It works fine at lower speeds. But at higher speeds I get a prism error (pasted at end of post). I can fix the problem by moving updates to my SimpleStringProperty into a Platform.RunLater, but this seems to go against any kind of MVC architecture. My SimpleStringProperty is part of my model, and not my view class which lives inside the JavaFX thread.
By adding Platform.Runlater, things get out of sync and some characters are missed, as I'm probably creating too many RunLaters. I've tried adding a semaphore and a changelistener to release the semaphore, so that I'm not updating the until the RunLater has finished. No joy.
Any help appreciated, thank you.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class TestCase extends Application {
String s = "The Hobbit is a tale of high adventure, undertaken by a company of dwarves, in search of dragon- \n" +
"guarded gold. A reluctant partner in this perilous quest is Bilbo Baggins, a comfort-loving, \n" +
"unambitious hobbit, who surprises even himself by his resourcefulness and his skill as a burglar. \n" +
"\n" +
"Encounters with trolls, goblins, dwarves, elves and giant spiders, conversations with the dragon, \n" +
"Smaug the Magnificent, and a rather unwilling presence at the Battle of the Five Armies are some of \n" +
"the adventures that befall Bilbo. But there are lighter moments as well: good fellowship, welcome \n" +
"meals, laughter and song.";
SimpleStringProperty stringProperty = new SimpleStringProperty("");
private void run() {
new Thread(() -> {
try {
for (int i = 0; i < s.length(); i++) {
Thread.sleep(10); // works at 15ms
stringProperty.setValue(stringProperty.getValue().concat(s.substring(i, i + 1))); //this line into Platform.runLater -- but out of sync
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
#Override
public void start(Stage primaryStage) throws Exception {
Text text = new Text();
text.setWrappingWidth(500);
text.textProperty().bind(stringProperty);
GridPane root = new GridPane();
root.setPrefSize(500,400);
Scene theScene = new Scene(root);
primaryStage.setScene(theScene);
root.getChildren().add(text);
primaryStage.show();
run();
}
}
Current error:
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at com.sun.javafx.text.PrismTextLayout.layout(PrismTextLayout.java:1267)
at com.sun.javafx.text.PrismTextLayout.ensureLayout(PrismTextLayout.java:223)
at com.sun.javafx.text.PrismTextLayout.getBounds(PrismTextLayout.java:246)
at javafx.scene.text.Text.getLogicalBounds(Text.java:358)
at javafx.scene.text.Text.impl_computeLayoutBounds(Text.java:1115)
at javafx.scene.Node$12.computeBounds(Node.java:3225)
at javafx.scene.Node$LazyBoundsProperty.get(Node.java:9308)
at javafx.scene.Node$LazyBoundsProperty.get(Node.java:9278)
at javafx.scene.Node.getLayoutBounds(Node.java:3240)
at javafx.scene.Node.prefHeight(Node.java:2770)
at javafx.scene.Node.minHeight(Node.java:2712)
at javafx.scene.layout.Region.computeChildPrefAreaHeight(Region.java:1762)
at javafx.scene.layout.GridPane.computePrefHeights(GridPane.java:1424)
at javafx.scene.layout.GridPane.layoutChildren(GridPane.java:1690)
at javafx.scene.Parent.layout(Parent.java:1087)
at javafx.scene.Scene.doLayoutPass(Scene.java:552)
at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
at com.sun.javafx.tk.Toolkit.lambda$runPulse$30(Toolkit.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:354)
at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:381)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:510)
at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:490)
at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$404(QuantumToolkit.java:319)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:748)
Here is a semaphored version with RunLater:
public class TestCase extends Application {
private String s = "The Hobbit is a tale of high adventure, undertaken by a company of dwarves, in search of dragon- \n" +
"guarded gold. A reluctant partner in this perilous quest is Bilbo Baggins, a comfort-loving, \n" +
"unambitious hobbit, who surprises even himself by his resourcefulness and his skill as a burglar. \n" +
"\n" +
"Encounters with trolls, goblins, dwarves, elves and giant spiders, conversations with the dragon, \n" +
"Smaug the Magnificent, and a rather unwilling presence at the Battle of the Five Armies are some of \n" +
"the adventures that befall Bilbo. But there are lighter moments as well: good fellowship, welcome \n" +
"meals, laughter and song.";
private SimpleStringProperty stringProperty = new SimpleStringProperty("");
private int i;
private Semaphore semaphore= new Semaphore(1);
private void run() {
new Thread(() -> {
try {
for (i = 0; i < s.length(); i++) {
Thread.sleep(15); // works at 15ms
Platform.runLater(() -> {
try {
semaphore.tryAcquire(100, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
stringProperty.setValue(stringProperty.getValue().concat(s.substring(i, i + 1))); //this line into Platform.runLater -- but out of sync
});
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
#Override
public void start(Stage primaryStage) throws Exception {
stringProperty.addListener((observable, oldValue, newValue) -> {
semaphore.release();
});
Text text = new Text();
text.setWrappingWidth(500);
text.textProperty().bind(stringProperty);
GridPane root = new GridPane();
root.setPrefSize(500,400);
Scene theScene = new Scene(root);
primaryStage.setScene(theScene);
root.getChildren().add(text);
primaryStage.show();
run();
}
}

Since the text is bound to your StringProperty, you must only change the string property on the FX Application Thread. Doing otherwise violates the threading rules of JavaFX, and is why you get the null pointer exception (due to some race condition failing somewhere in the internal API).
Your attempt to use Platform.runLater() is incorrectly implemented. By moving the index variable i to an instance variable, you are accessing it in one thread (the FX Application Thread, inside the Platform.runLater()) but modifying it in the for loop in the background thread. It's pretty easy to see that the index variable could get incremented more than once between the invocation of two consecutive runnables submitted to the FX Application Thread. You should only use final variables inside your Platform.runLater(...) calls, or variables that are only ever accessed from the FX Application Thread. The following works just fine:
private void run() {
new Thread(() -> {
try {
for (int i = 0; i < s.length(); i++) {
Thread.sleep(10); // works at 15ms
final String append = s.substring(i, i+1);
Platform.runLater(() ->
stringProperty.setValue(stringProperty.getValue().concat(append)));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Any time you are modifying the UI periodically like this, you should really consider using the animation API instead of using a background thread to implement the "pause" between "frames". There are many advantages here: mostly you save the creation of a background thread at all (even the implementation of the animation under the hood does not use additional threads), so you save resources and avoid any possibility of the race conditions you see in your attempted implementation of Platform.runLater(). Once you are familiar with the animation API, I think the code becomes easier to read too.
Here is a reimplementation of your example, using a Timeline instead of a thread:
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TestCase extends Application {
String s = "The Hobbit is a tale of high adventure, undertaken by a company of dwarves, in search of dragon- \n"
+ "guarded gold. A reluctant partner in this perilous quest is Bilbo Baggins, a comfort-loving, \n"
+ "unambitious hobbit, who surprises even himself by his resourcefulness and his skill as a burglar. \n"
+ "\n"
+ "Encounters with trolls, goblins, dwarves, elves and giant spiders, conversations with the dragon, \n"
+ "Smaug the Magnificent, and a rather unwilling presence at the Battle of the Five Armies are some of \n"
+ "the adventures that befall Bilbo. But there are lighter moments as well: good fellowship, welcome \n"
+ "meals, laughter and song.";
SimpleStringProperty stringProperty = new SimpleStringProperty("");
#Override
public void start(Stage primaryStage) throws Exception {
Text text = new Text();
text.setWrappingWidth(500);
GridPane root = new GridPane();
root.setPrefSize(500, 400);
Scene theScene = new Scene(root);
primaryStage.setScene(theScene);
root.getChildren().add(text);
primaryStage.show();
// Number of characters displayed in text:
IntegerProperty textLength = new SimpleIntegerProperty(0);
// "Animate" number of characters from 0 to total length of text,
// over a total of 10 seconds:
Timeline animation = new Timeline(
new KeyFrame(Duration.seconds(10),
new KeyValue(textLength, s.length())));
// ensure text displays the appropriate substring of s:
text.textProperty().bind(Bindings.createStringBinding(
() -> s.substring(0, textLength.get()),
textLength));
// start the animation:
animation.play();
}
public static void main(String[] args ) {
launch(args);
}
}
Also see the example in the Javadocs for Transition, which is pretty similar.

Related

JavaFX detect computer sleep or hibernate

I have a JavaFX application that gets CPU and Memory problems after the computer is going to sleep or hibernate.
In the application, I use a Canvas that is painted two times a second. This may cause issues. I am wondering if it's possible to detect when the computer is sleeping, and not repaint it. Maybe the canvas.isVisible() is already checking this?
You could only paint if your app has focus.
This can be achieved by pausing the animation when your application's main window no longer has focus.
You can monitor the stage's focusedProperty() to find out when the stage has focus.
I ran some tests on a Mac (OS X 12.3) with JavaFX 18 and found that when the computer is put to sleep (click the apple icon in the menu bar and select Sleep), the focus is removed from the application, which allows the animation for the application to be paused while it does not have focus.
Example application
import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class SleepyApp extends Application {
private long start;
private Counter continuousCounter = new Counter();
private Counter focusedCounter = new Counter();
#Override
public void start(Stage stage) throws Exception {
VBox layout = new VBox(10,
focusedCounter.getCounterLabel(),
continuousCounter.getCounterLabel()
);
layout.setPrefSize(80, 80);
layout.setAlignment(Pos.CENTER);
stage.setScene(new Scene(layout));
stage.show();
stage.focusedProperty().addListener((observable, wasFocused, isFocused) -> {
if (isFocused) {
focusedCounter.play();
} else {
focusedCounter.pause();
}
});
continuousCounter.play();
focusedCounter.play();
start = System.currentTimeMillis();
}
#Override
public void stop() throws Exception {
long stop = System.currentTimeMillis();
continuousCounter.stop();
focusedCounter.stop();
long elapsed = (stop - start) / 1_000;
System.out.println("Elapsed: " + elapsed + " seconds.");
System.out.println("Continuous Counter: " + continuousCounter.getCount() + " seconds.");
System.out.println("Focused Counter: " + focusedCounter.getCount() + " seconds.");
}
public static void main(String[] args) {
launch(args);
}
class Counter {
private final Timeline timeline;
private final IntegerProperty count;
private final Label counterLabel;
public Counter() {
count = new SimpleIntegerProperty(0);
counterLabel = new Label(count.asString().get());
counterLabel.textProperty().bind(
count.asString()
);
counterLabel.setStyle("-fx-font-size: 20px");
timeline = new Timeline(
new KeyFrame(
Duration.seconds(1),
e -> count.set(count.get() + 1)
)
);
timeline.setCycleCount(Timeline.INDEFINITE);
}
public int getCount() {
return count.get();
}
public Label getCounterLabel() {
return counterLabel;
}
public void play() {
timeline.play();
}
public void pause() {
timeline.pause();
}
public void stop() {
timeline.stop();
}
}
}
Output
In this case, the computer was put to sleep for 16 seconds.
The first number is the number of one per second animation frames rendered only when the application has focus.
The second number is the number of one per second animation frames rendered regardless of whether the application has focus (and sleep state).
Elapsed: 30 seconds.
Continuous Counter: 30 seconds.
Focused Counter: 14 seconds.
On isVisible()
node.isVisible() is not applicable for this case.
This is a description of the isVisible method:
Specifies whether this Node and any subnodes should be rendered as part of the scene graph. A node may be visible and yet not be shown in the rendered scene if, for instance, it is off the screen or obscured by another Node. Invisible nodes never receive mouse events or keyboard focus and never maintain keyboard focus when they become invisible.
As the documentation indicates, the node may be visible but not shown in the rendered scene, which will be the case when the computer is sleeping.

How can I change the scene By pressing a specific key(b) on the the keyboard?

In my application, there are two scenes: mainScene and bossScene where mainScene is used when starting up the application.
I'm trying to implement the boss key functionality where by pressing the 'b' key on the the keyboard should change the scene to bossScene. And also by pressing the button in bossScene should switch back to mainScene.
I'm getting an error on InteliJ saying "Cannot resolve method setOnKeyPressed in List
My Code:
public void start(Stage stage) throws Exception {
stage.setTitle("BossKey Example");
// Scene and layout for the main view
VBox root = new VBox();
Scene mainScene = new Scene(root, 500, 300);
// Scene for the BOSS view
Scene bossScene = new Scene(new Label("Nothing suspicious here"), 500, 300);
List<TextField> fields = new ArrayList<TextField>();
for (int i = 0; i < 10; i++) {
fields.add(new TextField());
}
fields.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent keyEvent) {
switch (keyEvent.getCharacter()){
case "b": stage.setScene(bossScene); break;
}
}
});
/////// Added addEventFilter, still not working
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, new
EventHandler<KeyEvent() {
#Override
public void handle(KeyEvent keyEvent) {
switch (keyEvent.getCharacter()){
case "b": stage.setScene(bossScene); break;
}
keyEvent.consume();
}
});
// Create components for main view
root.getChildren().addAll(fields);
root.getChildren().add(new Button("Hello!"));
stage.setScene(mainScene);
stage.show();
}
}
KeyCombination filters
You should use a key combination in an event filter, e.g., CTRL+B or SHORTCUT+B.
For details on how to apply key combinations, see:
javafx keyboard event shortcut key
Why a key combination is superior to filtering on the character "b":
If you filter on a "b" character, the feature won't work if caps lock is down.
If you filter on a "b" character, you will be unable to type "b" in the text field.
You might think you could write scene.setOnKeyPressed(...), however, that won't work as expected in many cases. A filter is required rather than a key press event handler because the key events may be consumed by focused fields like text fields if you use a handler, so a handler implementation might not activate in all desired cases.
Filtering on a key combination avoids the issues with trying to handle a character key press. The key combinations rely on key codes which represent the physical key pressed and don't rely on the state of other keys such as caps lock unless you explicitly add additional logic for that.
If you don't understand the difference between an event filter and an event handler and the capturing and bubbling phases of event dispatch, then study:
the oracle event handling tutorial.
KeyCombination filter implementation
final EventHandler<KeyEvent> bossEventFilter = new EventHandler<>() {
final KeyCombination bossKeyCombo = new KeyCodeCombination(
KeyCode.B,
KeyCombination.CONTROL_DOWN
);
public void handle(KeyEvent e) {
if (bossKeyCombo.match(e)) {
if (stage.getScene() == mainScene) {
stage.setScene(bossScene);
} else if (stage.getScene() == bossScene) {
stage.setScene(mainScene);
}
e.consume();
}
}
};
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
bossScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
Accelerator alternative
An accelerator could be used instead of an event filter. Information on applying an accelerator is also in an answer to the linked question, I won't detail this alternative further here.
Example Solution
Standalone executable example code:
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import java.io.IOException;
public class SceneSwap extends Application {
#Override
public void start(Stage stage) throws IOException {
final Scene mainScene = new Scene(
createLayout(
"Press CTRL+B to enter boss mode",
Color.PALEGREEN
)
);
final Scene bossScene = new Scene(
createLayout(
"Press CTRL+B to exit boss mode",
Color.PALEGOLDENROD
)
);
final EventHandler<KeyEvent> bossEventFilter = new EventHandler<>() {
final KeyCombination bossKeyCombo = new KeyCodeCombination(
KeyCode.B,
KeyCombination.CONTROL_DOWN
);
public void handle(KeyEvent e) {
if (bossKeyCombo.match(e)) {
if (stage.getScene() == mainScene) {
stage.setScene(bossScene);
} else if (stage.getScene() == bossScene) {
stage.setScene(mainScene);
}
e.consume();
}
}
};
mainScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
bossScene.addEventFilter(KeyEvent.KEY_PRESSED, bossEventFilter);
stage.setScene(mainScene);
stage.show();
}
private VBox createLayout(String text, Color color) {
VBox mainLayout = new VBox(10,
new Label(text),
new TextField()
);
mainLayout.setPadding(new Insets(10));
mainLayout.setStyle("-fx-background: " + toCssColor(color));
return mainLayout;
}
private String toCssColor(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);
int o = (int) Math.round(color.getOpacity() * 255.0);
return String.format("#%02x%02x%02x%02x" , r, g, b, o);
}
public static void main(String[] args) {
launch();
}
}

Updating two text objects in JavaFX, one field after another, only seeing final result of both changes

I have three text fields displayed, and I want to change the second one, see the result on the display (so wait a couple of seconds), then change the third one, and see the result on the display. Instead, I only see the result of both changes on the display (with no pause inbetween).
import javafx.application.Application;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.animation.PauseTransition;
import javafx.util.Duration;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
public class TestApp extends Application
{
private Text tone = new Text("one");
private Text ttwo = new Text("two");
private Text tthree = new Text("three");
private void process()
{
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> ttwo.setText("four"));
pauseTransition.play();
pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> tthree.setText("five"));
pauseTransition.play();
} // end of method "process"
#Override
public void start(Stage stage)
{
VBox vboxRoot = new VBox();
vboxRoot.getChildren().add(tone);
vboxRoot.getChildren().add(ttwo);
vboxRoot.getChildren().add(tthree);
Scene myScene = new Scene(vboxRoot,350,350);
stage.setScene(myScene);
stage.setTitle("Test");
stage.show();
process();
} // end of method "start"
} // end of class "TestApp"
So initially
one
two
three
is displayed; followed by
one
four
five
What I want to see is
one
four
three
a pause and then
one
four
five
I'm not sure if its a typo if your What I want to see is but if its not the reason you are getting
one
two
three
to initially display is because thats what you have them set as and in this piece of code below you setup 2 PauseTransitions that both have a 2 second wait before changing the text
private void process()
{
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> ttwo.setText("four"));
pauseTransition.play();
pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> tthree.setText("five"));
pauseTransition.play();
}
To fix this you can do a few things such as
Appropriately set what you want from the start
Run ttwo.setText("four"); at the start of your process() method
After doing that you get the starting result of
one
four
three
and after the pause transition finishes 2 seconds later you will see
one
four
five
After pauseTransition.play(); you assign new value to pauseTransition and you play it again, long before the first one completes.
A better approach would be :
Introduce a counter field : private int counter = 0;
And use it like so:
private void process() {
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event ->{
show(counter++);
if(counter < 5 ) pauseTransition.play();//stop criteria
});
pauseTransition.play();
}
private void show(int counter) {
//respond based on counter
}
The following is mcve the demonstrates the idea (it is not meant to demostrate the exact behavior you want which is not clear to me) :
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FxMain extends Application {
private int counter = 0;
private final Text tone = new Text("one"),
ttwo = new Text("two"),
tthree = new Text("three");
#Override
public void start(Stage primaryStage) throws Exception{
VBox root = new VBox(10);
root.setPadding(new Insets(10));
root.getChildren().addAll(tone,ttwo,tthree);
primaryStage.setScene(new Scene(root, 100,100));
primaryStage.sizeToScene();
primaryStage.show();
process();
}
private void process() {
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event ->{
show(counter++);
if(counter < 3 ) {
pauseTransition.play();//stop criteria
}
});
pauseTransition.play();
}
private void show(int counter) {
switch(counter){
case 0:
tone.setText("two");
break;
case 1:
ttwo.setText("three");
break;
default :
tthree.setText("four");
break;
}
}
public static void main(final String[] args) {
launch(args);
}
}
As others have mentioned, you play both PauseTransitions virtually in parallel. They are started within milliseconds (if not nanoseconds) of each other and I would not be surprised if they actually completed in the same frame. Because of this you see the result of both animations simultaneously.
One solution is to use a SequentialTransition; it will play a list of animations in the order of said list.
private void process() {
SequentialTransition st = new SequentialTransition();
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> ttwo.setText("four"));
st.getChildren().add(pauseTransition);
pauseTransition = new PauseTransition(Duration.seconds(2));
pauseTransition.setOnFinished(event -> tthree.setText("five"));
st.getChildren().add(pauseTransition);
st.play();
}
Another solution is to use a Timeline made up of multiple KeyFrames configured to execute at increasing times.
private void process() {
new Timeline(
new KeyFrame(Duration.seconds(2), event -> ttwo.setText("four")),
new KeyFrame(Duration.seconds(4), event -> tthree.setText("five"))
).play();
}
You mention your real goal may be more complicated than setting the text properties of some Text objects. You can adapt either solution to a more general purpose mechanism. Here's an example for a Timeline that will execute an arbitrary number of actions, with a fixed delay between each action (including after the last action):
private static Timeline createTimeline(Duration period, Runnable... actions) {
var frames = new ArrayList<KeyFrame>(actions.length + 1);
var time = Duration.ZERO;
for (var action : actions) {
frames.add(new KeyFrame(time, event -> action.run()));
time = time.add(period);
}
frames.add(new KeyFrame(time)); // adds a delay after last action
return new Timeline(frames.toArray(KeyFrame[]::new));
}
But what happens if I use your approach for one set of actions; and then move onto something else that generates another set of actions. How can I be sure the first set of actions has completed (i.e. been displayed) before I start the second set?
You can use the Animation#onFinished property, in combination with a Queue<Animation>, to play the next Animation when the previous one completes.
private final Queue<Animation> animationQueue = ...;
private Animation currentAnimation;
private void playAnimation(Animation animation) {
if (animation.getCycleCount() == Animation.INDEFINITE) {
throw new IllegalArgumentException();
}
animation.setOnFinished(event -> {
currentAnimation = animationQueue.poll();
if (currentAnimation != null) {
currentAnimation.playFromStart();
}
});
if (currentAnimation != null) {
animationQueue.add(animation);
} else {
currentAnimation = animation;
animation.playFromStart();
}
}

Background thread directly accessing UI anyway

Here is my code, can someone explain why it works every time?
package dingding;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Dingding extends Application {
TextField tfAuto = new TextField("0");
AutoRunThread runner = new AutoRunThread();
boolean shouldStop = false;
private class AutoRunThread extends Thread {
#Override
public void run() {
while (true) {
int i = Integer.parseInt(tfAuto.getText());
++i;
tfAuto.setText(String.valueOf(i));
try {
Thread.sleep(1000);
} catch (Throwable t) {
}
if (shouldStop) {
runner = null;
shouldStop = false;
return;
}
}
}
}
#Override
public void start(Stage primaryStage) {
Button btnStart = new Button("Increment Automatically");
Button btnStop = new Button("Stop Autotask");
btnStart.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (runner == null) {
runner = new AutoRunThread();
runner.setDaemon(true);
}
if (runner != null && !(runner.isAlive())) {
runner.start();
}
}
});
btnStop.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
shouldStop = true;
}
});
VBox rootBox = new VBox();
HBox autoBox = new HBox();
autoBox.getChildren().addAll(tfAuto, btnStart, btnStop);
rootBox.getChildren().addAll(autoBox);
Scene scene = new Scene(rootBox, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
As I said in my comment, improperly synchronized code doesn't guarantee errors per se. However, that doesn't mean said code, when used in a multi-threaded context, is actually working—you're merely getting lucky. Eventually you'll run into undefined behavior such as corrupted state, stale values, and unexpected exceptions. This is because, without synchronization, actions performed by one thread are not guaranteed to be visible to any other thread. You need a happens-before relationship, better described in the package documentation of java.util.concurrent and this SO question.
JavaFX, like most UI frameworks/toolkits, is single threaded. This means there's a special thread—in this case, the JavaFX Application Thread— that is responsible for all UI related actions1. It is this thread, and this thread only, that must be used to access and/or modify state related to a "live" scene graph (i.e. nodes that are in a scene that's in a window that's showing2). Using any other thread can lead to the undefined behavior described above.
Some UI related functions actually ensure they're being called on the JavaFX Application Thread, usually throwing an IllegalStateException if not. However, the remaining functions will silently let you call them from any thread—but that doesn't mean it's safe to do so. This is done this way, I believe, because checking the thread in every UI related function is a maintenance nightmare and would incur a not-insignificant performance cost.
1. It's slightly more complicated that this; JavaFX also has a "prism render thread" and a "media thread". See Understanding JavaFX Architecture for more information. But note that, from an application developer's point of view, the only thread that matters is the JavaFX Application Thread.
2. This is documented by Node. Note that some nodes, such as WebView, are more restrictive when it comes to threading; this will be documented in the appropriate places.

how latch has no effect in javafx?

I encounter a problem in developing javafx, I find latch has no effect in JavaFx, for example, in the following code:
public class JavafxLatchDemo1 extends Application {
#Override
public void start(Stage primaryStage) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
TextArea txtOut = new TextArea();
StackPane root = new StackPane();
root.getChildren().add(txtOut);
Scene scene = new Scene(root, 300, 250);
//invoke rpc function
Callable<Integer> fibCall = new fibCallable(latch, txtOut);
FutureTask<Integer> fibTask = new FutureTask<Integer>(fibCall);
Thread fibThread = new Thread(fibTask);
fibThread.start();
latch.await(); //阻塞等待计数为0
txtOut.appendText("\n Say 'Hello World'");
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
class fibCallable implements Callable<Integer>{
CountDownLatch latch;
TextArea txtInput;
fibCallable(CountDownLatch mylatch, TextArea txtIn){
latch = mylatch;
txtInput = txtIn;
}
#Override
public Integer call() throws Exception {
int temp1=1,temp2=0;
System.out.println("Client will pay money for eshop");
for(int i=0; i<10; i++){
temp1 = temp1 + temp2;
temp2 = temp1;
}
System.out.println("Client already decide to pay money for eshop");
Platform.runLater(()->{
txtInput.appendText("\nWhy, am I first?");
});
latch.countDown(); //计数减1
return (new Integer(temp1));
}
}
Since I set a latch to stop JavaFx main thread in latch.await();, and want the callable thread fibCallable output the content first: so I expect:
Why, am I first?
Say 'Hello World'
but the real output is opposite:
Say 'Hello World'
Why, am I first?
why? and a solution?
Platform.runLater() submits a runnable to be executed on the FX Application Thread. The start() method is also executed on the FX Application Thread. So the Runnable you submitted with Platform.runLater() cannot be executed until anything already executing on that thread completes.
So you start your fibThread in the background and then immediately wait for the latch: this blocks the FX Application Thread. The fibThread does a little bit of work and then submits a call to the (blocked) FX Application Thread. Then the fibThread releases the latch: the currently-blocked FX Application thread unblocks and finishes the current method call (appending the text "Say Hello World" to the text area and displaying the stage), and at some point after that the runnable submitted to Platform.runLater() executes on the same thread.
A "quick and dirty" fix is simply to wrap the second call to txtOut.appendText(...) in another Platform.runLater():
Platform.runLater(() -> txtOut.appendText("\n Say 'Hello World'"));
This is guaranteed to work, because runnables passed to Platform.runLater() are guaranteed to be executed in the order in which they are passed, and the countdown latch establishes a "happens-before" relationship between the two calls to Platform.runLater().
Note however that you are blocking the FX Application Thread with the call to latch.await(), which is bad practice (and will delay the display of the stage until the background thread completes). You should really put the call to latch.await(), along with the second Platform.runLater() in another background thread. Also note that you don't really need the latch at all, as you already have a FutureTask, and you can just wait for its result (this will be equivalent to waiting for the latch). So you can do
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class JavafxLatchDemo1 extends Application {
#Override
public void start(Stage primaryStage) throws InterruptedException {
// CountDownLatch latch = new CountDownLatch(1);
TextArea txtOut = new TextArea();
StackPane root = new StackPane();
root.getChildren().add(txtOut);
Scene scene = new Scene(root, 300, 250);
//invoke rpc function
// Callable<Integer> fibCall = new fibCallable(latch, txtOut);
Callable<Integer> fibCall = new fibCallable(txtOut);
FutureTask<Integer> fibTask = new FutureTask<Integer>(fibCall);
Thread fibThread = new Thread(fibTask);
fibThread.start();
// latch.await(); //阻塞等待计数为0
new Thread(() -> {
try {
// wait for fibTask to complete:
fibTask.get();
// and now append text to text area,
// but this now must be done back on the FX Application Thread
Platform.runLater(() -> txtOut.appendText("\n Say 'Hello World'"));
} catch (Exception ignored) {
// ignore interruption: thread is exiting anyway....
}
}).start();
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
class fibCallable implements Callable<Integer>{
// CountDownLatch latch;
TextArea txtInput;
fibCallable(TextArea txtIn){
txtInput = txtIn;
}
#Override
public Integer call() throws Exception {
int temp1=1,temp2=0;
System.out.println("Client will pay money for eshop");
for(int i=0; i<10; i++){
temp1 = temp1 + temp2;
temp2 = temp1;
}
System.out.println("Client already decide to pay money for eshop");
Platform.runLater(()->{
txtInput.appendText("\nWhy, am I first?");
});
// latch.countDown(); //计数减1
return (new Integer(temp1));
}
}
}
Finally, note that JavaFX has a concurrency API of its own, that supports various callbacks on the FX Application Thread directly. This API usually means you can avoid getting your hands dirty with latches and locks, etc.

Resources