JavaFX17-现代-Java-客户端权威指南-二-

223 阅读1小时+

JavaFX17 现代 Java 客户端权威指南(二)

原文:The Definitive Guide to Modern Java Clients with JavaFX 17

协议:CC BY-NC-SA 4.0

四、JavaFX 控制深入

乔纳森·贾尔斯写的

当 JavaFX 在 2007 年首次发布时,它没有任何用户界面控件可供用户放入他们的用户界面。开发人员不得不满足于要么创建自己的基本 UI 控件,要么从 Java 附带的 Swing 工具包中导入 UI 组件。从 JavaFX 1.2 开始,这种情况开始改善,引入了许多非常重要的 UI 控件,如ButtonProgressBarListView。在随后的版本中,JavaFX 开始获得一套完整且广受好评的 UI 控件,提供了为企业环境中的应用程序构建用户界面的能力。

本章将介绍核心 JavaFX 17 版本中的大多数 UI 控件。由于本章的页数有限,代码示例有意保持简短。请放心,本书的代码库包括一个全面的演示应用程序,涵盖了所有 JavaFX UI 控件,代码可以复制/粘贴到您自己的应用程序中。

用户界面控件模块

从 JavaFX 9 开始,几乎所有的 UI 控件都封装在javafx.controls模块中。 1 该模块被拆分成四个导出包,如下图所示:

  • javafx.scene.chart:这个包包含了构建图表的图表组件,比如折线图、条形图、面积图、饼图、气泡图和散点图。这些将不作为本章的一部分。

  • javafx.scene.control:这个包包含 javafx 中几乎所有用户界面控件的 API。这是我们将在本章介绍的主要软件包。

  • javafx.scene.control.cell:这个包包含了大量预先构建的“细胞工厂”的 API,我们将在本章后面的“高级控件”部分更深入地讨论这些 API。

  • javafx.scene.control.skin:这个包包含每个 UI 控件的“皮肤”或可视组件。我们不会在本章中讨论这个包,因为它超出了本书的范围。

什么是 UI 控件?

在 JavaFX 的上下文中,一个有效的问题是:什么是 UI 控件 **?**一个简单的定义可能是,它是一个可视组件,构成用户界面的一小部分,并且通常是交互式的(但不总是)。从最严格的意义上来说,UI 控件从Control类扩展而来,但是一个更宽松的定义允许任何从Node扩展而来的组件被认为是 UI 控件。出于本章的考虑,讨论的大多数 UI 控件都是从Control类扩展而来的。

这就不可避免的引出了下一个问题:什么是控制类? Control是从Parent扩展而来的类,而Parent本身又是从Node扩展而来的。在通常引用的 MVC 2 命名法中,一个Control可以被认为是模型。在任何使用 JavaFX UI 控件构建的用户界面中,开发人员应该只与Control类进行交互,因为这些是所有 API 操作和读取控件状态的地方。

因为Control职业是从Node延伸出来的,它被赋予了Node所拥有的所有能力。这意味着 UI 控件可以根据需要修改效果、旋转、缩放和许多其他属性。还可以以标准方式添加鼠标、滑动、拖动、触摸、按键输入等事件处理程序。将 UI 控件添加到场景图的方式也与任何Node相同——通过将它添加到带有相关尺寸信息、布局约束等的布局容器中。

从 JavaFX 9 开始,如前所述,所有 UI 控件的视觉效果,即皮肤,也已经成为javafx.scene.control.skin包中的公共 API。皮肤是公共 API 的原因是为了使开发人员能够对它们进行子类化,从而覆盖 UI 控件的默认视觉效果。

JavaFX 基本控件

JavaFX 中存在一个 UI 控件子集,可以认为它对几乎所有的用户界面都至关重要,但是无论从最终用户的角度还是从 UI 开发人员的角度来看,它们都是简单易用的,从这个意义上来说,它们是基本的。本节将依次介绍这些基本控件。从 JavaFX 17 开始,基本 UI 控件可以分为三个子组:

  1. “贴标签”控件:ButtonCheckBoxHyperlinkLabelRadioButtonToggleButton

  2. “文本输入”控件:TextFieldTextAreaPasswordField

  3. “其他”简单控件:ProgressBarProgressIndicatorSlider

标签控件

大多数显示只读文本的控件都是从一个叫做Labeled的公共抽象超类扩展而来的。这个类指定了一组公共属性,用于处理对齐、字体、图形(和图形定位)、换行等等,当然,也用于显示文本本身。因为Labeled是抽象的,所以一般不直接使用,但是很多实际的 UI 控件都是从中延伸出来的,包括ButtonCheckBoxHyperlinkLabelRadioButtonToggleButton。除了这些基本控制外,其他更高级的控制(将在本章后面介绍)也受益于Labeled,包括MenuButtonTitledPaneCell

Labeled最重要的属性 3 如表 4-1 所示。

表 4-1

标签类的属性

|

1 属性

|

类型

|

描述

| | --- | --- | --- | | alignment | ObjectProperty<Pos> | 指定文本和图形的对齐方式。 | | contentDisplay | ObjectProperty<ContentDisplay> | 指定图形相对于文本的位置。 | | font | ObjectProperty<Font> | 文本使用的默认字体。 | | graphic | ObjectProperty<Node> | Labeled的可选图标。 | | textAlignment | ObjectProperty<TextAlignment> | 指定多行文字时文字行的行为。 | | text | StringProperty | 要在标签中显示的文本。 | | wrapText | BooleanProperty | 指定超出宽度时文本是否应换行。 |

如上所述,因为Labeled类是抽象的,大多数开发人员不直接使用这个类。相反,它们使用 JavaFX 附带的一个具体子类,现在将更详细地介绍这个子类。

标签

因为Labeled非常全面,所以具体的Label类非常简单——它只增加了一个额外的 API。这被称为labelFor,用于使用助记符使用户界面控件的键盘导航更简单,以及为盲人和弱视者改善文本到语音的输出。当使用一个Label来描述另一个控件(例如一个Slider)时,最好通过说label.labelFor(slider)来将Label实例与Slider实例相关联。这意味着当焦点对准Slider控件时,面向全盲或部分盲人群的屏幕阅读软件可以读出Label的文本,以帮助向用户描述Slider的用途。

纽扣

Button类通过提供可点击的视觉启示,使用户能够执行一些动作。当用户点击一个按钮时,它变成“待命”,当鼠标被释放时,它“开火”,然后变成“解除待命”按钮最重要的属性如表 4-2 所示。

表 4-2

Button(和 ButtonBase)类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | armed | ReadOnlyBooleanProperty 4 | 指示用户当前是否正在单击按钮。 | | cancelButton | BooleanProperty | 如果为真,按钮将处理Escape键的按下。 | | defaultButton | BooleanProperty | 如果为真,按钮将处理Enter键的按下。 | | onAction | ObjectProperty<EventHandler<ActionEvent>> | 触发Button时执行的回调。 |

关联的方法只有一种,那就是fire()法。可以调用这个方法以编程方式触发Button,从而导致关联的onAction事件被调用。更常见的情况是当用户直接点击按钮时,结果是一样的——触发按钮并调用安装的任何onAction事件处理程序。处理动作事件的代码如清单 4-1 所示。

var button = new Button("Click Me!");
button.setOnAction(event -> System.out.println("Button was clicked"));

Listing 4-1Creating a JavaFX Button instance that handles clicks by printing to the console

检验盒

通常,CheckBox使用户能够指定某事是真还是假。在 JavaFX 中这是可能的,但是也有能力显示第三种状态:indeterminate。默认情况下,JavaFX CheckBox只会在选中和未选中状态之间切换(这反映在selected属性中)。为了支持通过indeterminate状态的切换,开发人员必须将allowIndeterminate属性设置为 true。启用时,可以读取indeterminate属性和selected属性,以确定CheckBox的状态。

因为CheckBox是一个Labeled控件,所以它支持在复选框旁边显示textgraphic。只有几个非常重要的附加属性,如表 4-3 所示。清单 4-2 显示了复选框的典型用法。

表 4-3

复选框类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | allowIndeterminate | BooleanProperty | 确定CheckBox是否应该切换到不确定状态。 | | indeterminate | BooleanProperty | 指定CheckBox当前是否不确定。 | | selected | BooleanProperty | 指定当前是否选择了CheckBox。 |

CheckBox cb = new CheckBox("Enable Power Plant");
cb.setIndeterminate(false);
cb.setOnAction(e -> log("Action event fired"));
cb.selectedProperty()
    .addListener(i -> log("Selected state change to " + cb.isSelected()));

Listing 4-2Creating a CheckBox instance that is determinate (i.e., only toggles between selected and unselected)

超链接

Hyperlink控件本质上是一个Button控件,以超链接的形式呈现——带下划线的文本——就像人们期望在网站上看到的一样。因此,Hyperlink的 API 相当于Button类,只是增加了一个小的属性:一个visited属性来指示用户是否点击了链接,如表 4-4 所示。如果visited为真,开发者可以选择不同的Hyperlink样式。清单 4-3 显示了超链接的典型用法。

表 4-4

超链接类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | visited | BooleanProperty | 当用户第一次触发超链接时切换到 true。 |

var hyperlink = new Hyperlink("Click Me!");
hyperlink.setOnAction(event -> log("Hyperlink was clicked"));

Listing 4-3Creating a Hyperlink instance and listening for it to be clicked

开关按钮

ToggleButton是一个Button(意味着它仍然可以触发动作事件),但通常这不是最好的方法。这是因为ToggleButton的意图是toggle它的selected属性状态在被选中和未被选中之间,每次点击一次。当选择一个ToggleButton时,它的视觉外观是不同的,看起来像是被“推入”ToggleButton实例可以添加到ToggleGroup来控制选择。

什么是 ToggleGroup?

ToggleGroup是一个简单的类,它包含了一系列Toggle实例,它管理这些实例的选定状态。ToggleGroup保证一次最多只能选择一个Toggle

Toggle是一个具有两个属性的接口——selectedtoggleGroup。实现这个接口的类包括ToggleButtonRadioButtonRadioMenuItem

如何使用 ToggleButton 和 ToggleGroup?

归结起来就是,通过实例化一个ToggleGroup实例和多个ToggleButton实例,并将每个ToggleButton上的toggleGroup属性设置为单个ToggleGroup实例,一个ToggleButton可以与一个ToggleGroup相关联。这显示在清单 4-4 中。

在这样做的时候,这个组中的ToggleButton实例有一个附加的约束:任何时候只能选择一个ToggleButton。如果用户选择新的ToggleButton,先前选择的ToggleButton将被取消选择。当 ToggleButtons 被放置在ToggleGroup中时,没有被选中的ToggleButton实例有效(即被选中的ToggleButton可以不被选中)。ToggleButton 的主要属性如表 4-5 所示。

表 4-5

ToggleButton 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | selected | BooleanProperty | 指示是否选择了切换。 | | toggleGroup | ObjectProperty<ToggleGroup> | 这个ToggleButton所属的ToggleGroup。 |

// create a few toggle buttons
ToggleButton tb1 = new ToggleButton("Toggle button 1");
ToggleButton tb2 = new ToggleButton("Toggle button 2");
ToggleButton tb3 = new ToggleButton("Toggle button 3");

// create a toggle group and add all the toggle buttons to it
ToggleGroup group = new ToggleGroup();
group.getToggles().addAll(tb1, tb2, tb3);
// it is possible to add an onAction listener for each button
tb1.setOnAction(e -> log("ToggleButton 1 was clicked on!"));
// but it is better to add a listener to the toggle group  selectedToggle property
group.selectedToggleProperty()
    .addListener(i -> log("Selected toggle is " + group.getSelectedToggle()));

Listing 4-4Creating three ToggleButtons and adding them to a single ToggleGroup and listening to selection changes

单选按钮

RadioButton是一个ToggleButton,应用了不同的样式,当放在ToggleGroup中时,行为也略有不同。虽然ToggleGroup中的 ToggleButtons 可以全部取消选择,但是对于ToggleGroup中的 radio button,用户没有办法取消选择所有的 radio button。这是因为,从视觉上看,一个RadioButton只能被点击进入选中状态。后续的点击没有影响(当然不会导致取消选择)。因此,取消选择一个RadioButton的唯一方法是在同一个ToggleGroup中选择一个不同的RadioButton

因为 RadioButton 的 API 本质上等同于 ToggleButton,所以请参考清单 4-4 中的 ToggleButton 代码示例。唯一的区别是用 RadioButton 实例替换 ToggleButton 实例。

文本输入控件

在简单的Labeled控件之后,下一组控件是主要用于文本输入的三个控件,即TextAreaTextFieldPasswordFieldTextField设计用于接收用户的单行输入,而TextArea设计用于接收多行输入。PasswordFieldTextField扩展而来,允许用户通过屏蔽用户输入来输入敏感信息。在所有这三种情况下,这些控件都不接受富文本输入(参见本章后面的HTMLEditor控件了解富文本输入的一个选项)。

TextAreaTextField从一个名为TextInputControl的抽象类扩展而来,该抽象类提供了一组基本功能,以及许多适用于这两个类的属性和方法(其中最重要的显示在表 4-6 中)。例如,TextInputControl支持插入符号定位(插入符号是一种闪烁的光标,指示文本输入将出现的位置)、文本选择和格式化,当然还有编辑。

表 4-6

TextInputControl 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | anchor | ReadOnlyIntegerProperty | 文本选择的锚点。锚点和插入符号之间的范围代表文本选择范围。 | | caretPosition | ReadOnlyIntegerProperty | 插入符号在文本中的当前位置。 | | editable | BooleanProperty | 用户是否可以编辑控件中的文本。 | | font | ObjectProperty<Font> | 用于呈现文本的字体。 | | length | ReadOnlyIntegerProperty | 在控件中输入的字符数。 | | promptText | StringProperty | 没有用户输入时显示的文本。 | | selectedText | ReadOnlyStringProperty | 通过鼠标、键盘或编程方式在控件中选择的文本。 | | textFormatter | ObjectProperty<TextFormatter<?>> | 请参见“文本格式化程序”一节 | | text | StringProperty | 该控件的文本内容。 |

文本格式化程序

在我们深入研究具体的控件之前,我们将首先快速转移话题,讨论一下前文提到的TextFormatter API。一个TextFormatter有两种不同的机制,使它能够影响文本输入控件中接受和显示的内容:

  1. 可以截取和修改用户输入的过滤器。这有助于保持文本的理想格式。可以使用默认的文本提供者来提供初始文本。

  2. 值转换器和值可用于提供表示 v 类型值的特殊格式,如果控件是可编辑的,并且用户更改了文本,则值会更新以对应于文本。

有可能只有一个过滤器或值转换器的格式化程序。然而,如果没有提供值转换器,设置一个值将导致一个IllegalStateException,并且该值总是空的。

文本字段、密码字段和文本区域

如前所述,TextField控件用于从用户处接收单行无格式文本。这对于请求用户名、电子邮件地址等的表单非常理想。两个关键属性是textonActiontext属性已经讨论过了,因为它是从TextInputControl继承而来的,而onAction的功能正如我们已经讨论过的Button和类似的类一样:当 Enter 键被按下时,TextField会发出一个ActionEvent信号,提醒开发人员用户已经选择“提交”他们的输入。清单 4-5 展示了使用文本字段控件的标准方法。

TextField textField = new TextField();
textField.setPromptText("Enter name here");

// this is fired when the user hits the Enter key
textField.setOnAction(e -> log("Entered text is: " + textField.getText()));

// we can also observe input in real time
textField.textProperty()
    .addListener((o, oldValue, newValue) -> log("current text input is " + newValue));

Listing 4-5Creating and using a TextField control

PasswordField 的功能与 TextField 完全相同,只是它隐藏了用户输入,因此在一定程度上防止了用户背后的窥探。此外,出于安全原因,PasswordField 不支持剪切和复制操作(但是粘贴仍然有效)。PasswordField 上没有其他属性或 API。

TextArea 控件是为多行用户输入设计的,但同样只支持无格式文本。TextArea 控件最适合在不需要单行输入的情况下使用。例如,如果你想让你的用户提供反馈(可能跨越多个句子或段落),文本区域是最好的选择。因为 TextArea 是为多行输入设计的,所以有一些有用的属性值得熟悉,如表 4-7 所示。

表 4-7

TextArea 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | prefColumnCount | IntegerProperty | 文本列的首选数量。 | | prefRowCount | IntegerProperty | 首选文本行数。 | | wrapText | BooleanProperty | 当一行超出可用宽度时,是换行还是让文本区域水平滚动。 |

其他简单控件

除了Labeled控件和文本输入控件之外,还有另外三个可以被认为是“简单”的控件:ProgressBarProgressIndicatorSlider

进度条和进度条指示器

JavaFX 提供了两个向用户显示进度的 UI 控件:ProgressBarProgressIndicator。就 API 而言,它们非常接近,因为ProgressBar扩展了ProgressIndicator并且没有增加额外的 API。ProgressIndicator最重要的属性如表 4-8 所示。

这两个控件都可以用来显示进度,或者可以设置为不确定的状态,以向用户指示工作正在进行,但此时进度未知。

为了显示进度,开发人员应该将progress属性设置为 0.0 到 1.0 之间的值。这乍看起来可能违反直觉——为什么使用 0.0 到 1.0 之间的范围,而不是 0-100 的范围?答案并不清楚,这是否是整个 JavaFX UI 工具包在处理百分比时有意识的设计选择。要使进度控件切换到不确定的形式,只需将progress属性值设置为–1。当这个操作完成后,indeterminate属性将从假变为真。

清单 4-6 中显示了一个简单的使用示例。

表 4-8

ProgressIndicator 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | indeterminate | ReadOnlyBooleanProperty | 一个布尔标志,指示不确定进度动画是否正在播放。 | | progress | DoubleProperty | 实际进度(在 0.0 和 1.0 之间),或者可以设置为–1 表示不确定。 |

ProgressBar p2 = new ProgressBar();
p2.setProgress(0.25F);

Listing 4-6Creating a ProgressBar that will show 25% progress

滑块

滑块控件用于使用户能够在某个最小/最大范围内指定一个值。这是通过向用户显示“轨迹”和“拇指”来实现的用户可以拖动滑块来更改值。因此毫不奇怪,滑块控件的三个最重要的属性是它的minmaxvalue属性,如表 4-9 所示。清单 4-7 显示了一个使用滑块的简单例子。

表 4-9

Slider 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | blockIncrement | DoubleProperty | 点击轨道时Slider移动的幅度。 | | max | DoubleProperty | Slider所代表的最大值。 | | min | DoubleProperty | 由Slider表示的最小值。 | | orientation | ObjectProperty<Orientation> | Slider是水平还是垂直。 | | value | DoubleProperty | 由Slider表示的当前值。 |

Slider slider = new Slider(0.0f, 1.0f, 0.5f);
slider.valueProperty()
    .addListener((o, oldValue, newValue) -> log("Slider value is " + newValue));

Listing 4-7Creating a slider that will have a range between 0.0 and 1.0

容器控件

现在我们已经学习了简单的 UI 控件,我们可以继续学习一些更令人兴奋的控件。本节将讨论“容器”控件,即用于包含和显示其他用户界面元素的控件。这些容器控件提供了一些额外的功能,可以折叠它们的内容,提供一个选项卡式的界面来改变视图,或者其他功能。

手风琴和标题面板

TitledPane是一个显示标题区域和内容区域的容器,能够通过单击标题区域来展开和折叠内容区域。这对于用户界面中的侧面板等是有用的,因为它允许信息被显示,但是可选地被用户折叠,使得他们只看到他们需要看到的。

TitledPaneLabeled扩展而来,所以正如我们之前讨论的,有大量的属性可以定制显示。但是应该注意,这些Labeled属性只应用于TitledPane的标题区域,而不是内容区域。标题面板的主要性能如表 4-10 所示。

表 4-10

TitledPane 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | animated | BooleanProperty | 当TitledPane展开和折叠时是否有动画。 | | collapsible | BooleanProperty | 用户是否可以折叠TitledPane。 | | content | ObjectProperty<Node> | 在TitledPane的内容区域显示的节点。 | | expanded | BooleanProperty | TitledPane当前是否展开。 | | text | StringProperty | 显示在TitledPane标题区域的文本。 |

随着TitledPane的引入,我们可以继续关注Accordion,这是一个简单的包含零个或多个 TitledPanes 的控件。当一个Accordion显示给用户时,它只允许一个TitledPane在任何时候展开。展开不同的TitledPane将导致当前展开的TitledPane被折叠。

只有一个值得注意的属性——expandedPane——即代表当前扩展的TitledPaneObjectProperty<TitledPane>,如表 4-11 所示。

表 4-11

Accordion 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | expandedPane | ObjectProperty<TitledPane> | Accordion中当前展开的TitledPane。 |

为了将标题窗格添加到Accordion中,我们使用getPanes()方法来检索标题窗格的ObservableList,并将适用的标题窗格添加到该列表中。这样做的结果是标题窗格将按照它们在列表中出现的顺序垂直堆叠显示。清单 4-8 中显示了一个代码示例。

TitledPane t1 = new TitledPane("TitledPane 1", new Button("Button 1"));
TitledPane t2 = new TitledPane("TitledPane 2", new Button("Button 2"));
TitledPane t3 = new TitledPane("TitledPane 3", new Button("Button 3"));
Accordion accordion = new Accordion();
accordion.getPanes().addAll(t1, t2, t3);

Listing 4-8Creating three TitledPanes and adding them all to a single Accordion

按钮栏

ButtonBar控件是在 JavaFX 8u40 版本中添加的,所以它相对较新,相对不为人知。ButtonBar可以被认为本质上是Button控件的HBox(尽管它可以与任何Node一起工作),增加的功能是为运行用户界面的操作系统按正确的顺序放置所提供的按钮。这对于对话框非常有用,例如,Windows、macOS 和 Linux 都有不同的按钮顺序。有少量有用的属性,如表 4-12 所示,清单 4-9 演示了如何创建和填充一个ButtonBar实例。

表 4-12

ButtonBar 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | buttonMinWidth | DoubleProperty | 放置在ButtonBar中的所有按钮的最小宽度。 | | buttonOrder | StringProperty | ButtonBar中按钮的排序。 |

// Create the ButtonBar instance
ButtonBar buttonBar = new ButtonBar();

// Create the buttons to go into the ButtonBar
Button yesButton = new Button("Yes");
ButtonBar.setButtonData(yesButton, ButtonData.YES);

Button noButton = new Button("No");
ButtonBar.setButtonData(noButton, ButtonData.NO);

// Add buttons to the ButtonBar
buttonBar.getButtons().addAll(yesButton, noButton);

Listing 4-9Creating a ButtonBar with “Yes” and “No” buttons. Ordering will depend on the operating system that this code is executed on

滚动窗格

ScrollPane是一个对几乎每个用户界面都至关重要的控件——当内容超出用户界面边界时,能够水平和垂直滚动。例如,想象一个图像处理程序,如 Adobe Photoshop。在这个用户界面中,您可以放大到绘图的一小部分,水平和垂直滚动条允许您移动这一部分以查看相邻的部分。

与其他一些 UI 工具包不同,没有必要用ScrollPane来包装 UI 控件,如ListViewTableView等,因为它们有内置的滚动功能,并由开发人员来处理。因此,ScrollPane通常由开发人员在做一些相对定制的事情时使用。清单 4-10 给出了一个例子,表 4-13 给出了 ScrollPane 的属性。

表 4-13

ScrollPane 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | content | ObjectProperty<Node> | 要显示的节点。 | | fitToHeight | BooleanProperty | 将尝试调整内容大小以匹配视窗高度。 | | fitToWidth | BooleanProperty | 将尝试调整内容大小以匹配视窗的宽度。 | | hbarPolicy | ObjectProperty<ScrollBarPolicy> | 设置何时显示水平滚动条的策略。 | | hmax | DoubleProperty | 最大允许值。 | | hmin | DoubleProperty | 允许的最小值。 | | hvalue | DoubleProperty | ScrollPane的当前水平位置。 | | vbarPolicy | ObjectProperty<ScrollBarPolicy> | 设置何时显示垂直滚动条的策略。这可以是 ScrollPane 中的枚举常量之一。ScrollBarPolicy:始终、按需或从不。 | | vmax | DoubleProperty | 允许的最大值。 | | vmin | DoubleProperty | 允许的最小值。 | | vvalue | DoubleProperty | ScrollPane的当前垂直位置。 |

// in this sample we create a linear gradient to make the scrolling visible
Stop[] stops = new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.RED)};
LinearGradient gradient = new LinearGradient(0, 0, 1500, 1000, false, CycleMethod.NO_CYCLE, stops);
// we place the linear gradient inside a big rectangle
Rectangle rect = new Rectangle(2000, 2000, gradient);
// which is placed inside a scrollpane that is quite small in comparison
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefSize(120, 120);
scrollPane.setContent(rect);
// and we then listen (and log) when the user is scrolling vertically or horizontally
ChangeListener<? super Number> o = (obs, oldValue, newValue) -> {
    log("x / y values are: (" + scrollPane.getHvalue() + ", " + scrollPane.getVvalue() + ")");
};
scrollPane.hvalueProperty().addListener(o);
scrollPane.vvalueProperty().addListener(o);

Listing 4-10Creating a ScrollPane instance

分屏

SplitPane控件接受两个或更多的孩子,并用可拖动的分隔线将他们画出来。然后,用户可以使用这个分隔器给一个孩子更多的空间,代价是占用另一个孩子的空间。一个SplitPane控件非常适合有一个主要内容区域的用户界面,然后在内容区域的左/右/底部有一个区域被用来显示更多上下文相关的信息。在这种情况下,用户可以根据需要给主要内容区域或特定于上下文的区域留出额外的空间。

历史上,UI 工具包只支持两个子代(即“左”和“右”或“上”和“下”),但是 JavaFX 取消了这一限制,允许无限数量的子代,只有一个限制:所有子代必须具有相同的分隔线方向。这意味着一个SplitPane对于所有分隔线只有一个方向属性(如表 4-14 所示)。然而,有一种方法可以解决这个问题:简单地将SplitPane实例嵌入到另一个实例中,这样最终的结果是由水平和垂直方向上的分隔线按照期望的顺序操作组成。

表 4-14

jsplitpane 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | orientation | ObjectProperty<Orientation> | 拆分窗格的方向。 |

SplitPane控件观察其子控件的最小和最大尺寸属性。它永远不会将节点的大小减小到其最小大小以下,也不会给它提供超过其最大大小的大小。因此,建议将添加到SplitPane的所有节点包装在单独的布局容器中,这样布局容器可以处理节点的大小调整,而不会影响 SplitPane 的功能。

分隔线的位置范围从 0 到 1.0(包括 0 和 1.0)。位置 0 将把分隔线放在SplitPane的左/顶端加上节点的最小尺寸。位置 1.0 会将分割线放置在SplitPane的最右/最下边缘减去节点的最小尺寸。分割器位置为 0.5 会将分割器放置在SplitPane的中间。将分隔线位置设置为大于节点的最大大小位置将导致分隔线设置在节点的最大大小位置。将分隔线位置设置为小于节点的最小大小位置将导致分隔线设置在节点的最小大小位置。

清单 4-11 显示了一个创建 SplitPane 的例子。

final StackPane sp1 = new StackPane();
sp1.getChildren().add(new Button("Button One"));

final StackPane sp2 = new StackPane();
sp2.getChildren().add(new Button("Button Two"));

final StackPane sp3 = new StackPane();
sp3.getChildren().add(new Button("Button Three"));

SplitPane splitPane = new SplitPane();
splitPane.getItems().addAll(sp1, sp2, sp3);
splitPane.setDividerPositions(0.3f, 0.6f, 0.9f);

Listing 4-11Creating a SplitPane instance with three children (and therefore two dividers)

塔帕布

是一个 UI 控件,可以向用户显示选项卡式界面。例如,你们中的大多数人都熟悉首选 web 浏览器中的选项卡式界面,因此不需要打开多个窗口——每个窗口对应一个想要打开的页面。

表 4-15 概述了最重要的属性,但两个最有用的属性是side属性和tabClosingPolicy属性。side属性用于指定标签页将显示在TabPane的哪一侧(默认为Side.TOP,这意味着标签页将位于TabPane的顶部)。tabClosingPolicy用于指定用户是否可以关闭标签——有一个TabClosingPolicy枚举,有三个有效值:

  1. UNAVAILABLE:用户不能关闭标签页。

  2. SELECTED_TAB:当前选中的标签页在标签页区域会有一个小的关闭按钮(显示为小“x”)。当选择不同的选项卡时,关闭按钮将从先前选择的选项卡中消失,而显示在新选择的选项卡上。

  3. ALL_TABS:所有在TabPane中可见的标签页都会有一个小的关闭按钮。

JavaFX TabPane通过暴露一个Tab实例的ObservableList来工作。每个Tab实例由一个title属性和一个content属性组成。当一个Tab被添加到tabs列表中时,它将按照在列表中出现的顺序显示在用户界面中。清单 4-12 展示了这一点。

表 4-15

tabpage 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | rotateGraphic | BooleanProperty | 当选项卡放置在左侧/右侧时,图形是否应该旋转以正确显示。 | | selectionModel | ObjectProperty<SingleSelectionModel> | 在TabPane中使用的选择模型。 5 | | side | ObjectProperty<Side> | 将显示选项卡的位置。 | | tabClosingPolicy | ObjectProperty<TabClosingPolicy> | 如前文所述。 |

TabPane tabPane = new TabPane();
tabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);

for (int i = 0; i < 5; i++) {
    Tab tab = new Tab("Tab " + I, new Rectangle(200, 200, randomColor()));
    tabPane.getTabs().add(tab);
}

Listing 4-12How to instantiate and use a TabPane

工具栏

ToolBar控件是一个非常简单的 UI 控件。在其最常见的排列中,它可以被认为是一种风格化的HBox——也就是说,它以背景渐变的方式水平呈现添加到它上面的任何节点。添加到ToolBar中最常见的元素是其他 UI 控件,如ButtonToggleButtonSeparator,但是对于在ToolBar中可以放置什么没有限制,只要它是Node

ToolBar控件确实提供了一项有用的功能——它支持溢出的概念,因此,如果要显示的元素多于显示所有元素的空间,它会从ToolBar中删除“溢出”的元素,并显示一个溢出按钮,单击该按钮会弹出一个包含所有ToolBar溢出元素的菜单。

如表 4-16 所述,ToolBar提供了垂直方向,因此它可以放置在应用程序用户界面的左侧或右侧,尽管这不如放置在用户界面顶部常见,通常就在菜单栏下方。

创建工具栏的例子如清单 4-13 所示。

表 4-16

ToolBar 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | orientation | ObjectProperty<Orientation> | ToolBar应该是水平还是垂直。 |

ToolBar toolBar = new ToolBar();
toolBar.getItems().addAll(
    new Button("New"),
    new Button("Open"),
    new Button("Save"),
    new Separator(),
    new Button("Clean"),
    new Button("Compile"),
    new Button("Run"),
    new Separator(),
    new Button("Debug"),
    new Button("Profile")
);

Listing 4-13Instantiating a ToolBar with multiple Button and Separator instances

其他控制

html 编辑器

HTMLEditor控件使用户能够创建内部格式化为 HTML 内容的富文本输入。控件提供了许多用户界面控件来指定字体大小、颜色和类型,以及对齐方式等。

关于HTMLEditor控件需要注意的一点是,因为它依赖于 JavaFX WebView组件来呈现用户输入,所以这个控件不包含在javafx.controls模块中,而是包含在javafx.web模块中,并且可以在javafx.scene.web包中找到它。

尽管向最终用户提供了大量的功能,但是使用HTMLEditor的开发人员所能获得的 API 却少得惊人。没有相关的属性,唯一相关的方法是htmlText的 getter 和 setter 方法。这些方法使用一个String进行操作,并且期望这个String包含有效的 HTML。66

页码

理解分页控件最简单的方法是想象 Google 搜索结果页面,页面底部是“Gooooooooogle”文本和数字“1,2,3,…10”这些数字中的每一个都代表一页结果,用户可以点击它们进入该页。重要的是,Google 不会预先确定要放在当前页面之外的任何页面上的元素——其他页面只有在被请求时才会被确定。

这正是 JavaFX 中分页所提供的功能。分页类的关键属性如表 4-17 所示。分页是一种表示多个页面的抽象方式,其中只有当前显示的页面实际存在于场景图中,所有其他页面都是根据请求生成的。

这是我们在本章中第一次遇到利用 JavaFX UI 控件中的“回调”功能的情况。这是在pageFactory中使用的,允许根据用户的请求按需生成页面。随着本章的深入,我们将会更多地遇到这种方法,所以值得花时间来确保您理解清单 4-14 中发生了什么,尤其是在设置了pageFactory的地方。

表 4-17

分页类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | currentPageIndex | IntegerProperty | 正在显示的当前页面索引。 | | pageCount | IntegerProperty | 可显示的总页数。 | | pageFactory | ObjectProperty<Callback<Integer,Node>> | 回调函数,返回对应于给定索引的页面。 |

Pagination pagination = new Pagination(10, 0);
pagination.setPageFactory(pageIndex -> {
    VBox box = new VBox(5);
    for (int i = 0; i < 10; i++) {
        int linkNumber = pageIndex * 10 + i;
        Hyperlink link = new Hyperlink("Hyperlink #" + linkNumber);
        link.setOnAction(e -> log("Hyperlink #" + linkNumber + " clicked!"));
        box.getChildren().add(link);
    }
    return box;
});

Listing 4-14Instantiating a Pagination control with ten pages

滚动条

ScrollBar控件本质上是一个样式不同的Slider控件。它包括一个可以移动拇指的轨道,以及两端用于递增和递减值(从而移动拇指)的按钮。ScrollBar通常不在与Slider相同的环境中使用——相反,它通常被用作更复杂的 UI 控件的一部分。例如,它用在ScrollPane控件中以支持垂直和水平滚动,并且用在稍后讨论的ListViewTableViewTreeViewTreeTableView控件中。

表 4-18 介绍了ScrollBar控件最重要的属性。

表 4-18

ScrollBar 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | blockIncrement | DoubleProperty | 点按轨道时拇指移动的程度。 | | max | DoubleProperty | 最大允许值。 | | min | DoubleProperty | 允许的最小值。 | | orientation | ObjectProperty<Orientation> | ScrollBar是水平还是垂直。 | | unitIncrement | DoubleProperty | 调用 increment/decrement 方法时值的调整量。 | | value | DoubleProperty | ScrollBar的当前值。 |

分离器

Separator控件可能是整个 JavaFX UI 工具包中最简单的控件。它是一个缺乏任何交互性的控件,只是被设计成在用户界面的相关部分画一条线。例如,这通常在ToolBar控件中用于将按钮分组为子组。在弹出菜单中使用类似的方法,但是如前所述,在菜单的情况下,需要使用SeparatorMenuItem,而不是这里讨论的标准Separator控件。

默认情况下,Separator是垂直定向的,这样当放置在水平的ToolBar中时就可以正确绘制。这可以通过修改orientation属性来控制。

纺纱机

在 JavaFX 8u40 中,Spinner控件是最近才引入 JavaFX 的。一个Spinner可以被认为是一个单行的TextField,它可能是可编辑的,也可能是不可编辑的,增加了增量和减量箭头来遍历一些值。表 4-19 介绍了这种控制的最关键属性。

因为一个Spinner可以用于遍历各种类型的值(integerfloatdouble,甚至是某种类型的List),所以Spinner遵从一个SpinnerValueFactory来处理遍历值范围的实际过程(以及精确地如何遍历)。JavaFX 附带了许多内置的SpinnerValueFactory类型(用于doublesintegersLists),并且可以根据定制需求编写定制的SpinnerValueFactory实例。清单 4-15 中的代码示例演示了整数值工厂,双精度和列表值工厂以相同的方式运行。

表 4-19

Spinner 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | editable | BooleanProperty | 用户是否能够键入文本输入。 | | editor | ReadOnlyObjectProperty<TextField> | Spinner所使用的编辑器控件。 | | promptText | StringProperty | 没有用户输入时显示的提示文本。 | | valueFactory | ObjectProperty<SpinnerValueFactory<T>> | 如前文所述。 | | value | ReadOnlyObjectProperty<T> | 用户选择的值。 |

Spinner<Integer> spinner = new Spinner<>();
spinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(5, 10));

spinner.valueProperty().addListener((o, oldValue, newValue) -> {
        log("value changed: '" + oldValue + "' -> '" + newValue + "'");
});

Listing 4-15Creating a Spinner with an integer value factory

工具提示

工具提示是常见的 UI 元素,通常用于当鼠标悬停在Node上时,在场景图中显示关于Node的附加信息。任何Node都可以显示工具提示。在大多数情况下,会创建一个Tooltip,并修改它的text属性以向用户显示纯文本。然而,Tooltip能够在其中显示任意的节点场景图——这是通过创建场景图并在 tooltip graphic属性中设置它来实现的。

您可以使用清单 4-16 中所示的方法在任何节点上设置工具提示。

Rectangle rect = new Rectangle(0, 0, 100, 100);
Tooltip t = new Tooltip("A Square");
Tooltip.install(rect, t);

Listing 4-16Adding a tooltip to any Node in the JavaFX scene graph

然后,该工具提示将参与典型的工具提示语义(即,在悬停时出现等等)。注意,Tooltip不需要卸载:当它没有被任何Node引用时,它将被垃圾收集。然而,也可以用同样的方式手动卸载工具提示。

单个工具提示可以安装在多个目标节点或多个控件上。

因为大多数工具提示都显示在 UI 控件上,所以所有控件都有特殊的 API 来减少安装Tooltip的麻烦。清单 4-17 中的例子展示了如何为Button控件创建工具提示。

Button button = new Button("Hover Over Me");
button.setTooltip(new Tooltip("Tooltip for Button"));

Listing 4-17Adding a tooltip to a UI control using convenience API

工具提示类的关键属性如表 4-20 所示。

表 4-20

工具提示类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | graphic | ObjectProperty<Node> | 要在工具提示弹出窗口中显示的图标或任意复杂的场景图形。 | | text | StringProperty | 要在工具提示弹出窗口中显示的文本。 | | wrapText | BooleanProperty | 当文本超出工具提示宽度时是否换行。 |

弹出控件

JavaFX 附带了一套全面的“弹出”控件。这意味着,在幕后,它们被放置在它们自己的窗口中,该窗口独立于用户界面的主阶段,因此,它们可能出现在窗口之外,无论它们比窗口本身更高还是更宽。开发人员不需要关心位置、大小或与此相关的任何细节,但是理解这些细节是很有用的。

本节涵盖了 JavaFX 中使用该弹出功能的所有 UI 控件。对于它们中的许多来说,它们也利用相同的 API 来构建菜单,所以在依次讨论每个控件之前,我们将首先讨论这个公共功能。

基于菜单的控件

菜单和菜单项

在 JavaFX 中构建菜单从MenuMenuItem类开始。值得注意的是,这两个类都没有真正扩展Control,这是因为它们被设计用来表示菜单结构,但是实现是由 JavaFX 在幕后处理的。

MenuItem的行为方式与Button基本相同。它支持一组相似的属性—textgraphiconAction。除此之外,它还增加了对指定键盘快捷键的支持(例如,Ctrl+C)。这些详见表 4-21 。

因为MenuItem只是从Object扩展而来,它本身是没有用的,不能以标准方式添加到 JavaFX 用户界面中。使用MenuItem的方式是通过Menu类,它充当MenuItem实例的容器。Menu类有一个getItems()方法,它以大多数其他 JavaFX APIs 的标准方式工作——开发人员将MenuItem实例添加到getItems()方法中,然后每当Menu向用户显示时,这些项目就会显示在其中。

这引出了几个重要的问题:

  1. **Java FX 如何支持嵌套菜单(即一个菜单包含一个子菜单,子菜单本身可能包含更多子菜单)?**这可以简单地通过Menu类本身从MenuItem扩展来处理。这意味着每当 API 允许一个MenuItem,它也隐含地支持Menu

  2. Java FX 如何支持带有复选框或单选状态的菜单项? JavaFX 附带了两个子类——CheckMenuItemRadioMenuItem——支持这一点。CheckMenuItem有一个selected属性,每当用户点击菜单项时,该属性将在 true 和 false 之间切换。RadioMenuItem的功能与RadioButton类似——它应该与一个ToggleGroup相关联,然后 JavaFX 将强制每次最多选择一个RadioMenuItem

  3. **如何将菜单项分组?**在用户界面中处理这种情况的常见方式是使用分隔符。正如本章前面提到的,不可能将一个Separator直接添加到一个Menu中(因为它不是从MenuItem扩展而来的),因此 JavaFX 附带了SeparatorMenuItem,它将一个Separator放置到Menu中的位置,即SeparatorMenuItem在菜单项列表中的位置。

  4. 自定义菜单元素呢?例如,如果我想在菜单中显示滑块或文本字段,该怎么办? JavaFX 通过CustomMenuItem类支持这一点。通过使用这个类,开发人员可以将任意的Node嵌入到content属性中。

表 4-21

MenuItem 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | accelerator | ObjectProperty<KeyCombination> | 访问此菜单项的键盘快捷键。 | | disable | BooleanProperty | 菜单项是否应该是用户交互的。 | | graphic | ObjectProperty<Node> | 显示在菜单项文本左侧的图形。 | | onAction | ObjectProperty<EventHandler<ActionEvent>> | 单击菜单项时要调用的事件处理程序。 | | text | StringProperty | 要在菜单项中显示的文本。 | | visible | BooleanProperty | 菜单项在菜单中是否可见。 |

菜单条

到目前为止,我们已经讨论了指定一个Menu所需的 API,但是没有讨论如何将它显示给用户。到目前为止,将菜单添加到 JavaFX 用户界面的最常见方式是通过MenuBar控件。这个类传统上被放在用户界面的顶部(例如,如果使用了一个BorderLayout,它通常被设置为顶部节点),它的构造简单,只需创建一个实例,然后将Menu实例添加到调用getMenus()返回的列表中。

在某些操作系统上(尤其是 macOS),通常很少会在应用程序窗口的顶部看到菜单栏,因为 macOS 在屏幕的最顶部有一个“系统菜单栏”。该系统菜单栏是特定于应用程序上下文的,因为每当聚焦的应用程序改变时,它就改变其内容。JavaFX 支持这一点,MenuBar类有一个useSystemMenuBar属性,如果设置为 true,将从应用程序窗口中删除MenuBar,转而使用系统菜单栏本地呈现菜单栏。这将在有系统菜单栏(macOS)的平台上自动发生,但对没有系统菜单栏的平台没有影响(在这种情况下,MenuBar将定位在用户界面中,无论它是由应用程序开发人员指定出现的)。

清单 4-18 展示了如何创建一个带有菜单和菜单项的MenuBar

// Firstly we create our menu instances (and populate with menu items)
final Menu fileMenu = new Menu("File");
final Menu helpMenu = new Menu("Help");

// we are creating a Menu here to add as a submenu to the File menu
Menu newMenu = new Menu("Create New...");
newMenu.getItems().addAll(
        makeMenuItem("Project", console),
        makeMenuItem("JavaFX class", console),
        makeMenuItem("FXML file", console)
);
// add menu items to each menu
fileMenu.getItems().addAll(
        newMenu,
        new SeparatorMenuItem(),
        makeMenuItem("Exit", console)
);
helpMenu.getItems().addAll(makeMenuItem("Help", console));
// then we create the MenuBar instance and add in the menus
MenuBar menuBar = new MenuBar();
menuBar.getMenus().addAll(fileMenu, helpMenu);

Listing 4-18Creating a MenuBar with two menus (the first of which has a submenu)

菜单按钮和拆分菜单按钮

JavaFX 应用程序中向用户显示菜单的另一种方式是通过MenuButtonSplitMenuButton类。这些类关系非常密切,但是它们的工作方式稍有不同,所以我们将在下面分别介绍它们。

MenuButton是一个类似按钮的控件,无论何时点击它,都会显示一个包含所有添加到项目列表中的MenuItem元素的菜单。因为MenuButton类扩展自ButtonBase(?? 本身扩展自Labeled),所以与 JavaFX Button控件有大量的 API 重叠。比如MenuButton有相同的onAction事件,还有textgraphic属性等等。但是请注意,对于MenuButton,设置onAction没有任何效果,因为MenuButton不会触发onAction事件,因为这是用来显示弹出窗口的。表 4-22 概述了 MenuItem 引入的属性,清单 4-19 演示了如何在代码中使用 MenuButton。

SplitMenuButton扩展了MenuButton类,但与MenuButton不同的是,SplitMenuButton的视觉效果将按钮本身分成两部分——一个“动作”区域和一个“菜单打开”区域。当用户点击“动作”区域时,SplitMenuButton实质上就像是一个Button一样——执行与onAction属性相关的任何代码。当用户点击“菜单打开”区域时,弹出菜单被显示,用户可以像往常一样与菜单交互。清单 4-20 演示了如何在代码中使用 SplitMenuButton。

表 4-22

MenuButton 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | popupSide | ObjectProperty<Side> | 上下文菜单应该显示在相对于按钮的一侧。 |

SplitMenuButton splitMenuButton = new SplitMenuButton();
// this is the text in the 'action' area
splitMenuButton.setText("Perform action!");
// these are the menu items to display in the popup menu
splitMenuButton.getItems().addAll(
        makeMenuItem("Burgers", console),
        makeMenuItem("Pizza", console),
        makeMenuItem("Hot Dog", console));
// splitMenuButton does fire an onAction event,
// when the 'action' area is pressed
splitMenuButton.setOnAction(e -> log("SplitMenuButton onAction event"));

Listing 4-20An example of using SplitMenuButton

MenuButton menuButton = new MenuButton("Choose a meal...");
menuButton.getItems().addAll(
        makeMenuItem("Burgers", console),
        makeMenuItem("Pizza", console),
        makeMenuItem("Hot Dog", console));
// because the MenuButton does not have an 'action' area,
// onAction does nothing
menuButton.setOnAction(e -> log("MenuButton onAction event"));

Listing 4-19An example of using MenuButton

上下文菜单

是一个包含菜单项的弹出控件。这意味着它永远不会被添加到场景图中,而是被直接调用(通过两个show()方法)或作为用户使用普通鼠标或键盘操作请求显示上下文菜单的结果(最常见的是通过按鼠标右键)。

为了尽可能容易地指定和显示上下文菜单,根Control类有一个contextMenu属性。当某些事件发生时(例如,按下鼠标右键),UI 控件被配置为检查是否指定了上下文菜单,如果是,则自动显示它。例如,Tab类就有一个这样的contextMenu属性,每当用户右键单击TabPane内的Tab,就会出现开发者指定的上下文菜单。清单 4-21 显示了一个设置了上下文菜单的按钮,每当鼠标右键被按下时就会显示出来。

// create a standard JavaFX Button
Button button = new Button("Right-click Me!");
button.setOnAction(event -> log("Button was clicked"));
// create a ContextMenu
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().addAll(
        makeMenuItem("Hello", console),
        makeMenuItem("World!", console),
        new SeparatorMenuItem(),
        makeMenuItem("Goodbye Again!", console)
);

Listing 4-21Specifying a ContextMenu and adding it to a Button instance

在某些情况下,我们希望在一个没有从Control扩展的类上显示一个ContextMenu。在这些情况下,我们可以简单地利用ContextMenu上的两个show()方法之一,在相关事件发生时显示它。有两种show()方法可用:

  1. show(Node anchor, double screenX, double screenY):该方法将在指定的屏幕坐标显示上下文菜单。

  2. show(Node anchor, Side side, double dx, double dy):该方法将在指定锚节点的指定侧(顶部、右侧、底部或左侧)显示上下文菜单,x 轴和 y 轴的移动量分别由dxdy指定(还要注意,如果需要的话dxdy可以是负数,但最常见的是这些值可以是零)。

通过将这两个 show 方法中的一个与适当的事件处理程序(特别是 onContextMenuRequested 和 onMousePressed API,在所有 JavaFX 节点子类上都可用)结合使用,我们可以获得想要的结果。清单 4-22 展示了如何在 JavaFX Rectangle 类上显示 ContextMenu(缺少 Control 子类的 setContextMenu API)。

Rectangle rectangle = new Rectangle(50, 50, Color.RED);
rectangle.setOnContextMenuRequested(e -> {
    // show the contextMenu to the right of the rectangle with zero
    // offset in x and y directions
    contextMenu.show(rectangle, Side.RIGHT, 0, 0);
});

Listing 4-22Adding a ContextMenu to a JavaFX Rectangle by manually showing it when requested

选择框

ChoiceBox是一个 JavaFX UI 控件,单击时显示弹出菜单,但它不是通过MenuItem实例构造的。相反,ChoiceBox是一个泛型类(例如,ChoiceBox<T>,其中类的类型也是用于items列表的类型。换句话说,不是让用户指定菜单项,而是用零个或多个 T 类型的对象来构造一个ChoiceBox,这些是在弹出菜单中显示给用户的内容。

因为 T 类的默认toString()方法可能不合适或者过于易读,ChoiceBox支持converter属性的概念(属于StringConverter<T>类型)。如果指定了一个converter,那么ChoiceBox将从items列表(类型为 T)中获取每个元素,并通过converter传递,这将返回一个更易于阅读的字符串,显示在弹出菜单中。

当用户在ChoiceBox中做出选择时,value属性将被更新以反映这个新的选择。当value属性改变时,ChoiceBox控件也会触发一个onAction ActionEvent,因此开发人员可以选择是观察value属性还是添加一个onAction事件处理程序。

由于ChoiceBox的 UI 设计,这个控件最适合相对较小的元素列表。如果要显示的元素数量很大,通常建议开发人员使用ComboBox控件。

ChoiceBox 的主要属性在表 4-23 中列出,它们的使用在清单 4-23 中演示。

表 4-23

ChoiceBox 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | converter | ObjectProperty<StringConverter<T>> | 允许一种方法来转换项目列表的可视化表示。 | | items | ObjectProperty<ObservableList<T>> | 要在ChoiceBox中显示的项目。 | | selectionModel | ObjectProperty<SingleSelectionModel<T>> | ChoiceBox的选择模式。 | | showing | ReadOnlyBooleanProperty | 指示ChoiceBox弹出窗口是否可见。 | | value | ObjectProperty<T> | ChoiceBox中的当前选择。 |

ChoiceBox<String> choiceBox = new ChoiceBox<>();
choiceBox.getItems().addAll(
    "Choice 1",
    "Choice 2",
    "Choice 3",
    "Choice 4"
);
choiceBox.getSelectionModel()
        .selectedItemProperty()
        .addListener((o, oldValue, newValue) -> log(newValue));

Listing 4-23Creating a ChoiceBox with four choices and a listener

基于组合框的控件

除了基于菜单的控件之外,还有许多基于用户交互弹出的其他控件。本节介绍了一组控件,它们都可以归类为“组合框”控件在 JavaFX 中,这个集合中的控件都是从ComboBoxBase类(其属性如表 4-24 所示)扩展而来的,称为ComboBoxColorPickerDatePicker

因为所有的ComboBoxBase子类共享一个共同的父类,所以它们的 API 是统一的,并且有许多显著的相似之处:

  • 它们显示为一个按钮,单击该按钮将弹出一些 UI,允许用户进行选择。

  • 有一个value属性代表用户选择的当前值。

  • 通过适当地设置editable属性,它们通常可以是可编辑的或不可编辑的。当控件可编辑时,它会在旁边显示一个TextField和一个按钮——TextField允许用户输入,按钮会显示弹出窗口。当控件不可编辑时,整个控件将显示为一个按钮。

  • show()hide()两种方法可以通过编程使弹出窗口显示出来,或者如果已经显示出来,则隐藏起来。

表 4-24

ComboBoxBase 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | editable | BooleanProperty | 控件是否显示接收用户输入的文本输入区域。 | | onAction | ObjectProperty<EventHandler<ActionEvent>> | 用户设置新值时的事件处理程序。 | | promptText | StringProperty | 要显示的提示文本——是否显示取决于子类别。 | | value | ObjectProperty<T> | 用户最近的选择(或输入,如果可编辑的话)。 |

组合框

ComboBox在概念上与ChoiceBox控件非常相似,但是当需要显示大量元素时,它的功能更全面,性能更高。组合框中添加的属性如表 4-25 所示,并在清单 4-24 中用代码演示。

表 4-25

ComboBox 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | cellFactory | ObjectProperty<Callback<ListView<T>,ListCell<T>>> | 用于自定义项目的呈现。 | | converter | ObjectProperty<StringConverter<T>> | 将用户键入的输入(可编辑时)转换为 T 类型的对象以设置为值。 | | items | ObjectProperty<ObservableList<T>> | 要在弹出窗口中显示的元素。 | | placeholder | ObjectProperty<Node> | 当组合框没有项目时显示什么。 | | selectionModel | ObjectProperty<SingleSelectionModel<T>> | ComboBox的选择型号。 |

ComboBox<String> comboBox = new ComboBox<>();
comboBox.getItems().addAll(
        "Apple",
        "Carrot",
        "Orange",
        "Banana",
        "Mango",
        "Strawberry"
);
comboBox.getSelectionModel()
        .selectedItemProperty()
        .addListener((o, oldValue, newValue) -> log(newValue));

Listing 4-24Creating a ComboBox with multiple choices and a listener

颜色选择器

ColorPicker控件是ComboBox的一种特殊形式,专门用于允许用户选择颜色值。7ColorPicker控件并没有在ComboBoxBase之上增加任何额外的功能,但是用户界面当然是大不相同的。使用颜色选择器与其他控件非常相似,如清单 4-25 所示。

ColorPicker控件提供了一个带有预定义颜色集的调色板。如果用户不想从预定义的颜色集中进行选择,他们可以通过与自定义颜色对话框进行交互来创建自定义颜色。该对话框提供 RGB、HSB 和 web 交互模式,以创建新的颜色。它还允许修改颜色的不透明度。

一旦定义了新的颜色,用户可以选择是保存它还是直接使用它。如果保存了新颜色,该颜色将出现在调色板上的自定义颜色区域。

final ColorPicker colorPicker = new ColorPicker();
colorPicker.setOnAction(e -> {
    Color c = colorPicker.getValue();

System.out.println("New Color RGB = "+c.getRed()+" "+c.getGreen()+" "+c.getBlue());
});

Listing 4-25Creating a ColorPicker and listening for selection changes

日期选择器

就像ColorPicker是选择颜色的ComboBoxBase的专门化一样,DatePicker是选择日期的ComboBoxBase的专门化——在这里是一个java.time.LocalDate值。DatePicker中引入的属性如表 4-26 所示,其在代码中的使用如清单 4-26 所示。

表 4-26

DatePicker 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | chronology | ObjectProperty<Chronology> | 使用哪个日历系统。 | | converter | ObjectProperty<StringConverter<LocalDate>> | 将文本输入转换为本地日期,反之亦然。 | | dayCellFactory | ObjectProperty<Callback<DatePicker,DateCell>> | 单元格工厂自定义弹出窗口中的单个日单元格。 | | showWeekNumbers | BooleanProperty | 弹出窗口是否应该显示周数。 |

final DatePicker datePicker = new DatePicker();
datePicker.setOnAction(e -> {
    LocalDate date = datePicker.getValue();
    System.err.println("Selected date: " + date);
});

Listing 4-26Creating a DatePicker and listening for selection changes

JavaFX 对话框

自 8u40 发布以来,JavaFX 附带了一套全面的对话框 API 来提醒、查询和通知用户。有一个 API 可以在创建自定义对话框的过程中弹出一个信息提示。最简单的情况是,开发人员应该使用Alert类来显示预构建的对话框。想提示用户输入文本或从选项列表中选择的开发者分别使用TextInputDialogChoiceDialog会更好。使用 Dialog 和 DialogPane 类可以创建完全自定义的对话框。

在讨论对话框时,有两个术语是开发人员应该熟悉的。它们是“模态的”和“阻塞的”,当它们之间有明显的区别时,通常可以互换使用。尽管如此,这两个术语很容易定义:

  • 一个模态对话框出现在另一个窗口的顶部,阻止用户点击那个窗口,直到对话框被关闭。

  • 阻塞对话框会导致代码执行在导致对话框出现的那一行停止。这意味着,一旦对话框关闭,将从该行代码继续执行。这可以被认为是一个同步对话。使用阻塞对话框更简单,因为开发人员可以从对话框中检索返回值并继续执行,而不需要依赖侦听器和回调。

在 JavaFX 中,默认情况下所有的对话框都是模态的,但是通过在Dialog上使用initModality(Modality)方法也有可能是非模态的。对于阻塞,这取决于开发者——他们可以选择调用showAndWait()进行阻塞,调用show()进行非阻塞对话。

警报

对于只想向用户显示对话框的开发人员来说,Alert 类是最简单的选项。有许多带有不同图标和默认按钮的预置选项。创建一个警报只是调用指定了所需的AlertType的构造器。AlertType用于配置默认显示哪些按钮和图形。以下是这些选项的简要总结:

  • 确认:最好用于确认用户在执行某个操作之前是确定的。显示一个蓝色问号图像和“取消”和“确定”按钮。

  • 错误:最好用于通知用户出现了错误。显示一个红色“X”图像和一个“确定”按钮。

  • 信息:最好用来通知用户一些有用的信息。显示一个蓝色的“I”图像(代表“信息”)和一个“确定”按钮。

  • None:这将导致没有图像和按钮被设置。除非要提供自定义实现,否则很少使用这种方法。

  • 警告:最好用来警告用户一些事实或悬而未决的问题。显示一个黄色感叹号图像和一个“确定”按钮。

在大多数情况下,开发人员只需从前面文本中概述的选项中选择适当的警告类型,然后提供他们希望向用户显示的文本。一旦创建了警报,它就可以如清单 4-27 所示显示。

alert.showAndWait()
      .filter(response -> response == ButtonType.OK)
      .ifPresent(response -> formatSystem());

Listing 4-27Creating an alert, waiting to see if the user selects the OK button, and, if so, performing an action

选择对话框

是一个向用户显示选项列表的对话框,用户最多可以从中选择一项。换句话说,这个对话框将使用一个控件,比如一个ChoiceBoxComboBox(这是一个实现细节;开发者不能指定他们的偏好)以使用户能够做出选择。这个选择随后将被返回给开发人员,以采取适当的行动,如清单 4-28 所示。

ChoiceDialog<String> dialog = new ChoiceDialog<>("Cat", "Dog", "Cat", "Mouse");
dialog.showAndWait()
    .ifPresent(result -> log("Result is " + result));

Listing 4-28Creating a ChoiceDialog with default choice of “Cat” and three choices. Dialog is modal and blocking, and if the user clicks the “OK” button, output is printed to console

ChoiceDialog 类的关键属性如表 4-27 所示。

表 4-27

ChoiceDialog 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | selectedItem | ReadOnlyObjectProperty<T> | 用户在对话框中选择的项目。 |

TextInputDialog

TextInputDialog 类似于 ChoiceDialog,只是它不允许用户从弹出列表中进行选择,而是允许用户提供单行文本输入。

表 4-28 中显示了 TextInputDialog 类的关键方法,以及如何在清单 4-29 中创建 TextInputDialog 的示例。

表 4-28

TextInputDialog 类上的方法

|

方法

|

类型

|

描述

| | --- | --- | --- | | getEditor() | TextField | 对话框中显示的是用户的TextField。 |

TextInputDialog dialog = new TextInputDialog ("Please enter your name");
dialog.showAndWait()
    .ifPresent(result -> log("Result is " + result));

Listing 4-29Creating a TextInputDialog. Dialog is modal and blocking, and if the user clicks the “OK” button, their input is printed to console

对话框和对话页面

对话框是 JavaFX 中最灵活的对话框选项,支持对话框的完整配置。这允许创建诸如用户名/密码提示、复杂表单等对话框。

当一个对话框被实例化时,开发者可以指定一个单一的泛型类型 R,它代表了result属性的类型。这很重要,因为这是我们作为开发人员在对话框关闭时将收到的内容。

这可能会引出一个明显的问题:R 型应该是什么?答案是,这取决于用户到底需要什么。例如,在密码提示的情况下,它可能是一个UsernamePassword类的实例。

因为对话框类不知道它正在显示的内容,因此不知道如何将用户输入的值转换成 R 类型的实例,所以开发人员有必要设置resultConverter属性。当 R 型不是VoidButtonType时,这是必需的。如果不注意这一点,开发人员会发现他们的代码中抛出了ClassCastException,因为没有通过结果转换器从ButtonType转换过来。

一旦对话框被实例化,下一步就是配置它。与创建自定义对话框相关的最重要属性如表 4-29 所示。

表 4-29

对话框类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | contentText | StringProperty | 要显示的主要文本。 | | dialogPane | ObjectProperty<DialogPane> | Dialog中的根节点。包含此处显示的大多数其他属性。 | | graphic | ObjectProperty<Node> | 要显示的图形。 | | headerText | StringProperty | 要在标题区域显示的文本(在contentText上方)。 | | result | ObjectProperty<R> | 对话框关闭后返回的值。 | | resultConverter | ObjectProperty<Callback<ButtonType, R>> | 将用户按钮点击转换成结果的 API。 | | title | StringProperty | 向用户显示的对话框标题。 |

在内部,Dialog 将可视区域的所有布局处理委托给嵌入式 DialogPane 实例。事实上,许多属性只是转发到这个对话框。8DialogPane API 提供了很多在对话框级别没有公开的附加功能,开发者可以通过调用 Dialog 实例上的 getDialogPane()来检索当前安装的 Dialog pane。

高级控制

需要涵盖的最后一组控件是“高级”控件:ListViewTreeViewTableViewTreeTableView。这些控件包含最多的 API 和最多的功能。这四个控件有很多共同的概念,所以本节将深入探讨ListView(四个控件中最简单的一个),然后在更高的层次上讨论其他三个控件。

列表视图

一个ListView控件用于向用户显示元素列表。ListView是一个泛型类,所以ListView<T>能够包含 T 类型的项目。与大多数 UI 控件一样,用项目填充ListView非常容易——只需将 T 类型的元素添加到items列表中。元素在项目列表中出现的顺序将与它们在ListView中显示的顺序一致。

因为ListView(与本节中的所有“高级”控件一样)是“虚拟化的”,所以当列表中的元素数量增加时,它不会付出性能损失。这是因为,在幕后,ListView只创建足够的“单元格”来包含ListView可见区域中的元素。例如,如果ListView的高度足以容纳 20 行,那么ListView可以选择创建 22 个单元格,并在用户滚动列表时重用这些单元格。

ListView控件具有selectionModelfocusModel属性,使开发人员能够精确控制用户界面中选择和关注的内容。这些概念将在后面的“选择和焦点模型”部分更深入地讨论。

通常,ListView垂直滚动,但是通过改变orientation属性,它也可以被配置为水平滚动。该属性和其他重要属性如表 4-30 所示。

表 4-30

ListView 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | cellFactory | ObjectProperty<Callback<ListView<T>, ListCell<T>>> | 参见“细胞和细胞工厂”一节。 | | editable | BooleanProperty | ListView 是否支持编辑单元格。 | | focusModel | ObjectProperty<FocusModel<T>> | 请参考“选择和焦点模型”一节。 | | items | ObjectProperty<ObservableList<T>> | 要在列表视图中显示的元素。 | | orientation | ObjectProperty<Orientation> | ListView 是垂直还是水平的。 | | placeholder | ObjectProperty<Node> | 项目列表为空时在 ListView 中显示的文本。 | | selectionModel | ObjectProperty<MultipleSelectionModel<T>> | 请参考“选择和焦点模型”一节。 |

细胞和细胞工厂

在本节的高级控件(ListView、TreeView、TableView 和 TreeTableView)中,它们的 API 的一个共同点是它们都支持“单元工厂”的概念这在概念上类似于我们已经在本章中讨论过的其他工厂,例如Pagination控件中的页面工厂。

单元格工厂的目的是在 UI 控件(例如 ListView)请求时创建单元格,这就引出了一个问题:单元格到底是什么?在 JavaFX 意义上,它是从 javafx.scene.control.Cell 类扩展而来的类。Cell 类被标记,这意味着它公开了本章开始时讨论的所有 API,并且一个单元格被用来呈现 ListView 和其他控件中的单个“行”。单元格还用于 TableView 和 TreeTableView 中的每个单独的“单元格”。

每个单元格都与一个数据项相关联(由Cell item 属性表示)。单元格单独负责呈现该项。根据所使用的单元格类型,项目可以表示为一个字符串,或者使用其他 UI 控件,比如CheckBoxSlider

单元格“堆叠”在类似 ListView 的 UI 控件中,如前所述,单元格工厂用于根据控件的需求生成这些单元格。但是当单元被重用时,它们是如何更新的呢?有一个名为updateItem的关键方法,每当 UI 控件(例如 ListView)要重用一个单元格时就会调用它。当开发人员提供定制的单元格工厂时,他们必须覆盖这个方法,因为它提供了一个钩子,开发人员可以在单元格被更新以包含新内容时,也更新单元格的表示以更好地表示这个新内容。

因为到目前为止,单元格最常见的用例是向用户显示文本,所以这个用例专门针对Cell进行了优化。这是通过从Labeled扩展而来的Cell来完成的。这意味着Cell的子类只需要设置text属性,而不是创建一个单独的Label并在Cell中设置。然而,对于不仅仅需要纯文本的情况,可以将任何Node放在Cell graphic属性中。尽管有这个术语,图形可以是任何一种图形,并且是完全交互式的。例如,一个ListCell可能配置有一个Button作为它的图形。表 4-31 概述了Cell类的一些更关键的属性。

表 4-31

Cell 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | editable | BooleanProperty | Cell实例是否可以进入编辑状态。 | | editing | ReadOnlyBooleanProperty | Cell当前是否处于编辑状态。 | | empty | ReadyOnlyBooleanProperty | Cell是否有项目。 | | item | ObjectProperty<T> | Cell当前表示的对象。 | | selected | ReadOnlyBooleanProperty | 用户是否选择了Cell。 |

还有其他的使用案例。在ListView中支持编辑很容易——当一个单元格进入“编辑”状态时,同样的updateItem方法被调用,在这段代码中,开发人员可以选择检查单元格的编辑状态,如果是这样,开发人员可以选择删除文本并用一个TextField替换它,允许用户直接自定义输入。

当使用 UI 控件(如 ListView)时,开发人员不直接使用 Cell,而是使用特定于控件的子类(在 ListView 中,这将是 ListCell)。对于 TableView 和 TreeTableView 控件,实际上有两种单元格类型——TableRow/TreeTableRow 和 table cell/TreeTableCell——但我们将在本章后面讨论这种区别。尽管有这种额外的复杂性,开发人员可以安慰自己,总的来说,他们必须简单地理解 Cell 类的基础知识,他们将能够以几乎相同的方式为所有 UI 控件创建单元格工厂。清单 4-30 展示了开发者如何创建一个定制的ListCell类。

public class ColorRectCell extends ListCell<String> {
    private final Rectangle rect = new Rectangle(100, 20);
    @Override public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setGraphic(null);
        } else {
            rect.setFill(Color.web(item));
            setGraphic(rect);
        }
    }
}

Listing 4-30Creating a custom ListCell subclass and overriding updateItem

单元格编辑

当自定义单元格可编辑时,我们只需扩展清单 4-31 中所示的updateItem方法,添加检查来查看单元格是否用于表示控件中的当前编辑索引,就可以启用对它的支持。

对于许多常见的情况,已经存在许多预构建的单元工厂,它们支持使用核心 JavaFX APIs 进行编辑,包含在javafx.scene.control.cell包中,在下一节中将更详细地讨论。

在预建的可编辑单元格不存在的情况下,只要用户执行相关的交互操作(通常是在单元格内双击,但也有键盘快捷键),就可以按照清单 4-31 中所示的代码在编辑和非编辑状态之间轻松切换。注意,要启用编辑,不仅单元格必须支持编辑,而且开发人员必须将ListView editable属性设置为true

public class EditableListCell extends ListCell<String> {
    private final TextField textField;
    public EditableListCell() {
        textField = new TextField();
        textField.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == KeyCode.ENTER) {
                commitEdit(textField.getText());
            } else if (e.getCode() == KeyCode.ESCAPE) {
                cancelEdit();
            }
        });
        setGraphic(textField);
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }
    @Override public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        setText(item);
        setContentDisplay(isEditing() ?
            ContentDisplay.GRAPHIC_ONLY : ContentDisplay.TEXT_ONLY);
    }
    @Override public void startEdit() {
        super.startEdit();
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        textField.requestFocus();
    }
    @Override public void commitEdit(String s) {
        super.commitEdit(s);
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }
    @Override public void cancelEdit() {
        super.cancelEdit();
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }
}

Listing 4-31Creating a ListCell that supports editing state

预制细胞工厂

如前所述,JavaFX 附带了许多预构建的单元工厂,使得定制 ListView 和其他内容变得非常容易。如果您需要接受文本输入,有一个 TextFieldListCell 类(用于 ListView)。如果您想在 TableColumn 中显示进度,存在一个 ProgressBarTableCell(用于 TableView)。利用这些预建的单元工厂是显而易见的,因为它们已经在最广泛的配置中进行了开发和测试,并且已经被开发来避免常见的性能问题。表 4-32 总结了所有可用的预制细胞工厂,清单 4-32 演示了如何使用其中一个细胞工厂。

表 4-32

预制细胞工厂

|

类型

|

支持的 UI 控件

| | --- | --- | | CheckBox | ListViewTableViewTreeViewTreeTableView | | ChoiceBox | ListViewTableViewTreeViewTreeTableView | | ComboBox | ListViewTableViewTreeViewTreeTableView | | ProgressBar | TableViewTreeTableView | | TextField | ListViewTableViewTreeViewTreeTableView |

ListView<String> listView = new ListView<>();
listView.setEditable(true);
listView.setCellFactory(param -> new TextFieldListCell<>());

Listing 4-32Using a pre-built cell factory to customize the editing style of a ListView

TreeView

TreeView控件是 JavaFX UI toolkit 中的定位控件,用于向用户显示树状数据结构,例如,用于表示文件系统或公司层次结构。TreeView控件通过在树枝上显示“公开”节点(即箭头)来显示分层结构,允许它们展开和折叠。当树分支展开时,其子节点显示在分支下方,但有一定的缩进量,以清楚地表明子节点属于其父节点。

与 JavaFX ListView控件不同,Java FXListView控件只公开一个项目列表,而TreeView控件只包含一个开发人员必须指定的root属性。root属性属于类型TreeItem<T>(其中 T 对应于TreeView实例本身的类型,因为TreeView也有一个泛型类型)。不出所料,root属性表示了TreeView的根元素,所有后代都是从它派生的。树形视图的主要属性如表 4-33 所示。

表 4-33

TreeView 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | cellFactory | ObjectProperty<Callback<TreeView<T>,TreeCell<T>>> | 用于创建所有单元的单元工厂。 | | editable | BooleanProperty | TreeView是否能够进入编辑状态。 | | editingItem | ReadOnlyObjectProperty<TreeItem<T>> | 当前正在编辑的TreeItem。 | | expandedItemCount | ReadOnlyIntegerProperty | 在TreeView中可见的树节点总数。 | | focusModel | ObjectProperty<FocusModel<TreeItem<T>>> | 请参考“选择和焦点模型”一节。 | | root | ObjectProperty<TreeItem<T>> | TreeView中的根树项目。 | | selectionModel | ObjectProperty<MultipleSelectionModel<TreeItem<T>>> | 请参考“选择和焦点模型”一节。 | | showRoot | BooleanProperty | 无论根是否显示。否则,根的所有子元素都将显示为根元素。 |

TreeItem是一个相对简单的类,其行为方式与前面讨论的MenuItem相似,因为它不是从Control甚至Node扩展而来的类。它纯粹是一个模型类,用来表示树项目的抽象概念(或者是一个有自己子节点的树枝,或者是一个没有子节点的树叶)。TreeItem 的属性如表 4-34 所示。

表 4-34

TreeItem 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | expanded | BooleanProperty | 此 TreeItem 是展开还是折叠。 | | graphic | ObjectProperty<Node> | 在任何文本或其他表示旁边显示的图形。 | | leaf | ReadOnlyBooleanProperty | 此 TreeItem 是叶节点还是有子节点。 | | parent | ReadOnlyObjectProperty<TreeItem<T>> | 此 TreeItem 的父 TreeItem,如果它是根,则为 null。 | | value | ObjectProperty<T> | TreeItem 的值–这是将在 TreeView/TreeTableView 控件的单元格中呈现的内容。 |

表视图

TableView顾名思义,使开发者能够向用户展示表格数据。因此,这个控件可以被认为是一个支持多列数据的ListView,而不是一个ListView中的单个列。随之而来的是大量的附加功能:可以对列进行排序、重新排序、调整大小和嵌套,单个列可以安装自定义的单元工厂,可以设置调整大小策略来控制如何为列分配可用空间,等等。TableView 的主要属性如表 4-35 所示。

表 4-35

TableView 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | columnResizePolicy | ObjectProperty<Callback<ResizeFeatures, Boolean>> | 这在调整列或表的大小时处理重新分配列空间。 | | comparator | ReadOnlyObjectProperty<Comparator<S>> | 基于 sortOrder 列表中的表列的当前比较器。 | | editable | BooleanProperty | TableView是否能够进入编辑状态。 | | editingCell | ReadOnlyObjectProperty<TablePosition<S,?>> | 当前正在编辑的任何单元格的位置。 | | focusModel | ObjectProperty<TableViewFocusModel<S>> | 请参考“选择和焦点模型”一节。 | | items | ObjectProperty<ObservableList<S>> | 要在 TableView 中显示的元素。 | | placeholder | ObjectProperty<Node> | 项目列表为空时在 TableView 中显示的文本。 | | rowFactory | ObjectProperty<Callback<TableView<S>,TableRow<S>>> | rowFactory 负责创建一整行 TableCells(针对所有列)。 | | selectionModel | ObjectProperty<TableViewSelectionModel<S>> | 请参考“选择和焦点模型”一节。 | | sortPolicy | ObjectProperty<Callback<TableView<S>,Boolean>> | 指定应该如何执行排序。 | | tableMenuButtonVisible | BooleanProperty | 指定菜单按钮是否应显示在 TableView 的右上角。 |

TableView有一个通用类型 S,用于指定项目列表中允许的元素的值。这个列表中的每个元素代表了TableView中一整行的支持对象。例如,如果TableView要显示人对象,那么我们将定义一个TableView<Person>并将所有相关的人添加到items列表中。

开发人员有时会对TableView有一个items列表感到惊讶,因为这会引出一个问题:如何将这些项目转换成需要在TableView的每个“单元格”中显示的值(例如,假设我们的TableView有显示一个人的名字、姓氏和电子邮件地址的列)?答案是这是开发人员创建的每个TableColumn实例的责任,因此在这种情况下,我们期望开发人员创建三个TableColumn实例,名字、姓氏和电子邮件地址各一个。

表列和树表列

TableColumn存在于 JavaFX UI 控件中没有从Control扩展的类集合中(我们之前讨论过的例子包括MenuItemMenuTreeItem)。TableColumnTableColumnBase扩展而来,由于TreeTableView有相似(但不完全相同)的 API,因此有必要创建TreeTableColumn。尽管TableViewTreeTableView需要不同的类,但仍然有大量的重叠,这就是为什么大多数 API 都在TableColumnBase上。TableColumnBase 的关键属性如表 4-36 所示。

表 4-36

TableColumnBase 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | comparator | ObjectProperty<Comparator<T>> | 当此列是表sortOrder列表的一部分时使用的比较器。 | | editable | BooleanProperty | 指定此列是否支持编辑。 | | graphic | ObjectProperty<Node> | 要在列标题区域显示的图形。 | | parentColumn | ReadOnlyObjectProperty<TableColumnBase<S,?>> | 请参考“嵌套列”一节。 | | resizable | BooleanProperty | 用户是否可以更改列的宽度。 | | sortable | BooleanProperty | 用户是否可以对列进行排序。 | | sortNode | ObjectProperty<Node> | 当列是排序顺序列表的一部分时显示的“排序箭头”。 | | text | StringProperty | 要在列标题区域中显示的文本。 | | visible | BooleanProperty | 该列是否向用户显示。 | | width | ReadOnlyDoubleProperty | 列的宽度。 |

TableColumn是一个泛型类,有两个泛型类型,S 和 T,其中 S 是与TableView泛型类型相同的类型,T 是将在TableColumn表示的特定列中使用的值的类型。

当创建一个TableColumn实例时,要设置的两个最重要的属性是列text属性(在列标题区域显示什么)和列cellValueFactory属性(用于填充列中的单个单元格)。 9

TableColumn 显然是为与 TableView 一起使用而设计的,但是它不能与 TreeTableView 一起使用,这可能会让你们中的一些人感到惊讶(很快会有更详细的介绍)。这是因为 TableColumn 做了一些 API 假设,将它直接绑定到 TableView API。因此,另一个名为 TreeTableColumn 的类与 TreeTableView 结合使用,可以达到相同的效果。在很大程度上,我们关心的 API 是可互换的,所以 Table 4-37 为 TableColumn 引入了这些 API,但是请放心,TreeTableView API 以几乎相同的形式存在。

表 4-37

TableColumn 和 TreeTableColumn 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | cellFactory | ObjectProperty<Callback<TableColumn<S,T>,TableCell<S,T>>> | 此表列中所有单元格的单元格工厂。 | | cellValueFactory | ObjectProperty<Callback<CellDataFeatures<S,T>,ObservableValue<T>>> | 此表格列中所有单元格的单元格值工厂。 | | sortType | ObjectProperty<SortType> | 指定当此列是排序的一部分时,它应该是升序还是降序。 |

ObservableList<Person> data = ...
TableView<Person> tableView = new TableView<Person>(data);
TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>
("First Name");
firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
    public ObservableValue<String> call(CellDataFeatures<Person, String> p) {

// p.getValue() returns the Person instance for a particular TableView row
        return p.getValue().firstNameProperty();
    }
});
tableView.getColumns().add(firstNameCol);

Listing 4-33Code required to create a TableColumn and specify a cell value factory

清单 4-33 中的方法假设从p.getValue()返回的对象有一个可以简单返回的 JavaFX ObservableValue。这样做的好处是,TableView将在内部创建绑定,以确保如果返回的ObservableValue发生变化,单元格内容将自动刷新。

还有一个更简洁的选择——它利用反射来达到同样的效果,而不需要编写前面的代码。清单 4-34 演示了这一点,其中我们使用了PropertyValueFactory类并传入了我们希望观察的属性的名称(在本例中是“名字”)。在内部,JavaFX 将试图找到一个名为firstNameProperty()的属性方法,如果找到了,就绑定到它。如果没有找到,它将寻找getFirstName()并显示返回值(显然没有绑定)。

TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>
("First Name");
firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName"));

Listing 4-34Example of using PropertyValueFactory

TableColumn必须与 JavaFX 之前创建的类交互,或者通常不希望使用 JavaFX API 获取属性的情况下,可以将返回值包装在ReadOnlyObjectWrapper实例中。参见清单 4-35 中的示例。

firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
    public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
        return new ReadOnlyObjectWrapper(p.getValue().getFirstName());
    }
});

Listing 4-35Wrapping non-property values for use in a JavaFX TableView

对于TreeTableView控件,存在一个类似于 PropertyValueFactory 的类,称为TreeItemPropertyValueFactory。它执行与PropertyValueFactory类相同的功能,但是它被设计成与作为TreeTableView类的数据模型的一部分的TreeItem类一起工作。

嵌套列

JavaFX TableView 和 TreeTableView 控件都内置了对列嵌套的支持。这意味着,例如,您可能有一个“姓名”列,其中包含两个子列,分别代表名字和姓氏。“Name”列在很大程度上是装饰性的——它不涉及提供单元格值工厂或单元格工厂(这是子列的责任),但用户可以使用它来重新排序列位置和调整所有子列的大小。

创建嵌套列很简单,如清单 4-36 所示。

TableColumn firstNameCol = new TableColumn("First Name");
TableColumn lastNameCol = new TableColumn("Last Name");
TableColumn nameCol = new TableColumn("Name");
nameCol.getColumns().addAll(firstNameCol, lastNameCol);

Listing 4-36Creating a “Name” column with two nested child columns

TableView 中的细胞工厂

我们已经在ListView的上下文中介绍了细胞工厂,但是TableView(和TreeTableView)中的细胞工厂稍微有些微妙。这是因为,与ListViewTreeView不同,在TableViewTreeTableView类中,有两个可能放置细胞工厂的地方。

首先,可以在TableViewTreeTableView控件上指定“行工厂”。行工厂负责显示整行信息,因此自定义行工厂必须小心谨慎地适当显示所有列。因此,开发人员很少创建行工厂。

相反,开发人员倾向于在单个TableColumn(或TreeTableColumn,对于TreeTableView的情况)上指定一个定制的细胞工厂。当在TableColumn上设置一个单元工厂时,它的功能与在ListView上设置的单元工厂的功能非常相似——它只专注于表示单个单元(即一列/一行的交叉点),而不是一整行。这很有效,因为在大多数情况下,我们希望以相同的方式显示给定列中的所有单元格,因此通过在TableColumn上指定一个定制的单元格工厂,我们可以很容易地实现这一点。事实上,为TableColumn编写定制单元工厂的方法本质上与为ListView编写定制单元工厂的方法完全相同。

TreeTableView

现在我们已经介绍了TreeViewTableView,剩下的是介绍TreeTableView,从 API 的角度来看,它包含了TreeViewTableView的元素。因此,为了简化讨论并避免重复,关于TreeTableView的这一部分将主要用于详细描述两个控件TreeTableView中的哪一个继承了它的 API。

TreeTableView的高度概括是它使用了与TreeView相同的TreeItem API,因此需要开发者在TreeTableView中设置根节点。这也意味着没有物品清单,比如在ListViewTableView中。类似地,TreeTableView控件使用与TableView控件相同的基于TableColumn的方法,除了不使用特定于TableViewTableColumn类之外,开发人员将需要使用大体相当的TreeTableColumn类。

就向最终用户显示的功能而言,TreeTableView基本上等同于TableView,增加了扩展/折叠分支以及在显示分支子节点时缩进分支子节点的能力。TreeTableView有很多属性,列于表 4-38 。

表 4-38

TreeTableView 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | columnResizePolicy | ObjectProperty<Callback<ResizeFeatures, Boolean>> | 这在调整列或表大小时处理重新分配列空间。 | | comparator | ReadOnlyObjectProperty<Comparator<TreeItem<S>>> | 基于 sortOrder 列表中的表列的当前比较器。 | | editable | BooleanProperty | TreeTableView是否能够进入编辑状态。 | | editingCell | ReadOnlyObjectProperty<TreeTablePosition<S,?>> | 当前正在编辑的任何单元格的位置。 | | expandedItemCount | ReadOnlyIntegerProperty | 在TreeTableView中可见的树节点总数。 | | focusModel | ObjectProperty<TreeTTableViewFocusModel<S>> | 请参考“选择和焦点模型”一节。 | | items | ObjectProperty<ObservableList<S>> | 要在 TableView 中显示的元素。 | | placeholder | ObjectProperty<Node> | 如果项目列表为空,则在 TreeTableView 中显示的文本。 | | root | ObjectProperty<TreeItem<S>> | TreeTableView中的根树项目。 | | rowFactory | ObjectProperty<Callback<TreeTableView<S>,TreeTableRow<S>>> | rowFactory 负责创建一整行的 TreeTableCells(针对所有列)。 | | selectionModel | ObjectProperty<TreeTableViewSelectionModel<S>> | 请参考“选择和焦点模型”一节。 | | sortPolicy | ObjectProperty<Callback<TreeTableView<S>,Boolean>> | 指定应该如何执行排序。 | | tableMenuButtonVisible | BooleanProperty | 指定菜单按钮是否应显示在 TreeTableView 的右上角。 | | treeColumn | ObjectProperty<TreeTableColumn<S,?>> | 哪一列应该在其中绘制显示节点。 |

选择和焦点模型

JavaFX 附带的许多 UI 控件始终公开选择或焦点模型。这种抽象使得开发人员更容易理解所有的 UI 控件,因为它们为常见的场景提供了相同的 API。在许多 API 中,SelectionModel API 被广泛使用,所以我们将首先介绍它。

选择模型

SelectionModel是一个抽象类,用一个通用类型 T 扩展,表示相关 UI 控件中所选项的类型。因为SelectionModel是抽象的,大多数用例通常基于提供的两个子类之一:SingleSelectionModelMultipleSelectionModel。顾名思义,SingleSelectionModel用于一次只能选择一个选项的 UI 控件中(例如在TabPane中,一次只能选择一个Tab),而MultipleSelectionModel支持同时存在多个选项(例如在ListView中可以同时选择多行)。

除了表 4-39 中提到的两个主要属性之外,还有许多执行选择、清除选择和查询给定索引或项目当前是否被选中的方法。

表 4-39

SelectionModel 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | selectedIndex | ReadOnlyIntegerProperty | UI 控件中当前选定的单元格索引。 | | selectedItem | ReadOnlyObjectProperty<T> | UI 控件中当前选定的项。 |

沿着继承层次进一步向下,MultipleSelectionModel引入了额外的 API,允许开发人员一次选择多行,并在状态改变时观察selectedIndicesselectedItems列表。

继承的最后一层是特定于表的选择模型(TableViewSelectionModelTreeTableViewSelectionModel)。这些类添加了 API 来改变行和单元格选择之间的选择模式,当处于单元格选择模式时,可以根据单元格的行/列交叉点来选择单元格。他们还提供了一个selectedCells列表来监听状态变化。

焦点模型

JavaFX 中的焦点概念在应用于 UI 控件时可能有点奇怪,因为这个术语有很多。在 JavaFX 中,更正确地使用焦点与用户通过用户界面“切换”时发生的事情有关——他们在各种 UI 控件和节点之间转移焦点。无论这些元素中的哪一个具有焦点,都可以接受所有其他的键盘输入。

一些 UI 控件已经重载了 focus 这个术语,也意味着更准确地说是“内部焦点”ListViewTreeViewTableViewTreeTableView控件都有焦点模型,允许编程操作和观察这个内部焦点。在这点上,焦点不是在一个Node上,而是在 UI 控件内部的一个元素上,我们不关心Node,而是在那一行中的值(类型 T)(以及那一行的索引位置)。

在许多方面,FocusModel可以被认为与SingleSelectionModel非常相似,因为只能有一个焦点元素,这使得FocusModel的 API 比我们已经讨论过的更常见的MultipleSelectionModel更简单。FocusModel 的两个主要属性如表 4-40 所示。

表 4-40

FocusModel 类的属性

|

财产

|

类型

|

描述

| | --- | --- | --- | | focusedIndex | ReadOnlyIntegerProperty | UI 控件中当前聚焦的单元格索引。 | | focusedItem | ReadOnlyObjectProperty<T> | UI 控件中当前获得焦点的项。 |

摘要

本章系统地介绍了 JavaFX 17 附带的所有 UI 控件。现在,您应该已经有足够的知识来更容易地创建由适当的 UI 控件组成的用户界面。

正如本章开头所提到的,本书的源代码示例中提供了一个配套应用程序,它演示了作为核心 JavaFX 发行版的一部分提供的所有 UI 控件。我们鼓励您执行此应用程序,以便更熟悉每个 UI 控件的操作方式,并更好地了解如何在您自己的开发中使用 UI 控件。

承认

如果没有 JavaFX 社区成员的贡献,为本章开发的配套应用程序就不可能完全实现。因此,我想借此机会感谢以下人士:阿比纳伊·阿加瓦尔、福阿德·阿尔马尔基、阿尔马斯·拜马甘别托夫、弗兰克·德尔博特、西里尔·费希尔和侯赛因·里马兹。谢谢!

此外,这一章是由 Abhinay Agarwal 专家审查。谢谢!

Footnotes 1

有一个例外:HTMLEditor 位于 javafx.web 模块中,因为它依赖于 WebView 组件。这将在本章后面详细讨论。

  2

MVC 代表“模型-视图-控制器”本质上,它是一种将用户界面的三个支柱清晰地分开的方式:API(模型),视觉(视图),以及视图影响模型的方式(控制器)。

  3

本章涵盖了很多 UI 控件,为了节省一些页面,并不是每个 UI 控件都包含所有的属性。在这一章中,我们将挑选出最关键的属性来突出显示,但是我们鼓励您参考相关类的 Javadoc 资料来查看完整的属性集。

  4

顾名思义,ReadOnlyBooleanProperty 是一种可以观察到变化的属性,但不能由用户直接修改。在按钮上装备属性的情况下,它只能通过用户按住鼠标按钮的交互来装备。此时,待命只读布尔属性将被修改以表示新的待命状态。

  5

选择模型将在本章后面讨论。

  6

注意,如果所提供的 HTML 的<body>标签上的contentEditable属性没有设置为true,那么HTMLEditor将变成只读的。您可以通过确保 body 元素包含contentEditable=”true”来确保文本保持可编辑。

  7

更具体地说,ColorPicker 是一个 ComboBoxBase <javafx.scene.paint.color>,因此不允许开发人员为此类指定任何其他通用类型。</javafx.scene.paint.color>

  8

创建自定义 DialogPane 实例超出了本章的范围,因此请考虑参考 Javadocs。

  9

值得注意的是,单元格值工厂负责从给定列的后备行对象中提取原始数据,与单元格工厂有很大不同,单元格工厂负责以人类可呈现的形式显示数据。为了避免混淆,请在阅读本节时辨别,以注意所讨论的是细胞价值工厂还是细胞工厂。