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();
}
}
Platform.runLater(...); that works just fine for me. If you create too many runnables on the FX Application Thread queue, you will degrade performance, but they are all guaranteed to run and guaranteed to run in the order they are submitted. You should post your attempt to usePlatform.runLater(). - James_Diin one thread and accessing it in another. Of course you will not get things the correct order. - James_D