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 线程被太多请求淹没。