Java 计时器 - 使用 Platform.runLater 更新标签
Java Timer - Updating Labels with Platform.runLater
此代码示例是秒表 class 的一部分,秒表是一个更大项目的一部分,该项目旨在成为模仿 Android 时钟的桌面 gui 应用程序。我有秒、分钟、小时等的标签,这些标签应该从计时器任务内的无限 while 循环更新,该任务是 运行 而布尔状态为真。 while 循环应该实时更新 GUI 标签。我让定时器任务每毫秒执行一次。为什么我的 GUI 在程序开始更新第一个标签时立即挂起,我该如何解决?下面是代码。
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){
}
您传递给 Platform#runLater(Runnable)
的 Runnable
包含一个无限循环。这意味着您在 JavaFX 应用程序线程 上执行无限循环,这就是您的 UI 变得无响应的原因。如果 FX 线程没有空闲来完成它的工作,那么就无法处理 user-generated 事件,并且无法安排渲染 "pulses"。后一点就是为什么 UI 尽管您连续调用 setText(...)
却不更新的原因。
如果您想继续当前的方法,解决方法是从 Runnable
实施中删除 for (;;)
循环。您将 TimerTask
设置为每毫秒执行一次,这意味着您所要做的就是计算新状态并在每次执行时设置一次标签。也就是说,run()
方法已经是"looped"了。例如:
TimerTask task = new TimerTask() {
@Override public void run() {
Platform.runLater(() -> {
// calculate new state...
// update labels...
// return (no loop!)
});
}
};
也就是说,没有理由为此使用后台线程。我建议使用 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 提供了一些为此目的使用动画的好例子。
就个人而言,我会使用 AnimationTimer
来实现秒表。这是一个例子:
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;
}
}
}
警告: 虽然 AnimationTimer
是 运行,但 Stopwatch
实例不能是垃圾收藏了。
上面暴露了一个属性,elapsedTime
,表示经过的时间,单位是毫秒。根据该值,您可以计算自启动秒表以来经过的天数、小时数、分钟数、秒数和毫秒数。您只需要收听 属性 并在 属性 更改时更新 UI。
此代码示例是秒表 class 的一部分,秒表是一个更大项目的一部分,该项目旨在成为模仿 Android 时钟的桌面 gui 应用程序。我有秒、分钟、小时等的标签,这些标签应该从计时器任务内的无限 while 循环更新,该任务是 运行 而布尔状态为真。 while 循环应该实时更新 GUI 标签。我让定时器任务每毫秒执行一次。为什么我的 GUI 在程序开始更新第一个标签时立即挂起,我该如何解决?下面是代码。
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){
}
您传递给 Platform#runLater(Runnable)
的 Runnable
包含一个无限循环。这意味着您在 JavaFX 应用程序线程 上执行无限循环,这就是您的 UI 变得无响应的原因。如果 FX 线程没有空闲来完成它的工作,那么就无法处理 user-generated 事件,并且无法安排渲染 "pulses"。后一点就是为什么 UI 尽管您连续调用 setText(...)
却不更新的原因。
如果您想继续当前的方法,解决方法是从 Runnable
实施中删除 for (;;)
循环。您将 TimerTask
设置为每毫秒执行一次,这意味着您所要做的就是计算新状态并在每次执行时设置一次标签。也就是说,run()
方法已经是"looped"了。例如:
TimerTask task = new TimerTask() {
@Override public void run() {
Platform.runLater(() -> {
// calculate new state...
// update labels...
// return (no loop!)
});
}
};
也就是说,没有理由为此使用后台线程。我建议使用 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 提供了一些为此目的使用动画的好例子。
就个人而言,我会使用 AnimationTimer
来实现秒表。这是一个例子:
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;
}
}
}
警告: 虽然 AnimationTimer
是 运行,但 Stopwatch
实例不能是垃圾收藏了。
上面暴露了一个属性,elapsedTime
,表示经过的时间,单位是毫秒。根据该值,您可以计算自启动秒表以来经过的天数、小时数、分钟数、秒数和毫秒数。您只需要收听 属性 并在 属性 更改时更新 UI。