Java FXML使用新线程更新UI会引发错误

时间:2016-07-28 08:57:05

标签: multithreading google-maps javafx timer fxml

我正在尝试定期更新FXML中的Google地图标记。我尝试使用Timer和新的Thread执行此操作,但无法使其中任何一个工作。

我使用简单的任务测试了新的Thread,以更新我的用户界面中的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$1.run(FXMLDocumentController.java:84=)

当我使用Timer时会发生类似的事情,它适用于更新标签的任务,但是如果我尝试更新标记位置,它会抛出消息:

Exception in thread "Timer-0" java.lang.IllegalStateException: Not on FX application thread; currentThread = Timer-0

2 个答案:

答案 0 :(得分:1)

对用户界面的更新,包括对webEngine.executeScript(...) 的调用必须才能在FX应用程序主题上执行。

另一方面,FX应用程序线程(有效地)是用于呈现UI和处理用户输入的线程。因此,如果您使用无限循环或其他长时间运行的进程阻止此线程,或者如果您计划在该线程上运行太多事情,则会使UI无响应。

您在代码中尝试执行的操作似乎是尽可能快地更新UI。如果您将循环放在FX应用程序线程中,您将完全阻止它:如果您将它放在后台线程上并使用Platform.runLater(...)计划更新,您将使FX应用程序线程泛滥太多更新并阻止它执行它通常的工作,它会变得没有反应。

这里的一般解决方案围绕这样一个事实:经常更新UI真的很多。人眼只能以有限的速率检测可见的变化,并且在技术方面,您受到限制,例如,物理屏幕和底层图形软件的刷新率。 JavaFX尝试以不超过60Hz的速度更新UI(在当前实现中)。因此,更新频率比基础JavaFX工具包更新场景更为重要。

AnimationTimer提供了handle方法,无论出现频率如何,每个场景更新都会保证调用一次。在FX应用程序线程上调用AnimationTimer.handle(...),因此您可以在此安全地更改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();  
}

唯一需要注意的是,因为在FX应用程序线程上调用handle(),所以不应在此处执行任何长时间运行的代码。看起来好像FileImport.getGpsPosition()方法执行了一些IO操作,所以应该将它委托给后台线程。这里的技巧,即Task等JavaFX类使用的技巧,是从后台线程不断更新值,调度Platform.runLater(...)的调用。一个尚未处理。

首先,只需定义一个表示位置的简单类(使其不可变,因此它是线程安全的):

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更新(原子地)获取当前(即最近的)值并将其设置为null,表示它已准备好接收新的UI更新。然后它处理新的更新。

通过这种方式,您可以“限制”UI更新,以便仅在处理当前更新时调度新更新,从而避免使用过多请求来充斥UI线程。

答案 1 :(得分:0)

必须在FX应用程序线程内更新所有JavaFX UI元素!

如果使用其他主题,请务必使用platform.Runlater()更新您的UI元素!