Timer in JavaFX updating Label (Bindings)

795 Views Asked by At

I want to add a clock to my application that tells how long you have been doing the task. To simplify it, I have included a counter that increments every second in a new thread and update the label 'setTimer' with the counter number. For this I have a label fx:id="setTimer" in my .fxml file and imported it into my class.

 @FXML
    private Label setTimer;

And created another class in my class that extends the thread TimerTask and increments the counter by one on each call. Created a new Object 'text', which should always be updated with the current value of the counter.

    SimpleStringProperty text = new SimpleStringProperty("undefined");
public class MyTask extends TimerTask {
        @Override
        public void run() {
            counter++;
            text.set(Integer.toString(counter));
        }
    }

To have this class called every second I created a timer in the initialize method and set it to one second.

@Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        MyTask myTask = new MyTask();
        Timer timer = new Timer(true);
        timer.scheduleAtFixedRate(myTask, 0 , 1000);
        setTimer.textProperty().bind(text);
    }

At the moment I get the exception 'Not on FX application thread; currentThread = Timer-0'.

I've tried many ways to solve my problem, but I haven't gotten to the right point. My idea of ​​what I want to do should be clear, and I would be happy if someone could help me. My Problem is to update the changes of the counter in the GUI. It doesn't have to be solved the way I thought it would, just need a tip on how to best implement it.

Thank you

2

There are 2 best solutions below

2
On BEST ANSWER

Ok, my comments are too long. This is how I would try to do it.

  1. Start the stopwatch on the application being loaded
  2. Create a new thread that launches itself every so often.
  3. Inside there, get the time from the Stopwatch in seconds (sw.getTime(TimeUntis.seconds)). Convert that to hours and minutes if you want like shown in this SO post
  4. Then, write the time to the UI using Platform.runLater(new Runnable(){ /* access ui element and write time here */ });
1
On

Using Platform.runLater() in a background thread is kind of a messy kludge that should probably be avoided. JavaFX has mechanisms to handle this kind of thing which you should use. Specifically, Task<> is designed to allow background threads to update data which is connected to JavaFX screen elements which need to be updated on the FXAT.

You CAN do what you're trying to do with a JavaFX Task, but using the Java Timer inside of it seems impossible, since there doesn't seem to be any way for a Java thread to wait on a Timer to complete. So, instead I've used a "for" loop with a sleep to do the same thing. It's clumsy, but it does demonstrate how to connect partial results from a Task to screen display:

public class Sample1 extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Scene scene = new Scene(new Timer1(), 300, 200);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

public class Timer1 extends VBox {

    public Timer1() {
        Text time = new Text();
        Button startButton = new Button("Start");
        Button stopButton = new Button("Stop");
        getChildren().addAll(time, startButton, stopButton);
        startButton.setOnAction(startEvt -> {
            Task<Integer> timerFxTask = new Task<>() {

                {
                    updateValue(0);
                }

                @Override
                protected Integer call() throws Exception {
                    for (int counter = 0; counter <= 1000; counter++) {
                        sleep(1000);
                        updateValue(counter);
                    }
                    return 1000;
                }
            };
            stopButton.setOnAction(stopEvt -> timerFxTask.cancel());
            time.textProperty().bind(Bindings.createStringBinding(() -> timerFxTask.getValue().toString(),
                    timerFxTask.valueProperty()));
            Thread timerThread = new Thread(timerFxTask);
            timerThread.start();
        });
    }
}

But there is a better way to do what you're trying to do, which is essentially an animation - and JavaFX has a facility to do exactly this. Usually, people use animations to morph the appearance of JavaFX screen elements, but you can also use it to animate the contents of a Text over time as well. What I've done here is create an IntegerProperty which can be transitioned from a start value to an end value interpolated linearly over time and then bound that value to the TextProperty of a Text on the screen. So you see it update once per second.

public class Sample1 extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Scene scene = new Scene(new Timer2(), 300, 200);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

public class Timer2 extends VBox {

    public Timer2() {
        Text time = new Text();
        Button startButton = new Button("Start");
        Button stopButton = new Button("Stop");
        getChildren().addAll(time, startButton, stopButton);
        startButton.setOnAction(startEvt -> {
            IntegerProperty counter = new SimpleIntegerProperty(0);
            Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1000), new KeyValue(counter, 1000)));
            stopButton.setOnAction(stopEvt -> timeline.stop());
            time.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(counter.get()), counter));
            timeline.play();
        });
    }
}