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

时间:2017-03-03 01:47:41

标签: java javafx fxml

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

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

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

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

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

研究:

我发现的唯一答案似乎是How to swich javafx application controller without reloading FXML file?。提到使用Singleton作为FXML控制器,但没有解决每次重新加载FXML文件本身的问题。

如果有人能指出此类设置菜单的基本示例,我会很高兴。

3 个答案:

答案 0 :(得分:3)

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

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

    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<String> 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<String> 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 应该{ {1}}已定义属性。

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

    fx:controller

    然后

    public class UserInfoController {
    
        @FXML
        private TextField name ;
        @FXML
        private ComboBox<String> department ;
    
        private final StringProperty nameProp = new SimpleStringProperty();
        private final ObjectProperty<String> 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<String> 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);
        }
    }
    

    这是有效的,因为在重新加载FXML文件时您不会创建新的控制器,并且控制器中的初始化方法会使用已存在的数据更新控件。 (注意调用public class MainController { @FXML private BorderPane root ; @FXML private ListView<String> 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<String> 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文件一次,因此视图只会持续其所有状态。

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

    主要fxml文件:

    fx:controller

    <!-- imports etc omitted --> <BorderPane xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.MainController"> <left> <ListView fx:id="selector" /> </left> <fx:define> <fx:include fx:id="userInfo" source="UserInfo.fxml" > </fx:define> <fx:define> <fx:include fx:id="prefs" source="Preferences.fxml" > </fx:define> <fx:define> <fx:include fx:id="appearance" source="Appearance.fxml" > </fx:define> </BorderPane> 的FXML注入规则是包含的FMXL文件的根注入了指定的<fx:include>(例如fx:id)以及这些包含文件的控制器( &#34;嵌套控制器&#34;)被注入到字段中,当userInfo被附加到"Controller"时(例如fx:id),名称为。所以这个主控制器现在看起来像

    userInfoController

答案 1 :(得分:1)

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

这是基本的应用程序:

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

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainController">
    <center>
        <TabPane  fx:id="navigationPane" id="navigationPane">
            <tabs>
                <Tab text="User Information">
                    <content>
                        <fx:include fx:id="userInfo" source="UserInfo.fxml"/>
                    </content>
                </Tab>
                <Tab text="Preferences">
                    <content>
                        <fx:include fx:id="prefs" source="Preferences.fxml"/>
                    </content>
                </Tab>
                <Tab text="Appearance">
                    <content>
                        <fx:include fx:id="appearance" source="Appearance.fxml"/>
                    </content>
                </Tab>
            </tabs>
        </TabPane>
    </center>
</BorderPane>

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

package application;

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

public class MainController {

    @FXML
    private TabPane navigationPane ;

    public void initialize() {

    }
}

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

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

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>

<VBox xmlns:fx="http://javafx.com/fxml/1" minWidth="600" minHeight="400" alignment="CENTER">
    <Label text="User Info Pane"/>
    <TextField  />
</VBox>

应用程序类只加载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<TabPane> {


    private final ListView<Tab> navigator ;

    public NavigationSkin(TabPane control) {
        super(control);

        navigator = new ListView<Tab>();

        navigator.setCellFactory(lv -> {
            ListCell<Tab> 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<? extends Tab> 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<Node> 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<Node> 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大小的各种方法。

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

enter image description here

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

答案 2 :(得分:0)

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

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

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

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

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

允许您在点击保存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<String> 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

<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" stylesheets="@root.css" xmlns="http://javafx.com/javafx/8.0.91" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.MainController">
   <left>
      <ListView fx:id="listView" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
   </left>
   <center>
      <BorderPane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
         <bottom>
            <HBox prefHeight="100.0" prefWidth="200.0" BorderPane.alignment="CENTER">
               <children>
                  <Button mnemonicParsing="false" text="Cancel" />
                  <Button mnemonicParsing="false" text="Save Changes" />
               </children>
            </HBox>
         </bottom>
         <center>
            <TabPane fx:id="tabPane" prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE" BorderPane.alignment="CENTER">
              <tabs>
                <Tab>
                     <content>
                        <fx:include source="settings1.fxml" />
                     </content>
                </Tab>
                  <Tab>
                      <content>
                          <fx:include source="settings2.fxml" />
                      </content>
                  </Tab>
                  <Tab>
                      <content>
                          <fx:include source="settings3.fxml" />
                      </content>
                  </Tab>
                  <Tab>
                      <content>
                          <fx:include source="settings4.fxml" />
                      </content>
                  </Tab>
              </tabs>
            </TabPane>
         </center>
      </BorderPane>
   </center>
</BorderPane>
  

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

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.91" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <Label layoutX="168.0" layoutY="14.0" text="Settings 1" />
      <TextField layoutX="127.0" layoutY="39.0" />
   </children>
</AnchorPane>
  

root.css。位于How to hide the TabBar in TabPane?

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