问题
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 seconds, minutes, hours, etc. that are supposed to be updated from an infinite while loop that is inside a timer task which is ran while a boolean state is true. The while loop is supposed to update the GUI labels with the real time. I have the timer task execute every millisecond. Why does my GUI hang as soon as the program gets to updating the first Label and how can I resolve it? Below is the code.
static int Milliseconds = 0;
static int Seconds = 0;
static int Minutes = 0;
static int Hours = 0;
static int Days = 0;
static Boolean State = false;
public static void display(){
Stage window = new Stage();
window.initModality(Modality.APPLICATION_MODAL);
window.setTitle("Timer");
window.setMinWidth(250);
window.setMinHeight(500);
GridPane gp = new GridPane();
Label days = new Label("0");
gp.setConstraints(days, 0,0);
Label hours = new Label("0");
gp.setConstraints(hours, 1,0);
Label minutes = new Label("0");
gp.setConstraints(minutes,2,0);
Label seconds = new Label("0");
gp.setConstraints(seconds,3,0);
Label milliseconds = new Label("0");
gp.setConstraints(milliseconds, 4,0);
//Handler mainHandler = new Handler()
// Task<Void> longRunningTask = new Task<Void>(){}
Timer mt = new Timer();
//Platform.runLater is not updating gui. It hangs the gui instead
TimerTask tm = new TimerTask() {
@Override
public void run() {
Platform.runLater(() -> {
for (; ; ) {
long timebefore = System.currentTimeMillis();
if (State) {
try {
if (Milliseconds > 999) {
Milliseconds = 0;
Seconds++;
}
if (Seconds > 59) {
Milliseconds = 0;
Seconds = 0;
Minutes++;
}
if (Minutes > 59) {
Milliseconds = 0;
Seconds = 0;
Minutes = 0;
Hours++;
}
if (Hours > 23) {
Milliseconds = 0;
Seconds = 0;
Minutes = 0;
Hours = 0;
Days++;
}
milliseconds.setText(" : " + Milliseconds);
Milliseconds++;
seconds.setText(" : " + Seconds);
minutes.setText(" : " + Minutes);
hours.setText(" : " + Hours);
days.setText(" : " + Days);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
};
Button start = new Button("Start");
gp.setConstraints(start, 0,1);
start.setOnAction(event -> {
State = true;
mt.scheduleAtFixedRate(tm, 1,1);
});
Button stop = new Button("Stop");
gp.setConstraints(stop,1,1);
stop.setOnAction(event-> {
State = false;
});
Button restart = new Button("Restart");
gp.setConstraints(restart, 2,1);
restart.setOnAction(event-> {
State = false;
Milliseconds = 0;
Seconds = 0;
Minutes = 0;
Hours = 0;
Days = 0;
});
gp.getChildren().addAll(milliseconds,seconds, minutes, hours, days, start, stop, restart);
Scene scene = new Scene(gp);
window.setScene(scene);
window.showAndWait();
}
public void Start(Timer mt){
}
回答1:
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.
来源:https://stackoverflow.com/questions/61330924/java-timer-updating-labels-with-platform-runlater