The important thing here, is you need to understand that any update made to the UI needs be done from within the UI's main thread. You should also avoid blocking the UI's main thread, this will prevent the UI from been updated.
To start with, have a read through Concurrency in JavaFX
When ever you're dealing with time or duration, you should always use a dedicated library, like java.time
. Why? Because there isn't actually 24 hours in a day or 60 seconds in a minute - I know, weird isn't it.
You could...
Use Timeline
import java.time.LocalDateTime;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
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.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Test extends Application {
private LocalDateTime startTime;
private Timeline timer;
private Label time;
@Override
public void start(Stage primaryStage) {
time = new Label("...");
Button btn = new Button();
btn.setText("Start");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
if (timer != null) {
timer.stop();
}
startTime = LocalDateTime.now();
timer = new Timeline(new KeyFrame(Duration.millis(500), new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
LocalDateTime now = LocalDateTime.now();
java.time.Duration duration = java.time.Duration.between(startTime, now);
time.setText(format(duration));
}
}));
timer.setCycleCount(Timeline.INDEFINITE);
timer.play();
}
});
VBox root = new VBox();
root.setAlignment(Pos.CENTER);
root.getChildren().add(time);
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
protected String format(java.time.Duration duration) {
long hours = duration.toHours();
long mins = duration.minusHours(hours).toMinutes();
long seconds = duration.minusMinutes(mins).toMillis() / 1000;
return String.format("%02dh %02dm %02ds", hours, mins, seconds);
}
public static void main(String[] args) {
launch(args);
}
}
Use AnimationTimer
This is possibly the preferred solution, as the timer is called in sync with the frame refresh of the animation engine
import java.time.LocalDateTime;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
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.layout.VBox;
import javafx.stage.Stage;
public class Test extends Application {
private LocalDateTime startTime;
private AnimationTimer timer;
private Label time;
@Override
public void start(Stage primaryStage) {
time = new Label("...");
Button btn = new Button();
btn.setText("Start");
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
if (timer != null) {
timer.stop();
}
startTime = LocalDateTime.now();
timer = new AnimationTimer() {
@Override
public void handle(long click) {
LocalDateTime now = LocalDateTime.now();
java.time.Duration duration = java.time.Duration.between(startTime, now);
time.setText(format(duration));
}
};
timer.start();
}
});
VBox root = new VBox();
root.setAlignment(Pos.CENTER);
root.getChildren().add(time);
root.getChildren().add(btn);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
protected String format(java.time.Duration duration) {
long hours = duration.toHours();
long mins = duration.minusHours(hours).toMinutes();
long seconds = duration.minusMinutes(mins).toMillis() / 1000;
return String.format("%02dh %02dm %02ds", hours, mins, seconds);
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}