JavaFX: Best practice for navigating between UI screens - java

I want to change UI screens from login.fxml to home.fxml.
Should I change the Stage or the Scene? I'm not sure which is the best practice?
Also, can I use a lambda expression for the handler in the controller?

First, let's start out with the Stage .vs. Scene issue:
As known, the JavaFX hierarchy is based on: Stage -> Scene -> Nodes (etc).
See here:
Practically speaking, a rule-of-thumb in my opinion is the future:
If you plan on going forward to a different place in the flow of your program (login -> profile, for example) - change the Stage.
If you are in the same enviroment (login for the first time -> login after multiple wrong tries) - change the Scene.
As for lambdas: Ahhmmm... if your current Java/JavaFX version has the abillity - there is no reason not to use.
For more about controller handlers in
Java 8, see this great tutorial.

I use this approach for changing scenes in JavaFX:
/**
* Controller class for menuFrame.fxml
*/
public class MenuFrameControl implements Initializable {
#FXML private Button sceneButton1;
#FXML private Button sceneButton2;
#FXML private Button sceneButton3;
/**
* Event handling method, loads new scene from .fxml file
* according to clicked button and initialize all components.
* #param event
* #throws IOException
*/
#FXML
private void handleMenuButtonAction (ActionEvent event) throws IOException {
Stage stage = null;
Parent myNewScene = null;
if (event.getSource() == sceneButton1){
stage = (Stage) sceneButton1.getScene().getWindow();
myNewScene = FXMLLoader.load(getClass().getResource("/mvc/view/scene1.fxml"));
} else if (event.getSource() == sceneButton2){
stage = (Stage) flightBtn.getScene().getWindow();
myNewScene = FXMLLoader.load(getClass().getResource("/mvc/view/scene2.fxml"));
} else if (event.getSource() == sceneButton3) {
stage=(Stage) staffBtn.getScene().getWindow();
myNewScene = FXMLLoader.load(getClass().getResource("/mvc/view/scene3.fxml"));
}
Scene scene = new Scene(myNewScene);
stage.setScene(scene);
stage.setTitle("My New Scene");
stage.show();
}
#Override
public void initialize(URL location, ResourceBundle resources) { }
So basically when you click the button, it saves actually displayed Stage object into stage variable. Then it loads new Scene object from .fxml file into myNewScene variable and then put this fresh loaded Scene object into your saved Stage object.
With this code you can make basic three button menu, where each button switch to different scene, using just single Stage object.

Related

JavaFX stage issue with hide and show

Im building an application that shows a window that ask the user if he want to suspend the computer with two button options, one of them its a YES and the PC suspends.
The other button named "Later" supposed to hide the window and after an hour it appears again and ask the same question.
Code for the "later buttton"
noButton.setOnAction(event -> {
Done=false; //boolean to close and open the frame
Gui gui = new Gui();
try {
gui.start(classStage);
} catch (Exception e) {
e.printStackTrace();
}
});
The boolean that you see in the code is bc it was the way i think i could control that, trust me i tried in different ways but no one just help me with the issue, here is the code of the GUI class
public class Gui extends Application {
public Stage classStage = new Stage();
public static boolean Done=true;
public static boolean flag=true;
public Gui() {
}
#Override
public void start(Stage primaryStage) throws Exception {
Done = Controller.isDone();
classStage = primaryStage;
Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds();
primaryStage.setX(primaryScreenBounds.getMaxX() - primaryScreenBounds.getWidth());
primaryStage.setY(primaryScreenBounds.getMaxY() - primaryScreenBounds.getHeight());
primaryStage.setAlwaysOnTop(true);
Parent root = FXMLLoader.load(getClass().getResource("MainWindow.fxml"));
primaryStage.setTitle("Alerta suspencion de equipo");
primaryStage.setScene(new Scene(root));
primaryStage.setResizable(false);
primaryStage.initStyle(StageStyle.UNDECORATED);
if (Controller.isDone() == true) {
primaryStage.show();
} else if(Controller.isDone() == false) {
primaryStage.hide();
Platform.exit(); // this is the only way that the windows close
}
}
i know that Platform.exit(); kills the program but when i only use .hide(); of the Stage nothing happens, the window never closed, the worst part is that when i use the Platform.exit() command i cant make the frame appear again...
Anyone knows a way maybe easier to hide and show a window after certain time? maybe im doing this wrong.
Regards.
It's not really clear what's going on in the code in your question. The bottom line is that you should never create an instance of Application yourself; the only Application instance should be the one created for you.
I don't actually see any need to have a separate class for the functionality you've shown (though you could, of course). All you need to do is hide classStage if the no button is pressed, and open it again in an hour:
noButton.setOnAction(event -> {
Done=false; //boolean to close and open the frame
classStage.hide();
PauseTransition oneHourPause = new PauseTransition(Duration.hours(1));
oneHourPause.setOnFinished(e -> showUI(classStage));
oneHourPause.play();
});
// ...
private void showUI(Stage stage) {
Rectangle2D primaryScreenBounds = Screen.getPrimary().getVisualBounds();
stage.setX(primaryScreenBounds.getMaxX() - primaryScreenBounds.getWidth());
stage.setY(primaryScreenBounds.getMaxY() - primaryScreenBounds.getHeight());
stage.setAlwaysOnTop(true);
Parent root = FXMLLoader.load(getClass().getResource("MainWindow.fxml"));
stage.setTitle("Alerta suspencion de equipo");
stage.setScene(new Scene(root));
stage.setResizable(false);
stage.initStyle(StageStyle.UNDECORATED);
stage.show();
}
Note that the FX Application will exit if the last window is closed, by default. So you should call Platform.setImplicitExit(false); in your init() or start() method.
I am assuming you are reloading the FXML file because the UI might have changed since it was previously loaded. Obviously if that's not the case, all you have to do is show the stage again as it is:
noButton.setOnAction(event -> {
Done=false; //boolean to close and open the frame
classStage.hide();
PauseTransition oneHourPause = new PauseTransition(Duration.hours(1));
oneHourPause.setOnFinished(e -> classStage.show());
oneHourPause.play();
});

Making a game in JavaFX8. How can I switch from Splash page to Cutscene to Level?

this is my first game using JavaFX so I admittedly have made some bad design decisions, probably.
Anyway, I want to transition from a splash page (Splash class) to a cutscene (Cutscene class) and then to a playable level (PlayableLevel class). The game is launched from my Main class and transitions are supposed to be done with keyboard inputs (Enter button).
The method in Main that starts the game looks like this:
public void start (Stage s) {
// create your own game here
Splash splashPage = new Splash();
Cutscene cs = new Cutscene();
PlayableLevel play = new PlayableLevel();
// attach game to the stage and display it
Scene scene0 = splashPage.init(SIZE, SIZE);
Scene scene1 = cs.init(SIZE, SIZE, 0);
Scene scene2 = play.init(SIZE, SIZE, 0);
s.setScene(scene0);
s.show();
// sets the game's loop
KeyFrame frame = new KeyFrame(Duration.millis(MILLISECOND_DELAY),
e -> myGame.step(SECOND_DELAY));
Timeline animation = new Timeline();
animation.setCycleCount(Timeline.INDEFINITE);
animation.getKeyFrames().add(frame);
animation.play();
}
My question in particular is what should I do to make it so that the Splash class can communicate with the Main class so that once a keystroke event is recorded, the stage can set a new scene? I'm currently reading about EventHandlers but I'm unsure of the exact implementation as of now.
EDIT: One idea I had was to make a linked list of Scenes, and then once some event happens (keystroke), then I would set the scene to the next one in the list.
You could do something like this:
public class Splash {
private Runnable nextSceneHandler ;
public void setNextSceneHandler(Runnable handler) {
nextSceneHandler = handler ;
}
public Scene init(double width, double height) {
Scene scene = new Scene();
// Just an example handler, you could do the same for
// button events, menus, etc., or even just handlers for the
// end of an animation
scene.setOnKeyPressed(e -> {
if (nextSceneHandler != null) {
if (e.getCode() == ...) {
nextSceneHandler.run();
}
}
}
// existing code...
}
// existing code ...
}
and similarly for CutScene.
Then
public void start (Stage s) {
// create your own game here
Splash splashPage = new Splash();
Cutscene cs = new Cutscene();
PlayableLevel play = new PlayableLevel();
// attach game to the stage and display it
Scene scene0 = splashPage.init(SIZE, SIZE);
Scene scene1 = cs.init(SIZE, SIZE, 0);
Scene scene2 = play.init(SIZE, SIZE, 0);
splashPage.setNextSceneHandler(() -> s.setScene(scene1));
cs.setNextSceneHandler(() -> s.setScene(scene2));
s.setScene(scene0);
s.show();
// sets the game's loop
KeyFrame frame = new KeyFrame(Duration.millis(MILLISECOND_DELAY),
e -> myGame.step(SECOND_DELAY));
Timeline animation = new Timeline();
animation.setCycleCount(Timeline.INDEFINITE);
animation.getKeyFrames().add(frame);
animation.play();
}
Your linked list idea should work too. You need a mechanism for passing the linked list instance (or perhaps an iterator from it) to each of the scene generating classes; their event handlers would execute code like
scene.getWindow().setScene(sceneIterator.next());
I kind of prefer setting the runnable on the objects, as it feels a little more flexible. Just a question of style though.

How do I use FXMLLoader inside EventHandler?

I am creating a ListView with the ability to double click each item and have a window popup with inputs structured by an FXML file. The FXML file itemStep contains fx:controller="controller.ItemStep"
listViewVariable.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if (mouseEvent.getButton().equals(MouseButton.PRIMARY)) {
if (mouseEvent.getClickCount() == 2) {
ItemStep item = listViewVariable.getSelectionModel()
.getSelectedItem();
if (item != null) {
try {
FXMLLoader isLoader = new FXMLLoader(Main.class.getResource("/view/itemStep.fxml"));
AnchorPane pane = isLoader.load();
Scene scene = new Scene(pane);
Stage stage = new Stage();
stage.setScene(scene);
item.setUrl(item.urlField.getText());
stage.show();
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
public void handle(WindowEvent we) {
item.setUrl(item.urlField.getText());
}
});
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
});
I continue to get the following error using the above. I need to be able use the FXML file within this Stage.
Caused by: java.lang.InstantiationException: controller.ItemStep
at java.lang.Class.newInstance(Unknown Source)
at sun.reflect.misc.ReflectUtil.newInstance(Unknown Source)
... 44 more
Caused by: java.lang.NoSuchMethodException: controller.ItemStep.<init>()
at java.lang.Class.getConstructor0(Unknown Source)
... 46 more
Your ItemStep class does not have a no argument constructor.
Does a new instance of ItemStep get recreated after each double[click]?
Yes, that is what you have wrote your code to do.
If you didn't wish to do that, you should invoke the load method on your FXMLLoader outside of your event handler, store the reference to the loaded pane in a final variable, and use that reference inside the event handler, rather than loading a new pane time every time the event handler is invoked.
when I invoke both the load FXMLLoader and final AnchorPane outside the event handler I get: AnchorPane#ed39805[styleClass=root]is already set as root of another scene
You can't add a node to more than one scene or to a single scene more than once, you either have to remove it from the original scene, or just re-use the original scene with a single instance of the node. I don't know what kind of behavior you desire, but probably, you want to just have a single window pop-up and make it modal, rather than creating a new window every time somebody clicks on something.
Basically, you do something like this (though this is just an outline that I have never compiled or executed as I don't completely know your complete requirements, nor what your ItemStep and urlField really are):
final FXMLLoader isLoader = new FXMLLoader(Main.class.getResource("/view/itemStep.fxml"));
final AnchorPane pane = isLoader.load();
final Scene scene = new Scene(pane);
final Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setScene(scene);
listViewVariable.setOnMouseClicked(event -> {
if (mouseEvent.getButton().equals(MouseButton.PRIMARY) && (mouseEvent.getClickCount() == 2)) {
ItemStep item = listViewVariable.getSelectionModel().getSelectedItem();
if (item != null) {
stage.showAndWait();
item.setUrl(item.urlField.getText());
}
}
});

JavaFX key press not captured

I am trying to capture key press events (page up and down) but there are no key events received at all. Here is the relevant code:
Constructor:
private MainLayout() {
imageView = new ImageView();
root = new StackPane();
root.getChildren().add(imageView);
root.setFocusTraversable(true); //no effect
//root.requestFocus(); //also no effect
registerEvents();
}
Both lines regarding the focus don't have an effect. The stack pane is directly added to scene.
There are no other nodes than Scene->StackPane->ImageView.
I am able to capture key events on the scene, but i need them captured in the stack pane
Here is registerEvents(), all other events are captured fine!:
private void registerEvents() {
OnScroll onScroll = new OnScroll();
root.setOnScroll(onScroll);
OnResize onResize = new OnResize();
root.heightProperty().addListener(onResize);
root.widthProperty().addListener(onResize);
OnMouseDown onMouseDown = new OnMouseDown();
root.setOnMousePressed(onMouseDown);
root.setOnMouseReleased((event) -> fitImage());
root.setOnDragOver((event) -> dragOver(event));
root.setOnDragDropped((event) -> dropFile(event));
root.setOnKeyPressed((event) -> {
LOG.debug("Key captured.");
if(event.getCode() == KeyCode.PAGE_UP){
imageView.setImage(ip.prev());
event.consume();
} else if(event.getCode() == KeyCode.PAGE_DOWN){
imageView.setImage(ip.next());
event.consume();
}
if(event.isConsumed()){
fitImage();
}
});
I don't see the log out put and a break point is also not caught. So how to catch and handle key events correctly?
Meanwhile i found the solution thanks to this answer. The trick is to setFocusTraversable(true) on ImageView (child of stack pane). Here is the working code:
#Inject
private MainLayout(ImageProvider ip) {
this.ip = ip;
imageView = new ImageView();
imageView.setFocusTraversable(true);
imageView.requestFocus();
root = new StackPane();
root.getChildren().add(imageView);
registerEvents();
}
I don't know if this answer will satisfy you, but I would move handling events from this class to the class where you initialize your scene, and attach the events to the scene itself (since StackPane is, in a way, the scene). I'm guessing that, since the constructor in your code is private, you are instantiating the class via public static method from another class.
public class MainClass extends Application {
private Scene scene = new Scene(MainLayout.getMainLayout());
#Override
public void start(Stage primaryStage) throws Exception {
registerEvents();
primaryStage.setScene(scene);
primaryStage.show();
}
private void registerEvents() {
OnScroll onScroll = new OnScroll();
scene.setOnScroll(onScroll);
OnResize onResize = new OnResize();
scene.heightProperty().addListener(onResize);
scene.widthProperty().addListener(onResize);
OnMouseDown onMouseDown = new OnMouseDown();
scene.setOnMousePressed(onMouseDown);
scene.setOnMouseReleased((event) -> fitImage());
scene.setOnDragOver((event) -> dragOver(event));
scene.setOnDragDropped((event) -> dropFile(event));
scene.setOnKeyPressed((event) -> {
LOG.debug("Key captured.");
if (event.getCode() == KeyCode.PAGE_UP) {
imageView.setImage(ip.prev());
event.consume();
} else if (event.getCode() == KeyCode.PAGE_DOWN) {
imageView.setImage(ip.next());
event.consume();
}
if (event.isConsumed()) {
fitImage();
}
});
}
}
Alternatively, if you want to keep the code for handling events in the MainLayout class, consider making registerEvents a public (or package local, depending on your design) method accepting Scene as a param.

JavaFX modal stage behaving strangely on MAC

I have a JavaFX application that has child windows or stages. The way I handle how all stages interact with each other is like this: I have a static collection of classes called GuiContainer in my Main class. The GuiContainer looks like this:
public class GuiContainer {
private final Stage stage;
private final Scene scene;
private final FXMLLoader loader;
private final Controller controller;
private final Parent parent;
public GuiContainer(Stage stage, Scene scene, FXMLLoader loader, Controller controller, Parent parent) {
this.stage = stage;
this.scene = scene;
this.loader = loader;
this.controller = controller;
this.parent = parent;
}
public Stage getStage() {
return stage;
}
public Scene getScene() {
return scene;
}
public FXMLLoader getLoader() {
return loader;
}
public Controller getController() {
return controller;
}
public Parent getParent() {
return parent;
}
public static final GuiContainer createMain(Stage stage, String title, int width, int height, String pathToView) throws Exception {
FXMLLoader loaderInner = new FXMLLoader(GuiContainer.class.getResource(pathToView));
Parent parentInner = loaderInner.load();
final Controller controllerInner = loaderInner.getController();
Scene sceneInner = new Scene(parentInner, width, height);
stage.setTitle(title);
stage.setScene(sceneInner);
GuiContainer guiContainer = new GuiContainer(
stage, sceneInner, loaderInner, controllerInner, parentInner
);
controllerInner.start(guiContainer);
return guiContainer;
}
public static final GuiContainer createModal(Window owner, String title, int width, int height, String pathToView) throws Exception {
if (owner == null) {
Log.error(GuiContainer.class.getSimpleName(), "Unable to create instance, missing window owner!");
return null;
}
Stage stageInner = new Stage();
FXMLLoader loaderInner = new FXMLLoader(GuiContainer.class.getResource(pathToView));
Parent parentInner = loaderInner.load();
Controller controllerInner = loaderInner.getController();
Scene sceneInner = new Scene(parentInner, width, height);
stageInner.setTitle(title);
stageInner.setScene(sceneInner);
stageInner.initStyle(StageStyle.DECORATED);
stageInner.initModality(Modality.WINDOW_MODAL);
stageInner.initOwner(owner);
GuiContainer guiContainer = new GuiContainer(
stageInner, sceneInner, loaderInner, controllerInner, parentInner
);
controllerInner.start(guiContainer);
return guiContainer;
}
}
This way I have access to any stage or controller from anywhere.
All possible GuiContainers are created only once at boot (in static main(String[] args)) and can then be statically accessed from anywhere, by anyone.
Now... in the main application (scene inside the primary stage) I have a TreeView that has a custom cell factory, and when a cell is right clicked an appropriate context menu is shown. Inside this context menu I open a child/modal stage, like so:
String containerName = "guiName";
GuiContainer container = Main.getGuiContainers().getOrDefault(containerName, null);
if(container == null) {
Log.error(getClass().getSimpleName(), "Unable to find GuiContainer: " + containerName);
return;
}
container.getStage().showAndWait();
Now, here comes the problem. JavaFX doesn't request focus on the child stage. For instance, I can't type into a TextField (on the child stage) because I have an onKeyPressed event registered on the primary stage and it captures all the keys I press. On this child stage I also have a ComboBox, and when I select an item from that ComboBox, the child stage finally comes into focus and the primary stage no longer captures the keys I press.
I also tried to change the modality to all posible values and read what they do on oracle.com, but none of the information helped me...
Is there something wrong with my code? Or could this be an issue with JavaFX? Any ideas?
EDIT: Here is my Application overriden start method: (I hope this provides more information)
#Override
public void start(Stage stage) throws Exception {
GuiContainer guiWindow1 = GuiContainer.createMain(stage, "Window1", 900, 460,
"/com/project/app/gui/views/Window1.fxml");
GuiContainer guiWindow2 = GuiContainer.createModal(stage, "Window2", 320, 240,
"/com/project/app/gui/views/Window2.fxml");
GuiContainer guiWindow3 = GuiContainer.createModal(stage, "Window3", 320, 240,
"/com/project/app/gui/views/Window3.fxml");
GuiContainer guiWindow4 = GuiContainer.createModal(stage, "Window4", 420, 360,
"/com/project/app/gui/views/Window4.fxml");
GuiContainer guiWindow5 = GuiContainer.createModal(stage, "Window5", 380, 460,
"/com/project/app/gui/views/Window5.fxml");
guiWindow5.getStage().setResizable(false);
guiContainers.put("guiWindow1", guiWindow1);
guiContainers.put("guiWindow2", guiWindow2);
guiContainers.put("guiWindow3", guiWindow3);
guiContainers.put("guiWindow4", guiWindow4);
guiContainers.put("guiWindow5", guiWindow5);
guiWindow1.getStage().show();
}
EDIT2: This is how I register the onKeyPressed listener: (in my parent controller init() method)
getGuiContainer().getScene().setOnKeyPressed((e) -> {
Log.info(getClass().getSimpleName(), "Key pressed: " + e.getCode().getName());
Macro macro = Main.getProject().getSelectedMacro();
if(macro == null) {
Log.error(getClass().getSimpleName(), "Unable to find selected macro");
return;
}
if (e.getCode() == KeyCode.ESCAPE) {
macro.getNodePlacement().reset();
macro.cancelSelect();
macro.repaint();
} else if(e.getCode() == KeyCode.DELETE) {
macro.deleteSelectedNodes();
}
});
EDIT3: This is how my abstract controller class looks like - I hope this provides a better insight
public abstract class Controller {
private GuiContainer guiContainer;
public final GuiContainer getGuiContainer() {
return guiContainer;
}
public final void start(GuiContainer guiContainer) {
this.guiContainer = guiContainer;
init();
}
public abstract void init(); // this is where the onKeyPressed is registered, when extending the abstract class
public abstract void reset();
public abstract void refresh();
}
IMPORTANT EDIT: This only happens on the MAC platform, I ran the same application on windows and there were no such problems.
could you please post the code of the constructor of your stage?
Just a note, from the doc:
Note that showing a modal stage does not necessarily block the caller.
The show() method returns immediately regardless of the modality of
the stage. Use the showAndWait() method if you need to block the
caller until the modal stage is hidden (closed). The modality must be
initialized before the stage is made visible.
EDIT - My mistake, I didn't notice the createModal() method.
What I ended up doing in some of my custom stages is:
Platform.runLater(() -> somecontrol.requestFocus());
(java 8, obviously, override run() method if lambda is not available with your current language level (< 8)).
But I only do this for non-modal stage. Modal stage should take the focus automatically.
According to the doc of Modality, APPLICATION_MODAL is supposed to do what you expect (including Blocking the events for other windows):
APPLICATION_MODAL Defines a modal window that blocks events from being
delivered to any other application window.
You call initModality with WINDOW_MODAL before calling initOwner. Please see:
WINDOW_MODAL public static final Modality WINDOW_MODAL Defines a modal
window that block events from being delivered to its entire owner
window hierarchy. Note: A Stage with modality set to WINDOW_MODAL, but
its owner is null, is treated as if its modality is set to NONE.
If requestFocus() (called after the modal stage has been displayed, and in the UI thread right?) doesn't take the focus, I guess your main window is automatically taking it back.

Categories