Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

I'm trying to build a simple GUI with JavaFX using SceneBuilder, where I'm using a MenuItem (in Main.fxml) to select a root folder. The folder's contents are then listed in a TextArea that again is wrapped in a TabPane (FileListTab.fxml, nested FXML that is included in Main.fxml).

I used this post as a starting point to get used to MVC. Unfortunately I don't know how to make my nested FXML listen or be bound to the outer one since I'm not explicitly calling it. Right now I'm stuck just to display my chosen folder in a label.

My minimal working code right now looks like this:

Main.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainController">
   <top>
      <MenuBar BorderPane.alignment="CENTER">
        <menus>
          <Menu mnemonicParsing="false" text="File">
            <items>
                  <MenuItem mnemonicParsing="false" onAction="#browseInputFolder" text="Open folder" />
            </items>
          </Menu>
        </menus>
      </MenuBar>
   </top>
   <center>
      <TabPane prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
        <tabs>
          <Tab text="File listing">
            <content>
                <fx:include fx:id="analysisTab" source="FileListTab.fxml" />
            </content>
          </Tab>
        </tabs>
      </TabPane>
   </center>
</BorderPane>

FileListTab.fxml

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" spacing="15.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="FileListController">
   <children>
      <HBox spacing="10.0">
         <children>
            <Label minWidth="100.0" text="Root folder:" />
            <Label fx:id="label_rootFolder" />
         </children>
      </HBox>
      <TextArea prefHeight="200.0" prefWidth="200.0" />
      <HBox spacing="10.0">
         <children>
            <Label minWidth="100.0" text="Found files:" />
            <Label fx:id="label_filesFound" />
         </children>
      </HBox>
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</VBox>

Model.java (the supposed to be shared model between controllers)

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Model {
    private StringProperty rootFolder;

    public String getRootFolder() {
        return rootFolderProperty().get();
    }

    public StringProperty rootFolderProperty() {
        if (rootFolder == null)
            rootFolder = new SimpleStringProperty();
        return rootFolder;
    }

    public void setRootFolder(String rootFolder) {
        this.rootFolderProperty().set(rootFolder);
    }
}

NestedGUI.java (Main class)

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

import java.io.IOException;

public class NestedGUI extends Application {
    Model model = new Model();

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

    @Override
    public void start(Stage primaryStage) {
        Parent root = null;
        try {
            FXMLLoader fxmlLoader = new FXMLLoader();
            fxmlLoader.setLocation(getClass().getClassLoader().getResource("Main.fxml"));
            root = (BorderPane) fxmlLoader.load();
            MainController controller = fxmlLoader.getController();
            controller.setModel(model);

         // This openes another window with the tab's content that is actually displaying the selected root folder
/*            FXMLLoader fxmlLoader2 = new FXMLLoader();
            fxmlLoader2.setLocation(getClass().getClassLoader().getResource("FileListTab.fxml"));
            VBox vBox = (VBox) fxmlLoader2.load();
            FileListController listController = fxmlLoader2.getController();
            listController.setModel(model);

            Scene scene = new Scene(vBox);
            Stage stage = new Stage();
            stage.setScene(scene);
            stage.show();*/

        } catch (IOException e) {
            e.printStackTrace();
        }

        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

MainController.java

import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;

import java.io.File;

public class MainController {
    Model model;

    public void setModel(Model model) {
        this.model = model;
    }

    public void browseInputFolder() {
        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Select folder");
        File folder = chooser.showDialog(new Stage());
        if (folder == null)
            return;

        String inputFolderPath = folder.getAbsolutePath() + File.separator;
        model.setRootFolder(inputFolderPath);
        System.out.print(inputFolderPath);
    }
}

FileListController.java

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class FileListController {
    Model model;

    @FXML
    Label label_rootFolder;

    public void setModel(Model model) {
        label_rootFolder.textProperty().unbind();
        this.model = model;
        label_rootFolder.textProperty().bind(model.rootFolderProperty());
    }
}

I looked through various posts here on SO, but either I didn't understand the answers or others had different problems. Can somebody give me some pointers? (hints to solve this, code snippets, links...) It looks like a pretty basic FXML-problem, but I just don't get it.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
420 views
Welcome To Ask or Share your Answers For Others

1 Answer

Simple solution

One option is just to inject the "nested controller" into the main controller as described in the FXML documentation.

The rule is that the field name for the controller should be the fx:id for the <fx:include> with the string "Controller" appended. So in your case, fx:id="analysisTab" and so the field will be FileListController analysisTabController. Once you have done that, you can pass the model to the nested controller when it is set in the main controller:

public class MainController {
    Model model;

    @FXML
    private FileListController analysisTabController ;

    public void setModel(Model model) {
        this.model = model;
        analysisTabController.setModel(model);
    }

    // ...
}

Advanced solution

One drawback to the simple solution above is that you have to propagate the model manually to all nested controllers, which can become tricky to maintain (especially if you have multiple levels of <fx:include>s). Another drawback is that you are setting the model after the controller is created and initialized (so, for example, the model is not available in the initialize() method, which is where you would most naturally like to use it).

A more advanced approach is to set a controllerFactory on the FXMLLoader. The controllerFactory is a function that maps the controller class (specified by the fx:controller attribute in the fxml file) to an object (almost always an instance of that class) that will be used as the controller. The default controller factory just invokes the no-argument constructor on the class. You can use this to invoke a constructor taking the model, so the model is available as soon as the controller is instantiated.

If you set a controller factory, the same controller factory is used for any included fxml files.

So you could rewrite your controllers to have constructors taking a model instance:

public class MainController {
    private final Model model;

    public MainController(Model model) {
        this.model = model;
    }

    public void browseInputFolder() {
        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Select folder");
        File folder = chooser.showDialog(new Stage());
        if (folder == null)
            return;

        String inputFolderPath = folder.getAbsolutePath() + File.separator;
        model.setRootFolder(inputFolderPath);
        System.out.print(inputFolderPath);
    }
}

and in the FileListController this means you can now access the model directly in the initialize() method:

public class FileListController {
    private final Model model;

    @FXML
    Label label_rootFolder;

    public FileListController(Model model) {
        this.model = model ;
    }

    public void initialize() {
        label_rootFolder.textProperty().bind(model.rootFolderProperty());
    }
}

Now your application class needs to create a controller factory that invokes these constructors. This is the tricky part: you probably want to use some reflection here and implement logic of the form: "if the controller class has a constructor taking a model, invoke it with the (shared) model instance; otherwise invoke the default constructor". This looks like:

public class NestedGUI extends Application {
    Model model = new Model();

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

    @Override
    public void start(Stage primaryStage) {
        Parent root = null;
        try {
            FXMLLoader fxmlLoader = new FXMLLoader();
            fxmlLoader.setLocation(getClass().getClassLoader().getResource("Main.fxml"));

            fxmlLoader.setControllerFactory((Class<?> type) -> {
                try {
                    for (Constructor<?> c : type.getConstructors()) {
                        if (c.getParameterCount() == 1 && c.getParameterTypes()[0] == Model.class) {
                            return c.newInstance(model);
                        }
                    }
                    // default behavior: invoke no-arg constructor:
                    return type.newInstance();
                } catch (Exception exc) {
                    throw new RuntimeException(exc);
                }
            });

            root = (BorderPane) fxmlLoader.load();


        } catch (IOException e) {
            e.printStackTrace();
        }

        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }
}

At this point you are basically one step along the way to creating a dependency injection framework (you are injecting the model into the controllers using a factory class...)! So you might just consider using one instead of creating one from scratch. afterburner.fx is a popular dependency-injection framework for JavaFX, and the core of the implementation is essentially the ideas in the code above.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...