【问题标题】:What is the best way to manage multithreading in JavaFX 8?在 JavaFX 8 中管理多线程的最佳方法是什么?
【发布时间】:2014-09-30 00:25:07
【问题描述】:

我正在尝试找到一种使用多线程来影响 JavaFX GUI 元素的形状和内容的有效方法,例如简单的Pane。假设我有一个简单的Pane,我在给定的时间间隔内显示填充Circles,我希望有机会回答它们,例如通过点击相应的键。到目前为止,为此目的,我尝试使用实现Runnable 接口并在其中创建经典Thread 对象的类,以便从外部JavaFX Pane 添加和/或删除元素,这是从主Application 类的构造函数参数中传递给该“线程类”。比如说,两个类,1) 应用程序和 2) 线程类,看起来像这样:

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

public class ClassApplication extends Application {

    private Pane pane;

    public Parent createContent() {

        /* layout */
        BorderPane layout = new BorderPane();

        /* layout -> center */
        pane = new Pane();
        pane.setMinWidth(250);
        pane.setMaxWidth(250);
        pane.setMinHeight(250);
        pane.setMaxHeight(250);
        pane.setStyle("-fx-background-color: #000000;");

        /* layout -> center -> pane */
        Circle circle = new Circle(125, 125, 10, Color.WHITE);

        /* add items to the layout */
        pane.getChildren().add(circle);

        layout.setCenter(pane);
        return layout;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setWidth(300);
        stage.setHeight(300);
        stage.show();

        /* initialize custom Thread */
        ClassThread thread = new ClassThread(pane);
        thread.execute();
    }

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

...和“线程类”

import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;

public class ClassThread implements Runnable {

    private Thread t;
    private Pane pane;

    ClassThread(Pane p) {
        this.pane = p;

        t = new Thread(this, "Painter");
    }

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            Circle circle = new Circle(50, 50, 10, Color.RED);
            pane.getChildren().clear();
            pane.getChildren().add(circle);

        } catch (InterruptedException ie) {
            ie.printStackTrace();
        }
    }

    public void execute() {
        t.start();
    }
}

但是,这样的解决方案,在 Swing 应用程序中是可能的,在 JavaFX 中是不可能的,而且,是以下异常的原因:

Exception in thread "Painter" java.lang.IllegalStateException: Not on FX application thread; currentThread = Painter
    at com.sun.javafx.tk.Toolkit.checkFxUserThread(Unknown Source)
    at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(Unknown Source)
    at javafx.scene.Parent$2.onProposedChange(Unknown Source)
    at com.sun.javafx.collections.VetoableListDecorator.clear(Unknown Source)
    at ClassThread.run(ClassThread.java:21)
    at java.lang.Thread.run(Unknown Source)

...“21”行是:pane.getChildren().clear();

我已经得出结论,“从另一个线程的级别影响主 JavaFX 线程存在问题”。但是在这种情况下,如果我不能(更准确地说是“我不知道如何”)绑定到几个线程,我该如何动态更改 JavaFX GUI 元素的形状和内容?

更新:2014/08/07,03:42

阅读给定答案后,我尝试在代码中实现给定解决方案,以便在不同位置显示 10 个自定义 Circles,每个显示之间具有指定的时间间隔:

/* in ClassApplication body */
@Override
public void start(Stage stage) throws Exception {
    stage.setScene(new Scene(createContent()));
    stage.setWidth(300);
    stage.setHeight(300);
    stage.show();

    Timeline timeline = new Timeline();
    for (int i = 0; i < 10; i++) {
        Random r = new Random();
        int random = r.nextInt(200) + 25;
        KeyFrame f = new KeyFrame(Duration.millis((i + 1) * 1000), 
            new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent ae) {
                pane.getChildren().add(new Circle(
                    random, random, 10, Color.RED));
            }
        });
        timeline.getKeyFrames().add(f);
    }
    timeline.setCycleCount(1);
    timeline.play();
}

上面的解决方案效果很好。非常感谢。

【问题讨论】:

    标签: java multithreading javafx javafx-8


    【解决方案1】:

    除了使用低级 Thread API 和 Platform.runLater(...) 来安排代码在 FX 应用程序线程上执行(如 Tomas 的回答中所示)之外,另一种选择是使用 FX 并发 API。这提供了ServiceTask 类,它们旨在在后台线程上执行,以及保证在FX 应用程序线程上执行的回调方法。

    对于您的简单示例,这看起来有点过多的样板代码,但对于真正的应用程序代码,它非常好,在后台任务和完成时执行的 UI 更新之间提供了清晰的分离。另外,Tasks 可以提交给Executors。

    import javafx.application.Application;
    import javafx.concurrent.Task ;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;
    
    public class ClassApplication extends Application {
    
        private Pane pane;
    
        public Parent createContent() {
    
            /* layout */
            BorderPane layout = new BorderPane();
    
            /* layout -> center */
            pane = new Pane();
            pane.setMinWidth(250);
            pane.setMaxWidth(250);
            pane.setMinHeight(250);
            pane.setMaxHeight(250);
            pane.setStyle("-fx-background-color: #000000;");
    
            /* layout -> center -> pane */
            Circle circle = new Circle(125, 125, 10, Color.WHITE);
    
            /* add items to the layout */
            pane.getChildren().add(circle);
    
            layout.setCenter(pane);
            return layout;
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(createContent()));
            stage.setWidth(300);
            stage.setHeight(300);
            stage.show();
    
            Task<Void> task = new Task<Void>() {
                @Override
                public Void call() throws Exception {
                    Thread.sleep(2000);
                    return null ;
                }
            };
    
            task.setOnSucceeded(event -> {
                Circle circle = new Circle(50, 50, 10, Color.RED);
                pane.getChildren().setAll(circle);
            });
    
            new Thread(task).run();
        }
    
        public static void main(String args[]) {
            launch(args);
        }
    }
    

    如果您所做的只是暂停,您甚至可以使用(或滥用?)动画 API。有一个PauseTransition 会暂停指定时间,您可以使用它的onFinished 处理程序来执行更新:

    import javafx.application.Application;
    import javafx.animation.PauseTransition ;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;
    import javafx.util.Duration ;
    
    public class ClassApplication extends Application {
    
        private Pane pane;
    
        public Parent createContent() {
    
            /* layout */
            BorderPane layout = new BorderPane();
    
            /* layout -> center */
            pane = new Pane();
            pane.setMinWidth(250);
            pane.setMaxWidth(250);
            pane.setMinHeight(250);
            pane.setMaxHeight(250);
            pane.setStyle("-fx-background-color: #000000;");
    
            /* layout -> center -> pane */
            Circle circle = new Circle(125, 125, 10, Color.WHITE);
    
            /* add items to the layout */
            pane.getChildren().add(circle);
    
            layout.setCenter(pane);
            return layout;
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(createContent()));
            stage.setWidth(300);
            stage.setHeight(300);
            stage.show();
    
            PauseTransition pause = new PauseTransition(Duration.millis(2000));
            pause.setOnFinished(event -> pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED)));
            pause.play();
        }
    
        public static void main(String args[]) {
            launch(args);
        }
    }
    

    如果需要多次执行暂停,可以使用Timeline,调用setCycleCount(...)

    import javafx.application.Application;
    import javafx.animation.Timeline ;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.stage.Stage;
    import javafx.util.Duration ;
    
    public class ClassApplication extends Application {
    
        private Pane pane;
    
        public Parent createContent() {
    
            /* layout */
            BorderPane layout = new BorderPane();
    
            /* layout -> center */
            pane = new Pane();
            pane.setMinWidth(250);
            pane.setMaxWidth(250);
            pane.setMinHeight(250);
            pane.setMaxHeight(250);
            pane.setStyle("-fx-background-color: #000000;");
    
            /* layout -> center -> pane */
            Circle circle = new Circle(125, 125, 10, Color.WHITE);
    
            /* add items to the layout */
            pane.getChildren().add(circle);
    
            layout.setCenter(pane);
            return layout;
        }
    
        @Override
        public void start(Stage stage) throws Exception {
            stage.setScene(new Scene(createContent()));
            stage.setWidth(300);
            stage.setHeight(300);
            stage.show();
    
            KeyFrame keyFrame = new KeyFrame(Duration.millis(2000), 
                event -> pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED)));
            Timeline timeline = new Timeline(keyFrame);
    
            // Repeat 10 times:
            timeline.setCycleCount(10);
    
            timeline.play();
        }
    
        public static void main(String args[]) {
            launch(args);
        }
    }
    

    【讨论】:

    • 如果动画API的滥用仍然是太多样板,ReactFX有一个wrapper围绕它:FxTimer.runLater(Duration.ofMillis(2000), () -&gt; pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED)));
    • @James_D 这个解决方案的问题在于,它适用于单个延迟事件。但是如果在特定的时间间隔内显示多个Circles,就会出现问题。比如说,我有 10 个Circles,我希望它们一个接一个地显示,例如在同一 Pane 上间隔 2000 毫秒。我还尝试使用Timeline,它是KeyValueKeyFrame,但问题是,在KeyValue 对象中我无法在Pane 中添加和删除元素。
    • 添加了一个重复 10 次的示例。您可以将循环计数设置为Animation.INDEFINITE 以无限期重复。毫无疑问,@TomasMikula 有一个来自 ReactFX 的单线用于同样的事情;)。
    • @James_D 要无限重复,只需将我之前评论中的runLater 更改为runPeriodically。对于固定的重复次数,我没有更简单的解决方案。我想像这样:EventStreams.ticks(Duration.ofMillis(2000)).limit(10).subscribe(x -&gt; pane.getChildren().setAll(new Circle(50, 50, 10, Color.RED))),除了limit 运算符不存在。只是还没有人要求 :)
    • @bluevoxel 在您的代码中,您添加了 10 个KeyFrames,所有这些都在 time=1000ms 时执行。如果您想使用 10 个关键帧而不是 10 个周期,请使用Duration.millis((i+1) * 1000)。 KeyFrame 的持续时间自相矛盾地不是 KeyFrame 的持续时间,而是自动画开始以来的持续时间。
    【解决方案2】:

    您只能从 JavaFX 应用程序线程访问场景。您需要将访问场景图的代码封装在Platform.runLater()

    Platform.runLater(() -> {
        Circle circle = new Circle(50, 50, 10, Color.RED);
        pane.getChildren().clear();
        pane.getChildren().add(circle);
    });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-12-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-16
      • 2010-10-11
      • 1970-01-01
      相关资源
      最近更新 更多