【问题标题】:Java FXML Updating UI with new Thread throws errorJava FXML 使用新线程更新 UI 会引发错误
【发布时间】:2016-12-02 13:30:20
【问题描述】:

我正在尝试定期更新 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=)

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

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

【问题讨论】:

标签: multithreading google-maps javafx timer fxml


【解决方案1】:

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

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

【讨论】:

  • 当我在 Platform.runLater 中包装要执行的代码时,程序正在运行,但是它变得很慢,例如地图不再可滚动。
  • @Evo 你为什么不使用时间线来代替!会容易很多!
  • @Evo 确保您的 whileloop 包裹在平台 runlater 方法上!我是说!
  • 是的 Platform.runLater 在 while 循环之后出现,但它仍然使整个程序变慢。我将尝试使用时间轴来完成,但是我在理解我已经找到的关于该主题的示例时遇到了问题。
  • @Evo 不知何故,正在创建的对象多于应用程序可以处理的对象!希望你能深入了解它。祝你好运!
【解决方案2】:

对 UI 的更新,包括对 webEngine.executeScript(...) 的调用必须在 FX 应用程序线程上执行。

另一方面,FX 应用程序线程(实际上)是用于渲染 UI 和处理用户输入的线程。因此,如果您使用无限循环或其他长时间运行的进程阻塞该线程,或者如果您安排在该线程上运行的东西过多,则会导致 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 应用程序线程上调用的,所以您不应该在这里执行任何长时间运行的代码。看起来您的 FileImport.getGpsPosition() 方法似乎执行了一些 IO 操作,因此它可能应该委托给后台线程。这里的技巧是 JavaFX 类(如 Task)使用的技巧,它是不断更新后台线程中的值,并且安排对 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 更新。

UI 更新(以原子方式)获取当前(即最近的)值并将其设置为 null,表示它已准备好接收新的 UI 更新。然后它会处理新的更新。

通过这种方式,您可以“限制” UI 更新,以便仅在处理当前更新时安排新的更新,从而避免过多的请求淹没 UI 线程。

【讨论】:

  • 非常感谢 James_D!两种技术都有效。你是对的,我可能一直想从我的日志文件中获取新的 GPS 数据。不过,我将不得不仔细阅读您的代码几次才能完全理解它。谢谢你,我真的很感激!
猜你喜欢
  • 2015-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多