Java Timer - Updating Labels with Platform.runLater

后端 未结 1 2056
死守一世寂寞
死守一世寂寞 2021-01-26 08:59

This code sample is part of a Stopwatch class that is part of a larger project that is meant to be a desktop gui app that models after Android\'s Clock. I have labels for second

1条回答
  •  不思量自难忘°
    2021-01-26 09:44

    The Runnable you pass to Platform#runLater(Runnable) contains an infinite loop. That means you execute an infinite loop on the JavaFX Application Thread which is why your UI becomes unresponsive. If the FX thread is not free to do its job then no user-generated events can be processed and render "pulses" cannot be scheduled. That latter point is why the UI does not update despite you calling setText(...) continuously.

    The fix, if you want to continue your current approach, is to remove the for (;;) loop from your Runnable implementation. You setup the TimerTask to be executed once every millisecond which means all you have to do is calculate the new state and set the labels once per execution. In other words, the run() method is already "looped". For example:

    TimerTask task = new TimerTask() {
        @Override public void run() {
            Platform.runLater(() -> {
                // calculate new state...
    
                // update labels...
    
                // return (no loop!)
            });
        }
    };
    

    That said, there's no reason to use a background thread for this. I recommend using the animation API provided by JavaFX instead. It's asynchronous but executed on the FX thread, making it simpler to implement and reason about—using multiple threads is always more complicated. To do something similar to what you're currently doing you can use a Timeline or PauseTransition in place of the java.util.Timer. The JavaFX periodic background task Q&A gives some good examples of using animations for this purpose.

    Personally, I would use an AnimationTimer to implement a stopwatch. Here's an example:

    import java.util.concurrent.TimeUnit;
    import javafx.animation.AnimationTimer;
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanWrapper;
    import javafx.beans.property.ReadOnlyLongProperty;
    import javafx.beans.property.ReadOnlyLongWrapper;
    
    public class Stopwatch {
    
      private static long toMillis(long nanos) {
        return TimeUnit.NANOSECONDS.toMillis(nanos);
      }
    
      // value is in milliseconds
      private final ReadOnlyLongWrapper elapsedTime = new ReadOnlyLongWrapper(this, "elapsedTime");
      private void setElapsedTime(long elapsedTime) { this.elapsedTime.set(elapsedTime); }
      public final long getElapsedTime() { return elapsedTime.get(); }
      public final ReadOnlyLongProperty elapsedTimeProperty() { return elapsedTime.getReadOnlyProperty(); }
    
      private final ReadOnlyBooleanWrapper running = new ReadOnlyBooleanWrapper(this, "running");
      private void setRunning(boolean running) { this.running.set(running); }
      public final boolean isRunning() { return running.get(); }
      public final ReadOnlyBooleanProperty runningProperty() { return running.getReadOnlyProperty(); }
    
      private final Timer timer = new Timer();
    
      public void start() {
        if (!isRunning()) {
          timer.start();
          setRunning(true);
        }
      }
    
      public void stop() {
        if (isRunning()) {
          timer.pause();
          setRunning(false);
        }
      }
    
      public void reset() {
        timer.stopAndReset();
        setElapsedTime(0);
        setRunning(false);
      }
    
      private class Timer extends AnimationTimer {
    
        private long originTime = Long.MIN_VALUE;
        private long pauseTime = Long.MIN_VALUE;
        private boolean pausing;
    
        @Override
        public void handle(long now) {
          if (pausing) {
            pauseTime = toMillis(now);
            pausing = false;
            stop();
          } else {
            if (originTime == Long.MIN_VALUE) {
              originTime = toMillis(now);
            } else if (pauseTime != Long.MIN_VALUE) {
              originTime += toMillis(now) - pauseTime;
              pauseTime = Long.MIN_VALUE;
            }
    
            setElapsedTime(toMillis(now) - originTime);
          }
        }
    
        @Override
        public void start() {
          pausing = false;
          super.start();
        }
    
        void pause() {
          if (originTime != Long.MIN_VALUE) {
            pausing = true;
          } else {
            stop();
          }
        }
    
        void stopAndReset() {
          stop();
          originTime = Long.MIN_VALUE;
          pauseTime = Long.MIN_VALUE;
          pausing = false;
        }
      }
    }
    

    Warning: While the AnimationTimer is running the Stopwatch instance cannot be garbage collected.

    The above exposes a property, elapsedTime, which represents the elapsed time in milliseconds. From that value you can calculate the amount of days, hours, minutes, seconds, and milliseconds that have passed since you started the stopwatch. You simply have to listen to the property and update the UI when the property changes.

    0 讨论(0)
提交回复
热议问题