javauser-interfacejavafxscenemulti-page-application

JavaFX: Multiple 'views', 'pages' or scenes - how to approach?


I am trying to build a dummy webshop with JavaFX. I deliberately do not use fxml, scenebuilder, maven or any other build tool. Just plain JavaFX, in order to really get to understand the basics. However, I ran into a problem creating and navigating different 'pages'.

I have tried various creative solutions, like this one (is posting links allowed?), but none fully work for me, as I want every 'page', 'view' or scene in a seperate java class file, in order to keep everything structured and orderly.

I figured I'd make a Borderpane as a parent layout for every page

abstract class WindowBase extends BorderPane {

    public abstract BorderPane render(App app);

    public WindowBase() {
    
        Label labelTop = new Label("Top box");

        HBox topBox = new HBox();
        topBox.setStyle("-fx-background-color: red;");
        topBox.getChildren().addAll(labelTop);

        Label labelLeft = new Label("Left box");

        VBox leftBox = new VBox();
        leftBox.setStyle("-fx-background-color: green;");
        leftBox.getChildren().addAll(labelLeft);

        Label labelRight = new Label("Right box");

        VBox rightBox = new VBox();
        rightBox.setStyle("-fx-background-color: blue;");
        rightBox.getChildren().addAll(labelRight);

        Label labelBottom = new Label("Bottom box");

        HBox bottomBox = new HBox();
        bottomBox.setStyle("-fx-background-color: yellow;");
        bottomBox.getChildren().addAll(labelBottom);

        this.setTop(topBox);
        this.setLeft(leftBox);
        this.setRight(rightBox);
        this.setBottom(bottomBox);
    }
}

and a child, the home page

public class Homepage extends WindowBase {

    public BorderPane render(App app) {

        Button button = new Button("Go to shopping cart");
        button.setOnAction((event) -> app.toShoppingCart());

        StackPane centerPane = new StackPane();
        centerPane.getChildren().add(button);
        this.setCenter(centerPane);

        return this;
    }
}

and lastly my App.java that runs everything

public class App extends Application{

    private WindowBase view;

    public void start(Stage stage) throws Exception {

        view = new Homepage();

        stage.setScene(new Scene(view.render(this)));
        stage.setFullScreen(true);
        stage.show();

    }

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

    public void toHomepage() {
        this.view = new Homepage();
    }

    public void toShoppingCart() {
        this.view = new ShoppingCart();
    }
}

I understand that I can't pass this (App) as an argument to view.render(), use the parameter within the method render and expect to be able to manipulate it, because it only creates a new instance of App as soon as it gets there. However, I see no other way either.

I tried placing the navigation buttons in the App class, in order to be able to manipulate view, but then I cannot call on the buttons from the subsequent views.

There must be a way to achieve what I want without writing the complete GUI in one file, right? Should I make my view static in stead, is that it?

Instead of BorderPanes I am of course also okay with using Scenes, whatever works.


Solution

  • I have figured out exactly the solution that I wanted. Posting it here for whoever encounters the same situation.

    Class WindowBase

    public class WindowBase {
    
        public BorderPane getMainPane() {
        
            Label labelTop = new Label("Top box");
        
            HBox topBox = new HBox();
            topBox.setStyle("-fx-background-color: red;");
            topBox.getChildren().addAll(labelTop);
    
            Label labelLeft = new Label("Left box");
    
            VBox leftBox = new VBox();
            leftBox.setStyle("-fx-background-color: green;");
            leftBox.getChildren().addAll(labelLeft);
    
            Label labelRight = new Label("Right box");
    
            VBox rightBox = new VBox();
            rightBox.setStyle("-fx-background-color: blue;");
            rightBox.getChildren().addAll(labelRight);
    
            Label labelBottom = new Label("Bottom box");
    
            HBox bottomBox = new HBox();
            bottomBox.setStyle("-fx-background-color: yellow;");
            bottomBox.getChildren().addAll(labelBottom);
    
            BorderPane borderPane = new BorderPane();
    
            borderPane.setTop(topBox);
            borderPane.setLeft(leftBox);
            borderPane.setRight(rightBox);
            borderPane.setBottom(bottomBox);
    
            return borderPane;
        }
    }
    

    Class Homepage

    public class Homepage extends WindowBase {
    
        public BorderPane render(Button toShoppingCart) {
    
            Label label = new Label("This is the homepage.");
            label.setStyle(
                    "-fx-font: normal bold 30px 'elephant'; -fx-text-fill: black; -fx-background-color: red;");
    
            StackPane stackPane = new StackPane();
            stackPane.getChildren().addAll(label);
    
            BorderPane mainPane = getMainPane();
            VBox leftBox = (VBox) mainPane.getLeft();
            leftBox.getChildren().add(toShoppingCart);
    
            //I do not understand why this works. I abstracted leftBox from mainPane, then added shoppingCart, but never added the abstracted
            //  leftBox back to the mainPane before returning it. This should not work, but it does. leftBox.getChildren().add(toShoppingCart)
            //  should have no effect.
            //However, mainPane.getChildren().add(leftBox) throws an IllegalArgumentException about duplicate components, which is to be
            //  expected if the leftBox is already automatically added back to the mainPane.
    
            mainPane.setCenter(stackPane);
    
            return mainPane;
        }
    }
    

    Class ShoppingCart

    public class ShoppingCart extends WindowBase {
    
        public ShoppingCart() {
            super();
        }
    
        public BorderPane render(Button toHomepage) {
            
            Label label = new Label("This is the shopping cart.");
            label.setStyle(
                "-fx-font: normal bold 30px 'elephant'; -fx-text-fill: black; -fx-background-color: red;");
            
            StackPane centerPane = new StackPane();
            centerPane.getChildren().add(label);
        
            BorderPane mainPane = getMainPane();
            VBox leftBox = (VBox) mainPane.getLeft();
            leftBox.getChildren().add(toHomepage);
    
            mainPane.setCenter(centerPane);
    
            return mainPane;
        }
    }
    

    Class App

    public class App extends Application{
    
        Scene scene;
    
        @Override
        public void start(Stage stage) throws Exception {
    
            Homepage homePane = new Homepage();
            Button toHomepage = new Button("Back to home page");
    
            ShoppingCart shoppingPane = new ShoppingCart();
            Button toShoppingCart = new Button("To shopping cart");
    
            toHomepage.setOnAction(e -> scene.setRoot(homePane.render(toShoppingCart)));
            toShoppingCart.setOnAction(e -> scene.setRoot(shoppingPane.render(toHomepage)));
    
            scene = new Scene(homePane.render(toShoppingCart), 600, 400);
    
            stage.setScene(scene);
            stage.setFullScreen(true);
            stage.show();
    
        }
    
        public static void main(String[] args) throws Exception {
            
            launch(args);
        }
    }