Ueli Hofstetter
Ueli Hofstetter

Reputation: 2534

Multiple independent stages in JavaFX

Is there a way to launch multiple independent stages in JavaFX? By independent I mean that the stages are all created from the main thread.

At the moment my application is more or less an algorithm where I would like to plot some charts and tables during execution (mainly to check whether the results are correct/ to debug).

The problem is that I cannot figure out how to create and show multiple stages independently, i.e. I would like to do something like this

public static void main(){
    double[] x = subfunction.dosomething();
    PlotUtil.plot(x); //creates a new window and shows some chart/table etc.
    double[] y = subfunction.dosomethingelse();
    PlotUtil.plot(y); //creates a new window and shows some chart/table etc.
    .....
}

which would allow to use PlotUtil as one would use the plotting functions in other scripting languages (like Matlab or R).

So the main question is how to "design" PlotUtils? So far I tried two things

  1. PlotUtils uses Application.launch for each plot call (creating a new stage with a single scene every time) --> does not work as Application.launch can only be invoked once.
  2. Create some kind of "Main Stage" during the first call to PlotUtils, get a reference to the created Application and start subsequent stages from there --> does not work as using Application.launch(SomeClass.class) I am not able to get a reference to the created Application instance.

What kind structure/design would allow me to implement such a PlotUtils function?

Update 1:

I came up with the following idea and was wondering whether there are any major mistakes in this solution.

Interface to be implemented by all "Plots"

public abstract class QPMApplication implements StageCreator {
   @Override
   public abstract  Stage createStage();
}

Plotting functionality:

public class PlotStage {
    public static boolean toolkitInialized = false;

    public static void plotStage(String title, QPMApplication stageCreator) {
        if (!toolkitInialized) {
            Thread appThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    Application.launch(InitApp.class);
                }
            });
            appThread.start();
        }

        while (!toolkitInialized) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                Stage stage = stageCreator.createStage();
                stage.show();
            }
        });
    }

    public static class InitApp extends Application {
        @Override
        public void start(final Stage primaryStage) {
            toolkitInialized = true;
        }
    }
}

Using it:

public class PlotStageTest {

    public static void main(String[] args) {

        QPMApplication qpm1 = new QPMApplication() {
            @Override
            public Stage createStage() {
                Stage stage = new Stage();
                StackPane root = new StackPane();
                Label label1 = new Label("Label1");
                root.getChildren().add(label1);
                Scene scene = new Scene(root, 300, 300);
                stage.setTitle("First Stage");
                stage.setScene(scene);
                return stage;
            }
        };

        PlotStage.plotStage(qpm1);

        QPMApplication qpm2 = new QPMApplication() {
            @Override
            public Stage createStage() {
                Stage stage = new Stage();
                StackPane root = new StackPane();
                Label label1 = new Label("Label2");
                root.getChildren().add(label1);
                Scene scene = new Scene(root, 300, 200);
                stage.setTitle("Second Stage");
                stage.setScene(scene);
                return stage;
            }
        };

        PlotStage.plotStage(qpm2);

        System.out.println("Done");

    }
}

Upvotes: 1

Views: 7359

Answers (1)

James_D
James_D

Reputation: 209684

The easiest approach here would be just to refactor your application so that it is driven from the FX Application thread. For example, you could rewrite your original code block as

public class Main extends Application {

    @Override
    public void start(Stage primaryStageIgnored) {
        double[] x = subfunction.dosomething();
        PlotUtil.plot(x); //creates a new window and shows some chart/table etc.
        double[] y = subfunction.dosomethingelse();
        PlotUtil.plot(y); //creates a new window and shows some chart/table etc.
        //  .....
    }

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

Now PlotUtil.plot(...) merely creates a Stage, puts a Scene in it, and show()s it.

This assumes the methods you're calling don't block, but if they do you just have to wrap them in a Task and call PlotUtils.plot(...) in the onSucceeded handler for the task.

If you really want to drive this from a non-JavaFX application, there's a fairly well-known hack to force the JavaFX Application thread to start if it's not already started, by creating a new JFXPanel. A JFXPanel should be created on the AWT event dispatch thread.

Here's a very basic example of the second technique. Start the application and type "show" into the console. (Type "exit" to exit.)

import java.util.Scanner;
import java.util.concurrent.FutureTask;

import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import javax.swing.SwingUtilities;


public class Main {

    private JFXPanel jfxPanel ;

    public void run() throws Exception {
        boolean done = false ;
        try (Scanner scanner = new Scanner(System.in)) {
            while (! done) {
                System.out.println("Waiting for command...");
                String command = scanner.nextLine();
                System.out.println("Got command: "+command);
                switch (command.toLowerCase()) {
                case "exit": 
                    done = true;
                    break ;
                case "show":
                    showWindow();
                    break;
                default:
                    System.out.println("Unknown command: commands are \"show\" or \"exit\"");   
                }
            }
            Platform.exit();
        }
    }

    private void showWindow() throws Exception {
        ensureFXApplicationThreadRunning();
        Platform.runLater(this::_showWindow);
    }

    private void _showWindow() {
        Stage stage = new Stage();
        Button button = new Button("OK");
        button.setOnAction(e -> stage.hide());
        Scene scene = new Scene(new StackPane(button), 350, 75);
        stage.setScene(scene);
        stage.show();
        stage.toFront();
    }

    private void ensureFXApplicationThreadRunning() throws Exception {

        if (jfxPanel != null) return ;

        FutureTask<JFXPanel> fxThreadStarter = new FutureTask<>(() -> {
            return new JFXPanel();
        });
        SwingUtilities.invokeLater(fxThreadStarter);
        jfxPanel = fxThreadStarter.get();
    }

    public static void main(String[] args) throws Exception {
        Platform.setImplicitExit(false);
        System.out.println("Starting Main....");
        new Main().run();
    }

}

Here is something more along the lines I would actually follow, if I wanted the user to interact via the OS terminal (i.e. using System.in). This uses the first technique, where the application is driven by an FX Application subclass. Here I create two background threads, one to read commands from System.in, and one to process them, passing them via a BlockingQueue. Even though nothing is displayed in the main FX Application Thread, it is still a very bad idea to block that thread waiting for commands. While the threading adds a small level of complexity, this avoids the "JFXPanel" hack, and doesn't rely on there being an AWT implementation present.

import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;


public class FXDriver extends Application {

    BlockingQueue<String> commands ;
    ExecutorService exec ;

    @Override
    public void start(Stage primaryStage) throws Exception {

        exec = Executors.newCachedThreadPool(runnable -> {
            Thread t = new Thread(runnable);
            t.setDaemon(true);
            return t ;
        });

        commands = new LinkedBlockingQueue<>();

        Callable<Void> commandReadThread = () -> {
            try (Scanner scanner = new Scanner(System.in)) {
                while (true) {
                    System.out.print("Enter command: ");
                    commands.put(scanner.nextLine());
                }
            } 
        };

        Callable<Void> commandProcessingThread = () -> {
            while (true) {
                processCommand(commands.take());
            }
        };

        Platform.setImplicitExit(false);
        exec.submit(commandReadThread);
        exec.submit(commandProcessingThread);
    }

    private void processCommand(String command) {
        switch (command.toLowerCase()) {
        case "exit": 
            Platform.exit();
            break ;
        case "show":
            Platform.runLater(this::showWindow);
            break;
        default:
            System.out.println("Unknown command: commands are \"show\" or \"exit\"");   
        }
    }

    @Override
    public void stop() {
        exec.shutdown();
    }

    private void showWindow() {
        Stage stage = new Stage();
        Button button = new Button("OK");
        button.setOnAction(e -> stage.hide());
        Scene scene = new Scene(new StackPane(button), 350, 75);
        stage.setScene(scene);
        stage.show();
        stage.toFront();
    }

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

Upvotes: 4

Related Questions