与嵌套控制器共享模型

时间:2016-05-06 08:58:11

标签: java javafx

我正在尝试使用SceneBuilder使用JavaFX构建一个简单的GUI,我在其中使用MenuItem(在Main.fxml中)来选择根文件夹。然后,文件夹的内容将列在TextArea中,该文本区域再次包含在TabPane(FileListTab.fxmlMain.fxml中包含的嵌套FXML中)。

我使用this post作为习惯于MVC的起点。不幸的是,我不知道如何使我的嵌套FXML监听或绑定到外部,因为我没有明确地调用它。现在我只是在标签上显示我选择的文件夹。

我现在的最小工作代码如下所示:

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 (应该是控制器之间的共享模型)

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 (主类)

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());
    }
}

我在这里浏览了各种各样的帖子,但要么我不理解答案,要么其他人有不同的问题。 有人可以给我一些指示吗? (提示解决这个问题,代码片段,链接...)它看起来像一个非常基本的FXML问题,但我只是没有得到它。

2 个答案:

答案 0 :(得分:3)

我更喜欢的选项是使用自定义组件,而不是<fx:include fx:id="analysisTab" source="FileListTab.fxml" />。因此,在Main.fxml中,将<fx:include>行替换为:

<FileList fx:id="fileList"></FileList>

FileList是我们新的自定义组件。您还必须将<?import yourpackage.*?>添加到Main.fxml的顶部,以使yourpackage的类可用于FXML。 (显然,yourpackage是包含此问题中所有类和文件的包。)

以下是yourpackage.FileList.java中自定义组件的类;主要是来自FileListController的代码+加载FXML所需的代码。但请注意,它扩展了JavaFX组件VBox,使其成为FXML组件。 VBox是您FileListTab.fxml中的根组件,也必须在下面type的{​​{1}}属性中声明。

FileList.fxml

package yourpackage; import java.io.IOException; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.control.Label; import javafx.scene.layout.VBox; public class FileList extends VBox { private Model model; // NOT REALLY NEEDED! KEEPING IT BECAUSE YOUR FileListController HAD IT TOO... @FXML Label label_rootFolder; public FileList() { java.net.URL url = getClass().getResource("/yourpackage/FileList.fxml"); FXMLLoader fxmlLoader = new FXMLLoader(url); fxmlLoader.setRoot(this); fxmlLoader.setController(this); try { fxmlLoader.load(); } catch( IOException e ) { throw new RuntimeException(e); } } public void setModel(Model model) { label_rootFolder.textProperty().unbind(); this.model = model; // NOT REALLY NEEDED! label_rootFolder.textProperty().bind(model.rootFolderProperty()); } } 。这是您自己的FileList.fxml,其FileListTab.fxml节点由VBoxfx:root属性替换,保留所有其他属性相同:

type="javafx.scene.layout.VBox"

您是否注意到上面<?import javafx.geometry.*?> <?import javafx.scene.control.*?> <?import javafx.scene.layout.*?> <fx:root xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" type="javafx.scene.layout.VBox" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" spacing="15.0" > <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> </fx:root> 组件中的fx:id?您现在可以将其注入<FileList>

MainController

传播@FXML private FileList fileList; 电话:

setModel()

答案 1 :(得分:3)

简单解决方案

一种选择就是将“嵌套控制器”注入主控制器,如FXML documentation中所述。

规则是控制器的字段名称应为fx:id的{​​{1}},并附加字符串<fx:include>。因此,在您的情况下,"Controller",因此该字段将为fx:id="analysisTab"。完成后,可以在主控制器中设置模型时将模型传递给嵌套控制器:

FileListController analysisTabController

高级解决方案

上述简单解决方案的一个缺点是您必须手动将模型传播到所有嵌套控制器,这可能会变得难以维护(特别是如果您有多个级别的public class MainController { Model model; @FXML private FileListController analysisTabController ; public void setModel(Model model) { this.model = model; analysisTabController.setModel(model); } // ... } )。另一个缺点是您在创建和初始化控制器后设置模型(例如,模型在<fx:include>方法中不可用,这是您最自然希望使用它的方法。)< / p>

更高级的方法是在initialize()上设置controllerFactoryFXMLLoader是一个函数,它将控制器类(由fxml文件中的controllerFactory属性指定)映射到将用作控制器的对象(几乎总是该类的实例)。默认控制器工厂只调用类上的无参数构造函数。您可以使用它来调用获取模型的构造函数,因此只要控制器被实例化,模型就可用。

如果设置了控制器工厂,则相同的控制器工厂将用于任何包含的fxml文件。

因此,您可以重写控制器以使构造函数采用模型实例:

fx:controller

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); } } 中,这意味着您现在可以直接使用FileListController方法访问模型:

initialize()

现在,您的应用程序类需要创建一个调用这些构造函数的控制器工厂。这是一个棘手的部分:你可能想在这里使用一些反射并实现表单的逻辑:“如果控制器类有一个构造函数接受模型,则使用(共享)模型实例调用它;否则调用默认构造函数”。这看起来像:

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());
    }
}

此时,您基本上是创建依赖注入框架的一步(您使用工厂类将模型注入控制器中......)!所以你可能只考虑使用一个而不是从头创建一个。 afterburner.fx是JavaFX的流行依赖注入框架,实现的核心基本上是上面代码中的想法。