In my JavaFX FXML app, I want a secondary window to pop up when the user clicks a menu item somewhere in the primary window so that the user can enter some input into it, which will be then fed to the application upon clicking a button, and the secondary window will be closed.
All the tutorials out there are slightly off the mark. They describe how to do it in pure JavaFX, which is apparently different from the way you'd use with FXML, or they explain how to switch Scenes, which closes the old Scene. I'd guess it would be simple enough, along the lines of defining the FXML layout and its Controller, creating a new Scene with them, and then calling something like
theStage.showScene(userInputWindow);
but a working solution seems much more complicated, and the reasoning behind it different from my assumptions. For example in this tutorial, I don't really understand why did they put that cast in there, what would the FXMLLoader() actually do, or indeed how would I adapt any of this to the task at hand. Also, the resource states the "the stage can only show 1 scene at a time". It seems extremely unlikely to me that a JavaFX app could lack such a trivial feature as showing a new window without closing the old one. Maybe I misunderstood something about what a Stage and a Scene are and what they can do. So I need to know:
How to achieve the effect described above in code?
What is the reasoning behind the solution; what do all the things involved do there?
You can only show one scene in a Stage
, but you can create multiple stages. If you want to use fxml for your secondary window, you should get your hands on the controller instance and design the controller in a way that allows you to access the user's input. You can use Stage.showAndWait
to "wait for the user to complete the input".
Note that here it's just a button that opens the new window, but you could use similar logic in the onAction
event handler of a menu item. (You need to use someNode.getScene().getWindow()
to get access to the parent window for Stage.initOwner
in this case; someNode
is a arbitrary Node
in the parent window; you could get the node from the event (((Node)event.getTarget())
) or use a node that you know is in the scene; in InputController.submit
mealField
is used for this purpose)
@Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Choose favorite meal");
Label label = new Label("I don't know your favorite meal yet!");
btn.setOnAction((ActionEvent event) -> {
FXMLLoader loader = new FXMLLoader(getClass().getResource("input.fxml"));
Scene newScene;
try {
newScene = new Scene(loader.load());
} catch (IOException ex) {
// TODO: handle error
return;
}
Stage inputStage = new Stage();
inputStage.initOwner(primaryStage);
inputStage.setScene(newScene);
inputStage.showAndWait();
String meal = loader.<InputController>getController().getMeal();
label.setText(meal == null ? "C'mon, tell me your favourite meal already!" : "Your favourite meal is "+meal+". Interesting!");
});
VBox root = new VBox(label, btn);
root.setSpacing(10);
root.setPadding(new Insets(10));
root.setPrefWidth(300);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public class InputController {
@FXML
private TextField mealField;
private boolean mealChosen;
@FXML
private void submit() {
mealChosen = true;
mealField.getScene().getWindow().hide();
}
public String getMeal() {
return mealChosen ? mealField.getText() : null;
}
}
<GridPane xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="mypackage.InputController" vgap="10" hgap="10" >
<columnConstraints>
<ColumnConstraints prefWidth="150.0" />
<ColumnConstraints prefWidth="150.0" />
</columnConstraints>
<children>
<TextField GridPane.columnIndex="1" fx:id="mealField" onAction="#submit" />
<Button mnemonicParsing="false" text="Ok" GridPane.columnIndex="1" GridPane.rowIndex="1" onAction="#submit" />
<Label text="Your favourite meal" />
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</GridPane>