I'm coming from a Swing background and trying to pick up JavaFx.
This ObservableList is being filled with strings, and added to a ListView.
When I add an item to the observable list in the same thread, everything works fine.
However, when I try to add an item to the observable list from a different thread, the items are being added twice. For the life of me, I cannot figure out why. Debug statements show the Thread is in fact only executing once.
Here's a fully working example:
import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.paint.Color;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.util.Callback;
public class FeedPanelViewer extends Application {
public static void main(String[] args) {
launch(args);
}
String greeting = "<html><body><p><strong>hi ya'll</strong></p></body></html>";
#Override
public void start(Stage stage) {
ObservableList<String> names = FXCollections.observableArrayList("Matthew", "Hannah", "Stephan", "Denise");
ListView<String> listView = new ListView<String>(names);
stage.setScene(new Scene(listView));
stage.show();
listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> list) {
return new HtmlFormatCell();
}
});
// This thread is definitely only adding items once
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Platform.runLater(() -> {
System.out.println("Got here");
names.add(greeting);
names.add("andrew");
});
}).start();
}
public class HtmlFormatCell extends ListCell<String> {
public HtmlFormatCell() {
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
if (item.contains("<p>")) {
Platform.runLater(() -> {
WebView web = new WebView();
WebEngine engine = web.getEngine();
engine.loadContent(item);
web.setPrefHeight(50);
web.setPrefWidth(300);
web.autosize();
setText("");
setGraphic(web);
});
} else {
setText(item == null ? "" : "-" + item);
setTextFill(Color.BLUE);
if (isSelected()) {
setTextFill(Color.GREEN);
}
}
}
}
}
}
If I comment out the two lines new Thread(() -> { and }).start();, this is what I see:
And with the Thread wrapped around the addition of the two list elements, I'm seeing this, which is rendering the cells twice, even though the thread is only executing once:
Can anyone help point out what is going on?
Thanks very much.
In updateItem, you should have an else branch for when the item is empty (i.e. when item == null, or, better yet, when empty is true) and clear the cell, i.e. setText(null); setGraphic(null);.
Your updateItem method should something like this
if(!empty) {
// populate the cell with graphic and/or text
} else {
setText(null);
setGraphic(null);
}
In your example, it is likely that the last two cells are empty, but have not been cleared.
Note 1: The way ListView allocates and populates cells is (seemingly) unpredictable and it can do a lot of redundant item updates.
Note 2: This does not by itself explain the difference in behavior you get with your two versions. My guess is that without the Thread wrapper, the call gets executed before the ListView's first layout pass, while with the Thread wrapper, it layouts the initial items and then updates the layout for the added items. This, together with the previous note, could explain the difference in the results.
Note 3: In updateItem, you don't have to wrap your calls in Platform.runLater, since updateItem is already executed on the JavaFX application thread.
Related
I want to have an editable ComboBox that contains items of some type (e.g. integers), where I can add and delete items, allow the user to edit existing items, and allow for duplicate items.
The problem is that whenever the user edits an existing item, and changes its value to a value of an item already present in the list, the editor (textfield) causes the selection model to select the item already present in the list instead of modifying the edited item.
I tried circumventing this by creating a wrapper class that contains the item and has an unique index. However, this causes problems in StringConverter.fromString because I have to create a new wrapper every time it converts.
An easy solution I think would be to stop the editor from searching through the items whenever an edit is made, so that the selection model does not change.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.Arrays;
public class ComboBoxTest extends Application {
private final ComboBox<Integer> comboBox = new ComboBox<>();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final Group root = new Group();
root.getChildren().add(comboBox);
final Scene scene = new Scene(root, 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
comboBox.getItems().addAll(Arrays.asList(1, 2, 3, 4));
comboBox.setConverter(
new StringConverter<Integer>() {
#Override
public String toString(Integer integer) {
return integer == null ? "" : String.valueOf(integer);
}
#Override
public Integer fromString(String s) {
return Integer.parseInt(s);
}
});
comboBox.setPromptText("select value");
comboBox.setEditable(true);
}
}
My question is about a strange behavior of a copound in a tableView.
The aim is to display an list of players participating to a match in a tableView. The informations displayed are the name of the player, his score, his number of successive busts and an indicator to know if it is his turn to play.
This indicator is a RadioButton as it looks better than a checkBox. When a turn comes to a player, the RadioButton will be setSelected(true), else, it'll be setSelected(false). The true or false information is given by the player's information used in the tableView. Of course, the RadioButton is in "read-only" mode.
Here is my code for the tableView :
TableView<PlayerProgressInformations> playersProgressTable = new TableView<PlayerProgressInformations>();
defineColumns(playersProgressTable);
playersProgressTable.setItems(playersAtThisTable);
for the defineColumns method :
TableColumn<PlayerProgressInformations, Boolean> colPlaying = new TableColumn<PlayerProgressInformations, Boolean>("Tour");
colPlaying.setPrefWidth(70);
TableCell<PlayerProgressInformations, Boolean>>) new RadioButton());
colPlaying.setCellValueFactory(new Callback<CellDataFeatures<PlayerProgressInformations, Boolean>, ObservableValue<Boolean>>() {
public ObservableValue<Boolean> call(CellDataFeatures<PlayerProgressInformations, Boolean> p) {
return new SimpleBooleanProperty(p.getValue().isPlaying());
}
});
colPlaying.setCellFactory(new Callback<TableColumn<PlayerProgressInformations, Boolean>, TableCell<PlayerProgressInformations, Boolean>>() {
#Override
public TableCell<PlayerProgressInformations, Boolean> call( TableColumn<PlayerProgressInformations, Boolean> param) {
RadioButtonCell<PlayerProgressInformations, Boolean> radioButtonCell =
new RadioButtonCell<PlayerProgressInformations, Boolean>();
return radioButtonCell;
}
});
And the RadioButtoCell class :
private class RadioButtonCell<S, T> extends TableCell<S,T> {
public RadioButtonCell () {
}
#Override
protected void updateItem (T item, boolean empty) {
System.out.println("Count value : "+count); //Indicator to check how many times is used the method "updateItem"
count+=1;
if (item instanceof Boolean) {
Boolean myBoolean = (Boolean) item;
if (!empty) {
System.out.println("Valeur du boolean : "+item);
RadioButton radioButton = new RadioButton();
radioButton.setDisable(true);
radioButton.setSelected(myBoolean);
radioButton.setStyle("-fx-opacity: 1");
setGraphic(radioButton);
}
}
}
}
The stranges behaviors are the ones bellow :
Problem 1 : When I join a first player to the game's table, the updateItem methode is called 17 times.
If a second one is joining, this number for the first player increase to 57, or 60 and 17 for the second player.
Finally, if a third one is joining, it is 90 times for the first player, 57 or 60 for the second one and 17 for the third one.
Why does this method is so often called ? And why those specific numbers ?
More over, after this "initialization" the method is called 2 times more after each turn as I expected : one time to unselect a RadioButton and one time to select the next one.
Problem 2 : When a first player join the table, he is of course the first to play and, on his screen, the RadioButton is selected.
When a second player join the table, this second player see a RadioButton selected for the first player and a RadioButton unselected for himself. That's the behavior expected. But for the first player, the 2 RadioButtons are unselected.
And if a 3rd player join the table, he'll see the RadioButton selected for the first player and unselected for himself and the 2nd player. This is also the result expected. But, for the second and the first players, all of the 3 RadioButtons are unselected.
Why this strange behavior ? More over, after a first turn, all the RadioButtons appears selected or unselected as expected as if the bug disappeared.
Could you help me to understand what is happening and how to solve those bugs ?
Thank you very much
Sample Code
Here is a table implementation that works. I guess you could compare it to your implementation to see what the differences are. Probably the main "fix" is the updateItem handling of empty and null values.
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
public class PlayerViewer extends Application {
private final ObservableList<Player> data =
FXCollections.observableArrayList(
new Player("Jacob", true),
new Player("Isabella", false),
new Player("Ethan", true)
);
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
TableView<Player> table = new TableView<>(data);
table.setPrefHeight(130);
table.setPrefWidth(150);
TableColumn<Player, String> handleCol = new TableColumn<>("Handle");
handleCol.setCellValueFactory(new PropertyValueFactory<>("handle"));
table.getColumns().add(handleCol);
TableColumn<Player, Boolean> playingCol = new TableColumn<>("Playing");
playingCol.setCellValueFactory(new PropertyValueFactory<>("playing"));
playingCol.setCellFactory(param -> new TableCell<>() {
RadioButton indicator = new RadioButton();
{
indicator.setDisable(true);
indicator.setStyle("-fx-opacity: 1");
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Boolean isPlaying, boolean empty) {
super.updateItem(isPlaying, empty);
if (empty || isPlaying == null) {
setGraphic(null);
} else {
indicator.setSelected(isPlaying);
setGraphic(indicator);
}
}
});
table.getColumns().add(playingCol);
stage.setScene(new Scene(table));
stage.show();
}
public static class Player {
private final SimpleStringProperty handle;
private final SimpleBooleanProperty playing;
private Player(String handle, boolean playing) {
this.handle = new SimpleStringProperty(handle);
this.playing = new SimpleBooleanProperty(playing);
}
public SimpleStringProperty handleProperty() {
return handle;
}
public String getHandle() {
return handle.get();
}
public void setHandle(String handle) {
this.handle.set(handle);
}
public SimpleBooleanProperty playingProperty() {
return playing;
}
public boolean isPlaying() {
return playing.get();
}
public void setPlaying(boolean playing) {
this.playing.set(playing);
}
}
}
Additional comments on your question
In terms "Problem 1", of how many times updateItem is called, that's an internal toolkit thing, your code shouldn't really care about that, it just needs to make sure that whenever it is called, that it does the right thing.
Regarding your "Problem 2", regarding the interaction of multiple views for multiple players, who knows? Impossible to say without further additional code, which would probably end up making this question too broad anyway. If you have a specific question about how to handle the interaction of displays for multiple players you will need to expand and clarify your question (likely as a new question with a mcve).
For your implementation, I would advise restyling the radio button (via CSS), so that it doesn't look like a standard user-selectable radio button (because you have disabled the selection capability and then removed the default disabled opacity setting). Or, you could use a custom indicator control such as the Bulb class from this answer, which might be preferred.
Here's another sample that works. (I was almost done with it when #jewelsea posted, so figured I would go ahead and post it anyway. It is similar but the differences between the two might be useful.)
The main issues in your code that I can see are:
Your cell value factory returns a new BooleanProperty every time it is invoked. This means the cell can't observe the correct property, and has no opportunity to update itself when the value changes. You should use JavaFX properties in your model class so that the cell can observe a single property.
Your cell implementation doesn't call the superclass implementation of updateItem(...). This means basic functionality won't happen, so updates may or may not occur when they are needed, and selection won't work, etc.
Your cell implementation doesn't deal with empty cells, so those will not necessarily display correctly.
In this implementation I used a Game class to keep the "current player" and gave each player a reference to the (same) game instance. The playing property in the Player is exposed as a read-only property and its value is bound to the game's current player. This means that only one player can have playing==true, without lots of "wiring" between the players.
The buttons allow for testing adding new players (who are automatically set as the "current" player) or moving the current player to the next player.
import java.util.Iterator;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class PlayerTable extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Player> table = new TableView<>();
TableColumn<Player, String> playerCol = new TableColumn<>("Name");
playerCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));
TableColumn<Player, Boolean> playingColumn = new TableColumn<>("Playing");
playingColumn.setCellValueFactory(cellData -> cellData.getValue().playingProperty());
playingColumn.setCellFactory(tc -> new RadioButtonTableCell<>());
table.getColumns().add(playerCol);
table.getColumns().add(playingColumn);
Game game = new Game();
Button newPlayerButton = new Button("New Player");
newPlayerButton.setOnAction(e -> addNewPlayer(game, table.getItems()));
Button nextPlayerButton = new Button("Next player");
nextPlayerButton.setOnAction(e -> selectNextPlayer(game, table.getItems()));
HBox controls = new HBox(5, newPlayerButton, nextPlayerButton);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(5));
BorderPane root = new BorderPane();
root.setCenter(table);
root.setBottom(controls);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addNewPlayer(Game game, List<Player> players) {
int playerNumber = players.size() + 1 ;
Player newPlayer = new Player(game, "Player "+playerNumber);
game.setCurrentPlayer(newPlayer);
players.add(newPlayer);
}
private void selectNextPlayer(Game game, List<Player> players) {
if (players.isEmpty()) return ;
for (Iterator<Player> i = players.iterator() ; i.hasNext() ;) {
if (i.next() == game.getCurrentPlayer()) {
if (i.hasNext()) {
game.setCurrentPlayer(i.next());
} else {
game.setCurrentPlayer(players.get(0));
}
return ;
}
}
game.setCurrentPlayer(players.get(0));
}
public static class RadioButtonTableCell<S> extends TableCell<S, Boolean> {
private RadioButton radioButton ;
public RadioButtonTableCell() {
radioButton = new RadioButton();
radioButton.setDisable(true);
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
radioButton.setSelected(item);
setGraphic(radioButton);
}
}
};
public static class Game {
private final ObjectProperty<Player> currentPlayer = new SimpleObjectProperty<>() ;
public ObjectProperty<Player> currentPlayerProperty() {
return currentPlayer ;
}
public Player getCurrentPlayer() {
return currentPlayerProperty().get();
}
public void setCurrentPlayer(Player player) {
currentPlayerProperty().set(player);
}
}
public static class Player {
private final String name ;
private final ReadOnlyBooleanWrapper playing ;
public Player(Game game, String name) {
this.name = name ;
playing = new ReadOnlyBooleanWrapper() ;
playing.bind(game.currentPlayerProperty().isEqualTo(this));
}
public String getName() {
return name ;
}
public ReadOnlyBooleanProperty playingProperty() {
return playing.getReadOnlyProperty() ;
}
public boolean isPlaying() {
return playingProperty().get();
}
}
public static void main(String[] args) {
launch(args);
}
}
I am using ControlsFX's CustomTextField. When I click on one of the autocomplete options, I need to clear the TextField and create a Tag so I can add it to a FlowPane. How do I set up an OnClick or OnSelectionChange listener or override the current OnClick?
I took a look at the CustomTextField documentation and I can't find a clear way of doing what you want. So I will guess you have to implement it yourself or to find a workaround. In case you decide to choose the second choice here is something that I believe works very well :
import java.util.ArrayList;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestApplication extends Application {
private boolean addedBySelection = false;
private ArrayList<String> tagList = new ArrayList<>();
private FlowPane tagPane;
#Override
public void start(Stage primaryStage) throws Exception {
VBox mainPane = new VBox(10);
mainPane.setStyle("-fx-background-color : white");
mainPane.setPadding(new Insets(15));
mainPane.setAlignment(Pos.CENTER);
tagPane = new FlowPane(15, 10);
tagPane.setPrefHeight(50);
CustomTextField field = new CustomTextField() {
#Override
public void paste() {
super.paste();
addedBySelection = false;
}
};
field.setOnKeyPressed(e -> {
addedBySelection = false;
});
field.setOnKeyReleased(e -> {
addedBySelection = true;
});
field.textProperty().addListener(e -> {
if (addedBySelection) {
System.out.println("Text Changed from the suggession list ");
addTag(field.getText());
addedBySelection = false;
field.clear();
addedBySelection = true;
} else {
System.out.println("User Input (Mouse paste, or typing) ");
}
});
TextFields.bindAutoCompletion(field, new String[] { "Java", "C++", "C#", "Python", "Haskell" });
mainPane.getChildren().addAll(field, tagPane);
Scene scene = new Scene(mainPane, 200, 100);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addTag(String tag) {
if (!tagList.contains(tag)) {
tagList.add(tag);
Label tagLabel = new Label(tag);
tagLabel.setStyle("-fx-background-color : #E1ECF4; -fx-text-fill : #6A739D;");
tagPane.getChildren().add(tagLabel);
}
}
public static void main(String[] args) {
launch(args);
}
}
I tried to keep it a simple as it could. The code above is doing exactly what you are after. The logic is to set a listener on the textProperty ( cause we can't set one on selection with the mouse from the autocomplete list ) and somehow find out if the user actually triggers the event using the autocomplete list or not. Thus I have a flag looking for user inputs (ex. key press) and 'releasing' the flag each time the keys are released. We have to catch the paste action as well in order to avoid mistakes if the user pastes a text on the field. One more last thing is the way we clear the field. We have to set our flag to false because the field.clear() will trigger an event as well and we don't want to fall into an event loop.
Note : With the current workaround, you will see that you are able to make a selection from the autocomplete list by pressing the enter key as well.
I have a JavaFX TableView with single cell selection enabled. When a user selects a cell the selection highlight will flicker when new data is added to the table
A small example that demonstrates the problem:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class SelectionBug extends Application
{
public static void main(
String[] args)
{
Application.launch(args);
}
#Override
public void start(
Stage primaryStage) throws Exception
{
final ObservableList<DummyData> list = FXCollections.observableArrayList();
final TableView<DummyData> tableView = new TableView<>(list);
tableView.getColumns().add(createColumn(item -> item.getColumn1()));
tableView.getColumns().add(createColumn(item -> item.getColumn2()));
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.getSelectionModel().setCellSelectionEnabled(true);
final Thread thread = new Thread(() ->
{
while (true)
{
Platform.runLater(() -> list.add(new DummyData()));
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
//do nothing
}
}
});
thread.setDaemon(true);
thread.start();
final BorderPane root = new BorderPane();
root.setCenter(tableView);
primaryStage.setScene(new Scene(root, 500, 500));
primaryStage.show();
}
private TableColumn<DummyData, String> createColumn(
final Callback<DummyData, String> dataGetter)
{
final TableColumn<DummyData, String> column = new TableColumn<>();
column.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(dataGetter.call(cellData.getValue())));
return column;
}
private static class DummyData
{
private final String mColumn1;
private final String mColumn2;
public DummyData()
{
final Random ramdom = new Random();
mColumn1 = Integer.toString(ramdom.nextInt(1000));
mColumn2 = Integer.toString(ramdom.nextInt(1000));
}
public String getColumn1()
{
return mColumn1;
}
public String getColumn2()
{
return mColumn2;
}
}
}
If you run that and select a cell, you'll see the flickering.
My digging so far suggests it's to do with cell recycling in the table view: I changed the Cell Factory to assign and log out a unique ID and the cell's item for each cell object and found that the ID <-> item relationship is not constant; each cell object gets moved around the tableview showing different data with every update to the data model. This means that the selected property is modified on every update, causing the pseudoClassState to change. I suspect it's a subtle timing issue with when the cell is taken out of the tableview and when the cell's selected property is changed
Has anyone else seen this problem, and does anyone have any kind of workaround?
Probably a bit late for you, but I had a similar problem and managed to solve it by wrapping the TableView in an extra AnchorPane.
First of all I'm new at JavaFX, so sorry if this question is stupid. How can I get an return Object from a Task?
Heres my Problem:
I want to get a List of Objects from a Mock. The Mock has a delay from 1 up to 5 seconds. But I dont want, that my GUI freeze in this time.
In Java.Swing it was easy with an Thread, but JavaFX has, as far as I know, Tasks.
I've read much tutorials, but everywhere they return a text property. So here is my question: how can I set the value of an Object with the result of the calculation from a Task/Thread (in my case a List)
Thank you.
Ray,
You are right in that the examples seem to gloss over getting back results from tasks. There are two ways that you can get back results that I know of:
Through the getValue() method of the Task class (this is the way I have done it)
Through the get() method of the parent FutureTask class (I haven't used this, but in principle it should work).
With the first getValue() aproach you need to make sure the task sets the value through the updateValue(...) method in the call method of the task. Then put a listener on the WorkerStateEvent
myTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
#SuppressWarnings("unchecked")
#Override
public void handle(WorkerStateEvent event) {
ReturnType rt = (ReturnType) event.getSource().getValue()
// ... other stuff to do here ...
}
});
The first approach is a little verbose but it works and allows for some more complicated operations after the task has finished.
The second approach is a little simpler and straightforward but doesn't give you as much control over what to do when the task finishes. With the get() method of FutureTask, the code should block until the Task returns with the value. So using it should be as simple as:
//
// Start the task in a thread (using whatever approach you like)
//before calling the get() method.
//
ReturnType rt = myTask.get();
I have used Future objects with other code, but I have not used FutureTask with the FX api, so I can not tell you if there are hidden gotchas in it.
Good luck,
chooks
The Task is a generic type. That means that if you apply a type to a Task like Task<Integer> the Task class will have functions that returns you an Integer. One of this functions is the valueProperty(), that can be bind to other scene elements. Being shown in a Label, or whatever. I recommend you to read the javafx binding tutorial, to get a better comprehension.
Here is a sample of Task using or their properties.
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestingTasks extends Application{
public static void main(String[] args) {launch(args);}
#Override
public void start(Stage stage) throws Exception {
VBox vbox = new VBox(10);
vbox.setAlignment(Pos.TOP_CENTER);
ListView<String> list = new ListView<>();
HBox hbox = new HBox(10);
hbox.setAlignment(Pos.CENTER_LEFT);
Label labelMessage = new Label();
hbox.getChildren().addAll(new Label("Message: "), labelMessage);
ProgressBar progress = new ProgressBar(-1);
progress.setVisible(false);
Button button = new Button("Executing Task");
button.setOnAction(event(button, list, progress, labelMessage));
vbox.getChildren().addAll(list, hbox, button, progress);
Scene scene = new Scene(vbox, 400, 300);
stage.setScene(scene);
stage.show();
}
private EventHandler<ActionEvent> event(final Button button, final ListView<String> list, final ProgressBar progress, final Label labelMessage) {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
Task<ObservableList<String>> task = generateTask();
list.itemsProperty().bind(task.valueProperty());
progress.visibleProperty().bind(task.runningProperty());
labelMessage.textProperty().bind(task.messageProperty());
button.disableProperty().bind(task.runningProperty());
task.runningProperty().addListener(listenerRunningTask());
Thread t = new Thread(task);
t.setDaemon(true);
t.start();
}
};
}
private Task<ObservableList<String>> generateTask() {
return new Task<ObservableList<String>>() {
#Override
protected ObservableList<String> call() throws Exception {
updateMessage("Waiting...");
Thread.sleep(5000);
updateMessage("Waking up");
return FXCollections.observableArrayList("One", "Two", "Three");
}
};
}
private ChangeListener<? super Boolean> listenerRunningTask() {
return new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if(oldValue && !newValue){
//TODO when finish
}
}
};
}
}
So basically, you can return a variable in the Task, or wait the Task ends and execute something, create your own bindings...
If you want to modify something of the screen from the thread, you need to do it from the FX Thread, the Task function call is outside the FX Thread, for that reason the screen it isn't freeze. But all the bind elements will occur in the FX Thread, so are safe to modify the GUI.
If you want to modify safely the GUI from a not FX Thread, just do:
Platform.runLater(new Runnable() {
#Override
public void run() {
//Safe modification in the FX Thread
}
});
Also take a look on concurrency in JavaFX2. This explain more deeply the concurrency, Service, Task...
Hope it helps!