Call JavaFX in Java program and wait for wait to e

2019-07-27 09:17发布

In my java program i give some options to the user and one of thems calls a JavaFXProgram to display something. I only want to run more code in the Java program when this JavaFX that got called actually exits, it may take 5 seconds, it may take a minute. Ideally what i would like is something like we have in Android. We call startActivityForResult() and then wait for the call of onActivityResult(). How can i achieve similar behaviour in my situation?

I have this code that i wrote to try to replicate the problem i has having. It's similar idea but somehow this calls JavaFX, goes to start of loop and retrieves input from the user without a problem. In my other program i always get Exception in thread "main" java.util.InputMismatchException when it goes back again to scan for input. But as i said, ideally, i would like to only run more code after JavaFX Application closes.

package JavaCallsJavaFXandWaits;

import java.util.Scanner;
import javafx.application.Application;

public class MyJavaProgram {
    public static void main(String[] args) {
        int input;
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("0 - exit");
            System.out.println("1 - display something to me");
            input = scanner.nextInt();
            switch (input) {
                case 0:
                    break;
                case 1:
                    Application.launch(JavaCallsJavaFXandWaits.MyJavaFXProgram.class, null);
                    // how to get notified of MyJavaFXProgram exit? I only want to run code after it exits
                    break;

            }
            if (input == 0) break;
        }


    }

}

package JavaCallsJavaFXandWaits;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MyJavaFXProgram extends Application {

    @Override
    public void start(Stage primaryStage) {
        Text oText = new Text("My JavaFXProgram");

        StackPane root = new StackPane();
        root.getChildren().add(oText);

        Scene scene = new Scene(root, 800, 600);

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

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

}

edit1:

I just noticed that if i try to display something two times (eg: choose 1, close JavaFX application, then choose 1 again) it crashes with Exception in thread "main" java.lang.IllegalStateException: Application launch must not be called more than once. It seems that JavaFX Application is not exiting properly in this code too.

1条回答
男人必须洒脱
2楼-- · 2019-07-27 09:53

Your code doesn't work as you have it, because it doesn't fit in the JavaFX Application life-cycle, which is fully documented in the API documentation for Application. In brief, the Application class represents the entire application, (or perhaps the lifecycle of the application).

To show a window in JavaFX, you must do so on the FX application thread, and the FX toolkit must be started in order to start this thread (among other things). The Application.launch() method starts the FX toolkit, starts the FX application thread, creates an instance of your application class, calls init() on that instance, and then calls start() on that instance (the call to start() happens on the FX Application Thread).

As documented, Application.launch() blocks (does not return) until the FX toolkit shuts down (i.e. the application exits), and must be called only once. (Since it represents the entire application, this makes sense, and there is no way to work around calling it twice.)

Your application structure doesn't really make any sense from a user perspective either. Why ask your user to interact with the command line to present options to a GUI-based application? You should present those options in the GUI. Just show a window with the options at startup, and then show a window corresponding to the option chosen. If you want to ensure that the user cannot return to the original window before the chosen option is complete, simply make the new window modal.

For example, if you refactor MyJavaFXProgram so it is not an Application subclass (which you should do, since it is not the starting point of the application):

import javafx.scene.Parent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;

public class MyJavaFXProgram  {

    private StackPane view ;

    public MyJavaFXProgram() {
        Text oText = new Text("My JavaFXProgram");

        view = new StackPane();
        view.getChildren().add(oText);

    }

    public Parent getView() {
        return view ;
    }

}

then you can do

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;

public class MyJavaProgram extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Button showMeSomethingButton = new Button("Show me something");
        showMeSomethingButton.setOnAction(e -> {
            MyJavaFXProgram myProgram = new MyJavaFXProgram();
            showInModalWindow(myProgram.getView());
        });
        Button exitButton = new Button("Exit");
        exitButton.setOnAction(e -> Platform.exit());

        VBox root = new VBox(10, exitButton, showMeSomethingButton);
        root.setPadding(new Insets(20));
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void showInModalWindow(Parent view) {
        Stage stage = new Stage();
        stage.initModality(Modality.APPLICATION_MODAL);
        stage.setScene(new Scene(view));
        stage.show();
    }


}

If you really want to drive all this from the command line (and I honestly can't see a valid reason to do that), then it gets tricky and you necessarily have to get your hands dirty with managing interactions between threads at some point. Probably the simplest approach is to make sure the FX toolkit starts up when your application starts, and then to proceed more or less as you have in your code, but with MyJavaFXProgram refactored again so that it is not a subclass of Application. There is a hack to start the FX toolkit, by creating a JFXPanel, but that really is a bit of a hack and I prefer to do it explicitly by having an Application class that doesn't actually do anything, calling launch(), and waiting for its initialization to complete. I showed how to do this in an answer to this question, so I'll just borrow that solution from there. Here is the Application class that is used to start the FX toolkit (but is not the main class that you execute):

import java.util.concurrent.CountDownLatch;

import javafx.application.Application;
import javafx.stage.Stage;

public class FXStarter extends Application {

    private static final CountDownLatch latch = new CountDownLatch(1);

    public static void awaitFXToolkit() throws InterruptedException {
       latch.await();
    }

    @Override
    public void init() {
        latch.countDown();
    }

    @Override
    public void start(Stage primaryStage) {
        // no-op
    }
}

The idea is to call Application.launch(FXStarter.class), but since launch() blocks, you need to do that on a background thread. Since that means your ensuing code might (probably will) execute before launch() has actually completed the work you need it to complete, you need to wait until it's done its job, which you can do with FXStarter.awaitFXToolkit(). Then you can execute your loop. The only remaining thing to worry about is to make sure that you create and show new windows on the FX Application Thread. So your MyJavaProgram now looks like:

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

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class MyJavaProgram {
    public static void main(String[] args) throws Exception {

        // start FX toolkit on background thread:
        new Thread(() -> Application.launch(FXStarter.class)).start();
        // wait for toolkit to start:
        FXStarter.awaitFXToolkit();

        // make sure closing first window does not exit FX toolkit:
        Platform.setImplicitExit(false);

        int input;
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("0 - exit");
            System.out.println("1 - display something to me");
            input = scanner.nextInt();
            switch (input) {
                case 0:
                    break;
                case 1:
                    // task to show UI:
                    FutureTask<Void> showProgramTask = new FutureTask<>(() -> {
                        MyJavaFXProgram program = new MyJavaFXProgram();
                        Stage stage = new Stage();
                        stage.setScene(new Scene(program.getView(), 400, 400));
                        stage.setOnShown(e -> {
                            stage.toFront();
                            stage.requestFocus();
                        });
                        // showAndWait will block execution until window is hidden:
                        stage.showAndWait();
                        return null ;
                    });
                    // show UI on FX Application Thread:
                    Platform.runLater(showProgramTask);
                    // block until task completes (i.e. window is hidden):
                    showProgramTask.get() ;
                    break;

            }
            if (input == 0) break;
        }

        // all done, exit FX toolkit:
        Platform.exit();
        scanner.close();
    }

}

(This uses the same version of MyJavaFXProgram above.)

查看更多
登录 后发表回答