是否可以重新加载相同的FXML / Controller实例?

目标:实施标准的“设置”GUI窗口。 左侧ListView中的类别和右侧Pane的相应选项。 在此处输入图像描述 (请忽略重复类别的明显错误;仍在处理它)

我有一个整个设置窗口的主窗口,其中包含一个包含所有类别设置的ListView 。 窗口的右侧有一个AnchorPane ,当从列表中选择一个时,它用于为每个类别加载单独的FXML文件。

当用户选择一个类别时,我需要它们能够编辑右侧的设置,切换到另一个类别并进行更多更改。 然而,如果他们回到第一类,那里所做的改变仍然存在。

我明显的问题是,每次用户更改类别时, FXMLLoader重新加载FXML文件和控制器,将所有控件重置为默认值。

那么是否可以重用已加载和更改的FXML文件?

研究:

我发现似乎解决问题的唯一答案是如何在不重新加载FXML文件的情况下swich javafx应用程序控制器? 。 提到使用Singleton作为FXML控制器,但没有解决每次重新加载FXML文件本身的问题。

如果有人可以指出这种类型的设置菜单的基本示例,我会很高兴。

基本上有三种方法可以做到这一点:

  1. 定义表示数据的模型( Settings ),并创建它的单个实例。 每次重新加载FXML文件,并将单个实例传递给控制器​​。 使用模型中的数据绑定UI中的数据。 这样,当您重新加载FXML时,它将使用相同的数据进行更新。 (这是我的首选。)
  2. 一次创建控制器。 每次重新加载FXML文件,每次都设置相同的控制器。 使initialize()方法从本地存储的字段或模型更新UI。 当您重新加载FXML文件时将替换@FXML字段,并且将调用initialize()方法,使用现有数据更新新控件。 (这感觉有点人为。从口头上讲,任何名为initialize()方法都只能执行一次。但是,这是完全可行的。)
  3. 加载每个FXML文件一次并缓存UI(可能还有控制器)。 然后,当用户在列表视图中选择某些内容时,只显示已加载的视图。 这可能是最简单的,但在内存中花费更多,因为您始终将所有视图保留在内存中。

假设您有一个模型,可能如下所示:

 public class Settings { private final UserInfo userInfo ; private final Preferences prefs ; private final Appearance appearance ; public Settings(UserInfo userInfo, Preferences prefs, Appearance appearance) { this.userInfo = userInfo ; this.prefs = prefs ; this.appearance = appearance ; } public Settings() { this(new UserInfo(), new Preferences(), new Appearance()); } public UserInfo getUserInfo() { return userInfo ; } public Preferences getPreferences() { return prefs ; } public Appearance getAppearance() { return appearance ; } } 

 public class UserInfo { private final StringProperty name = new SimpleStringProperty() ; private final StringProperty department = new SimpleStringProperty() ; // etc... public StringProperty nameProperty() { return name ; } public final String getName() { return nameProperty().get(); } public final void setName(String name) { nameProperty().set(name); } // etc... } 

(类似于PreferencesAppearance等)

现在,您可以为使用模型的各个屏幕定义控制器,例如

 public class UserInfoController { private final UserInfo userInfo ; @FXML private TextField name ; @FXML private ComboBox department ; public UserInfoController(UserInfo userInfo) { this.userInfo = userInfo ; } public void initialize() { name.textProperty().bindBidirectional(userInfo.nameProperty()); department.valueProperty().bindBidirectional(userInfo.departmentProperty()); } } 

然后你的主控制器看起来像:

 public class MainController { @FXML private BorderPane root ; @FXML private ListView selector ; private Settings settings = new Settings() ; // or pass in from somewhere else.. public void initialize() { selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> { if ("User Information".equals(newSelection)) { loadScreen("UserInfo.fxml", new UserInfoController(settings.getUserInfo())); } else if ("Preferences".equals(newSelection)) { loadScreen("Preferences.fxml", new PreferencesController(settings.getPreferences())); } else if ("Appearance".equals(newSelection)) { loadScreen("Appearance.fxml", new AppearanceController(settings.getAppearance())); } else { root.setCenter(null); } } private void loadScreen(String resource, Object controller) { try { FXMLLoader loader = new FXMLLoader(getClass().getResource(resource)); loader.setController(controller); root.setCenter(loader.load()); } catch (IOException exc) { exc.printStackTrace(); root.setCenter(null); } } } 

(显然,您可以通过定义封装控制器的资源名称,显示名称和工厂的简单视图类,并使用它填充列表视图,而不是打开字符串,来使列表视图的处理程序更清晰。)

请注意,由于您在代码中在FXMLLoader上设置控制器,因此UserInfo.fxmlPreferences.fxmlAppearance.fxml不应定义fx:controller属性。


第二种选择只是对此进行了温和的重构。 创建一次控制器并保持对它们的引用。 请注意,如果您愿意,可以在此版本中删除模型,因为控制器具有数据,因此您可以只返回它们。 所以这看起来像

 public class UserInfoController { @FXML private TextField name ; @FXML private ComboBox department ; private final StringProperty nameProp = new SimpleStringProperty(); private final ObjectProperty departmentProp = new SimpleObjectProperty(); public StringProperty nameProperty() { return nameProp; } public final String getName() { return nameProperty().get(); } public final void setName(String name) { nameProperty().set(name); } public ObjectProperty departmentProperty() { return departmentProp ; } public final String getDepartment() { return departmentProperty().get(); } public final void setDepartment(String department) { departmentProperty().set(department); } public void initialize() { // initialize controls with data currently in properties, // and ensure changes to controls are written back to properties: name.textProperty().bindBidirectional(nameProp); department.valueProperty().bindBidirectional(departmentProp); } } 

接着

 public class MainController { @FXML private BorderPane root ; @FXML private ListView selector ; private UserInfoController userInfoController = new UserInfoController(); private PreferencesController preferencesController = new PreferencesController(); private AppearanceController appearanceController = new AppearanceController(); public void initialize() { // initialize controllers with data if necessary... selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> { selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> { if ("User Information".equals(newSelection)) { loadScreen("UserInfo.fxml", userInfoController); } else if ("Preferences".equals(newSelection)) { loadScreen("Preferences.fxml", preferencesController); } else if ("Appearance".equals(newSelection)) { loadScreen("Appearance.fxml", appearanceController); } else { root.setCenter(null); } } } private void loadScreen(String resource, Object controller) { // as before... } } 

这是有效的,因为在重新加载FXML文件时不会创建新的控制器,并且控制器中的初始化方法会使用已存在的数据更新控件。 (注意调用bindBidirectional方法的哪个方向。)


第三个选项可以在主控制器中实现,也可以在主fxml文件中实现。 要在控制器中实现它,您基本上可以

 public class MainController { @FXML private BorderPane root ; @FXML private ListView selector ; private Parent userInfo ; private Parent prefs; private Parent appearance; // need controllers to get data later... private UserInfoController userInfoController ; private PreferencesController prefsController ; private AppearanceController appearanceController ; public void initialize() throws IOException { FXMLLoader userInfoLoader = new FXMLLoader(getClass().getResource("userInfo.fxml)); userInfo = userInfoLoader.load(); userInfoController = userInfoLoader.getController(); FXMLLoader prefsLoader = new FXMLLoader(getClass().getResource("preferences.fxml)); prefs = prefsLoader.load(); prefsController = prefsLoader.getController(); FXMLLoader appearanceLoader = new FXMLLoader(getClass().getResource("appearance.fxml)); appearance = appearanceLoader.load(); appearanceController = appearanceLoader.getController(); // configure controllers with data if needed... selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> { if ("User Information".equals(newSelection)) { root.setCenter(userInfo); } else if ("Preferences".equals(newSelection)) { root.setCenter(prefs); } else if ("Appearance".equals(newSelection)) { root.setCenter(prefs); } else { root.setCenter(null); } } } } 

请注意,您将恢复到FXML文件中通常的fx:controller属性。

这将起作用,因为您只加载一次FXML文件,因此视图只会持续其所有状态。

如果要在此方法中定义FXML中的视图,您可以:

主要的fxml文件:

                

FXML注入规则是包含的FMXL文件的根用指定的fx:id (例如userInfo )注入,并且那些包含文件的控制器(“嵌套控制器”)被注入到当"Controller"附加到fx:id (例如,添加到userInfoController ),名称赋予字段。 所以这个主控制器现在看起来像

 public class MainController { @FXML private BorderPane root ; @FXML private ListView selector ; @FXML private Parent userInfo ; @FXML private Parent prefs; @FXML private Parent appearance; // need controllers to get data later... @FXML private UserInfoController userInfoController ; @FXML private PreferencesController prefsController ; @FXML private AppearanceController appearanceController ; public void initialize() { // configure controllers with data if needed... selector.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> { if ("User Information".equals(newSelection)) { root.setCenter(userInfo); } else if ("Preferences".equals(newSelection)) { root.setCenter(prefs); } else if ("Appearance".equals(newSelection)) { root.setCenter(prefs); } else { root.setCenter(null); } } } } 

这是一种完全不同的创建“导航窗格”的方法,就像你展示的那样,部分受到Hypnic Jerk的回答的启发。 这里的关键观察是你想要的function与TabPane基本相同:你有一系列节点,一次显示一个节点,有一个机制可以选择显示哪一个(通常是制表符,但这里你有一个ListView )。 因此,这种方法只是使选项卡窗格使用ListView而不是通常的选项卡显示“选择器”。 它通过为选项卡窗格创建新的Skin来实现此目的。

这是基本的应用程序:

      

此测试的控制器不执行任何操作:

 package application; import javafx.fxml.FXML; import javafx.scene.control.TabPane; public class MainController { @FXML private TabPane navigationPane ; public void initialize() { } } 

而且各个窗格只是占位符:

         

应用程序类只加载FXML,关键的是,设置一个样式表:

 package application; import java.io.IOException; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class NavTabPaneTest extends Application { @Override public void start(Stage primaryStage) throws IOException { Parent root = FXMLLoader.load(getClass().getResource("NavPaneTest.fxml")); Scene scene = new Scene(root); scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm()); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } 

并且样式表指定皮肤:

 #navigationPane { -fx-skin: "application.skin.NavigationSkin" ; } 

最后是完成工作的部分:皮肤:

 package application.skin; import java.util.function.ToDoubleFunction; import javafx.beans.binding.Bindings; import javafx.collections.ListChangeListener.Change; import javafx.scene.Node; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.SkinBase; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; public class NavigationSkin extends SkinBase { private final ListView navigator ; public NavigationSkin(TabPane control) { super(control); navigator = new ListView(); navigator.setCellFactory(lv -> { ListCell cell = new ListCell<>(); cell.itemProperty().addListener((obs, oldTab, newTab) -> { cell.textProperty().unbind(); cell.graphicProperty().unbind(); if (newTab == null) { cell.setText(null); cell.setGraphic(null); } else { cell.textProperty().bind(newTab.textProperty()); cell.graphicProperty().bind(newTab.graphicProperty()); } }); return cell ; }); navigator.setItems(control.getTabs()); navigator.getSelectionModel().selectedItemProperty().addListener( (obs, oldItem, newItem) -> control.getSelectionModel().select(newItem)); navigator.getSelectionModel().select(control.getSelectionModel().getSelectedItem()); control.getSelectionModel().selectedItemProperty().addListener((obs, oldItem, newItem) -> { for (Tab t : control.getTabs()) { t.getContent().setVisible(t == control.getSelectionModel().getSelectedItem()); } navigator.getSelectionModel().select(newItem); }); getChildren().add(navigator); for (Tab t : control.getTabs()) { getChildren().add(t.getContent()); t.getContent().setVisible(t == control.getSelectionModel().getSelectedItem()); } control.getTabs().addListener((Change c) -> { while (c.next()) { if (c.wasRemoved()) { getChildren().subList(c.getFrom()+1, c.getFrom()+c.getRemovedSize()+1).clear(); } if (c.wasAdded()) { for (int i = 0 ; i < c.getAddedSize() ; i++) { getChildren().add(c.getFrom() + i + 1, c.getAddedSubList().get(i).getContent()); } } } getSkinnable().requestLayout(); }); } @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { double navPrefWidth = navigator.prefWidth(-1); navigator.resizeRelocate(contentX, contentY, navPrefWidth, contentHeight); for (Tab t : getSkinnable().getTabs()) { t.getContent().resizeRelocate(navPrefWidth, 0, contentWidth - navPrefWidth, contentHeight); } } @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { return computeHeight(n -> n.maxHeight(width - leftInset - rightInset)); } @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return computeWidth(n -> n.maxWidth(height - topInset - bottomInset)) ; } @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { return computeHeight(n -> n.minHeight(-1)); } @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return computeWidth(n -> n.minWidth(-1)) ; } @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { return computeHeight(n -> n.prefHeight(-1)); } @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return computeWidth(n -> n.prefWidth(height - topInset - bottomInset)) ; } private double computeWidth(ToDoubleFunction width) { double navWidth = width.applyAsDouble(navigator); double max = 0 ; for (Tab tab : getSkinnable().getTabs()) { double tabWidth = width.applyAsDouble(tab.getContent()); max = Math.max(max, tabWidth); } return navWidth + max ; } private double computeHeight(ToDoubleFunction height) { double max = height.applyAsDouble(navigator) ; for (Tab tab : getSkinnable().getTabs()) { max = Math.max(max, height.applyAsDouble(tab.getContent())); } return max ; } } 

这将创建一个ListView并对侦听器和绑定执行一些魔术,以确保它始终与选项卡窗格中的选项卡列表具有相同的内容,并且列表视图中的选定项目是选定的选项卡。 (如果以编程方式更改选定的选项卡,则需要确保列表视图更新,并且如果用户在列表视图中更改了所选项,请确保选定的选项卡更改。)其余选项仅覆盖layoutChildren()方法和计算min / max / pref大小的各种方法。

结果是传统的“导航窗格”:

在此处输入图像描述

当然,因为所有选项卡内容都被加载一次,并且只是切换进出视图(通过改变它们的可见性),在恢复到先前视图时丢失数据的问题就消失了。

好的,所以我做了一点测试,我可能想出办法来做到这一点。

首先解释我的思考过程,然后是代码。

看起来你基本上想要一个TabPane ,没有Tab sie,点击ListView ,切换到某个FXML文件。 好吧,我做了一个小实验,看看我能做些什么。

首先,我使用左侧的SplitPaneListView和两个BorderPanes 。 在嵌套的BorderPane ,我放了一个TabPane ,这是你添加fxml文件的地方。 我用fx:include来节省时间/代码。 另外,由于这是标准的,添加或删除Setting项目是添加/删除几行。

因此,当您从ListView选择一个项目时,它将更改为相应的选项卡,其中FXML文件与选择具有相同的index (有一个警告)。 这可以编辑以满足您的需求,但由于这是概念validation,我不会太过分了。

应该允许您在按下保存Button之前保持用户的“软保存”更改。

代码如下:

Main.java

 public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage)throws Exception{ FXMLLoader loader = new FXMLLoader(getClass().getResource("root.fxml")); Scene scene = new Scene(loader.load()); primaryStage.setScene(scene); primaryStage.show(); } } 

MainController.java

 public class MainController { @FXML private ListView listView; @FXML private TabPane tabPane; public void initialize() { ObservableList list = FXCollections.observableArrayList(); list.add("Settings 1"); list.add("Settings 2"); list.add("Settings 3"); list.add("Settings 4"); listView.setItems(list); listView.getSelectionModel().selectedItemProperty().addListener(listener -> { tabPane.getSelectionModel().select(listView.getSelectionModel().getSelectedIndex()); }); } } 

root.fxml

     

settings1.fxml,settings2.fxml,settings3.fxml,settings4.fxml。 唯一不同的是Label被更改以反映FXML文件。

       

root.css。 如何隐藏TabPane中的TabBar?

 .tab-pane { -fx-tab-max-height: 0 ; } .tab-pane .tab-header-area { visibility: hidden ; }