Вызовите JavaFX в программе Java и дождитесь выхода, прежде чем запускать дополнительный код.

В моей Java-программе я даю пользователю некоторые параметры, и один из них вызывает JavaFXProgram для отображения чего-либо. Я только хочу запустить больше кода в программе Java, когда этот JavaFX, который был вызван, действительно выйдет, это может занять 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);
    }

}

изменить1:

Я только что заметил, что если я попытаюсь отобразить что-то два раза (например: выбрать 1, закрыть приложение JavaFX, затем снова выбрать 1), он вылетает с Exception in thread "main" java.lang.IllegalStateException: Application launch must not be called more than once. Кажется, что в этом коде приложение JavaFX также не работает должным образом.


person power_output    schedule 21.01.2017    source источник
comment
Разве это не тот метод, который вы ищете showAndWait() ?   -  person Bo Halim    schedule 21.01.2017
comment
Разве это уже не делается? Application.launch() не возвращается, пока приложение JavaFX не завершит работу. Но также в целом этот подход не будет работать, потому что вы не можете вызывать launch() более одного раза.   -  person James_D    schedule 21.01.2017
comment
Почему вы в любом случае хотите управлять приложением с графическим интерфейсом из командной строки? Это довольно сложно сделать: вам нужно запачкать руки с некоторыми потоками и т. Д. Почему бы просто не показать опцию, чтобы показать мне что-то в простом окне?   -  person James_D    schedule 21.01.2017
comment
@BoHalim showAndWait() на самом деле здесь не работает (по крайней мере, напрямую). Конечно, вы можете вызывать showAndWait() только из потока приложения FX, и в частности он должен вызываться из обработчика событий или вызова Platform.runLater(), и нельзя вызывать на основном этапе. Конечно, отправка его на Platform.runLater() позволяет продолжить работу с текущим кодом. Таким образом, вы должны сделать это с некоторой умеренно сложной многопоточностью.   -  person James_D    schedule 21.01.2017
comment
@James_D, вы правы, Application.launch() не возвращается, пока не завершится JavaFX. У меня была ошибка, которая все испортила, но теперь, как вы уже сказали, я столкнулся с проблемой вызова launch() более одного раза. Я не понимаю, почему это проблема. Я вызываю приложение, но затем оно закрывается, так как же оно узнает, что оно вызывается дважды? Есть ли способ сбросить это?   -  person power_output    schedule 21.01.2017
comment
@James_D Я делаю это из командной строки, потому что хочу иметь кучу опций для установки некоторых настроек, а затем одну опцию для отображения приложения (оно отображает разные вещи в зависимости от настроек). Я хочу этого снова и снова. Из командной строки это кажется довольно интуитивным способом сделать это, но я не знал о дополнительных сложностях, поэтому это может быть не так.   -  person power_output    schedule 21.01.2017
comment
Это просто кажется более неудобным для вашего пользователя, даже не задумываясь о том, проще ли кодировать. Почему бы просто не открыть окно с кучей опций (представленных в удобном графическом интерфейсе) при запуске, а затем, когда опция выбрана, показать модальное окно, соответствующее этой опции? Тогда вашему пользователю не придется переключаться между режимом командной строки и режимом графического интерфейса.   -  person James_D    schedule 22.01.2017


Ответы (1)


Ваш код не работает так, как у вас есть, потому что он не соответствует жизненному циклу приложения JavaFX, который полностью задокументирован в документации по API для Application. Короче говоря, класс Application представляет все приложение (или, возможно, жизненный цикл приложения).

Чтобы отобразить окно в JavaFX, вы должны сделать это в потоке приложения FX, а для запуска этого потока должен быть запущен инструментарий FX (среди прочего). Метод Application.launch() запускает инструментарий FX, запускает поток приложения FX, создает экземпляр класса вашего приложения, вызывает init() в этом экземпляре, а затем вызывает start() в этом экземпляре (вызов start() происходит в потоке приложения FX).

Как задокументировано, Application.launch() блокирует (не возвращает) до тех пор, пока инструментарий FX не завершит работу (т. е. приложение не закроется), и должен вызываться только один раз. (Поскольку он представляет все приложение, это имеет смысл, и нет возможности обойти его дважды.)

Структура вашего приложения также не имеет никакого смысла с точки зрения пользователя. Зачем просить вашего пользователя взаимодействовать с командной строкой, чтобы представить параметры для приложения на основе графического интерфейса? Вы должны представить эти параметры в графическом интерфейсе. Просто покажите окно с параметрами при запуске, а затем покажите окно, соответствующее выбранному параметру. Если вы хотите гарантировать, что пользователь не сможет вернуться в исходное окно до завершения выбранной опции, просто сделайте новое окно модальным.

Например, если вы реорганизуете 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. Существует хак для запуска инструментария FX путем создания JFXPanel, но это действительно немного хак, и я предпочитаю делать это явно, имея класс Application, который на самом деле ничего не делает, вызывая launch() и ожидая его инициализация завершена. Я показал, как это сделать, в ответе на этот вопрос, поэтому я просто позаимствую это решение у там. Вот класс Application, который используется для запуска инструментария FX (но не является основным классом, который вы выполняете):

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, что и выше.)

person James_D    schedule 22.01.2017
comment
Просто хотел отметить, что есть абсолютно веские причины для запуска графического интерфейса из приложений командной строки, чтобы получить некоторый пользовательский ввод. Одним из примеров является написание задач Gradle, которые должны быть интерактивными. Когда Gradle работает в режиме демона, он не дает доступа к консоли (System.console() возвращает null), поэтому единственным более-менее разумным подходом было бы предоставить пользователю графический интерфейс для ввода данных, по крайней мере, если сам Gradle имеет общедоступный API для приема пользовательского ввода (по состоянию на июль 2018 г. его нет). - person Vladimir Matveev; 04.07.2018