Reputation: 201
I have a simple JavaFX app that has two little circles that are supposed to change their location every 0.5 s. Later on this is supposed to become a planets simulation. At the moment the location of my space objects changes in a separate thread which is launched when the button "Start simulation" is pressed. Simultaneously I want my circles (representing the planets) to be drawn again and again always using the current location stored in the spaceObject objects. When I limit the re-drawing to three times (instead of an unlimited amount via a while ( true ) {
which is what I actually want) I see that the GUI is not updating while the loop is running. But after the loop is finished the circles move to the new location while the calculations thread in the background is still running. Why is my GUI thread blocked for the time of the while ( i < 3 ) {
and how can I simultaneously update my GUI with the current location of the circles? Here is my code:
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>
Thanks a lot in advance for your help.
Upvotes: 0
Views: 946
Reputation: 201
Thanks everybody for your help. I ended up using Timeline
. All I had to change is the Controller and it looks like this (only the relevant code is shown):
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();
}
}
Upvotes: 0
Reputation: 3119
Try something like this:
@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();
}
You want to make sure all of your calls to Platform.runLater() are short, have no sleeps involved, return quickly, and do minimal calculations -- all of these calls have to be done "in-between" other updates to the UI, like resizing windows, managing button presses, etc.
By the way -- I'm not sure if you need a "calcThread" and an "updaterThread". I suspect they should be one thread. But this is a good proof of concept.
Upvotes: 1