8

I'm trying to simulate a basic thermostat in an application GUI.

I want to update a label box value every 2 secs with the new temperature value.

For example, my intial temperature will be displayed as 68 degrees and updated to 69, to 70, etc. till 75 every 2 seconds.

This is a piece of code I wrote in Java fx. controlpanel is object of te form where the label box is present. It updates only the final value as 75. It doesnt update it every 2 secs. I have written a method pause to cause a 2 secs delay. All labels are updated with their final values but not updated every 2 secs. When I debug, I can see that the values are increased by one every 2 secs. This code is written in button onClick event

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {                                         
    int i=0;
    Timer asd = new Timer(1000,null);

    asd.setDelay(1000);

    while(i < 10)
    {
         jTextField1.setText(Integer.toString(i));
         i++;

         asd.start();
    }
 }  
Sergey Grinev
  • 34,078
  • 10
  • 128
  • 141
user1364861
  • 301
  • 1
  • 2
  • 16

3 Answers3

17

To solve your task using Timer you need to implement TimerTask with your code and use Timer#scheduleAtFixedRate method to run that code repeatedly:

Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            System.out.print("I would be called every 2 seconds");
        }
    }, 0, 2000);

Also note that calling any UI operations must be done on Swing UI thread (or FX UI thread if you are using JavaFX):

private int i = 0;
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
    Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    jTextField1.setText(Integer.toString(i++));
                }
            });
        }
    }, 0, 2000);
}

In case of JavaFX you need to update FX controls on "FX UI thread" instead of Swing one. To achieve that use javafx.application.Platform#runLater method instead of SwingUtilities

Sergey Grinev
  • 34,078
  • 10
  • 128
  • 141
15

Here is an alternate solution which uses a JavaFX animation Timeline instead of a Timer.

I like this solution because the animation framework ensures that everything happens on the JavaFX application thread, so you don't need to worry about threading issues.

temperatureprobe

import javafx.animation.*;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;

import java.util.Random;

public class ThermostatApp extends Application {
  @Override public void start(final Stage stage) throws Exception {
    final Thermostat       thermostat       = new Thermostat();
    final TemperatureLabel temperatureLabel = new TemperatureLabel(thermostat);

    VBox layout = new VBox(10);
    layout.getChildren().addAll(temperatureLabel);
    layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 20; -fx-font-size: 20;");

    stage.setScene(new Scene(layout));
    stage.show();
  }

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

class TemperatureLabel extends Label {
  public TemperatureLabel(final Thermostat thermostat) {
    textProperty().bind(
      Bindings.format(
        "%3d \u00B0F",
        thermostat.temperatureProperty()
      )
    );
  }
}

class Thermostat {
  private static final Duration PROBE_FREQUENCY = Duration.seconds(2);

  private final ReadOnlyIntegerWrapper temperature;
  private final TemperatureProbe       probe;
  private final Timeline               timeline;

  public ReadOnlyIntegerProperty temperatureProperty() {
    return temperature.getReadOnlyProperty();
  }

  public Thermostat() {
    probe       = new TemperatureProbe();
    temperature = new ReadOnlyIntegerWrapper(probe.readTemperature());

    timeline = new Timeline(
        new KeyFrame(
          Duration.ZERO,
          new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent actionEvent) {
              temperature.set(probe.readTemperature());
            }
          }
        ),
        new KeyFrame(
          PROBE_FREQUENCY
        )
    );
    timeline.setCycleCount(Timeline.INDEFINITE);
    timeline.play();
  }
}

class TemperatureProbe {
  private static final Random random = new Random();

  public int readTemperature() {
    return 72 + random.nextInt(6);
  }
}

The solution is based upon the countdown timer solution from: JavaFX: How to bind two values?

Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • What about using animation timer? Does that also run on the javafx application thread? – smac89 Oct 02 '19 at 06:48
  • 2
    Yes the animation timer does run on the JavaFX application, and yes it could be used in a way that would answer this question. Using a timeline is more direct though as it is a higher-level construct which exactly fulfills what the question is asking, invoking a UI update every 2 seconds in a way that is intuitive to the KeyFrame structure setup that Timeline uses. AnimationTimer is appropriate if you want to do a game loop or a physics model where the timing interval of an update might be constantly changing. – jewelsea Oct 02 '19 at 17:32
  • Thanks for the explanation, yes I just tried `Timeline` and it is much easier to use over `AnimationTimer` – smac89 Oct 02 '19 at 21:29
0

Calling Platform.runLater worked for me:

Platform.runLater(new Runnable() {

    @Override
    public void run() {

    }
});