将所有内容整合在一起
此页面由 Gail C. Anderson 和 Paul Anderson 贡献,根据 UPL 许可,摘自 使用 JavaFX 17 的现代 Java 客户端权威指南,由 Apress 慷慨提供。概述
现在是时候构建一个更有趣的 JavaFX 应用程序了,它实现了一个主从视图。当我们向您展示此应用程序时,我们将解释几个 JavaFX 功能,这些功能可以帮助您控制 UI 并保持数据和应用程序的一致性。
首先,我们使用 Scene Builder 来构建和配置 UI。我们的示例包括一个 Person 模型类和一个底层的 ObservableList,它保存数据。该程序允许用户进行更改,但我们不会持久化任何数据。JavaFX 具有 ObservableList,它管理数据集合,您可以编写侦听器和绑定表达式来响应任何数据更改。该程序使用事件处理程序和绑定表达式的组合来保持应用程序状态的一致性。
主从 UI
对于 UI,我们在左侧窗口(主视图)中使用 JavaFX ListView 控件,在右侧使用表单(从视图)。在 Scene Builder 中,我们选择一个 AnchorPane 作为顶级组件和场景图根。一个 SplitPane 布局窗格将应用程序视图分成两部分,每部分都具有 AnchorPane 作为其主容器。
该 ListView 控件允许您对 Person 对象执行选择。这里,第一个 Person 被选中,该 Person 的详细信息显示在右侧的表单控件中。表单控件具有以下布局
- 该表单包含一个
GridPane(两列四行),它包含TextField,用于Person的 firstname 和 lastname 字段。 - 一个
TextArea保存Person的 notes 字段。第一列中的标签标记了这些控件中的每一个。 - 该
GridPane的最底行包含一个ButtonBar,它跨越两列,默认情况下在右侧对齐。该ButtonBar将其所有按钮的大小调整为最宽按钮标签的宽度,以便按钮具有统一的大小。 - 这些按钮允许您执行新建(创建
Person并将该Person添加到列表中)、更新(编辑选定的Person)和删除(从列表中删除选定的Person)。 - 绑定表达式查询应用程序的状态并启用或禁用按钮。
Person UI 应用程序的场景图的层次结构视图如下所示
应用程序的文件结构如下所示
Person.java 包含 Person 模型代码,SampleData.java 提供用于初始化应用程序的数据。FXMLController.java 是 JavaFX 控制器类,PersonUI.java 包含主应用程序类。在 resources 下,FXML 文件 Scene.fxml 描述了 UI。
模型
Person 类是此应用程序使用的“模型”。
package org.modernclient.model;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.util.Callback;
import java.util.Objects;
public class Person {
private final StringProperty firstname = new SimpleStringProperty(
this, "firstname", "");
private final StringProperty lastname = new SimpleStringProperty(
this, "lastname", "");
private final StringProperty notes = new SimpleStringProperty(
this, "notes", "sample notes");
public Person() {
}
public Person(String firstname, String lastname, String notes) {
this.firstname.set(firstname);
this.lastname.set(lastname);
this.notes.set(notes);
}
public String getFirstname() {
return firstname.get();
}
public StringProperty firstnameProperty() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname.set(firstname);
}
public String getLastname() {
return lastname.get();
}
public StringProperty lastnameProperty() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname.set(lastname);
}
public String getNotes() {
return notes.get();
}
public StringProperty notesProperty() {
return notes;
}
public void setNotes(String notes) {
this.notes.set(notes);
}
@Override
public String toString() {
return firstname.get() + " " + lastname.get();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return Objects.equals(firstname, person.firstname) &&
Objects.equals(lastname, person.lastname) &&
Objects.equals(notes, person.notes);
}
@Override
public int hashCode() {
return Objects.hash(firstname, lastname, notes);
}
}
可观察列表
在使用 JavaFX 集合时,您通常会使用 ObservableList,它使用侦听器检测列表更改。此外,显示数据列表的 JavaFX 控件需要可观察列表。这些控件会自动更新 UI 以响应列表修改。当我们引导您完成示例程序时,我们将解释其中的一些复杂之处。
实现 ListView 选择
一个 ListView 控件显示可观察列表中的项目,并允许您选择一个或多个项目。要将选定的 Person 显示在右侧视图中的表单字段中,您需要使用 selectedItemProperty 的更改侦听器。每次用户从 ListView 中选择不同的项目或取消选择选定项目时,都会调用此更改侦听器。您可以使用鼠标进行选择,也可以使用箭头键、Home(第一个项目)和 End(最后一个项目)。在 Mac 上,使用 Fn + 左箭头键选择 Home,使用 Fn + 右箭头键选择 End。对于取消选择(在 Mac 上使用 Command-单击,在 Linux 或 Windows 上使用 Control-单击),新值为 null,我们将清除所有表单控件字段。您可以在下面观察 ListView 选择更改侦听器。
listView.getSelectionModel().selectedItemProperty().addListener(
personChangeListener = (observable, oldValue, newValue) -> {
// newValue can be null if nothing is selected
selectedPerson = newValue;
modifiedProperty.set(false);
if (newValue != null) {
// Populate controls with selected Person
firstnameTextField.setText(selectedPerson.getFirstname());
lastnameTextField.setText(selectedPerson.getLastname());
notesTextArea.setText(selectedPerson.getNotes());
} else {
firstnameTextField.setText("");
lastnameTextField.setText("");
notesTextArea.setText("");
}
});
布尔属性 modifiedProperty 跟踪用户是否更改了表单中的三个文本控件中的任何一个。我们在每次 ListView 选择后重置此标志,并在绑定表达式中使用此属性来控制 Update 按钮的禁用属性。
使用多选
默认情况下,ListView 控件实现单选,因此最多只能选择一个项目。 ListView 还提供多选,您可以通过配置选择模式来启用它,如下所示
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
使用此设置,每次用户使用 CTRL-Shift 或 CTRL-Command 将另一个项目添加到选择中时,都会调用 selectedItemProperty 侦听器,并使用新的选择。getSelectedItems() 方法返回所有当前选定的项目,而 newValue 参数是最近选择的项目。例如,以下更改侦听器收集多个选定项目并打印它们
listView.getSelectionModel().selectedItemProperty().addListener(
personChangeListener = (observable, oldValue, newValue) -> {
ObservableList<Person> selectedItems = listView.getSelectionModel().getSelectedItems();
// Do something with selectedItems
// System.out.println(selectedItems);
});
我们的 Person UI 应用程序对 ListView 使用单选模式。
ListView 和排序
假设您想按姓氏,然后按名字对姓名列表进行排序。JavaFX 有几种对列表进行排序的方法。由于我们需要保持姓名排序,因此我们将底层的 observableArrayList 包装在一个 SortedList 中。要保持 ListView 中的列表排序,我们使用排序后的列表调用 ListView 的 setItems() 方法。比较器指定排序顺序。首先,我们比较每个人的姓氏进行排序,然后根据需要比较名字。要设置排序,setComparator() 方法使用匿名类,或者更简洁地使用 lambda 表达式
// Use a sorted list; sort by lastname; then by firstname
SortedList<Person> sortedList = new SortedList(personList);
sortedList.setComparator((p1, p2) -> {
int result = p1.getLastname().compareToIgnoreCase(p2.getLastname());
if (result == 0) {
result = p1.getFirstname().compareToIgnoreCase(p2.getFirstname());
}
return result;
});
listView.setItems(sortedList);
请注意,比较器参数 p1 和 p2 被推断为 Person 类型,因为 SortedList 是泛型的。
Person UI 应用程序操作
我们的 Person UI 应用程序实现了三个操作:删除(从底层列表中删除选定的 Person 对象)、新建(创建 Person 对象并将其添加到底层列表中)和更新(对选定的 Person 对象进行更改并更新底层列表)。让我们详细介绍每个操作,重点关注学习更多关于 JavaFX 功能的信息,这些功能可以帮助您构建此类应用程序。
删除 Person
控制器类包含 Delete 按钮的操作事件处理程序。以下是定义 Delete 按钮的 FXML 代码片段
<Button fx:id="removeButton" mnemonicParsing="false"
onAction="#removeButtonAction" text="Delete" />
fx:id 属性命名按钮,以便 JavaFX 控制器类可以访问它。onAction 属性对应于控制器代码中的 ActionEvent 处理程序。我们在此应用程序中不使用键盘快捷键,因此我们将属性 mnemonicParsing 设置为 false。
注意
当 mnemonicParsing 为 true 时,您可以指定一个键盘快捷键来激活带标签的控件,例如 Alt-F 用于打开“文件”菜单。您可以通过在标签中目标字母之前添加下划线字符来定义键盘快捷键。
您无法直接更新 SortedList,但您可以对其底层列表(ObservableList personList)应用更改。每次您添加或删除项目时,SortedList 始终保持其元素排序。
以下是控制器类中的事件处理程序
@FXML
private void removeButtonAction(ActionEvent actionEvent) {
personList.remove(selectedPerson);
}
此处理程序从支持的可观察数组列表中删除选定的 Person 对象。 ListView 控件的选择更改侦听器设置 selectedPerson。
请注意,我们不需要在这里将selectedPerson与null进行比较。为什么?您会看到,当selectedItemProperty为null时,我们将禁用“删除”按钮。这意味着当用户取消选中ListView控件中的元素时,永远不会调用“删除”按钮的操作事件处理程序。以下是控制“删除”按钮禁用属性的绑定表达式
removeButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNull());
此优雅的语句使事件处理程序更加紧凑,从而减少了错误。按钮的disableProperty和选择模型的selectedItemProperty都是JavaFX可观察对象。因此,您可以在绑定表达式中使用它们。调用bind()的属性会在bind()参数的值发生变化时自动更新。
添加人员
“新建”按钮将Person添加到列表中,然后更新ListView控件。新项目始终会被排序,因为当元素被添加到包装列表中时,列表会重新排序。以下是定义“新建”按钮的FXML。与“删除”按钮类似,我们定义了fx:id和onAction属性
<Button fx:id="createButton" mnemonicParsing="false" onAction="#createButtonAction" text="New" />
在什么情况下应该禁用“新建”按钮?
- 单击“新建”时,
ListView中不应该选择任何项目。因此,如果selectedItemProperty不为null,我们将禁用“新建”按钮。请注意,您可以使用Command-click或Control-click取消选中所选项目。 - 如果姓氏或名字字段为空,我们不应该创建新的
Person。因此,如果这两个字段中的任何一个为空,我们将禁用“新建”按钮。但是,我们允许“备注”字段为空。以下是实现这些限制的绑定表达式
createButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNotNull()
.or(firstnameTextField.textProperty().isEmpty()
.or(lastnameTextField.textProperty().isEmpty())));
现在让我们向您展示“新建”按钮的事件处理程序
@FXML
private void createButtonAction(ActionEvent actionEvent) {
Person person = new Person(firstnameTextField.getText(),
lastnameTextField.getText(), notesTextArea.getText());
personList.add(person);
// and select it
listView.getSelectionModel().select(person);
}
首先,我们使用表单的文本控件创建一个新的Person对象,并将此Person添加到包装列表(ObservableList personList)中。为了使Person的数据立即可见并可编辑,我们选择新添加的Person。
更新人员
更新Person不像其他操作那样简单。在我们深入了解原因的细节之前,让我们先看看“更新”按钮的FXML代码,它与其他按钮类似
<Button fx:id="updateButton" mnemonicParsing="false"
onAction="#updateButtonAction" text="Update" />
默认情况下,排序列表不会响应更改的单个数组元素。例如,如果Person“Ethan Nieto”更改为“Ethan Abraham”,列表将不会像添加或删除项目时那样重新排序。有两种方法可以解决此问题。首先是删除该项目并使用新值将其添加回来。
第二种方法是为底层对象定义一个提取器。提取器定义了在发生更改时应观察的属性。通常,不会观察到对单个列表元素的更改。提取器返回的可观察对象在列表ChangeListener中标记更新更改。因此,为了使ListView控件在对单个元素进行更改后显示正确排序的列表,您需要定义一个带有提取器的ObservableList。
提取器的优点是您只包含影响排序的属性。在我们的示例中,属性firstname和lastname影响列表的顺序。这些属性应该放在提取器中。
提取器是模型类中的静态回调方法。以下是我们Person类的提取器
public class Person {
...
public static Callback<Person, Observable[]> extractor =
p-> new Observable[] {p.lastnameProperty(), p.firstnameProperty()};
}
现在,控制器类可以使用此提取器来声明一个名为personList的ObservableList,如下所示
private final ObservableList<Person> personList =
FXCollections.observableArrayList(Person.extractor);
设置提取器后,排序列表会检测到firstnameProperty和lastnameProperty的更改,并在需要时重新排序。
接下来,我们定义“更新”按钮何时启用。在我们的应用程序中,如果未选择任何项目,或者firstname或lastname文本字段变为空,则应禁用“更新”按钮。最后,如果用户尚未对表单的文本组件进行更改,我们将禁用“更新”。我们使用JavaFX布尔属性modifiedProperty跟踪这些更改,该属性使用JavaFX布尔属性帮助类SimpleBooleanProperty创建。我们在JavaFX控制器类中将此布尔值初始化为false,如下所示
private final BooleanProperty modifiedProperty = new SimpleBooleanProperty(false);
我们在ListView选择更改侦听器中将此布尔属性重置为false。当在任何三个可以更改的字段(名字、姓氏和备注控件)中发生按键时,modifiedProperty将设置为true。以下是按键事件处理程序,它在检测到这三个控件的焦点内的按键时被调用
@FXML
private void handleKeyAction(KeyEvent keyEvent) {
modifiedProperty.set(true);
}
当然,FXML标记必须为所有三个文本控件配置属性onKeyReleased,以调用按键事件处理程序。以下是firstname TextField的FXML,它将handleKeyAction事件处理程序链接到此控件的按键释放事件
<TextField fx:id="firstnameTextField" onKeyReleased="#handleKeyAction"
prefWidth="248.0"
GridPane.columnIndex="1"
GridPane.hgrow="ALWAYS" />
以下是“更新”按钮的绑定表达式,如果selectedItemProperty为null,modifiedProperty为false,或者文本控件为空,则该表达式将被禁用
updateButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNull()
.or(modifiedProperty.not())
.or(firstnameTextField.textProperty().isEmpty()
.or(lastnameTextField.textProperty().isEmpty())));
现在让我们向您展示“更新”按钮的操作事件处理程序。当用户在ListView控件中选择一个项目并在任何文本字段中至少进行一次更改后单击“更新”按钮时,将调用此处理程序。
但还有一项家务事要做。在使用表单控件中的值开始更新所选项目之前,我们必须删除selectedItemProperty上的侦听器。为什么?回想一下,对firstname或lastname属性的更改将动态影响列表,并可能对其进行重新排序。此外,这可能会改变ListView对当前所选项目的理解,并调用ChangeListener。为了防止这种情况,我们在更新期间删除侦听器,并在更新完成后添加侦听器。在更新期间,所选项目保持不变(即使列表重新排序)。因此,我们清除modifiedProperty标志以确保“更新”按钮被禁用
@FXML
private void updateButtonAction(ActionEvent actionEvent) {
Person p = listView.getSelectionModel().getSelectedItem();
listView.getSelectionModel().selectedItemProperty()
.removeListener(personChangeListener);
p.setFirstname(firstnameTextField.getText());
p.setLastname(lastnameTextField.getText());
p.setNotes(notesTextArea.getText());
listView.getSelectionModel().selectedItemProperty()
.addListener(personChangeListener);
modifiedProperty.set(false);
}
带有记录的人员UI
Java 16 中令人兴奋的新功能之一是记录。记录允许您对保存不可变数据并描述状态的类进行建模,通常只需一行代码。让我们重构我们的Person UI示例以使用Java记录作为Person模型类。我们这样做有几个原因。
- 使用JavaFX的现代Java客户端将随着应用程序利用新的Java功能而不断发展。毕竟,JavaFX是用Java API实现的,当然可以利用新的功能,因为它们变得可用。
- 我们的UI示例非常适合记录,因为使用Person记录而不是类是一种简单的方法。
- 我们最初使用JavaFX属性实现
Person,这些属性是可观察的和可变的。但是,在我们的应用程序环境中,这种可变性是必要的还是可取的?• Java记录有助于使您的代码更具可读性,因为通常一行代码就定义了模型类的状态。
人员记录
我们用它的名称及其不可变组件声明一个记录;每个组件都有一个名称和类型。这些组件是生成的类中的最终实例字段。Java为字段生成访问器方法、构造函数以及equals()、hashCode()和toString()方法的默认实现。
以下是新的Person类,它比非记录版本短得多
public record Person (String firstname, String lastname, String notes) {
@Override
public String toString() {
return firstname + " " + lastname;
}
}
请注意,我们提供了自己的toString()实现来替换自动生成的toString(),因为ListView使用它来显示每个Person对象。生成的访问器方法是firstname()、lastname()和notes(),以匹配记录头中声明的元素。我们更新我们的应用程序以使用这些名称而不是传统的getter形式。这会影响selectedItemProperty更改侦听器和排序列表比较器。
createButtonAction或removeButtonAction事件处理程序不需要进行任何更改。创建我们的Person对象样本列表(SampleData.java)的代码也不需要进行任何更改。
但是,记录确实需要对updateButtonAction事件处理程序进行更改。由于Person对象现在是不可变的,因此我们无法更新其字段。因此,要更新Person,我们必须创建一个新的Person对象,删除旧的,并将新的添加到备份列表中。排序列表会自动使用新数据进行更新。以下是新的updateButtonAction事件处理程序。
@FXML
private void updateButtonAction(ActionEvent actionEvent) {
Person person = new Person(firstnameTextField.getText(), lastnameTextField.getText(),
notesTextArea.getText());
personList.remove(listView.getSelectionModel().getSelectedItem());
personList.add(person);
listView.getSelectionModel().select(person);
modifiedProperty.set(false);
}
通过删除和添加Person,更新过程变得更加简单。不再需要检测更改的提取器,也不需要在更新期间暂时删除selectedItemProperty更改侦听器。
通过将Person限制为不可变容器,我们极大地简化了Person和程序的可读性。但是,JavaFX属性和绑定仍然是维护UI状态的理想功能。
关键要点总结
本系列涵盖了很多内容。让我们回顾一下关键要点
- JavaFX是一个现代的UI工具包,可以在桌面、移动和嵌入式环境中高效运行。
- JavaFX使用剧院隐喻。运行时系统创建主舞台并调用应用程序的
start()方法。 - 您创建一个分层场景图并将根节点安装到场景中。
- JavaFX运行时系统在JavaFX应用程序线程上执行所有UI更新和场景图修改。任何长时间运行的工作都应该委托给单独线程中的后台任务,以保持UI的响应能力。JavaFX有一个完善的并发库,可以帮助您将UI代码与后台代码分开。
- JavaFX支持2D和3D图形。2D图形中的原点是场景的左上角。
- JavaFX包含一组丰富的布局控件,允许您在场景中排列组件。您可以嵌套布局控件并指定调整大小标准。
- JavaFX将场景图定义为节点的分层集合。节点由它们的属性描述。
- JavaFX 属性是可观察的。您可以附加监听器并使用丰富的绑定 API 将属性相互链接并检测更改。
- JavaFX 允许您定义称为转换的高级动画。
- 场景图的层次结构意味着父节点可以将渲染工作委托给它们的子节点。
- JavaFX 支持各种事件,使您可以对用户输入和场景图的更改做出反应。
- 虽然您可以完全用 Java 编写 JavaFX 应用程序,但更好的方法是用 FXML 编写可视化描述,FXML 是一种用于指定 UI 内容的标记语言。FXML 有助于将可视化代码与模型和控制器代码分离。
- 每个 FXML 文件通常描述一个场景并配置一个控制器。
上次更新: 2023 年 9 月 12 日


