是什么阻止了我在 JavaFX 应用程序中的 UI 线程?

What is blocking my UI thread in a JavaFX app?

我有一个简单的 JavaFX 应用程序,它有两个小圆圈,它们应该每 0.5 秒更改一次位置。稍后这应该成为行星模拟。目前,我的 space 对象的位置在一个单独的线程中发生变化,该线程在按下按钮 "Start simulation" 时启动。同时,我希望始终使用存储在 spaceObject 对象中的当前位置一次又一次地绘制我的圆圈(代表行星)。当我将重新绘制限制为三次(而不是通过 while ( true ) { 进行无限次绘制时,这正是我真正想要的),我看到当循环为 运行 时 GUI 没有更新。但是在循环完成后,圆圈移动到新位置,而后台的计算线程仍然是 运行。为什么我的 GUI 线程在 while ( i < 3 ) { 时被阻塞,我如何用圆圈的当前位置同时更新我的​​ GUI?这是我的代码:

Main.java

package plantenbahnen;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

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

Controller.java

package plantenbahnen;

import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.Pane;

public class Controller implements Initializable {

    @FXML private Pane paneDraw;
    @FXML private Pane paneControls;

    private ArrayList<SpaceObject> universe = new ArrayList<>();
    private Thread calcThread;

    @Override
    public void initialize(URL url, ResourceBundle rb) {

        SpaceObject sun = new SpaceObject("sun", 600, 600);
        universe.add(sun);

        SpaceObject earth = new SpaceObject("earth", 450, 450);
        universe.add(earth);

        MyCalculations myCalc = new MyCalculations(universe);
        calcThread = new Thread(myCalc);

        Draw.drawPlanets(universe, paneDraw);
    }    

    @FXML private void buttonStartSimulation(ActionEvent event) throws InterruptedException {    
        calcThread.start();

        Platform.runLater(new Runnable() {
            @Override public void run() {
                int i = 0;
                //while ( true ) {   // this line is what I want
                while ( i < 3 ) {
                    Draw.drawPlanets(universe, paneDraw);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        System.out.println(e);
                    }
                    i++;
                }
            }
        });
    }    
}

MyCalculations.java

package plantenbahnen;

import java.util.ArrayList;

public class MyCalculations implements Runnable {

    ArrayList<SpaceObject> universe;

    public MyCalculations (ArrayList<SpaceObject> universe) {
        this.universe = universe;
    }

    @Override
    public void run(){
        double toAdd = 100.0;
        while ( true ) {
            for (SpaceObject so: universe) {
                so.setx(so.getx() + toAdd);
                so.sety(so.gety() + toAdd);
            }
            if ( toAdd > 0.0 ) {
                toAdd = -300.0;
            } else {
                toAdd = 300.0;
            }
        }
    }
}

Draw.java

package plantenbahnen;

import java.util.ArrayList;
import javafx.scene.Node;
import javafx.scene.layout.Pane;

public class Draw {

    public static void drawPlanets(ArrayList<SpaceObject> universe, Pane pane) {
        for (Node child: pane.getChildren()) {
            System.out.println(child);
        }

        // Clear objects first
        for (SpaceObject so: universe) {
            if ( pane.getChildren().contains(so) ) {
                pane.getChildren().remove(so);
                System.out.println("Removing ... " + so.getName());
            }
        }

        double paneHalfWidth = pane.getPrefWidth() / 2.0;
        double paneHalfHeight = pane.getPrefHeight() / 2.0;
        double scaleFactor = 0.1;

        for (SpaceObject so: universe) {
            so.setCenterX(so.getx() * scaleFactor + paneHalfWidth);
            so.setCenterY(so.gety() * scaleFactor + paneHalfHeight);
            System.out.println("x=" + so.getCenterX() + "   y=" + so.getCenterY());
            so.setRadius(2);
            //so.setColour(Color.BLACK);
            pane.getChildren().add(so);
        }
    }
}

SpaceObject.java

package plantenbahnen;

import javafx.scene.shape.Circle;

public class SpaceObject extends Circle {

    private double x,y;
    private String name;

    SpaceObject(String name, double x, double y) {
        this.x = x;
        this.y = y;
        this.name = name;
    }

    public double getx(){
        return this.x;
    }
    public void setx(double value){
        this.x=value;
    }
    public double gety(){
        return this.y;
    }

    public void sety(double value){
        this.y=value;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

FXMLDocument.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane fx:id="AnchorPane" prefHeight="700.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="plantenbahnen.Controller">
   <children>
      <Pane fx:id="paneDraw" prefHeight="700.0" prefWidth="800.0">
         <children>
            <Pane fx:id="paneControls" prefHeight="66.0" prefWidth="174.0">
               <children>
                  <Button fx:id="buttonStartSimulation" layoutX="26.0" layoutY="21.0" mnemonicParsing="false" onAction="#buttonStartSimulation" text="Start simulation" />
               </children>
            </Pane>
         </children></Pane>
   </children>
</AnchorPane>

非常感谢您的帮助。

尝试这样的事情:

 @FXML private void buttonStartSimulation(ActionEvent event) throws InterruptedException {    

    calcThread.start();

    Thread updaterThread = new Thread( () -> {
        @Override public void run () {
            int i = 0;
            while ( true ) {   // this line is what I want
                Platform.runLater( () -> Draw.drawPlanets(universe, paneDraw) );
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println(e);
                }
                i++;
            }
        }
    }
    updaterThread.setDaemon ( true );
    updaterThread.start();
}

您想确保对 Platform.runLater() 的所有调用都很短,不涉及睡眠,return 很快,并且只进行最少的计算——所有这些调用都必须完成 "in-between" 对 UI 的其他更新,例如调整大小 windows、管理按钮按下等

顺便说一下 -- 我不确定您是否需要 "calcThread" 和 "updaterThread"。我怀疑他们应该是一个线程。但这是一个很好的概念证明。

感谢大家的帮助。我最终使用了 Timeline。我只需要改变控制器,它看起来像这样(只显示相关代码):

public class Controller implements Initializable {

    // ...
    private Thread calcThread;
    Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(0.5), new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            Draw.drawPlanets(universe, paneDraw);
        }
    }));

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // ...
        MyCalculations myCalc = new MyCalculations(universe);
        calcThread = new Thread(myCalc);
        // ...
    }    

    @FXML private void buttonStartSimulation(ActionEvent event) throws InterruptedException {    
        calcThread.start();
        timeline.setCycleCount(Timeline.INDEFINITE);
        timeline.play();
    }    
}