目标:实施标准的“设置”GUI窗口。左侧ListView
中的类别和右侧Pane
中的相应选项。
(请忽略重复类别的明显错误;仍在处理它)
我有一个整个“设置”窗口的主窗口,其中包含一个ListView
,其中包含所有类别的设置。窗口的右侧有一个AnchorPane
,当从列表中选择一个时,它用于为每个类别加载单独的FXML文件。
当用户选择一个类别时,我需要他们能够编辑右侧的设置,切换到另一个类别并进行更多更改。然而,如果他们回到第一类,那里所做的改变仍然存在。
我明显的问题是每次用户更改类别时,FXMLLoader
会重新加载FXML文件和控制器,将所有控件重置为默认值。
那么可以重用已加载和更改的FXML文件吗?
研究:
我发现的唯一答案似乎是How to swich javafx application controller without reloading FXML file?。提到使用Singleton作为FXML控制器,但没有解决每次重新加载FXML文件本身的问题。
如果有人能指出此类设置菜单的基本示例,我会很高兴。
答案 0 :(得分:3)
我认为基本上有三种方法可以做到这一点:
Settings
),并创建它的单个实例。每次重新加载FXML文件,并将单个实例传递给控制器。使用模型中的数据绑定UI中的数据。这样,当您重新加载FXML时,它将使用相同的数据进行更新。 (这是我的首选。)initialize()
方法从本地存储的字段或模型更新UI。重新加载FXML文件时将替换@FXML
- 带注释的字段,并且将调用initialize()
方法,使用现有数据更新新控件。 (这感觉有点人为。从口头上讲,任何名为initialize()
的方法都只能执行一次。但是,这是完全可行的。)假设您有一个模型,可能如下所示:
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);
}
}
}
(显然,您可以通过定义封装控制器的资源名称,显示名称和工厂的简单视图类,并使用它填充列表视图,而不是打开字符串,来使列表视图的处理程序更清晰。 )
请注意,由于您要在代码FXMLLoader
上设置控制器,UserInfo.fxml
,Preferences.fxml
和Appearance.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大小的各种方法。
结果是传统的“导航窗格”:
当然,因为所有标签内容都被加载一次,只是切换进出视图(通过改变它们的可见性),在恢复到前一个视图时丢失数据的问题就消失了。
答案 2 :(得分:0)
好的,所以我做了一点测试,我可能想出办法来做到这一点。
首先解释我的思考过程,然后是代码。
看起来你基本上想要一个TabPane
,没有Tab
。即,单击ListView
,切换到某个FXML
文件。好吧,我做了一个小实验,看看我能做些什么。
首先,我使用左侧的SplitPane
,ListView
和两个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 ;
}