3

I am new to JavaFX

I wrote this code however, I dont know how to Display the Menu Bar in all my Scenes. Also I would like to create/fill my scenes with the Layout in my HelloApplication (however thats another issue).

I have a controller, for setting the Stage and launching it. My MenuBar is in the class MenuLeiste, but I would like it to appear in my Credits class aswell. Im very sorry for the lack of comments and the Layout of this comment.

    
    public class HelloApplication extends Application {

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

    @Override
    public void start(Stage primaryStage) throws Exception {

    //set window as primaryStage
    Stage window = primaryStage;


    //Layout of MenuLeiste is put in l1 and setted as scene1
    MenuLeiste l1 = new MenuLeiste();
    //menuscene gets its objects fromsceneViewMenu
    Scene menuscene = new Scene(l1.sceneViewMenu());

    window.setScene(menuscene);

    window.setHeight(600);
    window.setWidth(800);
    window.setTitle("Game Title");
    window.show();
    }
    }

My MenuLeiste class

    import javafx.scene.Scene;
    import javafx.scene.control.Menu;
    import javafx.scene.control.MenuBar;
    import javafx.scene.control.MenuItem;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;


    public class MenuLeiste {

        public VBox sceneViewMenu() {

        MenuBar menuBar = new MenuBar();
        VBox menuBox = new VBox(menuBar);

        Menu dataMenu = new Menu("Data");
                MenuItem exitItem = new MenuItem("Exit");
                exitItem.setOnAction(e -> System.exit(0));

        Menu extrasMenu = new Menu("Extras");
                MenuItem creditsItem = new MenuItem("Credits");
                creditsItem.setOnAction(e -> {
                        Credits c = new Credits();
                        Scene scene3 = new Scene(c.sceneView3());
                        Stage window = (Stage) menuBox.getScene().getWindow();
                        window.setScene(scene3);
                        });
        

        extrasMenu.getItems().addAll(creditsItem);
        dataMenu.getItems().addAll( exitItem);


        menuBar.getMenus().addAll(dataMenu,extrasMenu);

            //the scenes layout is saved in layout1
            VBox layout1 = new VBox(20);
            layout1.getChildren().addAll(menuBox);
        
            return layout1;
        }
        }

My Credits Class

package view;

import javafx.scene.Scene;`
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;`



public class Credits {
    public VBox sceneView3() 
    {

    Label label = new Label("Thanks");
    Button backButton = new Button("Back");

    backButton.setOnAction(e -> {
        MenuLeiste l1 = new MenuLeiste();
        Scene menuscene = new Scene(l1.sceneViewMenu());
        Stage window = (Stage) backButton.getScene().getWindow();
        window.setScene(menuscene);
    });
    
    VBox layout1 = new VBox(20);
    layout1.getChildren().addAll(label, backButton);
    
    return layout1;
    }
    }
lenieve
  • 33
  • 3
  • 3
    The idea would be to _not_ replace the window's `Scene`. Instead, modify the `root` of the scene. For example, you could have a `BorderPane` with the `top` node set to your `MenuBar`, and then you'd update the `center` node to change views. – Slaw Jun 22 '22 at 23:43
  • The [small framework here](https://gist.github.com/jewelsea/6460130) created to answer [Loading new fxml in the same scene](https://stackoverflow.com/questions/18619394/loading-new-fxml-in-the-same-scene) demonstrates a possible solution (using the principles that Slaw has comments). – jewelsea Jun 23 '22 at 01:53
  • Note that modifying the `root` node can be generalized to modifying _any_ node currently displayed in the scene, in case you have a more complicated application. – Slaw Jun 23 '22 at 02:02
  • @Slaw But how would I set my scene credits as the center of a Borderpane? Im not quite sure how I should implement that. – lenieve Jun 23 '22 at 10:40
  • `borderPane.setCenter(credits.sceneView3());` – jewelsea Jun 23 '22 at 17:18
  • Thanks for all your help! @jewelsea Im afraid I still dont understand how to implement this. Changing the MenuBar VBox to a BorderPane, but this made the MenuBar very small (... .... ....) however after consulting the Api I believe it to be an Allignment error on my side. But my main Issue is that I did as you said (`creditsItem.setOnAction(e -> { menuPane.setCenter(Credits.sceneView3());});` But it still only shows the credits scene. What am i doing wrong? Im at a loss. – lenieve Jun 23 '22 at 19:26

2 Answers2

2

When you want a node, or a group of nodes, to exist between different "scenes", often the best solution is to not replace the Scene. Instead, you'll want to modify the nodes displayed in the current scene. For example, in your case, you could have a BorderPane as the root of the scene with the MenuBar set as the top node. Then you replace the center node when you want to change the "view".

Here's a minimal runnable example demonstrating this concept. It makes use of "callbacks" to modify the center node of the BorderPane, while using the same MenuBar instance throughout. Though note the example only consists of views. If you have a backing model (i.e., data, business logic, etc.), you'll want to modify the code so you can pass it around. Also, if you write your application similarly to this example, it might be prudent to have all the views implement a common interface in a real application.

I don't have a separate "view class" for the menu bar, but that doesn't mean you can't have one (might even be better that way).

Main.java:

import java.util.function.Consumer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) {
    var root = new BorderPane();
    root.setTop(createMenuBar(root::setCenter));
    root.setCenter(new TitleView().getNode());

    primaryStage.setScene(new Scene(root, 600, 400));
    primaryStage.setTitle("Demo");
    primaryStage.show();
  }

  private MenuBar createMenuBar(Consumer<Node> onUpdateView) {
    var exitItem = new MenuItem("Exit");
    exitItem.setOnAction(e -> Platform.exit());

    var creditsItem = new MenuItem("Credits");
    creditsItem.setOnAction(e -> {
      e.consume();

      var view = new CreditsView();
      view.setOnGoBack(() -> onUpdateView.accept(new TitleView().getNode()));
      onUpdateView.accept(view.getNode());
    });

    return new MenuBar(
        new Menu("File", null, exitItem),
        new Menu("Extras", null, creditsItem)
    );
  }
}

TitleView.java:

import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;

public class TitleView {

  private Node node;

  public Node getNode() {
    if (node == null) {
      node = new StackPane(new Label("Welcome!"));
    }
    return node;
  }
}

CreditsView.java:

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;

public class CreditsView {

  private Runnable onGoBack;
  private Node node;

  public Node getNode() {
    if (node == null) {
      var stack = new StackPane();
      stack.setPadding(new Insets(10));

      var label = new Label("Credits View");
      stack.getChildren().add(label);

      var goBackBtn = new Button("Go back");
      goBackBtn.setOnAction(e -> {
        e.consume();
        if (onGoBack != null) {
          onGoBack.run();
        }
      });
      stack.getChildren().add(goBackBtn);
      StackPane.setAlignment(goBackBtn, Pos.TOP_LEFT);

      node = stack;
    }
    return node;
  }

  public void setOnGoBack(Runnable action) {
    onGoBack = action;
  }
}
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • 1
    On navigation, this example creates new nodes for the views, for example `new TitleView().getNode())` in the onGoBack handler and `new CreditsView()` in the Credits menu item handler. This is fine. Whether you do this or not is up to how you want to code your application. You can either create new view nodes for each navigation or keep a reference to the created nodes and reuse the existing ones (for example if you have a simple app without MVC with have state stored in the views and you want to keep that state). – jewelsea Jun 23 '22 at 23:31
  • 1
    Yes, I meant to mention that. To elaborate further: The view classes in my example store the node in a field in case one wants to use the same view instance instead of creating a new one every time. If you have a model-based architecture (e.g., MVC, MVVM, MVP, etc.), then it should be okay to recreate the view every time because all the state is stored in the model. Though there are at least two exceptions to this: The view is significantly expensive to create, or some state exists within the view that's not "persisted" to the model (e.g., filled-out text fields in a not-yet-submitted form). – Slaw Jun 23 '22 at 23:45
1

Here is some basic code that should be easy for a beginner to understand.

  1. The main layout and the potential views to be displayed within it are created upfront.

  2. A menu bar is placed at the top of the border pane in the main layout.

  3. Menu items can be used to switch between views by setting the node to be currently displayed in the center of the border pane.

  4. The existing views are reused rather than being recreated on each navigation.

    • You could not store references to existing views and create new views on each navigation if preferred.

The views themselves are just nodes, so the example could easily be adapted to use FXML because the output of the FXML loader is also a node. The same goes for anything else which may generate a node to be used as a view in this fashion.

Operation

The scene is initially displayed and the user clicks on the "View" menu to show the list of available views.

view one

The user selects the "View Two" menu item and the second view is displayed. The application menu remains visible and can be used for future operations.

view two

Sample Code

import javafx.application.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class ViewSwitcherApp extends Application {
    private BorderPane layout;
    private final Node viewOne = new ViewOne();
    private final Node viewTwo = new ViewTwo();

    @Override
    public void start(Stage stage) throws Exception {
        // View menu
        MenuItem viewOneMenuItem = new MenuItem("View One");
        viewOneMenuItem.setOnAction(e -> setView(viewOne));
        MenuItem viewTwoMenuItem = new MenuItem("View Two");
        viewTwoMenuItem.setOnAction(e -> setView(viewTwo));
        Menu viewMenu = new Menu(
                "View", null,
                viewOneMenuItem, viewTwoMenuItem
        );

        // File menu
        MenuItem exitMenuItem = new MenuItem("Exit");
        exitMenuItem.setOnAction(e -> Platform.exit());
        Menu fileMenu = new Menu(
                "File", null,
                exitMenuItem
        );

        MenuBar menuBar = new MenuBar(
                fileMenu, viewMenu
        );
        menuBar.setMinSize(MenuBar.USE_PREF_SIZE, MenuBar.USE_PREF_SIZE);

        // Layout scene
        layout = new BorderPane();
        layout.setTop(menuBar);
        setView(viewOne);

        stage.setScene(
                new Scene(layout, 300, 200)
        );
        stage.show();
    }

    private void setView(Node view) {
        layout.setCenter(view);
    }

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

class ViewOne extends StackPane {
    public ViewOne() {
        setStyle("-fx-background-color: lightblue; -fx-font-size: 30px;");
        getChildren().add(new Label("View One"));
    }
}

class ViewTwo extends StackPane {
    public ViewTwo() {
        setStyle("-fx-background-color: cornsilk; -fx-font-size: 30px;");
        getChildren().add(new Label("View Two"));
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • I have adapted all of the tips above and added to the code to fit my project, however I ran into an issue. The final version of the project is a simple client server communication. The Network part is working so right now I want to set up a simple system for sending messeges (eg button: send message and client will recieve message in a Text field. Now to my problem: I have a view Connect that containes a button for connecting to server/client. When pressing this I would like the View Node to be the Gameview (SendText B and recieveTextField) but I dont know how to implement this. – lenieve Jun 29 '22 at 09:35
  • So basically how would I change the Node without using ViewSwitcherApp since I cant use setView. Or is there a way to implement it in ViewSwitcherApp? – lenieve Jun 29 '22 at 09:37
  • @lenieve it is hard for me to understand what you are asking. I assume you are asking how to call the method `setView` from another class. You could make setView static and call it at a class level, or you can pass a reference to the ViewSwitcherApp instance to the other class, make the setView method public and then call setView on the instance, or you can use a [small view switching framework](https://stackoverflow.com/questions/18619394/loading-new-fxml-in-the-same-scene), like [this](https://gist.github.com/jewelsea/6460130). – jewelsea Jun 29 '22 at 10:34
  • After some trial and error I realized I had a spelling mistake... Thanks a lot fot the suggestions, I ended up making setView public and passing it to my views. – lenieve Jun 29 '22 at 15:38