是否可以重新加载相同的 FXML/Controller 实例?
Is it possible to reload the same FXML/Controller instance?
目标: 实施标准 "Settings" GUI window。左侧 ListView
中的类别和右侧 Pane
中的相应选项。
(请忽略重复类别的明显错误;仍在处理中)
我有一个主要 window 用于整体设置 window,其中包含一个 ListView
和所有设置类别。 window 的右侧有一个 AnchorPane
,用于在从列表中选择一个类别时为每个类别加载单独的 FXML 文件。
当用户选择一个类别时,我需要他们能够在右侧编辑设置,切换到另一个类别并进行更多更改。然而,如果他们 return 到第一类,则在那里所做的更改仍然存在。
我的明显问题是,每次用户更改类别时,FXMLLoader
都会重新加载 FXML 文件和控制器,将其中的所有控件重置为其默认值。
那么是否可以重复使用已经加载和更改的 FXML 文件?
研究:
我发现似乎可以解决该问题的唯一答案是 How to swich javafx application controller without reloading FXML file?。这提到为 FXML 控制器使用 Singleton,但没有解决每次重新加载 FXML 文件本身的问题。
如果有人能指出此类“设置”菜单的基本示例,我会很高兴。
好的,所以我做了一些测试,我可能想出了一个方法来做到这一点。
先说明一下我的思路,再上代码。
看起来您基本上想要 TabPane
,没有 Tab
。即点击ListView
,切换到某个FXML
文件。好吧,我做了一个小实验,看看我能做什么。
首先,我在左边用了一个SplitPane
,ListView
,还有两个BorderPanes
。在嵌套的 BorderPane
中,我放了一个 TabPane
,这是您将添加 fxml
文件的地方。我用 fx:include
,来保存 time/code。另外,由于这将是标准的,添加或删除 Setting
s 项目只需几行 added/removed.
因此,当您select ListView
的项目时,它将其更改为适当的选项卡,其中FXML
文件的文件与其具有相同的index
select离子(有一个警告)。可以对其进行编辑以满足您的需求,但由于这是概念验证,因此我不会深入研究。
这 应该 允许您在点击保存之前保留 "soft saves" 的用户更改 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. Only thing different is the Label
is changed to reflect the FXML
file.
<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. Located
.tab-pane {
-fx-tab-max-height: 0 ;
}
.tab-pane .tab-header-area {
visibility: hidden ;
}
我看到基本上有三种方法可以做到这一点:
- 定义表示数据的模型 (
Settings
),并创建它的单个实例。每次重新加载 FXML 文件,并将单个实例传递给控制器。将 UI 中的数据与模型中的数据绑定。这样,当您重新加载 FXML 时,它将使用相同的数据进行更新。 (这是我的首选。)
- 创建控制器一次。每次重新加载 FXML 文件,每次都设置相同的控制器。让
initialize()
方法从本地存储的字段或模型更新 UI。当您重新加载 FXML 文件时,@FXML
注释的字段将被替换,并且将调用 initialize()
方法,用现有数据更新新控件。 (这感觉有点做作。从道德上讲,任何名为 initialize()
的方法都应该只执行一次。但是,这是完全可行的。)
- 加载每个 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...
}
(Preferences
、Appearance
等也类似)
现在您可以为使用模型的各个屏幕定义控制器,例如
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);
}
}
}
(显然,您可以通过定义一个简单的视图 class 封装资源名称、显示名称和控制器的工厂,并用它填充列表视图,从而使列表视图的处理程序更清晰,而不是打开字符串。)
请注意,由于您在代码中将控制器设置在 FXMLLoader
上,因此 UserInfo.fxml
、Preferences.fxml
和 Appearance.fxml
应该 而不是 定义了 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);
}
}
然后
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 文件中恢复为常用的 fx:controller
属性。
这将起作用,因为您只加载 FXML 文件一次,因此视图会保持其所有状态。
如果您想用这种方法定义 FXML 中的视图,您可以:
主 fxml 文件:
<!-- 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>
<fx:include>
的 FXML 注入规则是,包含的 FMXL 文件的根目录被注入指定的 fx:id
(例如 userInfo
)和包含的控制器当 "Controller"
附加到 fx:id
时(例如 userInfoController
),文件 ("nested controllers") 被注入到名称为 give 的字段中。所以这个的主控制器现在看起来像
public class MainController {
@FXML
private BorderPane root ;
@FXML
private ListView<String> 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);
}
}
}
}
这是一种完全不同的方法来创建 "navigation pane" 就像您展示的那样,部分灵感来自 Hypnic Jerk 的回答。这里的关键观察是您想要的功能与 TabPane
基本相同:您有一系列节点,一次显示一个,具有选择显示哪个的机制(通常是选项卡,但在这里你有一个 ListView
)。因此,这种方法只是使选项卡窗格使用 ListView
而不是通常的选项卡来显示 "selector"。它通过为选项卡窗格创建一个新的 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>
应用程序 class 只是加载 FXML,而且至关重要的是,设置样式 sheet:
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);
}
}
样式sheet指定皮肤:
#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 大小的各种方法。
结果是传统的 "navigation pane":
当然,因为所有选项卡内容都加载一次,并且只是切换到视图和退出视图(通过更改它们的可见性),恢复到之前的视图时丢失数据的问题就消失了。
目标: 实施标准 "Settings" GUI window。左侧 ListView
中的类别和右侧 Pane
中的相应选项。
我有一个主要 window 用于整体设置 window,其中包含一个 ListView
和所有设置类别。 window 的右侧有一个 AnchorPane
,用于在从列表中选择一个类别时为每个类别加载单独的 FXML 文件。
当用户选择一个类别时,我需要他们能够在右侧编辑设置,切换到另一个类别并进行更多更改。然而,如果他们 return 到第一类,则在那里所做的更改仍然存在。
我的明显问题是,每次用户更改类别时,FXMLLoader
都会重新加载 FXML 文件和控制器,将其中的所有控件重置为其默认值。
那么是否可以重复使用已经加载和更改的 FXML 文件?
研究:
我发现似乎可以解决该问题的唯一答案是 How to swich javafx application controller without reloading FXML file?。这提到为 FXML 控制器使用 Singleton,但没有解决每次重新加载 FXML 文件本身的问题。
如果有人能指出此类“设置”菜单的基本示例,我会很高兴。
好的,所以我做了一些测试,我可能想出了一个方法来做到这一点。
先说明一下我的思路,再上代码。
看起来您基本上想要 TabPane
,没有 Tab
。即点击ListView
,切换到某个FXML
文件。好吧,我做了一个小实验,看看我能做什么。
首先,我在左边用了一个SplitPane
,ListView
,还有两个BorderPanes
。在嵌套的 BorderPane
中,我放了一个 TabPane
,这是您将添加 fxml
文件的地方。我用 fx:include
,来保存 time/code。另外,由于这将是标准的,添加或删除 Setting
s 项目只需几行 added/removed.
因此,当您select ListView
的项目时,它将其更改为适当的选项卡,其中FXML
文件的文件与其具有相同的index
select离子(有一个警告)。可以对其进行编辑以满足您的需求,但由于这是概念验证,因此我不会深入研究。
这 应该 允许您在点击保存之前保留 "soft saves" 的用户更改 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. Only thing different is the
Label
is changed to reflect theFXML
file.
<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. Located
.tab-pane {
-fx-tab-max-height: 0 ;
}
.tab-pane .tab-header-area {
visibility: hidden ;
}
我看到基本上有三种方法可以做到这一点:
- 定义表示数据的模型 (
Settings
),并创建它的单个实例。每次重新加载 FXML 文件,并将单个实例传递给控制器。将 UI 中的数据与模型中的数据绑定。这样,当您重新加载 FXML 时,它将使用相同的数据进行更新。 (这是我的首选。) - 创建控制器一次。每次重新加载 FXML 文件,每次都设置相同的控制器。让
initialize()
方法从本地存储的字段或模型更新 UI。当您重新加载 FXML 文件时,@FXML
注释的字段将被替换,并且将调用initialize()
方法,用现有数据更新新控件。 (这感觉有点做作。从道德上讲,任何名为initialize()
的方法都应该只执行一次。但是,这是完全可行的。) - 加载每个 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...
}
(Preferences
、Appearance
等也类似)
现在您可以为使用模型的各个屏幕定义控制器,例如
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);
}
}
}
(显然,您可以通过定义一个简单的视图 class 封装资源名称、显示名称和控制器的工厂,并用它填充列表视图,从而使列表视图的处理程序更清晰,而不是打开字符串。)
请注意,由于您在代码中将控制器设置在 FXMLLoader
上,因此 UserInfo.fxml
、Preferences.fxml
和 Appearance.fxml
应该 而不是 定义了 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);
}
}
然后
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 文件中恢复为常用的 fx:controller
属性。
这将起作用,因为您只加载 FXML 文件一次,因此视图会保持其所有状态。
如果您想用这种方法定义 FXML 中的视图,您可以:
主 fxml 文件:
<!-- 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>
<fx:include>
的 FXML 注入规则是,包含的 FMXL 文件的根目录被注入指定的 fx:id
(例如 userInfo
)和包含的控制器当 "Controller"
附加到 fx:id
时(例如 userInfoController
),文件 ("nested controllers") 被注入到名称为 give 的字段中。所以这个的主控制器现在看起来像
public class MainController {
@FXML
private BorderPane root ;
@FXML
private ListView<String> 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);
}
}
}
}
这是一种完全不同的方法来创建 "navigation pane" 就像您展示的那样,部分灵感来自 Hypnic Jerk 的回答。这里的关键观察是您想要的功能与 TabPane
基本相同:您有一系列节点,一次显示一个,具有选择显示哪个的机制(通常是选项卡,但在这里你有一个 ListView
)。因此,这种方法只是使选项卡窗格使用 ListView
而不是通常的选项卡来显示 "selector"。它通过为选项卡窗格创建一个新的 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>
应用程序 class 只是加载 FXML,而且至关重要的是,设置样式 sheet:
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);
}
}
样式sheet指定皮肤:
#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 大小的各种方法。
结果是传统的 "navigation pane":
当然,因为所有选项卡内容都加载一次,并且只是切换到视图和退出视图(通过更改它们的可见性),恢复到之前的视图时丢失数据的问题就消失了。