我正在尝试使用 SceneBuilder 使用 JavaFX 构建一个简单的 GUI,我在其中使用 MenuItem(在 Main.fxml
中)来选择根文件夹。然后文件夹的内容列在一个 TextArea 中,该 TextArea 再次包裹在一个 TabPane 中(FileListTab.fxml
,包含在 Main.fxml
中的嵌套 FXML)。
我用了this post作为习惯 MVC 的起点。不幸的是,我不知道如何让我的嵌套 FXML 监听或绑定(bind)到外部 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(应该是 Controller 之间的共享模型)
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();
}
}
主 Controller .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());
}
}
我在 SO 上浏览了各种帖子,但要么我不理解答案,要么其他人有不同的问题。 有人可以给我一些指示吗? (解决这个问题的提示、代码片段、链接...)它看起来像是一个非常基本的 FXML 问题,但我就是不明白。
最佳答案
简单的解决方案
一种选择是将“嵌套 Controller ”注入(inject)主 Controller ,如 FXML documentation 中所述。 .
规则是 Controller 的字段名称应该是fx:id
。对于 <fx:include>
使用字符串 "Controller"
附加。所以在你的情况下,fx:id="analysisTab"
所以该字段将是 FileListController analysisTabController
.完成后,您可以将模型传递给在主 Controller 中设置的嵌套 Controller :
public class MainController {
Model model;
@FXML
private FileListController analysisTabController ;
public void setModel(Model model) {
this.model = model;
analysisTabController.setModel(model);
}
// ...
}
高级解决方案
上述简单解决方案的一个缺点是您必须手动将模型传播到所有嵌套 Controller ,这可能会变得难以维护(特别是如果您有多个级别的 <fx:include>
s)。另一个缺点是您是在创建和初始化 Controller 之后设置模型(因此,例如,模型在 initialize()
方法中不可用,这是您最自然希望使用它的地方)。
更高级的方法是设置一个 controllerFactory
在 FXMLLoader
上. controllerFactory
是一个将 Controller 类(由 fxml 文件中的 fx:controller
属性指定)映射到将用作 Controller 的对象(几乎总是该类的实例)的函数。默认 Controller 工厂只调用类上的无参数构造函数。您可以使用它来调用采用模型的构造函数,因此模型在 Controller 实例化后立即可用。
如果您设置 Controller 工厂,则同一 Controller 工厂将用于所有包含的 fxml 文件。
所以你可以重写你的 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());
}
}
现在您的应用程序类需要创建一个调用这些构造函数的 Controller 工厂。这是棘手的部分:您可能想在这里使用一些反射并实现以下形式的逻辑:“如果 Controller 类具有采用模型的构造函数,则使用(共享)模型实例调用它;否则调用默认构造函数”。这看起来像:
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();
}
}
此时,您基本上是创建依赖注入(inject)框架的一步(您正在使用工厂类将模型注入(inject) Controller ......)!因此,您可能只考虑使用一个而不是从头开始创建一个。 afterburner.fx是一个流行的JavaFX依赖注入(inject)框架,实现的核心本质上就是上面代码中的思想。
关于java - 与嵌套 Controller 共享模型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37068243/