I have a FlowPane where a lot of elements are added (~1000), each one of these elements contains an ImageView and some other elements and is loaded from an fxml file. With this many entries, it takes a long time until the nodes are rendered, and then they are displayed all at once.
Because of that, I would like to add the nodes one by one, using a thread. I tried the following:
Platform.runLater(new Runnable() {
#Override
public void run() {
for (Object v : collection.getObjects()) {
addEntry(v);
flowPane.requestLayout();
}
}
});
What addEntry() does is basically just loading the Node from the fxml and adding it to the flowPane.
With this code, the flowPane is rendered immediately, but the nodes still appear all at once.
Can someone point me in the right direction? Thanks!
Your runlater is doing everything at once. It's like one call to update the gui with all the nodes. It needs to be called repeatedly in a loop.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class FlowPaneTest extends Application {
#Override
public void start(Stage primaryStage) {
final FlowPane flow = new FlowPane();
flow.getChildren().add(new Text("Starting "));
Task task = new Task() {
#Override protected Void call() throws Exception {
//for (Object v : collection.getObjects()){
for (int i = 0; i < 100; i++) {//use your loop instead
Platform.runLater(()->{
flow.getChildren().add(new Text("adding node "));
});
Thread.sleep(100);
}
return null;
}
};
Scene scene = new Scene(flow, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
}
}
Related
I'm trying to remove all the nodes from my pane sequentially 1 by 1 so I can see each line being removed.To do this I have made a new thread and used the task class and wrapped the method delWalls() in a Platform.runLater() . I then used Thread.sleep thinking it would slow the loop slow so I could see the UI updating as each line is removed However what happens is the whole UI freezes up and then after the loop is done all the nodes have disappeared? Is there a way around this ... thanks
*all nodes are lines btw
//loop calls delWalls() 1458 times to delete all 1458 nodes sequentailly
Task task = new Task<Void>() {
#Override
public Void call() {
Platform.runLater(() -> {
try {
for (int i = 0; i <= 1458 - 1; i++) {
Thread.sleep(2);
delWalls();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
return null;
}
};
new Thread(task).start();
}
//delWalls method deletes one node each time it is called.
public void delWalls() throws InterruptedException {
pane.getChildren().remove(0);
}
As #MadProgrammer said, you need to work with Timeline to get the desired effect.
Below is a quick sample demo of how it can be done. Click "Add" to add nodes sequentially, and once all 10 nodes are added, click "remove" to remove them one by one.
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class RemoveNodes_Demo extends Application {
#Override
public void start(Stage stage) throws Exception {
FlowPane pane = new FlowPane();
pane.setVgap(10);
pane.setHgap(10);
Button button1 = new Button("Add Nodes");
button1.setOnAction(e->{
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(400), x -> {
StackPane sp = new StackPane();
sp.setMinSize(100,100);
sp.setStyle("-fx-background-color:black,red;-fx-background-insets:0,2;");
pane.getChildren().add(sp);
}));
timeline.setCycleCount(10);
timeline.play();
});
Button button2 = new Button("Remove Nodes");
button2.setOnAction(e->{
if(!pane.getChildren().isEmpty()){
int count = pane.getChildren().size();
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(400), x -> {
if(!pane.getChildren().isEmpty()){
pane.getChildren().remove(0);
}
}));
timeline.setCycleCount(count);
timeline.play();
}
});
VBox root = new VBox(button1, button2,pane);
root.setSpacing(10);
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
}
public static void main(String... a) {
Application.launch(a);
}
}
I've been migrating a project of mine to JavaFX and started running into thread issues. I'll attach a short example. After much searching I managed to sort out the problem. I can't change the tableView data outside of the fx application thread. I switched my code over from using SwingWorker to a Task.
At first, that worked until I added a change listener to the table's observableList. I then received the error "Not on FX application thread;"
The error happened inside the onChanged method when I attempted to update a Label's value. I resolved this by wrapping it inside Platform.runLater().
I'm just confused as to why changing the label says it wasn't on the application thread. On what thread was this running? Also, am I adding rows to my table correctly by using a task? In my actual application, I could be adding 50k rows hence why the separate thread so as to not lock up the UI.
public class Temp extends Application{
private ObservableList<String> libraryList = FXCollections.observableArrayList();
public void start(Stage stage) {
Label statusLabel = new Label("stuff goes here");
TableView<String> table = new TableView<String>(libraryList);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
TableColumn<String, String> col = new TableColumn<String, String>("Stuff");
col.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(cellData.getValue()));
table.getColumns().add(col);
libraryList.addListener(new ListChangeListener<String>() {
public void onChanged(Change change) {
// Problem was caused by setting the label's text (prior to adding the runLater)
Platform.runLater(()->{
statusLabel.setText(libraryList.size()+" entries");
});
}
});
// dummy stuff
libraryList.add("foo");
libraryList.add("bar");
Button b = new Button("Press Me");
b.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent e) {
FileTask task = new FileTask();
new Thread(task).start();
}
});
BorderPane mainBody = new BorderPane();
mainBody.setTop(statusLabel);
mainBody.setCenter(table);
mainBody.setBottom(b);
Scene scene = new Scene(mainBody);
stage.setScene(scene);
stage.show();
}
class FileTask extends Task<Boolean>{
public FileTask(){
}
protected Boolean call() throws Exception{
Random rand = new Random();
for(int i = 0; i < 5; i++) {
String s = ""+rand.nextInt(Integer.MAX_VALUE);
libraryList.add(s);
}
return true;
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
It's working as expected, you have the application thread and the task thread, they kind of look like this:
App ------\ ----------------------
Task \-label.setText() Exception
You can't do any UI work on anything but the App thread, so adding your RunLater does this:
App ----\ -------------/ RunLater(label.setText()) ----------
Task \-add to list/
which works well. There are a few ways to manage this based on what you want to do:
If you want to update the Table list within the Task, you can move the RunLater call to inside the task, rather than inside the handler, this way it will still get you back to the App thread. This way if you're actually on the app thread, there is no need to call RunLater within the handler.
App ---\ -----------------------/ label.setText() ----------
Task \-RunLater(add to list)/
Another option is to just use a Task> which will run on the other thread, and return the full list of strings that are going to be added. This is more likely what you want if you're making network calls in the task, get a list of items, then add them once they are all downloaded to the table.
App -----\ ------------------------------/ label.setText() ---/ add to table list-------
Task \-build list, update progress /- return final list /
Hopefully the formatting stays.
Consider encapsulating the information needed by the view in a separate class (typically referred to as model).
The view should respond to changes in the model by means of listener or binding.
You can use a thread or threads, to update the model:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Temp extends Application{
#Override
public void start(Stage stage) {
Model model = new Model();
Label statusLabel = new Label("stuff goes here");
TableView<String> table = new TableView<>(model.getLibraryList());
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
TableColumn<String, String> col = new TableColumn<>("Stuff");
col.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(cellData.getValue()));
table.getColumns().add(col);
statusLabel.textProperty().bind(Bindings.concat(model.sizeProperty.asString(), " entries"));
// dummy stuff
model.add("foo"); model.add("bar");
Button b = new Button("Press Me");
b.setOnAction(e -> {
FileTask task = new FileTask(model);
new Thread(task).start();
});
BorderPane mainBody = new BorderPane();
mainBody.setTop(statusLabel);
mainBody.setCenter(table);
mainBody.setBottom(b);
Scene scene = new Scene(mainBody);
stage.setScene(scene);
stage.show();
}
class Model {
private final ObservableList<String> libraryList;
private final IntegerProperty sizeProperty;
Model(){
libraryList = FXCollections.observableArrayList();
sizeProperty = new SimpleIntegerProperty(0);
libraryList.addListener((ListChangeListener<String>) change -> {
Platform.runLater(()->sizeProperty.set(libraryList.size()));
});
}
//synchronize if you want to use multithread
void add(String string) {
Platform.runLater(()->sizeProperty.set(libraryList.add(string)));
}
ObservableList<String> getLibraryList() {
return libraryList;
}
IntegerProperty getSizeProperty() {
return sizeProperty;
}
}
class FileTask implements Runnable{
private final Model model;
public FileTask(Model model){
this.model = model;
}
#Override
public void run() {
Random rand = new Random();
for(int i = 0; i < 5; i++) {
String s = ""+rand.nextInt(Integer.MAX_VALUE);
model.add(s);
}
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
I want to immediately after increasing the value of i appear in the label
Example:
-in i=0 show 0
-in i=1 show 01
-in i=2 show 012
Can You Help me
import javafx.application.Application;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Example extends Application{
#Override
public void start (Stage primaryStage) {
Pane pane=new Pane();
Label label=new Label();
Button bt=new Button("Start");
pane.getChildren().addAll(bt,label);
bt.setOnAction(e->{
for (int i=0;i<10000000;i++) label.setText(label.getText()+i);
});
Scene scene = new Scene(pane,1000,500);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The problem is that you update the label's value while you are on the user interface's thread. JavaFX works with a model where the updates are done at each 'tick' (60 fps). All the updates done are only visible once your eventhander's code has finished.
Additionally, given that this is a long running task it will result in an unresponsive user interface.
You should use a Worker to do the long running task. See the tutorial on asynchronous processing. Note that it will not guarantee that you will see all values as the worker can be quicker than the user interface updates and the system will coalesce these updates.
You can use Timeline to accomplish this task.
import java.util.concurrent.atomic.AtomicLong;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
*
* #author blj0011
*/
public class JavaFXApplication177 extends Application
{
#Override
public void start(Stage primaryStage)
{
AtomicLong i = new AtomicLong();
Label label = new Label();
Button btn = new Button();
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(.5), (ActionEvent event) -> {//This controls how fast this should run. This example happens every half of a second
label.setText(label.getText() + Long.toString(i.getAndIncrement()));
}));
timeline.setCycleCount(10000000);//This controls the amount of time this should run
timeline.setOnFinished(event -> {//This tells what to do once cycle count is reached
btn.setDisable(false);
});
btn.setText("Start");
btn.setOnAction((ActionEvent event) -> {
btn.setDisable(true);
timeline.play();
});
StackPane stackPane = new StackPane(label);
VBox root = new VBox(stackPane, new StackPane(btn));
VBox.setVgrow(stackPane, Priority.ALWAYS);
Scene scene = new Scene(root, 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);
}
}
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Test5 extends Application {
private String text = "";
private int i;
#Override // Override the start method in the Application class
public void start(Stage primaryStage) {
StackPane pane = new StackPane();
Label lblText = new Label("");
pane.getChildren().add(lblText);
new Thread(new Runnable() {
#Override
public void run() {
try {
for (i=0;i<10000;i++) {
Platform.runLater(new Runnable() { // Run from JavaFX GUI
#Override
public void run() {
lblText.setText(lblText.getText()+i);
}
});
Thread.sleep(200);
}
}
catch (InterruptedException ex) {
}
}
}).start();
Scene scene = new Scene(pane, 200, 50);
primaryStage.setTitle("FlashText"); // Set the stage title
primaryStage.setScene(scene); // Place the scene in the stage
primaryStage.show(); // Display the stage
}
}
Service<Void> service = new Service<Void>() {
#Override
protected Task<Void> createTask() {
return new Task<Void>() {
#Override
protected Void call() throws Exception {
//Your First Task #1
//Here UI won't be interrupted
Platform.runLater(new Runnable() {
#Override
public void run() {
//Your Second Task After Completion Of First One #2
}
});
return null;
}
};
}
};
service.start();
}
#1. The task that you want to perform in the background ex. loading the data has to be placed here. It's working great for me.
#2. Once the background task is finished this thread will be executed so Ui and background thread will run separately and smoothly.
I know it's too late for this answer but I just wanted to share what I did this might help!
I've read similar questions but my UI is still freezing when I add many nodes to a VBox. I've provided a fully functional program below which demonstrates the problem clearly.
After 4 seconds, the ProgressIndicator freezes as 5000 nodes are added to the VBox. This is an excessive amount used to demonstrate the JavaFX thread freezing despite using Task (for non-UI work) and then Platform.runLater() for adding the nodes to the scene.
In my actual application, instead of adding blank TitlePanes I'm adding a TitlePane obtained from an FXML file via new FXMLLoader(), and the resulting loader.load() then initializes the associated controller, which in turn initializes some moderately demanding computations - which are being performed on the JavaFX thread! So even though I'm adding closer to 250 nodes, the UI still freezes when the Platform.runLater is eventually used. How do I keep the ProgressIndicator from freezing until the red background is shown?
Full Example:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Accordion;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.Timer;
import java.util.TimerTask;
public class FreezingUI extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
VBox mainBox = new VBox();
mainBox.setPrefHeight(800);
mainBox.setStyle("-fx-background-color: #f1f1f1; -fx-alignment: center");
Label label = new Label();
label.setMinHeight(50);
label.setStyle("-fx-font-size: 24px; -fx-text-fill: #515151");
ProgressIndicator progressIndicator = new ProgressIndicator(ProgressIndicator.INDETERMINATE_PROGRESS);
mainBox.getChildren().addAll(progressIndicator, label);
Scene scene = new Scene(mainBox, 500, 800);
primaryStage.setScene(scene);
primaryStage.show();
Timer timer = new Timer();
TimerTask task = new TimerTask(){
private int i = 4;
public void run(){
if (i >= 0) {
Platform.runLater(()->{
label.setText("Freezing in " + i--);
});
}else{
addNodesToUI(mainBox);
timer.cancel();
}
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
}
private void addNodesToUI(VBox mainBox) {
final int[] i = {0};
Platform.runLater(() -> {
Accordion temp = new Accordion();
mainBox.getChildren().add(temp);
while (i[0] < 5000) {
TitledPane tp = new TitledPane();
tp.setPrefWidth(300);
tp.setPrefHeight(12);
tp.setPadding(new Insets(10));
tp.setStyle("-fx-background-color: red;");
temp.getPanes().add(tp);
i[0]++;
}
});
}
}
This is happening because you are asking the UI thread to do a big bunch of things in one big lump. There is no way for the UI thread to exit the while loop until all 5000 nodes are created and added to the scene.
private void addNodesToUI(VBox mainBox) {
final int[] i = {0};
Accordion temp = new Accordion();
Platform.runLater(() -> {
mainBox.getChildren().add(temp);
});
while (i[0] < 5000) {
TitledPane tp = new TitledPane();
tp.setPrefWidth(300);
tp.setPrefHeight(12);
tp.setPadding(new Insets(10));
tp.setStyle("-fx-background-color: red;");
i[0]++;
Platform.runLater(() -> {
temp.getPanes().add(tp);
});
}
}
This will allow your nodes to be created in small batches. This way, the UI thread can attempt to render the UI while the nodes are added progressively.
For your FXML case, you can create and load the FXML in another thread. You only need to be in UI thread when you attach a scene branch into the scene. However, I would suspect that would only mitigate the effects, as you are still going to attach a big chunk at one go.
I have a Pane in which i add and remove nodes during a computation. Therefor i save a boolean which is set to true if the computation is running. of course i do some handling on starting and terminating a computation.
What i want to do now is: disable all MouseEvents on the children of the Pane if the computation starts and reenable them if the computation is terminated.
My tries until now where limited to completly remove the EventHandlers, but then i can't add them again later.
unfortunately i couldn't find a way to do this, so i hope for help here :)
Thanks in advance
Assuming you have implemented the long-running computation as a Task or Service (and if you haven't, you should probably consider doing so), you can just do something along the following lines:
Pane pane ;
// ...
Task<ResultType> computation = ... ;
pane.disableProperty().bind(computation.runningProperty());
new Thread(computation).start();
Calling setDisable(true) on a node will disable all its child nodes, so this will disable all the children of the pane, and re-enable them when the task is no longer running.
Here's an SSCCE:
import javafx.application.Application;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
public class ComputationSimulation extends Application {
#Override
public void start(Stage primaryStage) {
// text fields for input:
TextField xInput = new TextField();
TextField yInput = new TextField();
// Service for performing the computation.
// (For demo here, the computation just computes the sum of
// the two input values. Obviously this doesn't take long, so
// a random pause is inserted.)
Service<Integer> service = new Service<Integer>() {
#Override
protected Task<Integer> createTask() {
final int x = readTextField(xInput);
final int y = readTextField(yInput);
return new Task<Integer>() {
#Override
public Integer call() throws Exception {
// simulate long-running computation...
Thread.sleep((int)(Math.random() * 2000) + 1000);
// this doesn't really take much time(!):
return x + y ;
}
};
}
};
// Label to show result. Just use binding to bind to value of computation:
Label result = new Label();
result.textProperty().bind(service.valueProperty().asString());
// Button starts computation by restarting service:
Button compute = new Button("Compute");
compute.setOnAction(e -> service.restart());
// Pane to hold controls:
GridPane pane = new GridPane();
// Disable pane (and consequently all its children) when computation is running:
pane.disableProperty().bind(service.runningProperty());
// layout etc:
pane.setHgap(5);
pane.setVgap(10);
pane.addRow(0, new Label("x:"), xInput);
pane.addRow(1, new Label("y:"), yInput);
pane.addRow(2, new Label("Total:"), result);
pane.add(compute, 1, 3);
ColumnConstraints left = new ColumnConstraints();
left.setHalignment(HPos.RIGHT);
left.setHgrow(Priority.NEVER);
pane.getColumnConstraints().addAll(left, new ColumnConstraints());
pane.setPadding(new Insets(10));
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
// converts text in text field to an int if possible
// returns 0 if not valid text, and sets text accordingly
private int readTextField(TextField text) {
try {
return Integer.parseInt(text.getText());
} catch (NumberFormatException e) {
text.setText("0");
return 0 ;
}
}
public static void main(String[] args) {
launch(args);
}
}