Need clarification on changing data in javafx application thread - java

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

Related

Java - custom dialogs with background threads

I am trying to raise a custom loading dialog in java and then execute some synchronous function which takes a few seconds.
I would like the dialog to be present as long as the function executes and once it finishes I would close the dialog.
My Dialog looks as follows:
public abstract class LoaderControl extends Control implements SimpleDialogInfo {
private static final StyleablePropertyFactory<LoaderControl> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
private LoaderDialogResponse response;
private final DialogInfo dialogInfo;
private final SimpleStringProperty text = new SimpleStringProperty("");
private final SimpleBooleanProperty spinnerVisible = new SimpleBooleanProperty(true);
private UpdaterStates state;
private CloseDialogFunction onClose;
#Override
public void closeDialog(){
onClose.closeDialog();
}
#Override
public void setCloseDialog(CloseDialogFunction onClose){
this.onClose = onClose;
}
}
This is how I create it and show it:
public void createIndependentDialog(SimpleDialogInfo content, EventHandler<MouseEvent> onClose) {
Platform.runLater(() -> {
Stage stage = new Stage();
Parent p = new StackPane();
Scene s = new Scene(p);
stage.setScene(s);
MFXGenericDialog dialogContent = MFXGenericDialogBuilder.build()
.makeScrollable(true)
.setShowAlwaysOnTop(false)
.get();
MFXStageDialog dialog = MFXGenericDialogBuilder.build(dialogContent)
.toStageDialogBuilder()
.initModality(Modality.APPLICATION_MODAL)
.setDraggable(true)
.initOwner(stage)
.setTitle("Dialogs Preview")
.setOwnerNode(grid)
.setScrimPriority(ScrimPriority.WINDOW)
.setScrimOwner(true)
.get();
dialogContent.setMinSize(350, 200);
MFXFontIcon infoIcon = new MFXFontIcon(content.getDialogInfo().getIcon(), 18);
dialogContent.setHeaderIcon(infoIcon);
dialogContent.setHeaderText(content.getDialogInfo().getHeader());
dialogContent.setContent((Node) content);
MFXGenericDialog finalDialogContent = dialogContent;
MFXStageDialog finalDialog = dialog;
content.setCloseDialog(dialog::close);
convertDialogTo(String.format("mfx-%s-dialog", content.getDialogInfo().getDialogType()));
if(onClose != null)
dialogContent.setOnClose(onClose);
dialog.showAndWait();
});
}
This is how it looks like in the calling class:
DialogLoaderControlImpl preloader = new DialogLoaderControlImpl(new LoaderDialogInfo("Searching For New Versions"));
DialogsController.getInstance().createIndependentDialog(preloader);
someSynchronousMethod();
preloader.closeDialog();
The issue is that when I get to the "preloader.closeDialog()" line, the closeDialog function which should close the dialog is null (the onClose field is null).
In short:
The createIndependentDialog() method should raise a dialog and I would like to proceed to execute the method "someSynchronousMethod()" while the dialog is still shown and close it once the method finishes.
Please note that I use a Skin for the dialog which is not shown here but it works if I remove the Platform.runLater, but then it is stuck in the showAndWait() without advancing which is expected
Is there a way or a known design of some sort that will help to run tasks/methods with custom dialogs?
This can be done, but as pointed out in the comments, it is probably better to use some type of progress node. I used Alert in this example but Dialog should be very similar.
The key is closing the Alert/Dialog after the task is complete using the task's setOnSucceeded.
longRunningTask.setOnSucceeded((t) -> {
System.out.println("Task Done!");
alert.close();
});
Full Code
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application
{
public static void main(String[] args)
{
launch();
}
#Override
public void start(Stage stage)
{
Scene scene = new Scene(new StackPane(new Label("Hello World!")), 320, 240);
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
Task<Integer> longRunningTask = new Task<Integer>() {
#Override protected Integer call() throws Exception {
int iterations;
for (iterations = 0; iterations < 100000; iterations++) {
if (isCancelled()) {
break;
}
System.out.println("Iteration " + iterations);
}
return iterations;
}
};
Alert alert = new Alert(Alert.AlertType.INFORMATION);
Button okButton = (Button)alert.getDialogPane().lookupButton(ButtonType.OK);
okButton.setDisable(true);
longRunningTask.setOnSucceeded((t) -> {
System.out.println("Task Done!");
alert.close();
});
new Thread(longRunningTask).start();
alert.setTitle("Hello World");
alert.setHeaderText("Hello");
alert.setContentText("I will close when the long running task ends!");
alert.showAndWait();
}
}
Altered code from https://docs.oracle.com/javase/8/javafx/interoperability-tutorial/concurrency.htm.
One pitfall I can see is someone closing the Alert/Dialog before the task finishes.

UI not updating when nodes are being removed

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

JavaFX Application Thread to Task Communication

I've been learning about JavaFX's Tasks and using these to communicate with the Application thread using Platform.runLater or the task's updateValue method etc. However, my Task needs to know when a user presses a button on the GUI as this could change the value needed to be returned by the Task's updateValue method. How do I go about doing this? I know how to respond to button press events on single threaded applications but am not sure how do deal with it in a thread-safe manner.
Update:
This is what I have so far, is this a sensible way of implementing the button event?
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.PixelFormat;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.control.Button;
import java.nio.IntBuffer;
public class TaskExample extends Application {
private Canvas canvas;
private PixelWriter pixel_writer;
#Override
public void start(Stage primaryStage) throws Exception {
canvas = new Canvas(256, 256);
pixel_writer = canvas.getGraphicsContext2D().getPixelWriter();
MyTask task = new MyTask();
task.valueProperty().addListener((c) -> {
if(task.getValue() != null) {
update(task.getValue());
}
});
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
Button button = new Button("Button 1");
// On the button click event it calls the eventFired() method
button.setOnAction((event) -> {
task.eventFired();
});
Pane pane = new VBox();
pane.getChildren().addAll(canvas, button);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public void update(IntBuffer data) {
pixel_writer.setPixels(
0,
0,
256,
256,
PixelFormat.getIntArgbInstance(),
data,
256
);
}
public static void main(String[] args) {
launch(args);
}
class MyTask extends Task<IntBuffer> {
public void eventFired() {
System.out.println("Event fired");
}
public void update(IntBuffer data) {
updateValue(data);
}
#Override
protected IntBuffer call() throws InterruptedException {
while(true) {
for (int i=0; i<3; i++) {
Thread.sleep(1000);
IntBuffer data = IntBuffer.allocate(256*256);
for(int j=0; j<256*256; j++) {
switch(i) {
case 0: data.put(0xFF0000FF); break;
case 1: data.put(0xFF00FF00); break;
case 2: data.put(0xFFFF0000); break;
}
}
data.rewind();
update(data);
}
}
}
}
}
What I would do here is to think about ways to refactor what you are doing to avoid communication between two different threads. For example, instead of thinking of what you are doing as one long-running task that updates the UI as it progresses, think of it as a series of individual tasks that each update the UI when they complete. The ScheduledService class provides the machinery to manage these tasks and communicate between them and the FX Application Thread in a clean and safe way:
import java.nio.IntBuffer;
import java.util.Arrays;
import javafx.application.Application;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Button;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TaskExample extends Application {
private Canvas canvas;
private PixelWriter pixel_writer;
#Override
public void start(Stage primaryStage) throws Exception {
canvas = new Canvas(256, 256);
pixel_writer = canvas.getGraphicsContext2D().getPixelWriter();
MyService service = new MyService();
service.setPeriod(Duration.seconds(1));
service.valueProperty().addListener((ols, oldData, newData) -> {
if(newData != null) {
update(newData);
}
});
service.start();
Button button = new Button("Button 1");
Pane pane = new VBox();
pane.getChildren().addAll(canvas, button);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public void update(IntBuffer data) {
pixel_writer.setPixels(
0,
0,
256,
256,
PixelFormat.getIntArgbInstance(),
data,
256
);
}
public static void main(String[] args) {
launch(args);
}
class MyService extends ScheduledService<IntBuffer> {
// both instance variables accessed only on FX Application Thread:
private final int[] colors = {0xFF0000FF, 0xFF00FF00, 0xFFFF0000} ;
private int count = -1 ;
#Override
protected Task<IntBuffer> createTask() {
// invoked on FX Application Thread
count = (count + 1) % colors.length ;
return new MyTask(colors[count]);
}
}
class MyTask extends Task<IntBuffer> {
private final int color ;
MyTask(int color) {
// invoked on FX Application Thread:
this.color = color ;
}
#Override
protected IntBuffer call() {
// invoked on background thread:
IntBuffer data = IntBuffer.allocate(256*256);
int[] a = new int[256*256];
Arrays.fill(a, color);
data.put(a, 0, a.length);
data.rewind();
return data ;
}
}
}
You haven't been very specific about how the UI is supposed to interact with the background thread, but if you wanted to change the behavior of the service when the button is pressed, you would now be changing the behavior of the createTask method, which is invoked on the FX Application Thread, instead of changing the behavior of a method already running on a different thread. This avoids any "low-level" concerns about synchronization.
For example:
class MyService extends ScheduledService<IntBuffer> {
// all instance variables accessed only on FX Application Thread:
private final int[][] colors = {
{0xFF0000FF, 0xFF00FF00, 0xFFFF0000},
{0xFF00FFFF, 0xFFFF00FF, 0xFFFFFF00}
};
private int count = -1 ;
private int scheme = 0 ;
#Override
protected Task<IntBuffer> createTask() {
// invoked on FX Application Thread
count = (count + 1) % colors[scheme].length ;
return new MyTask(colors[scheme][count]);
}
public void changeScheme() {
// invoked on FX Application Thread
scheme = (scheme + 1) % colors.length ;
}
}
and then just
button.setOnAction(e -> service.changeScheme());
Adding a call to service.restart(); here will force the change to happen as soon as possible:
button.setOnAction(e -> {
service.changeScheme();
service.restart();
});
There is pretty much always a way to refactor your code to take advantage of the library classes like this to avoid low-level communication between threads.

Why can I update TableView from a non-UI thread but not ListView?

I have encountered something odd in JavaFX (java version 1.8.0_91). It was my understanding that if one wants to update the UI from a separate thread, one must either use Platform.runLater(taskThatUpdates) or one of the tools in the javafx.concurrent package.
However, if I have a TableView on which I call .setItems(someObservableList), I can update someObservableList from a separate thread and see the corresponding changes to my TableView without the expected Exception in thread "X" java.lang.IllegalStateException: Not on FX application thread; currentThread = X error.
If I replace TableView with ListView, the expected error occurs.
Example code for situation #1: updating a TableView from a different thread, with no call to Platform.runLater()--and no error.
public class Test extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) throws Exception {
// Create a table of integers with one column to display
TableView<Integer> data = new TableView<>();
TableColumn<Integer, Integer> num = new TableColumn<>("Number");
num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
data.getColumns().add(num);
// Create a window & add the table
VBox root = new VBox();
Scene scene = new Scene(root);
root.getChildren().addAll(data);
stage.setScene(scene);
stage.show();
// Create a list of numbers & bind the table to it
ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
data.setItems(someNumbers);
// Add a new number every second from a different thread
new Thread( () -> {
for (;;) {
try {
Thread.sleep(1000);
someNumbers.add((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Example code for situation #2: updating a ListView from a different thread, with no call to Platform.runLater()--produces an error.
public class Test extends Application {
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage stage) throws Exception {
// Create a list of integers (instead of a table)
ListView<Integer> data = new ListView<>();
// Create a window & add the table
VBox root = new VBox();
Scene scene = new Scene(root);
root.getChildren().addAll(data);
stage.setScene(scene);
stage.show();
// Create a list of numbers & bind the table to it
ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
data.setItems(someNumbers);
// Add a new number every second from a different thread
new Thread( () -> {
for (;;) {
try {
Thread.sleep(1000);
someNumbers.add((int) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Note that the only difference is the instantiation of data as a ListView<Integer> rather than a TableView<Integer>.
So what gives here? Is this happening because of the call to TableColumn::setCellValueFactory() in the first example?--that's my intuition. I would like to know why one does not cause an error and the other does, and more specifically what the rules are for how the .setItems call binds data to the view.
As #James_D already mentioned, you should not update things that belongs to the FX Application Thread on any other thread. But in your example you've been updating the ObservableList in another thread. No matter why this work for TableView and for ListView not, it is the wrong way of doing it.
Have a look at the Task class if you want to perform intermediate updates on backing ObservableLists.
From the Task-API-Doc
A Task Which Returns An ObservableList
Because the ListView, TableView, and other UI controls and scene graph
nodes make use of ObservableList, it is common to want to create and
return an ObservableList from a Task. When you do not care to display
intermediate values, the easiest way to correctly write such a Task is
simply to construct an ObservableList within the call method, and then
return it at the conclusion of the Task.
and another hint:
A Task Which Returns Partial Results
Sometimes you want to create a Task which will return partial results.
Perhaps you are building a complex scene graph and want to show the
scene graph as it is being constructed. Or perhaps you are reading a
large amount of data over the network and want to display the entries
in a TableView as the data is arriving. In such cases, there is some
shared state available both to the FX Application Thread and the
background thread. Great care must be taken to never update shared
state from any thread other than the FX Application Thread.
The easiest way to do this is to take advantage of the
updateValue(Object) method. This method may be called repeatedly from
the background thread. Updates are coalesced to prevent saturation of
the FX event queue. This means you can call it as frequently as you
like from the background thread but only the most recent set is
ultimately set.
An example of such a Task class for doing intermediate updates on ObservableLists for TableView and ListView is:
import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class IntermediateTest extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Integer> tv = new TableView<>();
TableColumn<Integer, Integer> num = new TableColumn<>("Number");
num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
tv.getColumns().add(num);
PartialResultsTask prt = new PartialResultsTask();
tv.setItems(prt.getPartialResults());
ListView<Integer> lv = new ListView<>();
PartialResultsTask prt1 = new PartialResultsTask();
lv.setItems(prt1.getPartialResults());
new Thread(prt).start();
new Thread(prt1).start();
// Create a window & add the table
VBox root = new VBox();
root.getChildren().addAll(tv, lv);
Scene scene = new Scene(root, 300, 450);
primaryStage.setTitle("Data-Adding");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public class PartialResultsTask extends Task<ObservableList<Integer>> {
private ReadOnlyObjectWrapper<ObservableList<Integer>> partialResults
= new ReadOnlyObjectWrapper<>(this, "partialResults",
FXCollections.observableArrayList(new ArrayList()));
public final ObservableList getPartialResults() {
return partialResults.get();
}
public final ReadOnlyObjectProperty<ObservableList<Integer>> partialResultsProperty() {
return partialResults.getReadOnlyProperty();
}
#Override
protected ObservableList call() throws Exception {
updateMessage("Creating Integers...");
Random rnd = new Random();
for (int i = 0; i < 10; i++) {
Thread.sleep(1000);
if (isCancelled()) {
break;
}
final Integer r = rnd.ints(100, 10000).findFirst().getAsInt();
Platform.runLater(() -> {
getPartialResults().add(r);
});
updateProgress(i, 10);
}
return partialResults.get();
}
}
}

JavaFX: Add a lot of Nodes to a FlowPane in a Thread

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

Categories

Resources