【问题标题】:Why can I update TableView from a non-UI thread but not ListView?为什么我可以从非 UI 线程而不是 ListView 更新 TableView?
【发布时间】:2016-07-13 00:23:12
【问题描述】:

我在 JavaFX(Java 版本 1.8.0_91)中遇到了一些奇怪的事情。我的理解是,如果想要从单独的线程更新 UI,则必须使用 Platform.runLater(taskThatUpdates)javafx.concurrent 包中的工具之一。

但是,如果我有一个 TableView 并在其上调用 .setItems(someObservableList),我可以从单独的线程更新 someObservableList 并查看对我的 TableView 的相应更改,而不会出现预期的 Exception in thread "X" java.lang.IllegalStateException: Not on FX application thread; currentThread = X 错误。

如果我将TableView 替换为ListView,则会出现预期的错误。

情况 #1 的示例代码:从不同的线程更新 TableView,没有调用 Platform.runLater()--并且没有错误。

public class Test extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        // Create a table of integers with one column to display
        TableView<Integer> data = new TableView<>();
        TableColumn<Integer, Integer> num = new TableColumn<>("Number");
        num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
        data.getColumns().add(num);

        // Create a window & add the table
        VBox root = new VBox();
        Scene scene = new Scene(root);
        root.getChildren().addAll(data);
        stage.setScene(scene);
        stage.show();

        // Create a list of numbers & bind the table to it
        ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
        data.setItems(someNumbers);

        // Add a new number every second from a different thread
        new Thread( () -> {
            for (;;) {
                try {
                    Thread.sleep(1000);
                    someNumbers.add((int) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

情况 #2 的示例代码:从不同的线程更新 ListView,而不调用 Platform.runLater()--产生错误。

public class Test extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        // Create a list of integers (instead of a table)
        ListView<Integer> data = new ListView<>();

        // Create a window & add the table
        VBox root = new VBox();
        Scene scene = new Scene(root);
        root.getChildren().addAll(data);
        stage.setScene(scene);
        stage.show();

        // Create a list of numbers & bind the table to it
        ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
        data.setItems(someNumbers);

        // Add a new number every second from a different thread
        new Thread( () -> {
            for (;;) {
                try {
                    Thread.sleep(1000);
                    someNumbers.add((int) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

请注意,唯一的区别是将data 实例化为ListView&lt;Integer&gt; 而不是TableView&lt;Integer&gt;

那么这里给出了什么?这是因为在第一个示例中调用了TableColumn::setCellValueFactory() 吗?--这是我的直觉。我想知道为什么一个不会导致错误而另一个会导致错误,更具体地说,.setItems 调用如何将数据绑定到视图的规则是什么。

【问题讨论】:

  • itemsProperty 的处理方式有所不同。在 TableView 中,该属性将失效,旧引用存储在弱引用对象中。在 ListView 中,只有一个没有自定义无效方法的 SimpleObjectProperty。所以在 ListView 中,更改将在应用程序线程中排队,而在 TableView 中不是?!
  • 奇案! 2 个观察结果: 1. ListView 示例在 imo 中的行为也不正确。第二次编辑后抛出异常。 2. 也许你是凭直觉去做的。将工厂更改为 num.setCellValueFactory(v -&gt; new SimpleObjectProperty&lt;&gt;( v.getValue())); 会产生(立即)异常。
  • JavaFX 工具包的 API 要求您更改属于 FX 应用程序线程上场景一部分的节点。如果您从不同的线程进行这些更改,API 根本没有定义这些行为:换句话说,在这种情况下,没有任何“正确行为”的概念。如果您违反此规则,该工具包会“尽最大努力”抛出运行时异常,但不保证在所有情况下都这样做 - 例如,它不会为了检查正确的线程而危及性能。
  • 未定义的行为是未定义的行为。即使有一天它做了你想做的事,你仍然应该认为它是不正确的,因为它可能不会像你明天所期望的那样,或者在你更新到更新版本的 Java 之后,或者在另一台计算机上,或者当你从侧面看它时.问“为什么未定义的行为 X 的表现就像在情况 Y 中定义的那样?”通常没有多大意义。 undefined 的整个概念并不意味着“它会抛出错误”或“它会崩溃”。它的意思是“不要指望它”,这就是你得到的全部保证。这正是您正在到达这里的原因。

标签: java javafx concurrency javafx-8


【解决方案1】:

正如@James_D 已经提到的,您不应该在任何其他线程上更新属于 FX 应用程序线程的内容。但是在您的示例中,您一直在另一个线程中更新 ObservableList。不管为什么这对 TableView 有效,而对 ListView 无效,这都是错误的做法。

如果您想对支持的 ObservableLists 执行中间更新,请查看 Task 类。

来自Task-API-Doc

返回 ObservableList 的任务

因为ListView、TableView等UI控件和场景图 节点使用 ObservableList,通常想要创建和 从任务返回一个 ObservableList。当你不关心显示 中间值,正确编写此类任务的最简单方法是 只需在调用方法中构造一个 ObservableList,然后 在任务结束时返回它。

还有一个提示:

返回部分结果的任务

有时您想创建一个返回部分结果的任务。 也许您正在构建一个复杂的场景图并希望展示 正在构建的场景图。或者,也许您正在阅读 网络上的大量数据并希望显示条目 在数据到达时在 TableView 中。在这种情况下,有一些 FX 应用程序线程和 后台线程。必须非常小心永远不要更新共享 来自 FX 应用程序线程以外的任何线程的状态。

最简单的方法是利用 更新值(对象)方法。这个方法可能会被重复调用 后台线程。合并更新以防止饱和 FX 事件队列。这意味着您可以尽可能频繁地调用它 就像从后台线程一样,但只有最近的一组是 最终确定。

这样一个用于对 TableView 和 ListView 的 ObservableLists 进行中间更新的 Task 类的示例是:

import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class IntermediateTest extends Application {

    @Override
    public void start(Stage primaryStage) {

        TableView<Integer> tv = new TableView<>();
        TableColumn<Integer, Integer> num = new TableColumn<>("Number");
        num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
        tv.getColumns().add(num);
        PartialResultsTask prt = new PartialResultsTask();
        tv.setItems(prt.getPartialResults());

        ListView<Integer> lv = new ListView<>();
        PartialResultsTask prt1 = new PartialResultsTask();
        lv.setItems(prt1.getPartialResults());
        new Thread(prt).start();
        new Thread(prt1).start();

        // Create a window & add the table
        VBox root = new VBox();
        root.getChildren().addAll(tv, lv);
        Scene scene = new Scene(root, 300, 450);

        primaryStage.setTitle("Data-Adding");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    public class PartialResultsTask extends Task<ObservableList<Integer>> {

        private ReadOnlyObjectWrapper<ObservableList<Integer>> partialResults
                = new ReadOnlyObjectWrapper<>(this, "partialResults",
                        FXCollections.observableArrayList(new ArrayList()));

        public final ObservableList getPartialResults() {
            return partialResults.get();
        }

        public final ReadOnlyObjectProperty<ObservableList<Integer>> partialResultsProperty() {
            return partialResults.getReadOnlyProperty();
        }

        @Override
        protected ObservableList call() throws Exception {
            updateMessage("Creating Integers...");
            Random rnd = new Random();
            for (int i = 0; i < 10; i++) {
                Thread.sleep(1000);
                if (isCancelled()) {
                    break;
                }
                final Integer r = rnd.ints(100, 10000).findFirst().getAsInt();
                Platform.runLater(() -> {
                    getPartialResults().add(r);
                });
                updateProgress(i, 10);
            }
            return partialResults.get();
        }
    }

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-09-05
    • 2016-05-08
    • 1970-01-01
    • 1970-01-01
    • 2021-06-08
    • 1970-01-01
    • 1970-01-01
    • 2014-02-07
    相关资源
    最近更新 更多