JavaFX detect computer sleep or hibernate - javafx

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.

Related

java.lang.IllegalArgumentException: Problem and Hitbox

I have two problems one this that, if i want to show score with the circle object:
layoutV.getChildren().addAll(virus, score);
I get the following error:
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Children: duplicate children added: parent = Pane#6661fc86[styleClass=root].
As far as I understand it is because the Task wants to show multiple scores. So should I use another scene or layout to show score?
My other problem is the hitbox of the object, right know everytime i click the score goes up. I looked up the mouse event getTarget but it does not seem like I can make it so that my object is the only target to use the mouse event on.
public class Main extends Application {
private Stage window;
private Pane layoutV;
private Scene scene;
private Circle virus;
private int score;
private Label scores;
#Override
public void start(Stage primaryStage) {
window = primaryStage;
window.setTitle("Enemy TEST");
this.score = 0;
scores = new Label("Score "+ score);
layoutV = new Pane();
scene = new Scene(layoutV, 600, 600);
window.setScene(scene);
window.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
Task task = new Task<Void>() {
#Override
protected Void call() throws Exception {
while (true) {
Platform.runLater(new Runnable() {
#Override
public void run() {
drawCircles();
}
});
Thread.sleep(1000);
}
}
};
public void drawCircles() {
double x = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double y = (double)(Math.random() * ((550 - 50) + 1)) + 50;
double r = (double)(Math.random() * ((30 - 10) + 1)) + 10;
virus = new Circle(x, y, r, Color.VIOLET);
layoutV.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
layoutV.getChildren().remove(e.getTarget());
this.score++;
System.out.println("score: "+ this.score);
}
});
layoutV.getChildren().addAll(virus);
scene.setRoot(layoutV);
window.setScene(scene);
}
public static void main(String[] args) {
launch(args);
}
}
You have lots of issues, not just the ones from your question:
Although it will work as you coded
it, I don't advise spawning a thread to draw your circles, instead
see:
JavaFX periodic background task
You don't need to set the root in the scene and the scene in the
window every time you draw a new circle.
Nor do you need to set the
mouse handler on the layout every time you draw a circle.
Rather than setting a mouse handler on the layout, you are better off setting a mouse handler on the circles themselves (which you can do before you add them to the scene).
score is an int, not a node you can only add nodes to the scene
graph.
See the documentation for the scene package:
A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in the children list of a Parent or as the clip of a Node. See the Node class for more details on these restrictions.
How you are adding the node more than once is not clear to me, because you are probably doing it in code different than the Main class you provided.
To add a circle with a score on top, use a StackPane with the score in a label, but make the label mouse transparent, so that it does not register any clicks:
Label scoreLabel = new Label(score + "");
scoreLabel.setMouseTransparent(true);
StackPane balloon = new StackPane(circle, scoreLabel);
layoutV.getChildren.add(balloon);
Add the click handler on the balloon.
And additional issues I don't detail here but are solved in the demo code provided.
To fix all your errors, I would write some code like below. Perhaps you can review it and compare it with your code to help understand one way to create this game.
The example code might not be exactly the functionality you are looking for (that is not really its purpose), but it should be enough to keep you on the right track for implementing your application.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.*;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class Inoculation extends Application {
public static final int W = 600;
public static final int H = 600;
private final IntegerProperty score = new SimpleIntegerProperty(0);
private final Pane playingField = new Pane();
#Override
public void start(Stage stage) {
StackPane overlay = createOverlay();
Pane layout = new StackPane(playingField, overlay);
stage.setScene(new Scene(layout, W, H));
stage.show();
Infection infection = new Infection(playingField, score);
infection.begin();
}
private StackPane createOverlay() {
Label totalScoreLabel = new Label();
totalScoreLabel.textProperty().bind(
Bindings.concat(
"Score ", score.asString()
)
);
StackPane overlay = new StackPane(totalScoreLabel);
StackPane.setAlignment(totalScoreLabel, Pos.TOP_LEFT);
overlay.setMouseTransparent(true);
return overlay;
}
public static void main(String[] args) {
launch(args);
}
}
class Infection {
private static final Duration SPAWN_PERIOD = Duration.seconds(1);
private static final int NUM_SPAWNS = 10;
private final Timeline virusGenerator;
public Infection(Pane playingField, IntegerProperty score) {
virusGenerator = new Timeline(
new KeyFrame(
SPAWN_PERIOD,
event -> spawnVirus(
playingField,
score
)
)
);
virusGenerator.setCycleCount(NUM_SPAWNS);
}
public void begin() {
virusGenerator.play();
}
private void spawnVirus(Pane playingField, IntegerProperty score) {
Virus virus = new Virus();
virus.setOnMouseClicked(
event -> {
score.set(score.get() + virus.getVirusScore());
playingField.getChildren().remove(virus);
}
);
playingField.getChildren().add(virus);
}
}
class Virus extends StackPane {
private static final int MAX_SCORE = 3;
private static final int RADIUS_INCREMENT = 10;
private final int virusScore = nextRandInt(MAX_SCORE) + 1;
public Virus() {
double r = (MAX_SCORE + 1 - virusScore) * RADIUS_INCREMENT;
Circle circle = new Circle(
r,
Color.VIOLET
);
Text virusScoreText = new Text("" + virusScore);
virusScoreText.setBoundsType(TextBoundsType.VISUAL);
virusScoreText.setMouseTransparent(true);
getChildren().setAll(
circle,
virusScoreText
);
setLayoutX(nextRandInt((int) (Inoculation.W - circle.getRadius() * 2)));
setLayoutY(nextRandInt((int) (Inoculation.H - circle.getRadius() * 2)));
setPickOnBounds(false);
}
public int getVirusScore() {
return virusScore;
}
// next random int between 0 (inclusive) and bound (exclusive)
private int nextRandInt(int bound) {
return ThreadLocalRandom.current().nextInt(bound);
}
}
Some additional notes on this implementation that might be useful to know:
The total score is placed in an overlayPane so that it is not obscured by elements added to the playingField (which contains the virus spawns).
The overlayPane is made mouseTransparent, so that it won't intercept any mouse events, and the clicks will fall through to the items in the playing field.
The app currently generates viruses within a fixed field size, regardless of whether you resize the window. That is just the way it is designed and coded, you could code it otherwise if wished. It would be more work to do so.
The Bindings class is used to create a string expression binding which concatenates the static string "Score " with an integer property representing the score. This allows the string representing the score to be bound to the score label text in the overlay so that it automatically updates whenever the score is changed.
The virus generation uses a timeline and is based on the concepts from:
JavaFX periodic background task
The application class is kept deliberately simple to handle mostly just the core application lifecycle, and the actual functionality of the application is abstracted to an Infection class which handles the spawning of the virus and a Virus class that generates a new virus.
This technique is used to center a score for each individual virus on the virus:
how to put a text into a circle object to display it from circle's center?
The virus itself is laid out in a StackPane. The pane has pick on bounds set to false. To remove the virus infection, you must click on the circle which represents the virus and not just anywhere in the square for the stack pane.
Because circle coordinates are in local co-ordinates and the circle is in a parent stack pane representing the virus, the circle itself does not need x and y values set, instead layout x and y values are set on the enclosing pane to allow positioning of the pane representing the entire virus.
The following technique is used to generate random integers in acceptable ranges using ThreadLocalRandom:
How do I generate random integers within a specific range in Java?

JavaFX setOnEndOfMedia on next MediaPlayer

I have List<MediaPlayer> that is populating by
private List<MediaPlayer> players() {
for (String file : path.list((dir1, name) -> {
if (name.endsWith(SUPPORTED_VIDEO_FILE_EXTENSIONS)){
return true;
}
return false;
})) {
players.add(createPlayer(Paths.get(path + "/" + file).toUri().toString()));
System.out.println(Paths.get(path + "/" + file).toUri().toString());
}
return players;
}
In my case size of List<MediaPlayer> is 3. Than i use it there
if (currentNumOfVideo == -1) {
currentNumOfVideo = 0;
}
mv = new MediaView();
MediaPlayer currentPlayer = players.get(currentNumOfVideo);
mv.setMediaPlayer(currentPlayer);
currentPlayer.play();
players.get(currentNumOfVideo).setOnEndOfMedia(() -> {
players.get(currentNumOfVideo).stop();
if (currentNumOfVideo < players.size()) {
currentNumOfVideo += 1;
mv.setMediaPlayer(players.get(currentNumOfVideo));
players.get(currentNumOfVideo).play();
} else {
currentNumOfVideo = 0;
mv.setMediaPlayer(players.get(currentNumOfVideo));
players.get(currentNumOfVideo).play();
}
});
First video playing, when it ends second video starts. After second video MediaPlayer stops and didn't play third video.
I understand that because of my setOnEndOfMedia that is only on first MediaPlayer. When the second video starts it doesn't have setOnEndOfMedia. How can I setOnEndOfMedia on every video in my List<MediaPlayer>.
Personally, I would not create all the MediaPlayer instances ahead-of-time. Instead, get a list of Media objects or at least the URIs pointing to the media. Then create a method which is responsible for playing the next video when the current video ends. That method will dispose the old MediaPlayer, create the next MediaPlayer, and configure it to call the same method upon completion. For example:
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
public class Main extends Application {
private MediaView mediaView;
private List<Media> media;
private int curIndex;
#Override
public void start(Stage primaryStage) throws Exception {
media = ...; // get media from somewhere
mediaView = new MediaView();
primaryStage.setScene(new Scene(new StackPane(mediaView), 720, 480));
primaryStage.show();
playNextVideo();
}
private void playNextVideo() {
disposePlayer();
if (curIndex == media.size()) {
return; // no more videos to play
}
MediaPlayer player = new MediaPlayer(media.get(curIndex++));
player.setAutoPlay(true); // play ASAP
player.setOnEndOfMedia(this::playNextVideo); // play next video when this one ends
mediaView.setMediaPlayer(player);
}
private void disposePlayer() {
MediaPlayer player = mediaView.getMediaPlayer();
if (player != null) {
player.dispose(); // release resources
}
}
}
This may cause a pause between videos. If that's not acceptable you could create the next MediaPlayer ahead-of-time, either at the same time as the current MediaPlayer or when the current player reaches a certain timestamp (e.g. 10 seconds before the end). But I still wouldn't create all the MediaPlayer instances ahead-of-time.

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.

JavaFX SimpleStringProperty Prism error

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.

Resources