-2

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.

  • 1
    *”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. “*. I don’t understand what this means. Why would “it” (whatever that is) create a new instance? – James_D Dec 24 '22 at 02:43
  • In the example cited [here](https://stackoverflow.com/a/73793604/230513), individual applications implement a common interface. – trashgod Dec 24 '22 at 19:54
  • @James_D Sorry, I meant to say that ìn App.java, render(this) only creates a new instance of App within the function scope of the render method. – Ontkibbeling Dec 25 '22 at 21:31
  • Java may be _pass-by-value_, but the "value" being passed is the reference. So, only the reference is copied, not the object itself. In other words, `render(this)` does _not_ create a new `App` instance. – Slaw Dec 25 '22 at 22:18
  • *” render(this) only creates a new instance of App within the function scope of the render method”*. Again, it doesn’t create a new `App` instance. It’s not clear what your misunderstanding is, but you have a very fundamental misunderstanding of Java basics. – James_D Dec 28 '22 at 22:48
  • @James_D It is quite clear what my misunderstanding is. It's about the nature of the arguments passed in a method. Maybe you could explain by means of the problem that I am still left with. Look at my own answer to the question. It works. However, the navigation buttons are still declared in App. I want to declare a button (and handler) within Homepage. Is this possible? I put most of the code of `render()` and `getMainPane()` in constructors and then tried passing `scene` to Homepage's constructor, but that wont work, as `scene` is only a copy of a reference there, whatever that really means. – Ontkibbeling Dec 29 '22 at 07:39
  • Java is pass-by-value. So if you pass a value to a method, the method creates a local variable (declared as the parameter) and copies the value that’s passed to the local variable. If you are passing a reference type (anything except a primitive type) the method has a *copy of the reference*. So in your case, your `render()` method has a copy of the reference to the value passed (`this`; ie a reference to the instance of `App` that called the `render` method). There is only one `App` instance (created by JavaFx when the application was launched). It copies the reference, not the object. – James_D Dec 29 '22 at 10:21
  • I strongly recommend you learn the basics of Java before you try to use a moderately advanced library such as JavaFX. Your misunderstandings have nothing at all to do with JavaFX. Step back and experiment with creating objects, passing references, etc., in a basic context not involving any advanced libraries. Only try to use JavaFX once you know the basics. – James_D Dec 29 '22 at 10:24
  • @James_D Thank you for the reply, but you are misjudging. This is exactly the level that I'm at. I'm right on the edge of my current knowledge. What you are pointing at is simply a miscommunication caused by terminology. Sure, it is a copy of an instance, instead of a new instance. I'm brushing over this, because this is irrelevant to the problem. I already noted myself that the reference is only local in my original post. The fact that it's only local causes my problem: how do I set the root of the scene in `App` from within `Homepage`? – Ontkibbeling Dec 29 '22 at 11:26
  • Pass a reference to the scene and call `setRoot(…)` on it. – James_D Dec 29 '22 at 12:34
  • *”it is a copy of an instance”*. No it’s not. This is what you’re misunderstanding. It’s a copy of the *reference*. Both the original reference and the copy (in the local method) refer to the same instance. – James_D Dec 29 '22 at 12:49
  • @James_D I tried passing a reference to the scene and calling setRoot(), but then I encountered the next problem; I can only pass the scene to the Homepage constructor after initializing it, but I initialize it with the Homepage as the root node. That creates a chicken and an egg if you follow me. – Ontkibbeling Dec 29 '22 at 15:11
  • The design seems a bit strange anyway. Why not have your `render()` method return a `Parent` instance, and move the responsibility of setting the root of the scene out of `Homepage`, which is not really its responsibility anyway. – James_D Dec 29 '22 at 17:39

1 Answers1

-1

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);
    }
}
  • 1
    Referring to the comments in the code: `mainPane.getLeft()` returns a reference to the `Node` (in this case a `VBox`) which is the child of `mainPane` that is displayed in the left. If you add more nodes to that `VBox`, then they become part of the structure of `mainPane` (part of the scene graph) and are displayed in the UI (assuming `mainPane` is displayed in the UI). If you do `mainPane.getChildren().add(leftBox)` then you are trying to make `leftBox` a child of `mainPane` when it is already a child of `mainPane`, so you get an exception. – James_D Dec 29 '22 at 12:55