在 Java 程序中调用 JavaFX 并等待等待退出,然后再运行更多代码

Posted

技术标签:

【中文标题】在 Java 程序中调用 JavaFX 并等待等待退出,然后再运行更多代码【英文标题】:Call JavaFX in Java program and wait for wait to exit before running more code 【发布时间】:2017-06-06 09:50:02 【问题描述】:

在我的 java 程序中,我为用户提供了一些选项,其中一个调用 JavaFXProgram 来显示某些内容。我只想在这个被调用的 JavaFX 实际退出时在 Java 程序中运行更多代码,可能需要 5 秒,可能需要一分钟。理想情况下,我想要的是我们在 android 中的东西。我们拨打startActivityForResult(),然后等待onActivityResult()的电话。 在我的情况下如何实现类似的行为?

我编写了这段代码来尝试复制我遇到的问题。这是类似的想法,但不知何故这会调用 JavaFX,进入循环开始并毫无问题地从用户那里检索输入。在我的另一个程序中,当它再次返回扫描输入时,我总是得到Exception in thread "main" java.util.InputMismatchException。但正如我所说,理想情况下,我只想在 JavaFX 应用程序关闭后运行更多代码。

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:

我刚刚注意到,如果我尝试显示某些内容两次(例如:选择 1,关闭 JavaFX 应用程序,然后再次选择 1),它会因Exception in thread "main" java.lang.IllegalStateException: Application launch must not be called more than once 而崩溃。似乎 JavaFX 应用程序也没有在此代码中正确退出。

【问题讨论】:

这不是你要找的方法showAndWait()吗? 不是已经这样做了吗? Application.launch() 在 JavaFX 应用程序退出之前不会返回。而且,一般来说这种方法是行不通的,因为你不能多次调用launch() 为什么要从命令行驱动一个GUI应用程序呢?这样做非常棘手:您必须弄脏一些线程等。为什么不在一个简单的窗口中显示“向我显示某些东西”的选项? @BoHalim showAndWait() 实际上在这里似乎并不能很好地工作(至少不是直接地)。当然,您只能从 FX 应用程序线程调用 showAndWait(),并且特别是必须从事件处理程序或对 Platform.runLater() 的调用中调用,并且不能在初级阶段被调用。当然,将其提交给Platform.runLater() 允许当前代码继续执行。所以你必须用一些中等复杂的线程来做到这一点。 @James_D 你是对的 Application.launch() 在 JavaFX 退出之前不会返回。我有一个错误正在把事情搞砸,但现在正如你已经说过的那样,我正面临调用 launch() 的问题不止一次。我不明白为什么这是一个问题。我调用 Application 但它退出了,那么它怎么知道它被调用了两次?有没有办法重置这个? 【参考方案1】:

您的代码无法正常工作,因为它不适合 JavaFX 应用程序生命周期,Application 的 API 文档中对此进行了完整记录。简而言之,Application 类代表整个应用程序,(或者可能是应用程序的生命周期)。

要在 JavaFX 中显示窗口,您必须在 FX 应用程序线程上执行此操作,并且必须启动 FX 工具包才能启动此线程(以及其他事项)。 Application.launch() 方法启动 FX 工具包,启动 FX 应用程序线程,创建应用程序类的实例,在该实例上调用 init(),然后在该实例上调用 start()(对 start() 的调用发生在FX 应用程序线程)。

作为documented,Application.launch() 阻塞(不返回)直到 FX 工具包关闭(即应用程序退出),并且只能调用一次。 (因为它代表了整个应用程序,所以这是有道理的,并且没有办法绕过调用它两次。)

从用户的角度来看,您的应用程序结构也没有任何意义。为什么要求您的用户与命令行交互以向基于 GUI 的应用程序提供选项?您应该在 GUI 中显示这些选项。只需在启动时显示一个带有选项的窗口,然后显示一个与所选选项相对应的窗口。如果您想确保用户在所选选项完成之前无法返回到原始窗口,只需将新窗口设为模态即可。

例如,如果您重构 MyJavaFXProgram,使其不是 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 ;
    


那你就可以了

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();
    



如果你真的想从命令行驱动所有这些(老实说我看不出这样做的正当理由),那么它会变得很棘手,你必须亲自动手管理线程之间的交互一点。可能最简单的方法是确保 FX 工具包在您的应用程序启动时启动,然后或多或少地按照您在代码中的方式进行,但再次重构 MyJavaFXProgram,使其不是 Application 的子类.通过创建JFXPanel 来启动 FX 工具包有一个 hack,但这确实是一个 hack,我更喜欢通过一个实际上不做任何事情的 Application 类来明确地做到这一点,调用 @ 987654339@,并等待其初始化完成。我在对this question 的回答中展示了如何做到这一点,所以我将从那里借用该解决方案。这是用于启动 FX 工具包的 Application 类(但不是您执行的主类):

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
    

这个想法是调用Application.launch(FXStarter.class),但由于launch() 阻塞,您需要在后台线程上执行此操作。因为这意味着您的后续代码可能(可能会)在launch() 实际完成您需要它完成的工作之前执行,您需要等到它完成它的工作,您可以使用FXStarter.awaitFXToolkit() 来完成。然后你可以执行你的循环。剩下的唯一需要担心的是确保您在 FX 应用程序线程上创建并显示新窗口。所以你的MyJavaProgram 现在看起来像:

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();
    


(这里使用了与上面相同版本的MyJavaFXProgram。)

【讨论】:

只是想指出,从命令行应用程序启动 GUI 以获得一些用户输入是绝对有正当理由的。一个例子是编写需要交互的 Gradle 任务。当 Gradle 在守护程序模式下工作时,它不会提供对控制台的访问权限(System.console() 返回 null),因此唯一或多或少的理智方法是至少为用户提供一个 GUI 以输入数据,除非Gradle 本身具有接受用户输入的公共 API(截至 2018 年 7 月还没有)。

以上是关于在 Java 程序中调用 JavaFX 并等待等待退出,然后再运行更多代码的主要内容,如果未能解决你的问题,请参考以下文章

让听众等待javafx

java中请问如何等待一个线程结束在运行其他的代码?

JavaFX 在转到下一个方法之前等待动画方法完成

如何让线程等待 JFrame 在 Java 中关闭?

java-线程的生命周期

等待 Java 进程的回答?