Java FXML 更新 UI 新线程抛出错误
Java FXML Updating UI with new Thread throws error
我正在尝试定期更新 FXML 格式的 Google 地图标记。我试着用一个定时器和一个新的 Thread
来做到这一点,但无法让它工作。
我用一个简单的任务测试了新的 Thread
来更新我的 UI 中的 TextField
,效果很好。
然而,当我使用我需要更新地图的实际代码时:
@FXML
public void handleTracking() throws IOException, InterruptedException {
new Thread() {
@Override
public void run() {
while (true) {
try {
double ar[] = FileImport.getGpsPosition();
System.out.println("Latitude: " + ar[0] + " Longitude: " + ar[1]);
double Ltd = ar[0];
double Lng = ar[1];
webEngine.executeScript(""
+ "window.lat = " + Ltd + ";"
+ "window.lon = " + Lng + ";"
+ "document.goToLocation(window.lat, window.lon);");
try {
Thread.sleep(555);
} catch (InterruptedException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (IOException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}.start();
}
我收到输出消息:
Exception in thread "Thread-26" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-26
at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1216)
at javafx.scene.web.WebEngine.executeScript(WebEngine.java:980)
at de.fkfs.v2x.eval.FXMLDocumentController.run(FXMLDocumentController.java:84=)
当我使用计时器时会发生类似的事情,它用于更新标签的任务,但是如果我尝试更新标记位置,它会抛出消息:
Exception in thread "Timer-0" java.lang.IllegalStateException: Not on FX application thread; currentThread = Timer-0
所有 JavaFX UI 元素必须在 FX 应用程序线程内更新!
如果使用其他线程,请务必使用 platform.Runlater()
来更新您的 UI 元素!
对 UI 的更新,包括对 webEngine.executeScript(...)
的调用必须 在 FX 应用程序线程上执行。
另一方面,FX 应用程序线程(实际上)是用于呈现 UI 和处理用户输入的线程。因此,如果您使用无限循环或其他长 运行ning 进程阻塞此线程,或者如果您在该线程上为 运行 安排了太多事情,您将使 UI 无响应。
您在代码中尝试做的似乎是尽可能快地更新 UI。如果您将循环放在 FX 应用程序线程中,您将完全阻塞它:如果您将它放在后台线程中并使用 Platform.runLater(...)
安排更新,您将用太多更新淹没 FX 应用程序线程并阻止它执行它的正常工作,它会变得没有反应。
这里的一般解决方案围绕这样一个事实,即如此频繁地更新 UI 确实是多余的。人眼只能以有限的速度检测到可见变化,并且在技术方面您受到限制,例如物理屏幕和底层图形软件的刷新率。 JavaFX 尝试以不超过 60Hz(在当前实现中)的频率更新 UI。因此,比底层 JavaFX 工具包更新场景更频繁地更新真的没有意义。
AnimationTimer
提供了一个 handle
方法,保证在每次场景更新时调用一次,无论发生的频率如何。 AnimationTimer.handle(...)
在 FX 应用程序线程上调用,因此您可以在此处安全地更改 UI。因此,您可以通过以下方式实施跟踪:
private AnimationTimer tracker ;
public void initialize() {
tracker = new AnimationTimer() {
@Override
public void handle(long timestamp) {
try {
double ar[] = FileImport.getGpsPosition();
// System.out.println("Latitude: " + ar[0] + " Longitude: " + ar[1]);
double Ltd = ar[0];
double Lng = ar[1];
webEngine.executeScript(""
+ "window.lat = " + Ltd + ";"
+ "window.lon = " + Lng + ";"
+ "document.goToLocation(window.lat, window.lon);");
} catch (IOException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
};
}
@FXML
public void handleTracking() {
tracker.start();
}
这里唯一需要注意的是,因为 handle()
是在 FX Application Thread 上调用的,所以你不应该在这里执行任何 long-运行ning 代码。看起来您的 FileImport.getGpsPosition()
方法执行了一些 IO 操作,因此它可能应该委托给后台线程。这里的技巧是 JavaFX classes 使用的技巧,例如 Task
,是从后台线程不断更新一个值,并且 仅 安排一个调用 Platform.runLater(...)
如果一个尚未挂起。
首先,只需定义一个简单的 class 来表示位置(使其不可变以便线程安全):
class Location {
private final double longitude ;
private final double latitude ;
public Location(double longitude, double latitude) {
this.longitude = longitude ;
this.latitude = latitude ;
}
public double getLongitude() {
return longitude ;
}
public double getLatitude() {
return latitude ;
}
}
现在:
@FXML
private void handleTracking() {
AtomicReference<Location> location = new AtomicReference<>(null);
Thread thread = new Thread(() -> {
try {
while (true) {
double[] ar[] = FileImport.getGpsPosition();
Location loc = new Location(ar[0], ar[1]);
if (location.getAndSet(loc) == null) {
Platform.runLater(() -> {
Location updateLoc = location.getAndSet(null);
webEngine.executeScript(""
+ "window.lat = " + updateLoc.getLatitude() + ";"
+ "window.lon = " + updateLoc.getLongitude() + ";"
+ "document.goToLocation(window.lat, window.lon);");
});
}
}
} catch (IOException exc) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
});
thread.setDaemon(true);
thread.start();
}
它的工作方式是为当前位置创建一个(线程安全的)持有者,并尽快更新它。当它更新它时,它(原子地)还会检查当前值是否为 null
。如果是 null
,它会通过 Platform.runLater()
安排 UI 更新。如果不是,它只是更新值但不安排新的 UI 更新。
UI 更新(原子地)获取当前(即最新)值并将其设置为 null,表示它已准备好接收新的 UI 更新。然后它处理新的更新。
这样您就可以 "throttle" UI 更新,以便仅在处理当前更新时安排新的更新,避免 UI 线程被太多请求淹没。
我正在尝试定期更新 FXML 格式的 Google 地图标记。我试着用一个定时器和一个新的 Thread
来做到这一点,但无法让它工作。
我用一个简单的任务测试了新的 Thread
来更新我的 UI 中的 TextField
,效果很好。
然而,当我使用我需要更新地图的实际代码时:
@FXML
public void handleTracking() throws IOException, InterruptedException {
new Thread() {
@Override
public void run() {
while (true) {
try {
double ar[] = FileImport.getGpsPosition();
System.out.println("Latitude: " + ar[0] + " Longitude: " + ar[1]);
double Ltd = ar[0];
double Lng = ar[1];
webEngine.executeScript(""
+ "window.lat = " + Ltd + ";"
+ "window.lon = " + Lng + ";"
+ "document.goToLocation(window.lat, window.lon);");
try {
Thread.sleep(555);
} catch (InterruptedException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
} catch (IOException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}.start();
}
我收到输出消息:
Exception in thread "Thread-26" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-26
at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1216)
at javafx.scene.web.WebEngine.executeScript(WebEngine.java:980)
at de.fkfs.v2x.eval.FXMLDocumentController.run(FXMLDocumentController.java:84=)
当我使用计时器时会发生类似的事情,它用于更新标签的任务,但是如果我尝试更新标记位置,它会抛出消息:
Exception in thread "Timer-0" java.lang.IllegalStateException: Not on FX application thread; currentThread = Timer-0
所有 JavaFX UI 元素必须在 FX 应用程序线程内更新!
如果使用其他线程,请务必使用 platform.Runlater()
来更新您的 UI 元素!
对 UI 的更新,包括对 webEngine.executeScript(...)
的调用必须 在 FX 应用程序线程上执行。
另一方面,FX 应用程序线程(实际上)是用于呈现 UI 和处理用户输入的线程。因此,如果您使用无限循环或其他长 运行ning 进程阻塞此线程,或者如果您在该线程上为 运行 安排了太多事情,您将使 UI 无响应。
您在代码中尝试做的似乎是尽可能快地更新 UI。如果您将循环放在 FX 应用程序线程中,您将完全阻塞它:如果您将它放在后台线程中并使用 Platform.runLater(...)
安排更新,您将用太多更新淹没 FX 应用程序线程并阻止它执行它的正常工作,它会变得没有反应。
这里的一般解决方案围绕这样一个事实,即如此频繁地更新 UI 确实是多余的。人眼只能以有限的速度检测到可见变化,并且在技术方面您受到限制,例如物理屏幕和底层图形软件的刷新率。 JavaFX 尝试以不超过 60Hz(在当前实现中)的频率更新 UI。因此,比底层 JavaFX 工具包更新场景更频繁地更新真的没有意义。
AnimationTimer
提供了一个 handle
方法,保证在每次场景更新时调用一次,无论发生的频率如何。 AnimationTimer.handle(...)
在 FX 应用程序线程上调用,因此您可以在此处安全地更改 UI。因此,您可以通过以下方式实施跟踪:
private AnimationTimer tracker ;
public void initialize() {
tracker = new AnimationTimer() {
@Override
public void handle(long timestamp) {
try {
double ar[] = FileImport.getGpsPosition();
// System.out.println("Latitude: " + ar[0] + " Longitude: " + ar[1]);
double Ltd = ar[0];
double Lng = ar[1];
webEngine.executeScript(""
+ "window.lat = " + Ltd + ";"
+ "window.lon = " + Lng + ";"
+ "document.goToLocation(window.lat, window.lon);");
} catch (IOException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
};
}
@FXML
public void handleTracking() {
tracker.start();
}
这里唯一需要注意的是,因为 handle()
是在 FX Application Thread 上调用的,所以你不应该在这里执行任何 long-运行ning 代码。看起来您的 FileImport.getGpsPosition()
方法执行了一些 IO 操作,因此它可能应该委托给后台线程。这里的技巧是 JavaFX classes 使用的技巧,例如 Task
,是从后台线程不断更新一个值,并且 仅 安排一个调用 Platform.runLater(...)
如果一个尚未挂起。
首先,只需定义一个简单的 class 来表示位置(使其不可变以便线程安全):
class Location {
private final double longitude ;
private final double latitude ;
public Location(double longitude, double latitude) {
this.longitude = longitude ;
this.latitude = latitude ;
}
public double getLongitude() {
return longitude ;
}
public double getLatitude() {
return latitude ;
}
}
现在:
@FXML
private void handleTracking() {
AtomicReference<Location> location = new AtomicReference<>(null);
Thread thread = new Thread(() -> {
try {
while (true) {
double[] ar[] = FileImport.getGpsPosition();
Location loc = new Location(ar[0], ar[1]);
if (location.getAndSet(loc) == null) {
Platform.runLater(() -> {
Location updateLoc = location.getAndSet(null);
webEngine.executeScript(""
+ "window.lat = " + updateLoc.getLatitude() + ";"
+ "window.lon = " + updateLoc.getLongitude() + ";"
+ "document.goToLocation(window.lat, window.lon);");
});
}
}
} catch (IOException exc) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
});
thread.setDaemon(true);
thread.start();
}
它的工作方式是为当前位置创建一个(线程安全的)持有者,并尽快更新它。当它更新它时,它(原子地)还会检查当前值是否为 null
。如果是 null
,它会通过 Platform.runLater()
安排 UI 更新。如果不是,它只是更新值但不安排新的 UI 更新。
UI 更新(原子地)获取当前(即最新)值并将其设置为 null,表示它已准备好接收新的 UI 更新。然后它处理新的更新。
这样您就可以 "throttle" UI 更新,以便仅在处理当前更新时安排新的更新,避免 UI 线程被太多请求淹没。