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.
Calling refresh() forces the TableView control to recreate and repopulate the cells necessary to populate the visual bounds of the control. In other words, this forces the TableView to update what it is showing to the user.
The TableView class provides built-in capabilities to sort data in columns. Users can alter the order of data by clicking column headers. The first click enables the ascending sorting order, the second click enables descending sorting order, and the third click disables sorting. By default, no sorting is applied.
To select a row with a specific index you can use the select(int) method. Here is an example of selecting a single row with a given index in a JavaFX TableView: selectionModel. select(1);
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();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With