0

I want my application to update some elements but when I call Pane.getChildren().add() from another thread:

Pane root = new Pane();
Path path;
        new Thread(new Runnable() {
        @Override
        public void run() {
            for (Coordinate coordinate: coordinates){
                try {
                    Thread.sleep(100);
                    root.getChildren().remove(path);
                    test++;
                    path = makePath(coordinates.subList(test, test+5));
                    root.getChildren().add(path);//!!!Exception here
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

I have this error:

Exception in thread "Thread-3" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-3
at javafx.graphics/com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:291)
at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
at javafx.graphics/javafx.scene.Parent$3.onProposedChange(Parent.java:478)
at javafx.base/com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:206)
at forTest.SkladPlanFx$2.run(SkladPlanFx.java:92)
at java.base/java.lang.Thread.run(Thread.java:844)

I tried to solve this according this Link but this approach do not helps: Platform.runLater(new Runnable(){});

Questions:

  1. Why doesn't it work?

  2. I want to my program to update itself UI in some delay, I think there is a right solution instead of mine)

Thanks in advance.

ADDITION for people who comment:

if I use Platform.runLater(new Runnable(){

It does not work different way. The program just froze and I understand why, but i do not understand why patch do not appear and don't change.

    Platform.runLater(new Runnable(){

@Override
        public void run() {
            for (Coordinate coordinate: coordinates){
                try {
                    Thread.sleep(100);
                    System.out.println(coordinate);
                    root.getChildren().remove(path);
                    test++;
                    path = makePath(coordinates.subList(test, test+5));
                    root.getChildren().add(path);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });

If i use button It work well:

        button.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            root.getChildren().remove(path);
            test++;
            path = makePath(coordinates.subList(test, test+5));
            Group group = new Group();
            root.getChildren().add(path);
        }
    });

ADDITION Here is "minimal reproducible example "that you can try.

press the button and there will be the result I need;

Delete comment " new Thread(new Runnable() {" and you will get "java.lang.IllegalStateException: Not on FX application thread"

Delete comment "Platform.runLater(new Runnable(){" and you get a frieze and motionless.

package forTest;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import java.util.Random;


public class SkladPlanFx extends Application {

    static int test = 10;
    Path path;

    @Override
    public void start(Stage primaryStage) {
        // create a button with specified text
        Button button = new Button("TEST");
        button.setLayoutX(500);
        button.setLayoutY(850);

        Pane root = new Pane();
        // create a scene specifying the root and the size
        Scene scene = new Scene(root, 1320, 930);

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                root.getChildren().remove(path);
                test++;
                path = makePath();
                Group group = new Group();
                root.getChildren().add(path);
            }
        });

        root.getChildren().add(button);
        // add scene to the stage
        primaryStage.setScene(scene);
        // make the stage visible
        primaryStage.show();

/*
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++){
                    try {
                        Thread.sleep(100);
                        root.getChildren().remove(path);
                        test++;
                        path = makePath();
                        root.getChildren().add(path);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
*/

/*
        Platform.runLater(new Runnable(){
            @Override
            public void run() {
                for (int i = 0; i < 20; i++){
                    try {
                        Thread.sleep(300);
                        System.out.println(i);
                        root.getChildren().remove(path);
                        test++;
                        path = makePath();
                        root.getChildren().add(path);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
*/


    }

   public Path makePath(){
      Path path = new Path();

       Random random = new Random();
      path.getElements().add(new MoveTo(test++,test++));


      for (int i = 0; i < 5; i++){
        path.getElements().add(new LineTo(test++, test++));
      }
        return path;
   }

    public static void main(String[] args) {
        Application.launch(SkladPlanFx.class, args);
    }
}
snailp4el
  • 125
  • 1
  • 11
  • 2
    Interesting the exception occurs when calling `add` but not `remove`. Anyway, it doesn't work because JavaFX, like most UI toolkits, is single-threaded and not thread-safe; reading and/or modifying state of objects connected to a live scene graph **must** occur on the _JavaFX Application Thread_. You mention using `Platform.runLater` didn't help. Unfortunately, you don't show that code so we can't help with that. How did you use it? – Slaw Aug 02 '19 at 08:19
  • 2
    You're asking about code you didn't post in the question. If you want an answer to `1.`, you need to do so. As for `2.`: Do you want the program to update the UI based on the results of longrunning operations or simply do some updates step by step? – fabian Aug 02 '19 at 08:19
  • Thanks for helping. I edited the topic, and add some addition. – snailp4el Aug 02 '19 at 11:41
  • 1
    ahh ... just saw the edit: but that's completely wrong (and pretty sure that's not done in any working example - make sure you follow them exactly as they are): hint: you must not wrap the complete task into a runLater, but only the part of it that modifies the scenegraph. Anyway, it's still not a [mcve] - which is __mandatory__ if you need further help. – kleopatra Aug 02 '19 at 11:57
  • 1
    also I would suggest that you read up on some basic tutorial on concurrency - my impression is that you do not fully understand the concept of threading (with exactly one of them being the fx-application thread) – kleopatra Aug 02 '19 at 12:01
  • 2
    If you indeed understand why the GUI freezes with the `Platform.runLater` code, you should also be able to understand why the GUI doesn't update until the loop is done. Clicking the button often enough with the last code snippet should yield the same result as using the penultimate code snippet, i.e. no path in `root` and a `IndexOutOfBoundsException` being thrown when `test` reaches `coordinates.size() - 4` – fabian Aug 02 '19 at 12:18
  • kleopatra I add a addition with "minimal reproducible" example. – snailp4el Aug 02 '19 at 12:58

1 Answers1

0

I have solved it this way.

I use "new Timer().schedule()" instead "Thread" plus Platform.runLater() inside this timer.

This way paths change OK.

package Fx;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import java.util.*;

public class SkladPlanFx extends Application {

    static int test = 0;
    Path path;

    public static void main(String[] args) {
        SkladPlanFx.launch(SkladPlanFx.class, args);
    }

    @Override
    public void start(Stage primaryStage) {

        Text text = new Text();

        text.setText("qqq");
        text.setLayoutX(450);
        text.setLayoutY(800);

        Pane root = new Pane();
        Scene scene = new Scene(root, 500, 500);

        primaryStage.setScene(scene);
        primaryStage.show();


        //Timer
        new Timer().schedule(
             new TimerTask() {
                 @Override
                 public void run() {
                     System.out.println("ping " + ++test);
                     text.setText(" " + test);

                     //here is the solution
                     Platform.runLater(new Runnable() {
                         @Override public void run() {
                             root.getChildren().remove(path);
                             test++;
                             //path = makePath(coordinates.subList(test, test+5));
                             path = makeTestPath();
                             root.getChildren().add(path);
                         }
                     });

                 }
             }, 0, 1000);

    }

    public Path makeTestPath() {
        Path path = new Path();
        path.setStroke(Color.RED);
        path.getElements().add(new MoveTo(getRandomNumberInRange(1, 500), getRandomNumberInRange(1, 500)));
        for (int i = 0; i < 10; i++) {
            path.getElements().add(new LineTo(getRandomNumberInRange(1, 500), getRandomNumberInRange(1, 500)));
        }
        return path;
    }

    private static int getRandomNumberInRange(int min, int max) {
        if (min >= max) {
            throw new IllegalArgumentException("max must be greater than min");
        }
        Random r = new Random();
        return r.nextInt((max - min) + 1) + min;
    }
}
snailp4el
  • 125
  • 1
  • 11
  • 3
    glad you found a workable solution :) Just: would be better to learn more about fx concurrency support (read and understand f.i. Task and make use of its notifications which are guaranteed to happen on the fx app thread) - but it's your decision, of course. And beware: with this code, you will get into trouble when the text node is part of the scenegraph because you update its text property off the fx app thread (might not blow immediately but introduce instabilities that will be hard to track once they do) – kleopatra Aug 05 '19 at 12:31