JavaFX17 学习手册(四)
八、样式化节点
在本章中,您将学习:
-
什么是级联样式表
-
样式、皮肤和主题之间的区别
-
JavaFX 中级联样式表样式的命名约定
-
如何向场景添加样式表
-
如何在 JavaFX 应用程序中使用和覆盖默认样式表
-
如何为节点添加内联样式
-
关于不同类型的级联样式表属性
-
关于级联样式表样式选择器
-
如何使用级联样式表选择器在场景图中查找节点
-
如何使用已编译的样式表
本章的例子在com.jdojo.style包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.style to javafx.graphics, javafx.base;
...
什么是级联样式表?
级联样式表(CSS)是一种用于描述 GUI 应用程序中 UI 元素的表示(外观或样式)的语言。CSS 主要是为网页设计 HTML 元素而开发的。它允许将表示与内容和行为分离。在典型的 web 页面中,内容和表示分别使用 HTML 和 CSS 来定义。
JavaFX 允许您使用 CSS 定义 JavaFX 应用程序的外观(或风格)。您可以使用 JavaFX 类库或 FXML 来定义 UI 元素,并使用 CSS 来定义它们的外观。
CSS 提供了编写规则来设置可视属性的语法。一个规则由一个选择器和一组属性-值对组成。选择器是一个字符串,它标识将应用规则的 UI 元素。属性-值对由属性名及其对应的值组成,用冒号(:)分隔。两个属性-值对由分号(;).属性-值对的集合包含在选择器前面的大括号({ })中。CSS 中的规则示例如下:
.button {
-fx-background-color: red;
-fx-text-fill: white;
}
这里,.button是一个选择器,指定规则将应用于所有按钮;-fx-background-color和-fx-text-fill是属性名,它们的值分别被设置为red和white。当应用前面的规则时,所有按钮都将具有红色背景色和白色文本色。
Tip
在 JavaFX 中使用 CSS 类似于在 HTML 中使用 CSS。如果你以前用过 CSS 和 HTML,这一章的信息听起来会很熟悉。理解如何在 JavaFX 中使用 CSS 并不需要以前的 CSS 经验。本章涵盖了使您能够在 JavaFX 中使用 CSS 的所有必要材料。
什么是样式、皮肤和主题?
CSS 规则也被称为样式。CSS 规则的集合被称为样式表。风格、皮肤和主题是三个相关的、高度混淆的概念。
样式提供了一种分离 UI 元素的表现和内容的机制。它们还有助于可视化属性及其值的分组,因此可以由多个 UI 元素共享。JavaFX 允许您使用 JavaFX CSS 创建样式。
皮肤是应用程序特定样式的集合,定义了应用程序的外观。换肤是动态改变应用程序外观(或皮肤)的过程。JavaFX 不提供特定的换肤机制。但是,使用 JavaFX CSS 和 JavaFX API(可用于Scene类和其他与 UI 相关的类),您可以轻松地为 JavaFX 应用程序提供皮肤。
主题是操作系统的视觉特征,反映在所有应用程序的 UI 元素的外观上。例如,更改 Windows 操作系统上的主题会更改所有正在运行的应用程序中 UI 元素的外观。对比皮肤和主题,皮肤是特定于应用程序的,而主题是特定于操作系统的。基于主题的皮肤是很典型的。也就是说,当当前主题改变时,您将改变应用程序的皮肤以匹配主题。JavaFX 不直接支持主题。
一个简单的例子
让我们看一个简单但完整的在 JavaFX 中使用样式表的例子。您将把所有按钮的背景颜色和文本颜色分别设置为红色和白色。清单 8-1 中显示了样式的代码。
.button {
-fx-background-color: red;
-fx-text-fill: white;
}
Listing 8-1The Content of the File buttonstyles.css
将清单 8-1 的内容保存在resources\css目录下的buttonstyles.css文件中。为了从代码内部访问资源文件夹,我们再次使用我们在第七章开始时介绍的ResourceUtil实用程序类。
一个场景包含一个样式表的字符串 URL 的ObservableList。您可以使用Scene类的getStylesheets()方法来获取ObservableList的引用。以下代码片段将buttonstyles.css样式表的 URL 添加到场景中:
Scene scene;
...
scene.getStylesheets().add(
"file://path/to/folder/resources/css/buttonstyles.css");
ResourceUtil类帮助我们构建正确的 URL 路径。
清单 8-2 包含了完整的程序,它显示了三个红色背景和白色文本的按钮。如果您得到以下警告信息,并且没有看到红底白字的按钮,则表明您没有将resources\css目录放在正确的文件夹中;参见ResourceUtil类。
// ButtonStyleTest.java
package com.jdojo.style;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ButtonStyleTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button yesBtn = new Button("Yes");
Button noBtn = new Button("No");
Button cancelBtn = new Button("Cancel");
HBox root = new HBox();
root.getChildren().addAll(yesBtn, noBtn, cancelBtn);
Scene scene = new Scene(root);
// Add a style sheet to the scene
var url = ResourceUtil.getResourceURLStr("css/buttonstyles.css");
scene.getStylesheets().add(url);
stage.setScene(scene);
stage.setTitle("Styling Buttons");
stage.show();
}
}
Listing 8-2Using a Style Sheet to Change the Background and Text Colors for Buttons
WARNING: com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged Resource "resources/css/buttonstyles.css" not found.
JavaFX CSS 中的命名约定
JavaFX 对 CSS 样式类和属性使用稍微不同的命名约定。CSS 样式的类名基于 JavaFX 类的简单名称,表示场景图中的节点。所有的样式类名都是小写的。例如,Button类的样式类名是button。如果 JavaFX 节点的类名由多个单词组成,例如TextField,则在两个单词之间插入一个连字符以获得样式类名。例如,TextField和CheckBox类的样式类分别是text-field和check-box。
Tip
理解 JavaFX 类和 CSS 样式类之间的区别很重要。JavaFX 类是 Java 类,例如javafx.scene.control.Button。CSS 样式类被用作样式表中的选择器,例如清单 8-1 中的button。
JavaFX 样式中的属性名以-fx-开头。例如,普通 CSS 样式中的属性名font-size在 JavaFX CSS 样式中变成了-fx-font-size。JavaFX 使用约定将样式属性名映射到实例变量。它接受一个实例变量;它在两个单词之间插入一个连字符;如果实例变量由多个单词组成,它会将名称转换为小写,并在前面加上前缀-fx-。例如,对于一个名为textAlignment的实例变量,样式属性名应该是-fx-text-alignment。
添加样式表
您可以向 JavaFX 应用程序添加多个样式表。样式表被添加到场景或父对象中。Scene和Parent类维护一个链接到样式表的字符串 URL 的可见列表。使用Scene和Parent类中的getStylesheets()方法来获取可观察列表的引用,并向列表中添加额外的 URL。以下代码将完成此任务:
// Add two style sheets, ss1.css and ss2.css to a scene
Scene scene = ...
scene.getStylesheets().addAll(
"file://.../resources/css/ss1.css",
"file://.../resources/css/ss2.css");
// Add a style sheet, vbox.css, to a VBox (a Parent)
VBox root = new VBox();
root.getStylesheets().add("file://.../vbox.css");
你必须用“…”来代替通过正确的路径,或者再次使用ResourceUtil类。当然,如果可以通过互联网获得样式表,也可以使用http://URL。
默认样式表
在前面的章节中,您开发了带有 UI 元素的 JavaFX 应用程序,而没有使用任何样式表。然而,JavaFX 运行时总是在幕后使用样式表。该样式表被命名为modena.css,它被称为默认样式表或用户代理样式表。JavaFX 应用程序的默认外观是在默认样式表中定义的。
modena.css文件打包在 JavaFX 运行时javafx.controls.jar文件中。如果你想知道如何为特定节点设置样式的细节,你需要看一下modena.css文件。您可以使用以下命令提取该文件:
jar -xf javafx.controls.jar ^
com/sun/javafx/scene/control/skin/modena/modena.css
该命令将modena.css文件放在当前目录下的com\sun\javafx\scene\control\skin\modena目录中。注意,jar命令在JAVA_HOME\bin目录中。
在 JavaFX 8 之前,Caspian 是默认的样式表。里海是在名为com/sun/javafx/scene/control/skin/caspian/caspian.css的文件中的jfxrt.jar文件中定义的。从 JavaFX 8 开始,Modena 是默认的样式表。Application类定义了两个名为STYLESHEET_CASPIAN和STYLESHEET_MODENA的String常量来表示这两个主题。使用Application类的以下静态方法来设置和获取应用程序范围的默认样式表:
-
public static void setUserAgentStylesheet(String url) -
public static String getUserAgentStylesheet()
使用setUserAgentStylesheet(String url)方法设置应用程序范围的默认值。值null将恢复平台默认样式表。以下语句将 Caspian 设置为默认样式表:
Application.setUserAgentStylesheet(Application.STYLESHEET_CASPIAN);
使用getUserAgentStylesheet()方法返回应用程序的当前默认样式表。如果其中一个内置样式表是默认的,它将返回null。
添加内联样式
场景图中节点的 CSS 样式可能来自样式表或内联样式。在上一节中,您学习了如何向Scene和Parent对象添加样式表。在本节中,您将学习如何为节点指定内联样式。
Node类有一个属于StringProperty类型的style属性。style属性保存节点的内联样式。您可以使用setStyle(String inlineStyle)和getStyle()方法来设置和获取一个节点的内联样式。
样式表中的样式和内联样式是有区别的。样式表中的样式由一个选择器和一组属性值对组成,它可能影响场景图中的零个或多个节点。样式表中受样式影响的节点数取决于与样式选择器匹配的节点数。内联样式不包含选择器。它只由一组属性值对组成。内联样式会影响设置它的节点。以下代码片段使用按钮的内联样式,以红色和粗体显示其文本:
Button yesBtn = new Button("Yes");
yesBtn.setStyle("-fx-text-fill: red; -fx-font-weight: bold;");
清单 8-3 显示六个按钮。它使用两个VBox实例来保存三个按钮。它将两个VBox实例放入一个HBox。内嵌样式用于为两个VBox实例设置 4.0px 的蓝色边框。HBox的内嵌样式设置了 10.0 像素的海军蓝边框。产生的屏幕如图 8-1 所示。
图 8-1
一个按钮、两个VBox实例和一个使用内嵌样式的HBox
// InlineStyles.java
package com.jdojo.style;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class InlineStyles extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button yesBtn = new Button("Yes");
Button noBtn = new Button("No");
Button cancelBtn = new Button("Cancel");
// Add an inline style to the Yes button
yesBtn.setStyle(
"-fx-text-fill: red; -fx-font-weight: bold;");
Button openBtn = new Button("Open");
Button saveBtn = new Button("Save");
Button closeBtn = new Button("Close");
VBox vb1 = new VBox();
vb1.setPadding(new Insets(10, 10, 10, 10));
vb1.getChildren().addAll(yesBtn, noBtn, cancelBtn);
VBox vb2 = new VBox();
vb2.setPadding(new Insets(10, 10, 10, 10));
vb2.getChildren().addAll(openBtn, saveBtn, closeBtn);
// Add a border to VBoxes using an inline style
vb1.setStyle(
"-fx-border-width: 4.0; -fx-border-color: blue;");
vb2.setStyle(
"-fx-border-width: 4.0; -fx-border-color: blue;");
HBox root = new HBox();
root.setSpacing(20);
root.setPadding(new Insets(10, 10, 10, 10));
root.getChildren().addAll(vb1, vb2);
// Add a border to the HBox using an inline style
root.setStyle(
"-fx-border-width: 10.0; -fx-border-color: navy;");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Inline Styles");
stage.show();
}
}
Listing 8-3Using Inline Styles
节点样式的优先级
在 JavaFX 应用程序中,节点的可视属性可能来自多个来源,这种情况非常普遍。例如,按钮的字体大小可以由 JavaFX 运行时设置,样式表可以添加到按钮的父级和场景中,可以为按钮设置内联样式,并且可以使用setFont(Font f)方法以编程方式添加。如果按钮的字体大小值可以从多个来源获得,JavaFX 将使用一个规则来决定使用哪个来源的值。
考虑下面的代码片段和清单 8-4 中显示的stylespriorities.css样式表:
.button {
-fx-font-size: 24px;
-fx-font-weight: bold;
}
Listing 8-4The Content of the stylespriorities.css File
Button yesBtn = new Button("Yes");
yesBtn.setStyle("-fx-font-size: 16px");
yesBtn.setFont(new Font(10));
Scene scene = new Scene(yesBtn);
scene.getStylesheets().addAll(
"file://pat/to/resources/css/stylespriorities.css");
...
按钮的字体大小是多少?它会是 JavaFX 运行时设置的默认字体大小,24px,在stylespriorities.css中声明,16px 由 inline 样式设置,还是 10px 由程序使用setFont()方法设置?正确答案是 16px,是内嵌样式设置的。
JavaFX 运行时使用以下优先级规则来设置节点的可视属性。使用具有属性值的较高优先级的源:
-
内嵌样式(最高优先级)
-
父样式表
-
场景样式表
-
使用 JavaFX API 在代码中设置的值
-
用户代理样式表(最低优先级)
添加到父节点的样式表比添加到场景中的样式表具有更高的优先级。这使得开发人员能够为场景图的不同分支定制样式。例如,您可以使用两个样式表来不同地设置按钮的属性:一个用于场景中的按钮,另一个用于任何HBox中的按钮。一个HBox中的按钮将使用其父按钮的样式,而所有其他按钮将使用场景中的样式。
使用 JavaFX API 设置的值,例如setFont()方法,具有第二低的优先级。
Note
使用 Java API 在样式表和代码中设置相同的节点属性是一个常见的错误。在这种情况下,样式表中的样式获胜,开发人员花费无数时间试图找到代码中设置的属性没有生效的原因。
用户代理使用的样式表的优先级最低。什么是用户代理?一般来说,用户代理是一个解释文档并将样式表应用于文档以进行格式化、打印或读取的程序。例如,web 浏览器是将默认格式应用于 HTML 文档的用户代理。在我们的例子中,用户代理是 JavaFX 运行时,它使用modena.css样式表为所有 UI 节点提供默认外观。
Tip
节点继承的默认字体大小由系统字体大小决定。并非所有节点都使用字体。字体仅由那些显示文本的节点使用,例如一个Button或一个CheckBox。为了试验默认字体,您可以更改系统字体,并使用这些节点的getFont()方法在代码中检查它。
清单 8-5 展示了从多个来源中选择一种风格的优先规则。它将样式表添加到场景中,如清单 8-4 所示。产生的屏幕如图 8-2 所示。
图 8-2
使用不同来源样式的节点
// StylesPriorities.java
package com.jdojo.style;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class StylesPriorities extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button yesBtn = new Button("Yes");
Button noBtn = new Button("No");
Button cancelBtn = new Button("Cancel");
// Change the font size for the Yes button
// using two methods: inline style and JavaFX API
yesBtn.setStyle("-fx-font-size: 16px");
yesBtn.setFont(new Font(10));
// Change the font size for the No button using the JavaFX API
noBtn.setFont(new Font(8));
HBox root = new HBox();
root.setSpacing(10);
root.getChildren().addAll(yesBtn, noBtn, cancelBtn);
Scene scene = new Scene(root);
// Add a style sheet to the scene
var url = ResourceUtil.getResourceURLStr(
"css/stylespriorities.css");
scene.getStylesheets().addAll(url);
stage.setScene(scene);
stage.setTitle("Styles Priorities");
stage.show();
}
}
Listing 8-5Testing Priorities of Styles for a Node
Yes按钮的字体大小值有四个来源:
-
内嵌样式(16px)
-
添加到场景中的样式表(24px)
-
JavaFX API (10px)
-
用户代理设置的默认字体大小(JavaFX 运行时)
Yes按钮从它的内嵌样式中获得 16px 的字体大小,因为它具有最高的优先级。No按钮的字体大小值有三个来源:
-
添加到场景中的样式表(24px)
-
JavaFX API (10px)
-
用户代理设置的默认字体大小(JavaFX 运行时)
No按钮从添加到场景中的样式表中获取 24px 字体大小,因为这在三个可用的源中具有最高的优先级。
Cancel按钮的字体大小值有两个来源:
-
添加到场景中的样式表(24px)
-
用户代理设置的默认字体大小(JavaFX 运行时)
Cancel按钮从添加到场景中的样式表中获取 24px 字体大小,因为这在两个可用的源中具有最高的优先级。所有按钮的文本都以粗体显示,因为您在样式表中使用了“-fx-font-weight: bold;”样式,并且该属性值不会被任何其他源覆盖。
此时,您可能会想到几个问题:
-
如何让
Cancel按钮使用 JavaFX 运行时设置的默认字体大小? -
如果按钮在
HBox中,如何使用一种字体大小(或任何其他属性),如果按钮在VBox中,如何使用另一种字体大小?
通过对样式表中声明的样式使用适当的选择器,可以实现所有这些和其他一些效果。我将很快讨论 JavaFX CSS 支持的不同类型的选择器。
继承 CSS 属性
JavaFX 为 CSS 属性提供了两种类型的继承:
-
CSS 属性类型的继承
-
CSS 属性值的继承
在第一种类型的继承中,JavaFX 类中声明的所有 CSS 属性都被它的所有子类继承。比如,Node类声明了一个cursor属性,它对应的 CSS 属性是-fx-cursor。因为Node类是所有 JavaFX 节点的超类,所以-fx-cursor CSS 属性可用于所有节点类型。
在第二种类型的继承中,节点的 CSS 属性可以从其父节点继承其值。节点的父节点是场景图中节点的容器,而不是它的 JavaFX 超类。默认情况下,节点的某些属性值是从其父节点继承的,对于某些属性,节点需要明确指定它要从其父节点继承属性值。
如果希望从父节点继承值,可以将inherit指定为节点的 CSS 属性值。如果一个节点默认从它的父节点继承一个 CSS 属性,您不需要做任何事情,也就是说,您甚至不需要将属性值指定为inherit。如果要覆盖继承的值,需要显式指定该值(覆盖父值)。
清单 8-6 展示了一个节点如何继承其父节点的 CSS 属性。它给HBox增加了两个按钮,OK 和 Cancel。下列 CSS 属性是在父按钮和 OK 按钮上设置的。“取消”按钮上没有设置 CSS 属性:
/* Parent Node (HBox)*/
-fx-cursor: hand;
-fx-border-color: blue;
-fx-border-width: 5px;
/* Child Node (OK Button)*/
-fx-border-color: red;
-fx-border-width: inherit;
-fx-cursor CSS 属性在Node类中声明,默认情况下由所有节点继承。HBox覆盖默认值并覆盖到HAND光标上。OK 和 Cancel 按钮都从它们的父按钮HBox继承了-fx-cursor的HAND光标值。当您将鼠标指向由HBox和这些按钮占据的区域时,您的鼠标指针将变为HAND光标。您可以使用 OK 和 Cancel 按钮上的"-fx-cursor: inherit"样式来实现默认的相同功能。
默认情况下,节点不会继承与边框相关的 CSS 属性。HBox将其-fx-border-color设置为蓝色,-fx-border-width设置为 5px。OK 按钮将其-fx-border-color设置为红色,将-fx-border-width设置为inherit。inherit值将使 OK 按钮的-fx-border-width从其父按钮HBox继承,即 5px。图 8-3 显示了添加该编码后的变化。
图 8-3
从其父级继承其边框宽度和光标 CSS 属性的按钮
// CSSInheritance.java
package com.jdojo.style;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class CSSInheritance extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button okBtn = new Button("OK");
Button cancelBtn = new Button("Cancel");
HBox root = new HBox(10); // 10px spacing
root.getChildren().addAll(okBtn, cancelBtn);
// Set styles for the OK button and its parent HBox
root.setStyle(
"-fx-cursor: hand;-fx-border-color: blue;-fx-border-width: 5px;");
okBtn.setStyle(
"-fx-border-color: red;-fx-border-width: inherit;");
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("CSS Inheritance");
stage.show();
}
}
Listing 8-6Inheriting CSS Properties from the Parent Node
Tip
默认情况下,节点从其父节点继承-fx-cursor、-fx-text-alignment和-fx-font CSS 属性。
CSS 属性的类型
Java(以及 JavaFX)中的所有值都有一个类型。样式中设置的 CSS 属性值也有类型。每种类型的值都有不同的语法。JavaFX CSS 支持以下类型:
-
inherit -
boolean -
string -
number, integer -
size -
length -
percentage -
angle -
duration -
point -
color-stop -
uri -
effect -
font -
paint -
color
请注意,CSS 类型与 Java 类型无关。它们只能用于指定 CSS 样式表或内联样式中的值。JavaFX 运行时负责在将这些类型分配给节点之前,将它们解析并转换为适当的 JavaFX 类型。
继承类型
在上一节中,您已经看到了一个使用inherit类型的例子。它用于从父节点继承节点的 CSS 属性值。
布尔类型
您可以将boolean类型值指定为true或false。它们也可以被指定为字符串:"true"或"false"。下面的样式将TextField节点的-fx-display-caret CSS 属性设置为false:
.text-field {
-fx-display-caret: false;
}
字符串类型
字符串值可以用单引号或双引号括起来。如果字符串值用双引号括起来,作为值的一部分的双引号应该被转义,例如\"或\22。类似地,单引号作为包含在单引号中的字符串值的一部分必须被转义,例如\'或\27。下面的样式使用字符串来设置皮肤和字体属性。它用双引号将皮肤属性的字符串值括起来,用单引号将字体属性的字体系列括起来:
.my-control {
-fx-skin: "com.jdojo.MySkin";
-fx-font: normal bold 20px 'serif';
}
Tip
字符串值不能直接包含换行符。要在字符串值中嵌入换行符,请使用转义序列\A或\00000a。
数字和整数类型
数值可以用整数或实数来表示。它们是使用十进制数字格式指定的。以下样式将不透明度设置为 0.60:
.my-style {
-fx-opacity: 0.60;
}
表示大小的 CSS 属性值可以用一个数字后跟一个长度单位来指定。长度的单位可以是px(像素)mm(毫米)cm(厘米)in(英寸)pt(点)pc(十二点活字)em或ex。还可以使用长度的百分比来指定大小,例如,节点的宽度或高度。如果指定了百分比单位,它必须紧跟在数字之后,例如 12px,2em,80%:
.my-style {
-fx-font-size: 12px;
-fx-background-radius: 0.5em;
-fx-border-width: 5%;
}
尺寸类型
尺寸是以长度或百分比为单位的数字;参见前面的数字类型。
长度和百分比类型
长度是一个数加上一个px, mm, cm, in, pt, pc, em, ex。百分比是一个数字加上一个“%”符号。
角度类型
使用数字和单位来指定角度。角度的单位可以是deg(度)rad(弧度)grad(梯度)或turn(转角)。以下样式将-fx-rotate CSS 属性设置为 45 度:
.my-style {
-fx-rotate: 45deg;
}
持续时间类型
持续时间是一个数字加上一个持续时间单位,可以是“s”(秒)、“ms”(毫秒)或“不定”
点式
使用 x 和 y 坐标指定一个点。可以使用由空格分隔的两个数字来指定,例如0 0, 100 0, 90 67,或者以百分比形式指定,例如2% 2%。以下样式指定从点(0,0)到(100,0)的线性渐变颜色:
.my-style {
-fx-background-color: linear-gradient(from 0 0 to 100 0, repeat,
red, blue);
}
色挡型
色标用于在线性或放射状颜色渐变中指定特定距离处的颜色。颜色光圈由颜色和光圈距离组成。颜色和距离由空格分隔。停止距离可以指定为百分比,例如 10%,或者指定为长度,例如 65px。颜色停止的一些例子是white 0%、yellow 50%和yellow 100px。请参阅第七章,了解更多关于如何使用颜色挡块的详细信息。
URI 型
可使用url(<address>)功能指定 URI。相对于 CSS 文件的位置解析相对文件<address>:
.image-view {
-fx-image: url("http://jdojo.com/myimage.png");
}
效果类型
可以分别使用dropshadow()和innershadow() CSS 函数为使用 CSS 样式的节点指定投影和内部阴影效果。他们的签名是
-
dropshadow(<blur-type>, <color>, <radius>, <spread>, <x-offset>, <y-offset>) -
innershadow(<blur-type>, <color>, <radius>, <choke>, <x-offset>, <y-offset>)
<blur-type>值可以是高斯、一次通过框、三次通过框或两次通过框。阴影的颜色在<color>中指定。<radius>值在 0.0 和 127.0 之间指定阴影模糊内核的半径。阴影的扩散/阻塞指定在 0.0 和 1.0 之间。最后两个参数以像素为单位指定 x 和 y 方向上的阴影偏移。以下样式显示了如何指定-fx-effect CSS 属性的值:
.drop-shadow-1 {
-fx-effect: dropshadow(gaussian, gray, 10, 0.6, 10, 10);
}
.drop-shadow-2 {
-fx-effect: dropshadow(one-pass-box, gray, 10, 0.6, 10, 10);
}
.inner-shadow-1 {
-fx-effect: innershadow(gaussian, gray, 10, 0.6, 10, 10);
}
字体类型
字体由四个属性组成:系列、大小、样式和粗细。有两种方法可以指定字体 CSS 属性:
-
使用四个 CSS 属性分别指定字体的四个属性:
-fx-font-family、-fx-font-size、-fx-font-style和-fx-font-weight。 -
使用一个简单的 CSS 属性
-fx-font将所有四个属性指定为一个值。
字体系列是一个字符串值,可以是系统上实际可用的字体系列,例如"Arial"、"Times",或者是通用的系列名称,例如"serif"、"sans-serif"、"monospace"。
字体大小可以用px、em、pt、in、cm等单位指定。如果省略字体大小的单位,则采用 px(像素)。
字体样式可以是normal、italic或oblique。
字体粗细可以指定为normal、bold、bolder、lighter、100、200、300、400、500、600、700、800或900。
以下样式分别设置字体属性:
.my-font-style {
-fx-font-family: "serif";
-fx-font-size: 20px;
-fx-font-style: normal;
-fx-font-weight: bolder;
}
指定字体属性的另一种方法是将字体的所有四个属性合并为一个值,并使用-fx-font CSS 属性。使用-fx-font属性的语法是
-fx-font: <font-style> <font-weight> <font-size> <font-family>;
以下样式使用-fx-font CSS 属性来设置字体属性:
.my-font-style {
-fx-font: italic bolder 20px "serif";
}
颜料和颜色类型
绘画类型值指定一种颜色,例如,矩形的填充颜色或按钮的背景颜色。您可以通过以下方式指定颜色值:
-
使用
linear-gradient()功能 -
使用
radial-gradient()功能 -
使用各种颜色值和颜色函数
关于如何使用linear-gradient()和radial-gradient()函数在字符串格式中指定渐变颜色的完整讨论,请参考第七章。这些函数用于指定颜色渐变。以下样式显示了如何使用这些函数:
.my-style {
-fx-fill: linear-gradient(from 0% 0% to 100% 0%, black 0%, red 100%);
-fx-background-color: radial-gradient(radius 100%, black, red);
}
您可以通过多种方式指定纯色:
-
使用命名的颜色
-
使用查找的颜色
-
使用
rgb()和rgba()功能 -
使用红、绿、蓝(RGB)十六进制表示法
-
使用
hsb()或hsba()功能 -
使用颜色功能:
derive()和ladder()
您可以使用预定义的颜色名称来指定颜色值,例如,red、blue、green或aqua:
.my-style {
-fx-background-color: red;
}
您可以将颜色定义为节点或其任何父节点上的 CSS 属性,稍后,当您想要使用它的值时,可以通过名称来查找它。以下样式定义了一个名为my-color的颜色,并在以后引用它:
.root {
my-color: black;
}
.my-style {
-fx-fill: my-color;
}
您可以使用rgb(red, green, blue)和rgba(red, green, blue, alpha)功能根据 RGB 分量定义颜色:
.my-style-1 {
-fx-fill: rgb(0, 0, 255);
}
.my-style-2 {
-fx-fill: rgba(0, 0, 255, 0.5);
}
您可以指定#rrggbb或#rgb格式的颜色值,其中rr、gg和bb分别是十六进制格式的红色、绿色和蓝色分量的值。请注意,您需要使用两位数字或一位十六进制数字来指定这三个组成部分。不能用一个十六进制数字指定某些组件,而用两个数字指定其他组件:
.my-style-1 {
-fx-fill: #0000ff;
}
.my-style-2 {
-fx-fill: #0bc;
}
您可以使用hsb(hue, saturation, brightness)或hsba(hue, saturation, brightness, alpha)功能指定色调、饱和度和亮度(HSB)颜色分量中的颜色值:
.my-style-1 {
-fx-fill: hsb(200, 70%, 40%);
}
.my-style-2 {
-fx-fill: hsba(200, 70%, 40%, 0.30);
}
您可以使用derive()和ladder()函数计算其他颜色的颜色。JavaFX 默认的 CSS,modena.css,使用了这种技术。它定义了一些基色,并从基色中派生出其他颜色。
derive函数有两个参数:
derive(color, brightness)
derive()功能导出指定颜色的更亮或更暗版本。亮度值的范围从–100%到 100%。–100%的亮度表示全黑,0%表示亮度没有变化,100%表示全白。以下样式将使用暗 20%的红色版本:
.my-style {
-fx-fill: derive(red, -20%);
}
ladder()函数将一种颜色和一个或多个色标作为参数:
ladder(color, color-stop-1, color-stop-2, ...)
将ladder()函数想象成使用色标创建渐变,然后使用指定颜色的亮度返回颜色值。如果指定颜色的亮度为 x%,将返回距离渐变起点 x%距离处的颜色。例如,对于 0%亮度,返回渐变 0.0 端的颜色;对于 40%的亮度,返回渐变 0.4 端的颜色。
考虑以下两种风格:
.root {
my-base-text-color: red;
}
.my-style {
-fx-text-fill: ladder(my-base-text-color, white 29%, black 30%);
}
ladder()功能将根据my-base-text-color的亮度返回颜色white或black。如果其亮度为 29%或更低,则返回white;否则,返回black。您可以在ladder()功能中指定任意数量的颜色停止,根据指定颜色的亮度从各种颜色中进行选择。
您可以使用这种技术动态改变 JavaFX 应用程序的颜色。默认的样式表modena.css定义了一些基色,并使用derive()和ladder()函数来派生不同亮度的其他颜色。您需要在样式表中为root类重新定义基本颜色,以进行应用程序范围的颜色更改。
指定背景颜色
一个节点(一个Region和一个Control)可以有多个背景填充,这是使用三个属性指定的:
-
-fx-background-color -
-fx-background-radius -
-fx-background-insets
-fx-background-color属性是逗号分隔的颜色值列表。列表中颜色的数量决定了将要绘制的矩形的数量。您需要使用另外两个属性为每个矩形指定四个角的半径值和四个边的插入值。颜色值的数量必须与半径值和插入值的数量相匹配。
属性是一个由逗号分隔的四个半径值组成的列表,用于填充矩形。列表中的一组半径值可以只指定一个值,例如 10,或者用空格分隔的四个值,例如 10 5 15 20。按顺序为左上角、右上角、右下角和左下角指定半径值。如果只指定了一个半径值,则所有拐角使用相同的半径值。
属性是一个由逗号分隔的四个插入值组成的列表,用于填充矩形。列表中的一组插入值可以只指定一个值,例如 10,或者用空格分隔的四个值,例如 10 5 15 20。按顺序为顶部、右侧、底部和左侧指定插入值。如果只指定了一个插入值,则所有边都使用相同的插入值。
我们来看一个例子。下面的代码片段创建了一个Pane,它是Region类的子类:
Pane pane = new Pane();
pane.setPrefSize(100, 100);
图 8-4 显示了提供以下三种样式时Pane的外观:
图 8-4
有三种不同背景填充的Pane
.my-style-1 {
-fx-background-color: gray;
-fx-background-insets: 5;
-fx-background-radius: 10;
}
.my-style-2 {
-fx-background-color: gray;
-fx-background-insets: 0;
-fx-background-radius: 0;
}
.my-style-3 {
-fx-background-color: gray;
-fx-background-insets: 5 10 15 20;
-fx-background-radius: 10 0 0 5;
}
这三种样式都使用灰色填充颜色,这意味着只绘制一个矩形。第一种样式在所有四个边上使用 5px 的插入,在所有角上使用 10px 的半径。第二种样式使用 0px 的嵌入和 0px 的半径,这使得填充矩形占据了窗格的整个区域。第三种样式在两侧使用不同的插图:顶部 5px,右侧 10px,底部 15px,左侧 20px。请注意,第三种样式的每一侧都有不同的未填充背景。第三种样式也为四个角的半径设置了不同的值:左上 10px,右上 0px,右下 0px,左下 5px。请注意,如果一个角的半径是 0px,角上的两条边以 90 度相交。
如果将以下样式应用于同一窗格,背景将被填充,如图 8-5 所示:
图 8-5
带有三种不同半径和插入值的背景填充的窗格
.my-style-4 {
-fx-background-color: red, green, blue;
-fx-background-insets: 5 5 5 5, 10 15 10 10, 15 20 15 15;
-fx-background-radius: 5 5 5 5, 0 0 10 10, 0 20 5 10;
}
该样式使用三种颜色,因此将绘制三个背景矩形。背景矩形按照样式中指定的顺序绘制:红色、绿色和蓝色。插入和半径值的指定顺序与颜色的顺序相同。该样式对红色使用相同的插入值和半径值。可以用一个值替换四个相似值的集合;即前面样式中的 5 5 5 5 可以用 5 代替。
指定边框
一个节点(一个Region和一个Control)可以通过 CSS 拥有多个边界。使用五个属性指定边框:
-
-fx-border-color -
-fx-border-width -
-fx-border-radius -
-fx-border-insets -
-fx-border-style
每个属性由逗号分隔的项目列表组成。每个项目可能由一组值组成,这些值由空格分隔。
边框颜色
-fx-border-color属性列表中的项目数量决定了所绘制的边框数量。以下样式将用红色绘制一个边框:
-fx-border-color: red;
下面的样式指定了一组red、green、blue和aqua颜色来分别绘制上、右、下和左侧的边框。请注意,它仍然只产生一个边框,而不是四个边框,四边的颜色不同:
-fx-border-color: red green blue aqua;
以下样式指定了两组边框颜色:
-fx-border-color: red green blue aqua, tan;
第一组由四种颜色组成red green blue aqua,第二组仅由一种颜色组成tan。这将导致两个边界。第一个边框将在四边涂上不同的颜色;第二个边框的四边将使用相同的颜色。
Tip
节点的形状可能不是矩形的。在这种情况下,只有集合中的第一个边框颜色(和其他属性)将用于绘制整个边框。
边框宽度
您可以使用-fx-border-width属性指定边框的宽度。您可以选择为边框的所有四条边指定不同的宽度。按顺序为顶部、右侧、底部和左侧指定不同的边框宽度。如果未指定宽度值的单位,则使用像素。
以下样式指定一个边框,所有边都以 2px 宽度涂为红色:
-fx-border-color: red;
-fx-border-width: 2;
下面的样式指定了三个边框,由在-fx-border-color属性中指定的三组颜色决定。前两个边框使用不同的四边边框宽度。第三个边框在所有边上都使用 3px 的边框宽度:
-fx-border-color: red green blue black, tan, aqua;
-fx-border-width: 2 1 2 2, 2 2 2 1, 3;
边界半径
您可以使用-fx-border-radius属性指定边框四个角的半径值。可以为所有拐角指定相同的半径值。按顺序为左上角、右上角、右下角和左下角指定不同的半径值。如果没有指定半径值的单位,则使用像素。
以下样式在所有四个角上指定一个红色边框,宽度为 2px,半径为 5px:
-fx-border-color: red;
-fx-border-width: 2;
-fx-border-radius: 5;
下面的样式指定了三个边框。前两个边界对四个角使用不同的半径值。第三个边界对所有角使用 0px 的半径值:
-fx-border-color: red green blue black, tan, aqua;
-fx-border-width: 2 1 2 2, 2 2 2 1, 3;
-fx-border-radius: 5 2 0 2, 0 2 0 1, 0;
边框嵌入
您可以使用-fx-border-insets属性指定边框四边的插入值。您可以为所有边指定相同的插入值。按顺序为顶部、右侧、底部和左侧指定不同的插入值。如果未指定插入值的单位,则使用像素。
下面的样式指定一个红色边框,宽度为 2px,半径为 5px,四边的嵌入量为 20px:
-fx-border-color: red;
-fx-border-width: 2;
-fx-border-radius: 5;
-fx-border-insets: 20;
下面的样式指定了三个边框,各边的插入距离分别为 10px、20px 和 30px:
-fx-border-color: red green blue black, tan, aqua;
-fx-border-width: 2 1 2 2, 2 2 2 1, 3;
-fx-border-radius: 5 2 0 2, 0 2 0 1, 0;
-fx-border-insets: 10, 20, 30;
Tip
插图是距将要绘制边框的节点一侧的距离。边界的最终位置还取决于其他属性,例如,-fx-border-width和-fx-border-style。
边框样式
属性定义了一个边框的样式。它的值可能包含如下几个部分:
-fx-border-style: <dash-style> [phase <number>] [<stroke-type>] [line-join <line-join-value>] [line-cap <line-cap-value>]
<dash-style>的值可以是none、solid、dotted、dashed或segments(<number>, <number>...)。<stroke-type>的值可以是centered、inside或outside。<line-join-value>的值可以是miter <number>、bevel或round。<line-cap-value>的值可以是square、butt或round。
最简单的边框样式是只指定<dash-style>的值:
-fx-border-style: solid;
segments()功能用于使用交替的破折号和间隙为图案添加边框:
-fx-border-style: segments(dash-length, gap-length, dash-length, ...);
该函数的第一个参数是破折号的长度;第二个论点是差距的长度;等等。在最后一个论点之后,这个模式从头开始重复。以下样式将使用 10px 破折号、5px 间距、10px 破折号等图案绘制边框:
-fx-border-style: segments(10px, 5px);
您可以向该函数传递任意数量的虚线和间隙线段。该函数希望您传递偶数个值。如果您传递奇数个值,这将导致值连接在一起,使它们的数量为偶数。比如你用了segments(20px, 10px, 5px),就跟你过了segments(20px, 10px, 5px, 20px, 10px, 5px)一样。
只有在使用segments()功能时,phase参数才适用。phase参数后面的数字指定了对应于笔画开始的虚线图案的偏移量。考虑以下样式:
-fx-border-style: segments(20px, 5px) phase 10.0;
它将phase参数指定为 10.0。虚线图案的长度为 25px。第一段将从模式开始处的 10px 开始。也就是说,第一个破折号的长度只有 10px。第二段将是一个 5px 的缺口,后跟一个 20px 的破折号,依此类推。phase的默认值为 0.0。
<stroke-type>有三个有效值:居中、内部和外部。它的值决定了边框相对于插图的绘制位置。假设您有一个 200 像素乘 200 像素的区域。假设您已经指定了上插图为 10px,上边框宽度为 4px。如果<stroke-type>被指定为居中,顶部的边界厚度将占据从区域顶部边界的第 8 个像素到第 12 个像素的区域。对于内部的<stroke-type>,边框粗细将占据从第 10 个像素到第 14 个像素的区域。对于作为外部的<stroke-type>,顶部的边框粗细将占据第六个像素到第十个像素的区域。
您可以使用line-join参数指定如何连接两个边界段。其值可以是miter、bevel或round。如果将line-join的值指定为miter,则需要传递一个斜接限制值。如果指定的斜接限制小于斜接长度,则改用斜角连接。斜接长度是斜接的内点和外点之间的距离。斜接长度是根据边框宽度来测量的。“斜接限制”参数指定两条相交边界线段的外侧边缘可以延伸多远以形成斜接。例如,假设斜接长度为 5,而您将斜接限制指定为 4,则使用斜角连接;但是,如果指定的斜接限制大于 5,则使用斜接联接。以下样式使用 30 的斜接限制:
-fx-border-style: solid line-join miter 30;
line-cap参数的值指定如何绘制边界线段的起点和终点。有效值为square、butt和round。下面的样式指定了一个round的line-cap:
-fx-border-style: solid line-join bevel 30 line-cap round;
我们来看一些例子。图 8-6 显示了 100 像素乘 50 像素的Pane类的四个实例,它们应用了以下样式:
图 8-6
使用边框样式
.my-style-1 {
-fx-border-color: black;
-fx-border-width: 5;
-fx-border-radius: 0;
-fx-border-insets: 0;
-fx-border-style: solid line-join bevel line-cap square;
}
.my-style-2 {
-fx-border-color: red, black;
-fx-border-width: 5, 5;
-fx-border-radius: 0, 0;
-fx-border-insets: 0, 5;
-fx-border-style: solid inside, dotted outside;
}
.my-style-3 {
-fx-border-color: black, black;
-fx-border-width: 1, 1;
-fx-border-radius: 0, 0;
-fx-border-insets: 0, 5;
-fx-border-style: solid centered, solid centered;
}
.my-style-4 {
-fx-border-color: red black red black;
-fx-border-width: 5;
-fx-border-radius: 0;
-fx-border-insets: 0;
-fx-border-style: solid line-join bevel line-cap round;
}
注意,第二种样式通过指定适当的插入和笔画类型(inside和outside)实现了两个边框的重叠,一个是纯红的,一个是点黑的。边框按照指定的顺序绘制。在这种情况下,首先绘制实线边框是很重要的;否则,您将看不到虚线边框。第三个绘制了两个边框,使它看起来像一个双边框类型。
Tip
一个Region也可以有一个背景图像和一个通过 CSS 指定的边框图像。请参考网上提供的 JavaFX CSS 参考指南,了解更多详情。JavaFX 中的节点支持许多其他 CSS 样式。这些节点的样式将在本书的后面讨论。
了解样式选择器
样式表中的每个样式都有一个关联的选择器,它标识场景图中关联的 JavaFX CSS 属性值所应用到的节点。JavaFX CSS 支持几种类型的选择器:类选择器、伪类选择器和 ID 选择器等等。让我们简单地看一下这些选择器类型。
使用类选择器
Node类定义了一个styleClass变量,它是一个ObservableList<String>。它的目的是维护一个节点的 JavaFX 风格类名列表。注意,JavaFX 类名和节点的样式类名是两回事。节点的 JavaFX 类名是一个 Java 类名,例如javafx.scene.layout.VBox,或者简称为VBox,用于创建该类的对象。节点的样式类名是 CSS 样式中使用的字符串名称。
您可以为一个节点分配多个 CSS 类名。下面的代码片段将两个样式类名"hbox"和"myhbox"分配给一个HBox:
HBox hb = new HBox();
hb.getStyleClass().addAll("hbox", "myhbox");
样式类选择器将关联的样式应用于所有节点,这些节点具有与选择器名称相同的样式类名称。样式类选择器以句点开头,后跟样式类名。请注意,节点的样式类名不以句点开头。
清单 8-7 显示了一个样式表的内容。它有两种风格。两种样式都使用样式类选择器,因为它们都以句点开头。第一个样式类选择器是“hbox”,这意味着它将用一个名为hbox的样式类匹配所有节点。第二种样式使用样式类名作为button。将样式表保存在CLASSPATH中名为resources\css\styleclass.css的文件中。
.hbox {
-fx-border-color: blue;
-fx-border-width: 2px;
-fx-border-radius: 5px;
-fx-border-insets: 5px;
-fx-padding: 10px;
-fx-spacing: 5px;
-fx-background-color: lightgray;
-fx-background-insets: 5px;
}
.button {
-fx-text-fill: blue;
}
Listing 8-7A Style Sheet with Two Style Class Selectors Named hbox and button
清单 8-8 有完整的程序来演示样式类选择器hbox和button的使用。产生的屏幕如图 8-7 所示。
图 8-7
使用样式表中的边框、填充、间距和背景色
// StyleClassTest.java
package com.jdojo.style;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class StyleClassTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Label nameLbl = new Label("Name:");
TextField nameTf = new TextField("");
Button closeBtn = new Button("Close");
closeBtn.setOnAction(e -> Platform.exit());
HBox root = new HBox();
root.getChildren().addAll(nameLbl, nameTf, closeBtn);
// Set the styleClass for the HBox to "hbox"
root.getStyleClass().add("hbox");
Scene scene = new Scene(root);
scene.getStylesheets().add(
"resources/css/styleclass.css");
stage.setScene(scene);
stage.setTitle("Using Style Class Selectors");
stage.show();
}
}
Listing 8-8Using Style Class Selectors in Code
注意,您已经将HBox(在代码中命名为 root)的样式类名设置为"hbox",这将使用类选择器hbox将 CSS 属性从样式应用到HBox。Close按钮的文本颜色是蓝色的,因为样式类选择器按钮有第二种样式。您没有将Close按钮的样式类名称设置为“button”。Button类将一个名为"button"的样式类添加到它的所有实例中。这就是Close按钮被button样式类别选择器选中的原因。
JavaFX 中大多数常用的控件都有一个默认的样式类名。如果需要,可以添加更多的样式类名。默认的样式类名是由 JavaFX 类名构造的。JavaFX 类名被转换为小写,并在两个单词中间插入一个连字符。如果 JavaFX 类名只由一个单词组成,那么相应的默认样式类名是通过将其转换成小写字母来创建的。例如,默认的样式类名称是Button的button、Label的label、Hyperlink的hyperlink、TextField的text-field、TextArea的text-area、check-box的CheckBox。
例如,Region、Pane、HBox、VBox等 JavaFX 容器类没有默认的样式类名。如果您想使用样式类选择器来设置它们的样式,您需要向它们添加一个样式类名。这就是为什么您必须在清单 8-8 中使用的HBox中添加一个样式类名来使用样式类选择器。
Tip
JavaFX 中的样式类名区分大小写。
有时,您可能需要知道节点的默认样式类名,以便在样式表中使用它。有三种方法可以确定 JavaFX 节点的默认样式类名:
-
猜测它使用描述的规则从 JavaFX 类名形成默认的样式类名。
-
使用在线 JavaFX CSS 参考指南查找名称。
-
写一小段代码。
下面的代码片段显示了如何打印Button类的默认样式类名。更改 JavaFX 节点类的名称,例如,从Button更改为TextField,以打印其他类型节点的默认样式类名称:
Button btn = new Button();
ObservableList<String> list = btn.getStyleClass();
if (list.isEmpty()) {
System.out.println("No default style class name");
} else {
for(String styleClassName : list) {
System.out.println(styleClassName);
}
}
button
根节点的类选择器
场景的root节点被分配一个名为"root"的样式类。您可以对由其他节点继承的 CSS 属性使用root样式类选择器。root节点是场景图中所有节点的父节点。最好将 CSS 属性存储在root节点中,因为可以从场景图中的任何节点查找它们。
清单 8-9 显示了保存在文件resources\css\rootclass.css中的样式表的内容。带有root类选择器的样式声明了两个属性:-fx-cursor和-my-button-color。所有节点都继承了-fx-cursor属性。如果这个样式表被附加到一个场景,所有的节点都会有一个HAND光标,除非它们覆盖了它。-my-button-color属性是查找属性,在第二种样式中查找,设置按钮的文本颜色。
.root {
-fx-cursor: hand;
-my-button-color: blue;
}
.button {
-fx-text-fill: -my-button-color;
}
Listing 8-9The Content of the Style Sheet with Root As a Style Class Selector
运行清单 8-10 中的程序,看看这些变化的效果。请注意,当您在场景中的任何地方移动鼠标时,除了在名称文本字段上,您会得到一个HAND光标。这是因为TextField类覆盖了-fx-cursor CSS 属性,将其设置为TEXT光标。
// RootClassTest.java
package com.jdojo.style;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class RootClassTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Label nameLbl = new Label("Name:");
TextField nameTf = new TextField("");
Button closeBtn = new Button("Close");
HBox root = new HBox();
root.getChildren().addAll(nameLbl, nameTf, closeBtn);
Scene scene = new Scene(root);
/* The root variable is assigned a default style
class name "root" */
var url =
ResourceUtil.getResourceURLStr("css/rootclass.css");
scene.getStylesheets().add(url);
stage.setScene(scene);
stage.setTitle("Using the root Style Class Selector");
stage.show();
}
}
Listing 8-10Using the Root Style Class Selector
使用 ID 选择器
Node类有一个StringProperty类型的id属性,可以用来为场景图中的每个节点分配一个唯一的id。维护场景图中id的唯一性是开发者的责任。为一个节点设置重复的id不是错误。
不要在代码中直接使用节点的id属性,除非您正在设置它。它主要用于使用 ID 选择器来设计节点的样式。下面的代码片段将Button的id属性设置为"closeBtn":
Button b1 = new Button("Close");
b1.setId("closeBtn");
样式表中的 ID 选择器以井号(#)开头。请注意,为节点设置的 ID 值不包括#符号。清单 8-11 显示了一个样式表的内容,它包含两个样式,一个带有类选择器".button",一个带有 ID 选择器"#closeButton"。将清单 8-11 的内容保存在CLASSPATH中名为resources\css\idselector.css的文件中。图 8-8 显示了程序运行后的结果。
图 8-8
使用类别和 ID 选择器的按钮
.button {
-fx-text-fill: blue;
}
#closeButton {
-fx-text-fill: red;
}
Listing 8-11A Style Sheet That Uses a Class Selector and an ID Selector
清单 8-12 展示了使用清单 8-11 中样式表的程序。该程序创建了三个按钮。它将按钮的 ID 设置为"closeButton"。其他两个按钮没有 ID。当程序运行时,Close按钮的文本是红色的,而另外两个按钮是蓝色的。
// IDSelectorTest.java
package com.jdojo.style;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class IDSelectorTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button openBtn = new Button("Open");
Button saveBtn = new Button("Save");
Button closeBtn = new Button("Close");
closeBtn.setId("closeButton");
HBox root = new HBox();
root.getChildren().addAll(openBtn, saveBtn, closeBtn);
Scene scene = new Scene(root);
var url = ResourceUtil.getResourceURLStr("css/idselector.css");
scene.getStylesheets().add(url);
stage.setScene(scene);
stage.setTitle("Using ID selectors");
stage.show();
}
}
Listing 8-12Using an ID Selector in a Style Sheet
你注意到Close按钮的样式有冲突吗?JavaFX 中的所有按钮都被赋予一个名为button的默认样式类,Close按钮也是如此。Close按钮也有一个与 ID 样式选择器相匹配的 ID。因此,样式表中的两个选择器都匹配Close按钮。在有多个选择器匹配一个节点的情况下,JavaFX 使用选择器的特异性来决定使用哪个选择器。在使用类选择器和 ID 选择器的情况下,ID 选择器具有更高的特异性。这就是 ID 选择器匹配Close按钮,而不是类别选择器的原因。
Tip
CSS 使用复杂的规则来计算选择器的特异性。更多详情请参考 www.w3.org/TR/CSS21/cascade.html#specificity 。
组合 ID 和类选择器
选择器可以使用样式类和 ID 的组合。在这种情况下,选择器匹配具有指定样式类和 ID 的所有节点。考虑以下样式:
#closeButton.button {
-fx-text-fill: red;
}
选择器#closeButton.button匹配所有具有closeButton ID 和button样式类的节点。您也可以颠倒顺序:
.button#closeButton {
-fx-text-fill: red;
}
现在,它匹配所有具有button样式类和closeButton ID 的节点。
通用选择器
星号(*)用作通用选择器,它匹配任何节点。通用选择器的特异性最低。以下样式使用通用选择器将所有节点的文本填充属性设置为蓝色:
* {
-fx-text-fill: blue;
}
当通用选择器没有自己出现时,可以忽略。比如选择器*.button和.button是一样的。
将多个选择器分组
如果相同的 CSS 属性应用于多个选择器,您有两种选择:
-
通过复制属性声明,可以使用多种样式。
-
您可以将所有选择器组合成一种样式,用逗号分隔选择器。
假设您想将button和label类的文本填充颜色设置为蓝色。下面的代码使用两种带有重复属性声明的样式:
.button {
-fx-text-fill: blue;
}
.label {
-fx-text-fill: blue;
}
这两种样式可以合并为一种样式,如下所示:
.button, .label {
-fx-text-fill: blue;
}
后代选择器
后代选择器用于匹配作为场景图中另一个节点的后代的节点。后代选择器由两个或更多由空格分隔的选择器组成。以下样式使用后代选择器:
.hbox .button {
-fx-text-fill: blue;
}
它将选择所有具有button样式类并且是具有hbox样式类的节点的后代的节点。术语后代在这个上下文中表示任何级别的孩子(直系或非直系)。
当您想要对 JavaFX 控件的某些部分进行样式化时,后代选择器就派上了用场。JavaFX 中的许多控件由子节点组成,这些子节点是 JavaFX 节点。在 JavaFX CSS 参考指南中,这些子节点被列为子结构。例如,CheckBox由样式类名为text的LabeledText(不是公共 API 的一部分)和样式类名为box的StackPane组成。box包含另一个样式类名为mark的StackPane。您可以为CheckBox类的子结构使用这些信息来设计子部分的样式。以下样式使用后代选择器将所有CheckBox实例的文本颜色设置为蓝色,并将框设置为虚线边框:
.check-box .text {
-fx-fill: blue;
}
.check-box .box {
-fx-border-color: black;
-fx-border-width: 1px;
-fx-border-style: dotted;
}
子选择器
子选择器匹配子节点。它由两个或多个选择器组成,由大于号(>)分隔。以下样式匹配具有button样式类的所有节点,这些节点是具有hbox样式类的节点的子节点:
.hbox > .button {
-fx-text-fill: blue;
}
Tip
CSS 支持其他类型的选择器,例如,兄弟选择器和属性选择器。JavaFX CSS 还不可靠地支持它们。
基于状态的选择器
基于状态的选择器也被称为伪类选择器。伪类选择器根据节点的当前状态匹配节点,例如,匹配具有焦点的节点或匹配只读的文本输入控件。伪类前面有一个冒号,并附加到现有的选择器中。例如,.button:focused是一个伪类选择器,它匹配一个具有button样式类名的节点,该节点也具有焦点;#openBtn:hover是另一个伪类选择器,当鼠标悬停在节点上时,它匹配 ID 为#openBtn的节点。清单 8-13 展示了具有伪类选择器的样式表的内容。当鼠标悬停在节点上时,它将文本颜色更改为红色。当您将此样式表添加到场景中时,当鼠标悬停在所有按钮上时,它们的文本颜色将变为红色。
.button:hover {
-fx-text-fill: red;
}
Listing 8-13A Style Sheet with a Pseudo-class Selector
JavaFX CSS 不支持 CSS 支持的:first-child和:lang伪类。JavaFX 不支持伪元素,这些元素允许您对节点的内容进行样式化(例如,TextArea中的第一行)。表 8-1 包含 JavaFX CSS 支持的伪类的部分列表。请参考在线 JavaFX CSS 参考指南获取 JavaFX CSS 支持的伪类的完整列表。
表 8-1
JavaFX CSS 支持的一些伪类
|伪类
|
适用于
|
描述
|
| --- | --- | --- |
| disabled | Node | 它适用于节点被禁用的情况。 |
| focused | Node | 当节点获得焦点时适用。 |
| hover | Node | 当鼠标悬停在节点上时应用。 |
| pressed | Node | 当鼠标按钮在节点上单击时应用。 |
| show-mnemonic | Node | 它适用于应该显示助记符的情况。 |
| cancel | Button | 如果事件未被消费,当Button将接收到VK_ESC时,它适用。 |
| default | Button | 如果事件未被消费,当Button将接收到VK_ENTER时,它适用。 |
| empty | Cell | 当Cell为空时适用。 |
| filled | Cell | 当Cell不为空时适用。 |
| selected | Cell, CheckBox | 它适用于选择节点的情况。 |
| determinate | CheckBox | 当CheckBox处于确定状态时适用。 |
| indeterminate | CheckBox | 当CheckBox处于不确定状态时适用。 |
| visited | Hyperlink | 当Hyperlink已被访问时适用。 |
| horizontal | ListView | 它适用于节点水平的情况。 |
| vertical | ListView | 它适用于节点垂直的情况。 |
使用 JavaFX 类名作为选择器
允许使用 JavaFX 类名作为样式中的类型选择器,但不建议这样做。考虑样式表的以下内容:
HBox {
-fx-border-color: blue;
-fx-border-width: 2px;
-fx-border-insets: 10px;
-fx-padding: 10px;
}
Button {
-fx-text-fill: blue;
}
请注意,类型选择器与类选择器的不同之处在于前者不以句点开头。类选择器是没有任何修改的节点的 JavaFX 类名(HBOX和HBox不一样)。如果将包含上述内容的样式表附加到场景中,所有的HBox实例都将有一个边框,所有的Button实例都将有蓝色文本。
不建议使用 JavaFX 类名作为类型选择器,因为当您创建 JavaFX 类的子类时,类名可能会有所不同。如果您依赖于样式表中的类名,新类将不会选择您的样式。
在场景图中查找节点
可以使用选择器在场景图中查找节点。Scene和Node类有一个lookup(String selector)方法,返回用指定的selector找到的第一个节点的引用。如果没有找到节点,则返回null。两个类中的方法工作方式略有不同。Scene类中的方法搜索整个场景图。Node类中的方法搜索调用它的节点及其子节点。Node类还有一个lookupAll(String selector)方法,该方法返回由指定的selector匹配的所有Node的一个Set,包括调用该方法的节点及其子节点。
下面的代码片段显示了如何使用 ID 选择器来使用查找方法。但是,在这些方法中,您并不局限于只使用 ID 选择器。您可以使用 JavaFX 中所有有效的选择器:
Button b1 = new Button("Close");
b1.setId("closeBtn");
VBox root = new VBox();
root.setId("myvbox");
root.getChildren().addAll(b1);
Scene scene = new Scene(root, 200, 300);
...
Node n1 = scene.lookup("#closeBtn"); // n1 is the reference of b1
Node n2 = root.lookup("#closeBtn"); // n2 is the reference of b1
Node n3 = b1.lookup("#closeBtn"); // n3 is the reference of b1
Node n4 = root.lookup("#myvbox"); // n4 is the reference of root
Node n5 = b1.lookup("#myvbox"); // n5 is null
Set<Node> s = root.lookupAll("#closeBtn"); // s contains the reference of b1
摘要
CSS 是一种用来描述 GUI 应用程序中 UI 元素表示的语言。它主要用在网页中,用于设计 HTML 元素的样式,并将表示从内容和行为中分离出来。在典型的 web 页面中,内容和表示分别使用 HTML 和 CSS 来定义。
JavaFX 允许您使用 CSS 定义 JavaFX 应用程序的外观。您可以使用 JavaFX 类库或 FXML 来定义 UI 元素,并使用 CSS 来定义它们的外观。
CSS 规则也称为样式。CSS 规则的集合被称为样式表。皮肤是特定于应用程序的样式的集合,它们定义了应用程序的外观。换肤是动态改变应用程序(或皮肤)外观的过程。JavaFX 不提供特定的换肤机制。主题是操作系统的视觉特征,反映在所有应用程序的 UI 元素的外观中。JavaFX 不直接支持主题。
您可以向 JavaFX 应用程序添加多个样式表。样式表被添加到场景或父对象中。Scene 和 Parent 类维护一个链接到样式表的字符串 URL 的可观察列表。
JavaFX 8 到 17 使用名为 Modena 的默认样式表。在 JavaFX 8 之前,默认的样式表叫做 Caspian。在 JavaFX 8 中,使用 Application 类的静态方法setUserAgentStylesheet(String url),您仍然可以将 Caspian 样式表作为默认样式表。您可以使用 Application 类中定义的常量STYLESHEET_CASPIAN和STYLESHEET_MODENA来引用里海和摩德纳样式表的 URL。
节点的可视属性通常来自多个来源。JavaFX 运行时使用以下优先级规则来设置节点的可视属性:内联样式(最高优先级)、父样式表、场景样式表、使用 JavaFX API 在代码中设置的值以及用户代理样式表(最低优先级)。
JavaFX 为 CSS 属性提供了两种类型的继承:CSS 属性类型和 CSS 属性值。在第一种类型的继承中,JavaFX 类中声明的所有 CSS 属性都被它的所有子类继承。在第二种类型的继承中,节点的 CSS 属性可以从其父节点继承其值。节点的父节点是场景图中节点的容器,而不是它的 JavaFX 超类。
样式表中的每个样式都有一个选择器,用于标识应用该样式的场景图中的节点。JavaFX CSS 支持几种类型的选择器:类选择器,并且大多数选择器的工作方式与它们在 web 浏览器中的工作方式相同。您可以使用场景和节点类的选择器和lookup(String selector)方法在场景图中查找节点。
下一章将讨论如何在 JavaFX 应用程序中处理事件。
九、事件处理
在本章中,您将学习:
-
什么是事件
-
什么是事件源、事件目标和事件类型
-
关于事件处理机制
-
如何使用事件过滤器和事件处理程序处理事件
-
如何处理鼠标事件、按键事件和窗口事件
本章的例子在com.jdojo.event包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.event to javafx.graphics, javafx.base;
...
什么是事件?
通常,术语事件用于描述感兴趣的事件。在 GUI 应用程序中,事件是用户与应用程序交互的发生。单击鼠标和按键盘上的键是 JavaFX 应用程序中的事件示例。
JavaFX 中的事件由javafx.event.Event类或其任何子类的对象表示。JavaFX 中的每个事件都有三个属性:
-
事件源
-
事件目标
-
事件类型
当应用程序中发生事件时,通常通过执行一段代码来执行一些处理。响应事件而执行的代码片段被称为事件处理器或事件过滤器。我将很快澄清这两者之间的区别。现在,把两者都看作一段代码,我将把它们都称为事件处理程序。当您想要处理 UI 元素的事件时,您需要向 UI 元素添加事件处理程序,例如,Window、Scene或Node。当 UI 元素检测到事件时,它会执行您的事件处理程序。
调用事件处理程序的 UI 元素是这些事件处理程序的事件源。当一个事件发生时,它会通过一连串的事件调度程序。事件的源是事件调度程序链中的当前元素。当事件通过事件调度程序链中的一个调度程序传递到另一个调度程序时,事件源会发生变化。
事件目标是事件的目的地。事件目标决定了事件在处理过程中行进的路线。假设鼠标点击发生在一个Circle节点上。在这种情况下,Circle节点是鼠标点击事件的事件目标。
事件类型描述发生的事件的类型。事件类型以分层的方式定义。每个事件类型都有一个名称和一个父类型。
JavaFX 中所有事件共有的三个属性由三个不同类的对象表示。特定事件定义了附加的事件属性;例如,表示鼠标事件的 event 类添加了描述鼠标光标位置和鼠标按钮状态等属性。表 9-1 列出了事件处理中涉及的类和接口。JavaFX 有一个事件交付机制,它定义了事件发生和处理的细节。我将在随后的章节中详细讨论所有这些。
表 9-1
事件处理中涉及的类
|名字
|
类别/接口
|
描述
|
| --- | --- | --- |
| Event | 班级 | 此类的一个实例表示一个事件。存在几个Event类的子类来表示特定类型的事件。 |
| EventTarget | 连接 | 此接口的一个实例代表一个事件目标。 |
| EventType | 班级 | 此类的一个实例代表一个事件类型,例如,按下鼠标、释放鼠标、移动鼠标。 |
| EventHandler | 连接 | 此接口的一个实例表示一个事件处理程序或一个事件过滤器。它的handle()方法在它注册的事件发生时被调用。 |
事件类层次结构
JavaFX 中表示事件的类通过类继承以分层的方式排列。图 9-1 显示了Event类的部分类图。Event类位于类层次结构的顶端,它继承了图中没有显示的java.util.EventObject类。
图 9-1
javafx.event.Event类的分部类层次结构
Event类的子类代表特定类型的事件。有时,Event类的一个子类被用来表示某种普通事件。例如,InputEvent类代表一个通用事件来指示一个用户输入事件,而KeyEvent和MouseEvent类分别代表特定的输入事件,比如来自键盘和鼠标的用户输入。WindowEvent类的对象代表一个窗口的事件,例如窗口的显示和隐藏。ActionEvent的一个对象用来表示几种事件,表示某种类型的动作,例如,触发一个按钮或一个菜单项。如果用户用鼠标点击按钮、按下某些键或在触摸屏上触摸它,可能会触发按钮。
Event类提供了所有事件通用的属性和方法。getSource()方法返回一个Object,它是事件的来源。Event类从EventObject类继承了这个方法。getTarget()方法返回EventTarget接口的一个实例,它是事件的目标。getEventType()方法返回一个EventType类的对象,它指示事件的类型。
Event类包含了consume()和isConsumed()方法。如前所述,在事件调度链中,事件从一个元素传递到另一个元素。在一个Event对象上调用consume()方法表明事件已经被消费,不需要进一步处理。在调用了consume()方法之后,事件不会移动到事件处理链中的下一个元素。如果调用了consume()方法,则isConsumed()方法返回true;否则返回false。
特定的Event子类定义了更多的属性和方法。例如,MouseEvent类定义了getX()和getY()方法,它们返回鼠标光标相对于事件源的 x 和 y 坐标。当我在本章或后续章节中讨论这些方法时,我将在特定于事件的类中解释这些方法的细节。
事件目标
一个事件目标是一个可以响应事件的 UI 元素(不一定只是Node s)。从技术上讲,想要响应事件的 UI 元素必须实现EventTarget接口。也就是说,在 JavaFX 中,实现EventTarget接口使得 UI 元素有资格成为事件目标。
Window、Scene和Node类实现了EventTarget接口。这意味着所有节点,包括窗口和场景,都可以响应事件。一些 UI 元素的类,例如Tab、TreeItem和MenuItem,并不从Node类继承。它们仍然可以响应事件,因为它们实现了EventTarget接口。如果开发自定义 UI 元素,并且希望 UI 元素响应事件,则需要实现此接口。
事件目标的职责是建立一个事件调度器链,也称为事件路径。一个事件调度器是一个EventDispatcher接口的实例。链中的每个调度程序都可以通过处理和使用来影响事件。链中的事件调度程序还可以修改事件属性,用新事件替换该事件,或者链接事件路由。通常,事件目标路由由与容器子层次结构中的所有 UI 元素关联的调度程序组成。假设您将一个Circle节点放在一个HBox中,后者放在一个Scene中。将Scene加到一个Stage上。如果鼠标点击Circle,则Circle成为事件目标。Circle构建了一个事件调度器链,其路线从头到尾依次为Stage、Scene、HBox和Circle。
事件类型
EventType类的一个实例定义了一个事件类型。为什么需要一个单独的类来定义事件类型?每个事件单独的事件类,例如KeyEvent、MouseEvent,不足以定义事件类型吗?不能根据事件类来区分一个事件和另一个事件吗?EventType类用于对事件类中的事件进行进一步分类。例如,MouseEvent类只告诉我们用户使用了鼠标。它没有告诉我们鼠标使用的细节,例如,鼠标是否被按下、释放、拖动或点击。EventType类用于对事件的这些子事件类型进行分类。EventType类是一个泛型类,其类型参数定义如下:
EventType<T extends Event>
事件类型是分层的。它们是按实现而不是按类继承来分层的。每个事件类型都有一个名称和一个父类型。EventType类中的getName()和getSuperType()方法返回事件类型的名称和父类型。常量Event.ANY,与常量EventType.ROOT相同,是 JavaFX 中所有事件的超类型。图 9-2 显示了在一些事件类中预定义的一些事件类型的部分列表。
图 9-2
某些事件类的预定义事件类型的部分列表
注意,图中的箭头并不表示类继承。它们表示依赖关系。例如,InputEvent.ANY事件类型依赖于Event.ANY事件类型,因为后者是前者的超类型。
具有子事件类型的事件类定义了一个ANY事件类型。例如,MouseEvent类定义了一个ANY事件类型,表示任何类型的鼠标事件,例如,鼠标释放、鼠标点击、鼠标移动。MOUSE_PRESSED和MOUSE_RELEASED是MouseEvent类中定义的其他事件类型。事件类中的ANY事件类型是同一事件类中所有其他事件类型的超类型。例如,MouseEvent.ANY事件类型是MOUSE_RELEASED和MOUSE_PRESSED鼠标事件的超类型。
事件处理机制
当事件发生时,作为事件处理的一部分,会执行几个步骤:
-
事件目标选择
-
事件路线构建
-
事件路径遍历
事件目标选择
事件处理的第一步是选择事件目标。回想一下,事件目标是事件的目的节点。基于事件类型选择事件目标。
对于鼠标事件,事件目标是鼠标光标处的节点。鼠标光标处可以有多个节点。例如,您可以在矩形上放置一个圆。鼠标光标处最顶端的节点被选为事件目标。
关键事件的事件目标是具有焦点的节点。节点如何获得焦点取决于节点的类型。例如,TextField可以通过在其中单击鼠标或使用焦点遍历键(如 Windows 格式的 Tab 或 Shift + Tab)来获得焦点。默认情况下,Circles或Rectangles等形状不会获得焦点。如果你想让它们接收按键事件,你可以通过调用Node类的requestFocus()方法给它们焦点。
JavaFX 支持支持触摸的设备上的触摸和手势事件。通过触摸触摸屏产生触摸事件。每个触摸动作都有一个称为触摸点的接触点。可以用多个手指触摸触摸屏,从而产生多个触摸点。触摸点的每种状态,例如按压、释放等,都会产生触摸事件。触摸点的位置决定了触摸事件的目标。例如,如果触摸事件的位置是圆内的点,则该圆成为触摸事件的目标。在触摸点处有多个节点的情况下,选择最上面的节点作为目标。
用户可以使用手势与 JavaFX 应用程序进行交互。通常,触摸屏和跟踪板上的手势由具有触摸动作的多个触摸点组成。手势事件的例子是旋转、滚动、滑动和缩放。旋转手势是通过绕着彼此旋转两个手指来执行的。通过在触摸屏上拖动手指来执行滚动手势。通过在触摸屏上向一个方向拖动一个手指(或多个手指)来执行滑动手势。执行缩放手势以通过将两个手指拖开或拉近来缩放节点。
手势事件的目标是根据手势的类型选择的。对于直接手势,例如在触摸屏上执行的手势,在手势开始时所有触摸点的中心点处的最顶端节点被选择作为事件目标。对于间接手势,例如在跟踪板上执行的手势,鼠标光标处最顶端的节点被选为事件目标。
事件路线构建
事件通过事件调度链中的事件调度程序传播。事件调度链是事件路由。事件的初始和默认路线由事件目标决定。默认事件路由由从阶段开始到事件目标节点的容器子路径组成。
假设你在一个HBox中放置了一个Circle和一个Rectangle,并且HBox是一个Stage的Scene的根节点。当您点击Circle时,Circle成为事件目标。Circle构造默认的事件路径,它是从阶段开始到事件目标(Circle)的路径。
事实上,事件路由由与节点相关联的事件调度程序组成。然而,出于实际和理解的目的,您可以将事件路由视为包含节点的路径。通常,您不直接与事件调度程序打交道。
图 9-3 显示了鼠标点击事件的事件路径。事件路线上的节点以灰色背景填充显示。事件路线上的节点由实线连接。注意,当点击Circle时,作为场景图一部分的Rectangle不是事件路径的一部分。
图 9-3
为事件构造默认的事件路径
一个事件调度链(或事件路线)有一个头和一个尾。在图 9-3 中,Stage和Circle分别是事件调度链的头和尾。随着事件处理的进展,可以修改初始事件路线。通常,但不是必须的,在事件遍历步骤中,事件通过其路由中的所有节点两次,如下一节所述。
事件路径遍历
事件路径遍历包括两个阶段:
-
捕获阶段
-
起泡阶段
一个事件在其路由中经过每个节点两次:一次在捕获阶段,一次在冒泡阶段。您可以为特定的事件类型向节点注册事件过滤器和事件处理程序。在捕获阶段和冒泡阶段,当事件通过节点时,分别执行注册到节点的事件过滤器和事件处理程序。事件过滤器和处理程序作为事件源在当前节点的引用中传递。随着事件从一个节点传播到另一个节点,事件源不断变化。然而,事件目标从事件路径遍历的开始到结束保持不变。
在路由遍历期间,节点可以使用事件过滤器或处理程序中的事件,从而完成事件的处理。消费一个事件只需调用事件对象上的consume()方法。当一个事件被消费时,事件处理被停止,即使路由中的一些节点根本没有被遍历。
事件捕获阶段
在捕获阶段,事件从其事件调度链的头部移动到尾部。图 9-4 显示了在我们的例子中的Circle在捕获阶段鼠标点击事件的移动。图中的向下箭头表示事件传播的方向。当事件通过一个节点时,为该节点注册的事件过滤器被执行。请注意,对于当前节点,事件捕获阶段只执行事件过滤器,而不执行事件处理程序。
图 9-4
事件捕获阶段
在图 9-4 中,Stage、Scene、HBox和Circle的事件过滤器按顺序执行,假设没有事件过滤器消耗事件。
您可以为一个节点注册多个事件过滤器。如果节点使用了它的一个事件过滤器中的事件,那么在事件处理停止之前,它的其他尚未执行的事件过滤器将被执行。假设您在我们的示例中为Scene注册了五个事件过滤器,执行的第一个事件过滤器使用该事件。在这种情况下,Scene的其他四个事件过滤器仍将被执行。对Scene执行第五个事件过滤器后,事件处理将停止,事件不会传播到剩余的节点(HBox和Circle)。
在事件捕获阶段,您可以拦截针对节点子节点的事件(并提供通用响应)。例如,在我们的示例中,您可以将鼠标点击事件的事件过滤器添加到Stage中,以拦截其所有子节点的所有鼠标点击事件。您可以通过在父节点的事件过滤器中使用事件来阻止事件到达其目标。例如,如果您在过滤器中为Stage使用鼠标点击事件,那么该事件将不会到达它的目标,在我们的例子中是Circle。
事件冒泡阶段
在冒泡阶段,事件从其事件调度链的尾部移动到头部。图 9-5 显示了Circle在冒泡阶段鼠标点击事件的行进。
图 9-5
事件冒泡阶段
图 9-5 中的向上箭头表示事件行进的方向。当事件通过一个节点时,执行该节点的注册事件处理程序。注意,事件冒泡阶段执行当前节点的事件处理程序,而事件捕获阶段执行事件过滤器。
在我们的例子中,Circle、HBox、Scene和Stage的事件处理程序按顺序执行,假设没有事件过滤器消耗事件。请注意,事件冒泡阶段从事件的目标开始,向上行进到父子层次结构中的最高父级。
您可以为一个节点注册多个事件处理程序。如果节点使用了它的一个事件处理程序中的事件,那么在事件处理停止之前,它的其他尚未执行的事件处理程序将被执行。假设在我们的例子中,您已经为Circle注册了五个事件处理程序,执行的第一个事件处理程序使用该事件。在这种情况下,Circle的其他四个事件处理程序仍然会被执行。在执行了Circle的第五个事件处理程序后,事件处理将停止,事件不会传播到剩余的节点(HBox、Scene和Stage)。
通常,事件处理程序注册到目标节点,以提供对事件的特定响应。有时,事件处理程序安装在父节点上,为其所有子节点提供默认事件响应。如果事件目标决定为事件提供特定的响应,它可以通过添加事件处理程序和使用事件来实现,从而阻止事件在事件冒泡阶段到达父节点。
让我们看一个微不足道的例子。假设您想在用户单击窗口中的任意位置时向用户显示一个消息框。您可以向窗口注册一个事件处理程序来显示消息框。当用户在窗口的圆圈内单击时,您希望显示特定的消息。您可以向 circle 注册一个事件处理程序,以提供特定的消息并使用该事件。这将在单击圆圈时提供特定的事件响应,而对于其他节点,窗口提供默认的事件响应。
处理事件
处理事件意味着执行应用程序逻辑以响应事件的发生。应用程序逻辑包含在事件过滤器和处理程序中,它们是EventHandler接口的对象,如以下代码所示:
public interface EventHandler<T extends Event> extends EventListener
void handle(T event);
}
EventHandler类是javafx.event包中的通用类。它扩展了java.util包中的EventListener标记接口。handle()方法接收事件对象的引用,例如KeyEvent和MouseEvent的引用等等。
事件过滤器和处理程序都是同一个EventHandler接口的对象。仅仅看着一个EventHandler对象是一个事件过滤器还是一个事件处理器,你是无法分辨的。事实上,您可以将同一个EventHandler对象同时注册为事件过滤器和处理程序。这两者之间的区别是在它们注册到节点时确定的。节点提供不同的方法来注册它们。在内部,节点知道一个EventHandler对象是注册为事件过滤器还是处理程序。它们之间的另一个区别是基于调用它们的事件遍历阶段。在事件捕获阶段,注册过滤器的handle()方法被调用,而注册处理程序的handle()方法在事件冒泡阶段被调用。
Tip
本质上,处理事件意味着为EventHandler对象编写应用程序逻辑,并将它们注册到节点,作为事件过滤器、处理程序或两者。
创建事件过滤器和处理程序
创建事件过滤器和处理程序就像创建实现EventHandler接口的类的对象一样简单。使用 lambda 表达式是创建事件过滤器和处理程序的最佳选择,如以下代码所示:
EventHandler<MouseEvent> aHandler = e -> /* Event handling code goes here */;
我在本书中使用 lambda 表达式来创建事件过滤器和处理程序。如果您不熟悉 lambda 表达式,我建议您至少学习一些基础知识,以便能够理解事件处理代码。
下面的代码片段创建了一个MouseEvent处理程序。它打印发生的鼠标事件的类型:
EventHandler<MouseEvent> mouseEventHandler =
e -> System.out.println("Mouse event type: " + e.getEventType());
注册事件过滤器和处理程序
如果您希望某个节点处理特定类型的事件,您需要向该节点注册这些事件类型的事件过滤器和处理程序。当事件发生时,节点的已注册事件过滤器和处理程序的handle()方法按照前面章节中讨论的规则被调用。如果节点不再对处理事件感兴趣,您需要从节点中注销事件过滤器和处理程序。注册和取消注册事件筛选器和处理程序也分别称为添加和删除事件筛选器和处理程序。
JavaFX 提供了两种向节点注册和取消注册事件过滤器和处理程序的方法:
-
使用
addEventFilter()、addEventHandler()、removeEventFilter()和removeEventHandler()方法 -
使用
onXXX便利属性
使用 addXXX( ) 和 removeXXX( ) 方法
您可以使用addEventFilter()和addEventHandler()方法分别向节点注册事件过滤器和处理程序。这些方法在Node类、Scene类和Window类中定义。一些类(例如MenuItem和TreeItem)可以是事件目标;然而,它们不是从Node类继承的。这些类只为事件处理程序注册提供了addEventHandler()方法,例如
-
<T extends Event> void addEventFilter(EventType<T> eventType, EventHandler<? super T> eventFilter) -
<T extends Event> void addEventHandler(EventType<T> eventType, EventHandler<? super T> eventHandler)
这些方法有两个参数。第一个参数是事件类型,第二个是EventHandler接口的一个对象。
您可以使用下面的代码片段来处理Circle的鼠标点击事件:
import javafx.scene.shape.Circle;
import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
...
Circle circle = new Circle (100, 100, 50);
// Create a MouseEvent filter
EventHandler<MouseEvent> mouseEventFilter =
e -> System.out.println("Mouse event filter has been called.");
// Create a MouseEvent handler
EventHandler<MouseEvent> mouseEventHandler =
e -> System.out.println("Mouse event handler has been called.");
// Register the MouseEvent filter and handler to the Circle
// for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED, mouseEventFilter);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseEventHandler);
这段代码创建两个EventHandler对象,在控制台上打印一条消息。在这个阶段,它们不是事件过滤器或处理程序。他们只是两个EventHandler物体。请注意,给引用变量命名并打印使用单词 filter 和 handler 的消息,不会对它们作为过滤器和处理程序的状态产生任何影响。最后两条语句将一个EventHandler对象注册为事件过滤器,将另一个注册为事件处理程序;两者都注册了鼠标单击事件。
允许将同一个EventHandler对象注册为事件过滤器和处理程序。下面的代码片段使用一个EventHandler对象作为Circle的过滤器和处理程序来处理鼠标点击事件:
// Create a MouseEvent EventHandler object
EventHandler<MouseEvent> handler = e ->
System.out.println("Mouse event filter or handler has been called.");
// Register the same EventHandler object as the MouseEvent filter and handler
// to the Circle for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED, handler);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);
Tip
您可以使用addEventFilter()和addEventHandler()方法为一个节点添加多个事件过滤器和处理程序。您需要为要添加的事件过滤器和处理程序的每个实例调用一次这些方法。
清单 9-1 有完整的程序来演示如何处理一个Circle对象的鼠标点击事件。它使用一个事件过滤器和一个事件处理器。运行程序并在圆圈内单击。单击圆圈时,首先调用事件过滤器,然后调用事件处理程序。从输出中可以明显看出这一点。每当您单击圆内的任何一点时,都会发生鼠标单击事件。如果在圆圈外单击,鼠标单击事件仍会发生;但是,您看不到任何输出,因为您没有在HBox、Scene和Stage上注册事件过滤器或处理程序。
// EventRegistration.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class EventRegistration extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle (100, 100, 50);
circle.setFill(Color.CORAL);
// Create a MouseEvent filter
EventHandler<MouseEvent> mouseEventFilter = e ->
System.out.println(
"Mouse event filter has been called.");
// Create a MouseEvent handler
EventHandler<MouseEvent> mouseEventHandler = e ->
System.out.println(
"Mouse event handler has been called.");
// Register the MouseEvent filter and handler to
// the Circle for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED,
mouseEventFilter);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED,
mouseEventHandler);
HBox root = new HBox();
root.getChildren().add(circle);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Registering Event Filters and Handlers");
stage.show();
stage.sizeToScene();
}
}
Mouse event filter has been called.
Mouse event handler has been called.
...
Listing 9-1Registering Event Filters and Handlers
要注销事件过滤器和事件处理程序,您需要分别调用removeEventFilter()和removeEventHandler()方法:
-
<T extends Event> void removeEventFilter(EventType<T> eventType, EventHandler<? super T> eventFilter) -
<T extends Event> void removeEventHandler(EventType<T> eventType, EventHandler<? super T> eventHandler)
下面的代码片段向一个Circle添加和移除一个事件过滤器,然后移除它们。注意,一旦从一个节点中删除了一个EventHandler,当事件发生时就不会调用它的handle()方法:
// Create a MouseEvent EventHandler object
EventHandler<MouseEvent> handler = e ->
System.out.println("Mouse event filter or handler has been called.");
// Register the same EventHandler object as the MouseEvent filter and handler
// to the Circle for mouse-clicked events
circle.addEventFilter(MouseEvent.MOUSE_CLICKED, handler);
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);
...
// At a later stage, when you are no longer interested in handling the mouse
// clicked event for the Circle, unregister the event filter and handler
circle.removeEventFilter(MouseEvent.MOUSE_CLICKED, handler);
circle.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);
在 XXX 上使用便利属性
Node、Scene和Window类包含事件属性来存储一些选定事件类型的事件处理程序。属性名使用事件类型模式。它们被命名为onXXX。例如,onMouseClicked属性存储鼠标点击事件类型的事件处理程序;属性存储键类型事件的事件处理程序;等等。您可以使用这些属性的setOnXXX()方法来注册节点的事件处理程序。例如,使用setOnMouseClicked()方法为鼠标点击事件注册一个事件处理程序,使用setOnKeyTyped()方法为键入事件注册一个事件处理程序,等等。各种类中的setOnXXX()方法被认为是注册事件处理程序的便利方法。
您需要记住关于onXXX便利属性的一些要点:
-
它们只支持事件处理程序的注册,不支持事件过滤器。如果您需要注册事件过滤器,请使用
addEventFilter()方法。 -
他们只支持为一个节点注册一个事件处理程序。可以使用
addEventHandler()方法为一个节点注册多个事件处理程序。 -
这些属性只存在于节点类型的常用事件中。例如,
onMouseClicked属性存在于Node和Scene类中,但不存在于Window类中;onShowing属性存在于Window类中,但不存在于Node和Scene类中。
清单 9-2 中的程序与清单 9-1 中的程序工作相同。这一次,您已经使用了Node类的onMouseClicked属性为这个圆注册了鼠标点击事件处理程序。注意,要注册事件过滤器,您必须像以前一样使用addEventFilter()方法。运行程序并在圆圈内单击。您将得到与运行清单 9-1 中的代码相同的输出。
// EventHandlerProperties.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class EventHandlerProperties extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle (100, 100, 50);
circle.setFill(Color.CORAL);
HBox root = new HBox();
root.getChildren().add(circle);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle(
"Using convenience event handler properties");
stage.show();
stage.sizeToScene();
// Create a MouseEvent filter
EventHandler<MouseEvent> eventFilter = e ->
System.out.println(
"Mouse event filter has been called.");
// Create a MouseEvent handler
EventHandler<MouseEvent> eventHandler = e ->
System.out.println(
"Mouse event handler has been called.");
// Register the filter using the addEventFilter() method
circle.addEventFilter(MouseEvent.MOUSE_CLICKED,
eventFilter);
// Register the handler using the setter method for
// the onMouseCicked convenience event property
circle.setOnMouseClicked(eventHandler);
}
}
Listing 9-2Using the Convenience Event Handler Properties
便利事件属性没有提供单独的方法来注销事件处理程序。将属性设置为null会取消注册已经注册的事件处理程序:
// Register an event handler for the mouse-clicked event
circle.setOnMouseClicked(eventHandler);
...
// Later, when you are no longer interested in processing the mouse-clicked
// event, unregister it.
circle.setOnMouseClicked(null);
定义onXXX事件属性的类也定义了返回注册事件处理程序的引用的getOnXXX() getter 方法。如果没有设置事件处理程序,getter 方法返回null。
事件过滤器和处理程序的执行顺序
相似和不同节点的事件过滤器和处理程序都有一些执行顺序规则:
-
事件过滤器在事件处理程序之前被调用。事件过滤器按照父子顺序从最顶端的父对象到事件目标执行。事件处理程序以与事件过滤器相反的顺序执行。也就是说,事件处理程序的执行从事件目标开始,并按父子顺序向上移动。
-
对于同一节点,特定事件类型的事件筛选器和处理程序在通用类型的事件筛选器和处理程序之前被调用。假设您已经为节点
MouseEvent.ANY和MouseEvent.MOUSE_CLICKED注册了事件处理程序。两种事件类型的事件处理程序都能够处理鼠标单击事件。当鼠标点击节点时,MouseEvent.MOUSE_CLICKED事件类型的事件处理程序在MouseEvent.ANY事件类型的事件处理程序之前被调用。请注意,鼠标按下事件和鼠标释放事件发生在鼠标单击事件发生之前。在我们的例子中,这些事件将由MouseEvent.ANY事件类型的事件处理程序来处理。 -
没有指定节点的相同事件类型的事件过滤器和处理程序的执行顺序。这条规则有一个例外。使用
addEventHandler()方法注册到节点的事件处理程序在使用setOnXXX()方便方法注册的事件处理程序之前执行。
清单 9-3 展示了不同节点的事件过滤器和处理程序的执行顺序。程序给一个HBox增加一个Circle和一个Rectangle。HBox被添加到Scene中。为鼠标点击事件的Stage、Scene、HBox和Circle添加事件过滤器和事件处理程序。运行程序,点击圆圈内的任意位置。输出显示了过滤器和处理程序的调用顺序。输出包含事件阶段、类型、目标、源和位置。请注意,当事件从一个节点传播到另一个节点时,事件源会发生变化。该位置相对于事件源。因为每个节点都使用自己的局部坐标系,所以鼠标单击的同一点相对于不同的节点具有不同的(x,y)坐标值。
// CaptureBubblingOrder.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
Listing 9-3Execution Order for Event Filters and Handlers
如果单击矩形,您会注意到输出显示了事件通过其父级的相同路径,就像它通过圆形一样。事件仍然通过矩形,这是事件目标。但是,您看不到任何输出,因为您没有为矩形注册任何事件过滤器或处理程序来输出任何消息。您可以点按圆形和矩形外的任何点,以查看事件目标和事件路径。
public class CaptureBubblingOrder extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle (50, 50, 50);
circle.setFill(Color.CORAL);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.TAN);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(circle, rect);
Scene scene = new Scene(root);
// Create two EventHandlders
EventHandler<MouseEvent> filter = e ->
handleEvent("Capture", e);
EventHandler<MouseEvent> handler = e ->
handleEvent("Bubbling", e);
// Register filters
stage.addEventFilter(MOUSE_CLICKED, filter);
scene.addEventFilter(MOUSE_CLICKED, filter);
root.addEventFilter(MOUSE_CLICKED, filter);
circle.addEventFilter(MOUSE_CLICKED, filter);
// Register handlers
stage.addEventHandler(MOUSE_CLICKED, handler);
scene.addEventHandler(MOUSE_CLICKED, handler);
root.addEventHandler(MOUSE_CLICKED, handler);
circle.addEventHandler(MOUSE_CLICKED, handler);
stage.setScene(scene);
stage.setTitle(
"Event Capture and Bubbling Execution Order");
stage.show();
}
public void handleEvent(String phase, MouseEvent e) {
String type = e.getEventType().getName();
String source = e.getSource().getClass().getSimpleName();
String target = e.getTarget().getClass().getSimpleName();
// Get coordinates of the mouse cursor relative to the
// event source
double x = e.getX();
double y = e.getY();
System.out.println(phase + ": Type=" + type +
", Target=" + target +
", Source=" + source +
", location(" + x + ", " + y + ")");
}
}
清单 9-4 展示了一个节点的事件处理程序的执行顺序。它显示一个圆。它为循环注册了三个事件处理程序:
-
一个用于
MouseEvent.ANY事件类型 -
一个用于使用
addEventHandler()方法的MouseEvent.MOUSE_CLICKED事件类型 -
一个用于使用
setOnMouseClicked()方法的MouseEvent.MOUSE_CLICKED事件类型
运行程序并在圆圈内单击。输出显示了调用三个事件处理程序的顺序。该顺序将类似于本节开始时的讨论中提出的顺序。
// HandlersOrder.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class HandlersOrder extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle(50, 50, 50);
circle.setFill(Color.CORAL);
HBox root = new HBox();
root.getChildren().addAll(circle);
Scene scene = new Scene(root);
/* Register three handlers for the circle that can handle
mouse-clicked events */
// This will be called last
circle.addEventHandler(MouseEvent.ANY, e ->
handleAnyMouseEvent(e));
// This will be called first
circle.addEventHandler(MouseEvent.MOUSE_CLICKED, e ->
handleMouseClicked("addEventHandler()", e));
// This will be called second
circle.setOnMouseClicked(e ->
handleMouseClicked("setOnMouseClicked()", e));
stage.setScene(scene);
stage.setTitle(
"Execution Order of Event Handlers of a Node");
stage.show();
}
public void handleMouseClicked(String registrationMethod,
MouseEvent e) {
System.out.println(registrationMethod +
": MOUSE_CLICKED handler detected a mouse click.");
}
public void handleAnyMouseEvent(MouseEvent e) {
// Print a message only for mouse-clicked events,
// ignoring other mouse events such as mouse-pressed,
// mouse-released, etc.
if (e.getEventType() == MouseEvent.MOUSE_CLICKED) {
System.out.println(
"MouseEvent.ANY handler detected a mouse click.");
}
}
}
addEventHandler(): MOUSE_CLICKED handler detected a mouse click.
setOnMouseClicked(): MOUSE_CLICKED handler detected a mouse click.
MouseEvent.ANY handler detected a mouse click.
Listing 9-4Order of Execution of Event Handlers for a Node
消费事件
通过调用事件的consume()方法来消耗事件。事件类包含方法,它由所有事件类继承。通常,在事件过滤器和处理程序的handle()方法中调用consume()方法。
使用事件向事件调度程序表明事件处理已完成,并且事件不应在事件调度链中继续传播。如果事件在节点的事件过滤器中被使用,则该事件不会传播到任何子节点。如果事件在节点的事件处理程序中使用,则该事件不会传播到任何父节点。
调用使用节点的所有事件筛选器或处理程序,而不管哪个筛选器或处理程序使用该事件。假设您为一个节点注册了三个事件处理程序,首先调用的事件处理程序使用事件。在这种情况下,仍然调用节点的另外两个事件处理程序。
如果父节点不希望其子节点响应某个事件,它可以在其事件过滤器中使用该事件。如果父节点对事件处理程序中的事件提供默认响应,则子节点可以提供特定响应并使用该事件,从而取消父节点的默认响应。
通常,节点在提供默认响应后会消耗大多数输入事件。规则是调用节点的所有事件过滤器和处理程序,即使其中一个使用了事件。这使得开发人员可以为节点执行他们的事件过滤器和处理程序,即使节点使用事件。
清单 9-5 中的代码展示了如何使用一个事件。图 9-6 显示运行程序时的屏幕。
图 9-6
消费事件
// ConsumingEvents.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
Listing 9-5Consuming Events
程序将一个Circle、一个Rectangle和一个CheckBox添加到一个HBox中。将HBox作为根节点添加到场景中。向Stage、Scene、HBox和Circle添加事件处理程序。注意,您有一个不同的事件处理程序用于Circle,只是为了保持程序逻辑简单。当复选框被选中时,圆的事件处理程序消耗鼠标点击事件,从而防止事件向上传播到HBox、Scene和Stage。如果未选中该复选框,圆上的鼠标点击事件将从Circle移动到HBox、Scene和Stage。运行该程序,并使用鼠标单击场景的不同区域来查看效果。请注意,HBox、Scene和Stage的鼠标单击事件处理程序会被执行,即使您单击了圆圈外的点,因为它们位于所单击节点的事件调度链中。
public class ConsumingEvents extends Application {
private CheckBox consumeEventCbx =
new CheckBox("Consume Mouse Click at Circle");
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle (50, 50, 50);
circle.setFill(Color.CORAL);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.TAN);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(circle, rect, consumeEventCbx);
Scene scene = new Scene(root);
// Register mouse-clicked event handlers to all nodes
,
// except the rectangle and checkbox
EventHandler<MouseEvent> handler = e ->
handleEvent(e);
EventHandler<MouseEvent> circleMeHandler = e ->
handleEventforCircle(e);
stage.addEventHandler(MOUSE_CLICKED, handler);
scene.addEventHandler(MOUSE_CLICKED, handler);
root.addEventHandler(MOUSE_CLICKED, handler);
circle.addEventHandler(MOUSE_CLICKED, circleMeHandler);
stage.setScene(scene);
stage.setTitle("Consuming Events");
stage.show();
}
public void handleEvent(MouseEvent e) {
print(e);
}
public void handleEventforCircle(MouseEvent e) {
print(e);
if (consumeEventCbx.isSelected()) {
e.consume();
}
}
public void print(MouseEvent e) {
String type = e.getEventType().getName();
String source = e.getSource().getClass().getSimpleName();
String target = e.getTarget().getClass().getSimpleName();
// Get coordinates of the mouse cursor relative to the
// event source
double x = e.getX();
double y = e.getY();
System.out.println("Type=" + type + ", Target=" + target
", Source=" + source +
", location(" + x + ", " + y + ")");
}
}
单击复选框不会执行HBox、Scene和Stage的鼠标点击事件处理程序,而单击矩形会执行。你能想出这种行为的原因吗?原因很简单。该复选框有一个默认的事件处理程序,它采取默认的操作并使用该事件,防止它沿事件调度链向上移动。矩形不使用事件,允许它沿事件调度链向上移动。
Tip
事件过滤器中的事件目标使用事件不会影响任何其他事件过滤器的执行。但是,它防止了事件冒泡阶段的发生。在最顶层节点的事件处理程序中使用事件对事件处理没有任何影响,最顶层节点是事件调度链的头。
处理输入事件
输入事件指示用户输入(或用户动作),例如点击鼠标、按键、触摸触摸屏等。JavaFX 支持多种类型的输入事件。图 9-7 显示了一些代表输入事件的类的类图。所有与输入事件相关的类都在javafx.scene.input包中。InputEvent类是所有输入事件类的超类。通常,节点在采取默认操作之前会执行用户注册的输入事件处理程序。如果用户事件处理程序使用事件,节点不会采取默认操作。假设您为一个TextField注册了键类型的事件处理程序,它使用该事件。当您键入一个字符时,TextField不会将其添加并显示为其内容。因此,使用节点的输入事件使您有机会禁用节点的默认行为。在接下来的部分中,我将讨论鼠标和按键输入事件。
图 9-7
某些输入事件的类层次结构
处理鼠标事件
MouseEvent类的一个对象代表一个鼠标事件。MouseEvent类定义了以下鼠标相关的事件类型常量。所有常量都是EventType<MouseEvent>类型。Node类包含大多数鼠标事件类型的便利的onXXX属性,可用于为节点添加一个特定鼠标事件类型的事件处理程序:
-
ANY:是所有鼠标事件类型的超类型。如果一个节点想要接收所有类型的鼠标事件,您应该为这种类型注册处理程序。InputEvent.ANY是这个事件类型的超类型。 -
MOUSE_PRESSED:按下鼠标按钮产生此事件。MouseEvent类的getButton()方法返回负责该事件的鼠标按钮。鼠标按钮由MouseButton枚举中定义的NONE、PRIMARY、MIDDLE和SECONDARY常量表示。 -
MOUSE_RELEASED:释放鼠标按钮会产生这个事件。该事件被传递到鼠标被按下的同一个节点。例如,您可以在圆上按下鼠标按钮,将鼠标拖到圆外,然后释放鼠标按钮。MOUSE_RELEASED事件将被传递给圆圈,而不是释放鼠标按钮的节点。 -
MOUSE_CLICKED:在节点上点击鼠标按钮时产生该事件。应该在同一个节点上按下并释放按钮,此事件才会发生。 -
MOUSE_MOVED:在没有按下任何鼠标键的情况下移动鼠标会产生这个事件。 -
MOUSE_ENTERED:鼠标进入一个节点时产生该事件。此事件不会发生事件捕获和冒泡阶段。也就是说,不调用该事件的事件目标的父节点的事件过滤器和处理程序。 -
MOUSE_ENTERED_TARGET:鼠标进入一个节点时产生该事件。它是MOUSE_ENTERED事件类型的变体。与MOUSE_ENTERED事件不同,事件捕获和冒泡阶段发生在这个事件中。 -
MOUSE_EXITED:当鼠标离开一个节点时产生该事件。此事件不会发生事件捕获和冒泡阶段,也就是说,它只被传递到目标节点。 -
MOUSE_EXITED_TARGET:当鼠标离开一个节点时产生该事件。它是MOUSE_EXITED事件类型的变体。与MOUSE_EXITED事件不同,事件捕获和冒泡阶段发生在这个事件中。 -
DRAG_DETECTED:当鼠标在一个节点上按下并拖动超过特定于平台的距离阈值时,会生成此事件。 -
MOUSE_DRAGGED:按下鼠标按钮移动鼠标会产生此事件。无论鼠标指针在拖动过程中的位置如何,该事件都被传递到按下鼠标按钮的同一个节点。
获取鼠标位置
MouseEvent类包含当鼠标事件发生时给你鼠标位置的方法。您可以获得相对于事件源节点、场景和屏幕的坐标系的鼠标位置。getX()和getY()方法给出了鼠标相对于事件源节点的(x,y)坐标。getSceneX()和getSceneY()方法给出了鼠标相对于添加节点的场景的(x,y)坐标。getScreenX()和getScreenY()方法给出了鼠标相对于添加节点的屏幕的(x,y)坐标。
清单 9-6 包含了展示如何使用MouseEvent类中的方法来知道鼠标位置的程序。它向舞台添加了一个MOUSE_CLICKED事件处理程序,当鼠标在其区域内的任何地方被单击时,舞台都可以接收到通知。运行程序并单击舞台中的任意位置,如果在桌面上运行,则不包括其标题栏。每次单击鼠标都会打印一条消息,描述源、目标以及鼠标相对于源、场景和屏幕的位置。
// MouseLocation.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class MouseLocation extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle (50, 50, 50);
circle.setFill(Color.CORAL);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.TAN);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(circle, rect);
// Add a MOUSE_CLICKED event handler to the stage
stage.addEventHandler(MouseEvent.MOUSE_CLICKED, e ->
handleMouseMove(e));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Mouse Location");
stage.show();
}
public void handleMouseMove(MouseEvent e) {
String source = e.getSource().getClass().getSimpleName();
String target = e.getTarget().getClass().getSimpleName();
// Mouse location relative to the event source
double sourceX = e.getX();
double sourceY = e.getY();
// Mouse location relative to the scene
double sceneX = e.getSceneX();
double sceneY = e.getSceneY();
// Mouse location relative to the screen
double screenX = e.getScreenX();
double screenY = e.getScreenY();
System.out.println("Source=" + source +
", Target=" + target +
", Location:" +
" source(" + sourceX + ", " + sourceY + ")" +
", scene(" + sceneX + ", " + sceneY + ")" +
", screen(" + screenX + ", " + screenY + ")");
}
}
Listing 9-6Determining the Mouse Location During Mouse Events
表示鼠标按钮
通常,鼠标有三个按钮。你也会发现有些只有一两个按钮。一些平台提供了模拟丢失鼠标按钮的方法。javafx.scene.input包中的MouseButton枚举包含代表鼠标按钮的常量。表 9-2 包含了在MouseButton枚举中定义的常量列表。
表 9-2
MouseButton枚举的常量
鼠标按钮枚举常量
|
描述
|
| --- | --- |
| NONE | 它表示没有按钮。 |
| PRIMARY | 它代表主要按钮。通常,它是鼠标中的左键。 |
| MIDDLE | 它代表中间的按钮。 |
| SECONDARY | 它代表二级按钮。通常,它是鼠标中的右键。 |
鼠标主按键和第二按键的位置取决于鼠标配置。通常,对于惯用右手的用户,左按钮和右按钮分别被配置为主要按钮和辅助按钮。对于惯用左手的用户,按钮以相反的顺序配置。如果你有一个两键鼠标,你没有中间键。
鼠标按钮的状态
代表鼠标事件的MouseEvent对象包含事件发生时鼠标按钮的状态。MouseEvent类包含许多报告鼠标按钮状态的方法。表 9-3 包含了这些方法的列表及其描述。
表 9-3
MouseEvent类中与鼠标按钮状态相关的方法
方法
|
描述
|
| --- | --- |
| MouseButton getButton() | 它返回负责鼠标事件的鼠标按钮。 |
| int getClickCount() | 它返回与鼠标事件相关的鼠标点击次数。 |
| boolean isPrimaryButtonDown() | 如果主按钮当前被按下,则返回true。否则返回false。 |
| boolean isMiddleButtonDown() | 如果当前按下了中间按钮,则返回true。否则返回false。 |
| boolean isSecondaryButtonDown() | 如果次级按钮当前被按下,则返回true。否则返回false。 |
| boolean isPopupTrigger() | 如果鼠标事件是平台的弹出菜单触发事件,则返回true。否则返回false。 |
| boolean isStillSincePress() | 如果鼠标光标停留在一个小区域内,即系统提供的滞后区域,在最后一次鼠标按下事件和当前鼠标事件之间,它返回true。 |
在许多情况下,getButton()方法可能会返回MouseButton.NONE,例如,当使用手指而不是鼠标在触摸屏上触发鼠标事件时,或者当鼠标事件(如鼠标移动事件)不是由鼠标按钮触发时。
理解getButton()方法和其他方法之间的区别很重要,例如isPrimaryButtonDown(),它返回按钮被按下的状态。getButton()方法返回触发事件的按钮。并非所有的鼠标事件都是由按钮触发的。例如,当鼠标移动时触发鼠标移动事件,而不是通过按下或释放按钮。如果一个按钮不负责鼠标事件,getButton()方法返回MouseButton.NONE。如果主按钮当前被按下,则isPrimaryButtonDown()方法返回true,不管它是否触发了事件。例如,当您按下主按钮时,鼠标按下事件发生。getButton()方法将返回MouseButton.PRIMARY,因为这是触发鼠标按下事件的按钮。isPrimaryButtonDown()方法返回true,因为当鼠标按下事件发生时这个按钮被按下。假设你一直按下主按钮,然后按下辅助按钮。另一个鼠标按下事件发生。然而,这一次,getButton()返回MouseButton.SECONDARY,并且isPrimaryButtonDown()和isSecondaryButtonDown()方法都返回true,因为这两个按钮在第二次鼠标按下事件时都处于按下状态。
一个弹出菜单,也称为上下文、上下文或快捷菜单,是一个给用户一组在应用程序的特定上下文中可用的选项的菜单。例如,当您在 Windows 平台上的浏览器中单击鼠标右键时,会显示一个弹出菜单。使用鼠标或键盘时,不同的平台触发弹出菜单事件的方式不同。在 Windows 平台上,通常是单击鼠标右键或按 Shift + F10 键。
如果鼠标事件是平台的弹出菜单触发事件,isPopupTrigger()方法返回true。否则,它返回false。如果根据此方法的返回值执行操作,则需要在按下鼠标和释放鼠标的事件中使用它。通常,当这个方法返回true时,您让系统显示默认的弹出菜单。
Tip
JavaFX 提供了一个上下文菜单事件,它是一种特定类型的输入事件。它由javafx.scene.input包中的ContextMenuEvent类表示。如果你想处理上下文菜单事件,使用ContextMenuEvent。
GUI 应用程序中的滞后现象
滞后是允许用户输入在时间或位置范围内的特征。接受用户输入的时间范围称为滞后时间。接受用户输入的区域被称为滞后区域。滞后时间和面积取决于系统。例如,现代 GUI 应用程序提供了通过双击鼠标按钮来调用的功能。两次点击之间存在时间差。如果时间间隔在系统的滞后时间内,则两次点击被认为是双击。否则,它们将被视为两次单独的单击。
通常,在鼠标单击事件期间,鼠标在按下和释放事件之间移动非常小的距离。有时,考虑鼠标点击时移动的距离是很重要的。如果从上次按下鼠标事件到当前事件,鼠标停留在系统提供的滞后区域,则isStillSincePress()方法返回true。当您想考虑鼠标拖动动作时,这个方法很重要。如果这个方法返回true,你可以忽略鼠标拖动,因为鼠标移动仍然在距离鼠标最后被按下的点的滞后距离之内。
修饰键的状态
修饰键用于更改其他键的正常行为。修饰键的一些例子是 Alt、Shift、Ctrl、Meta、Caps Lock 和 Num Lock。并非所有平台都支持所有的修饰键。元密钥存在于 Mac 上,不存在于 Windows 上。有些系统允许您模拟修饰键的功能,即使修饰键实际上并不存在,例如,您可以使用 Windows 上的Windows键作为Meta键。MouseEvent方法包含了当鼠标事件发生时报告某些修饰键的按下状态的方法。表 9-4 列出了MouseEvent类中与修饰键相关的方法。
表 9-4
MouseEvent类中与修饰键状态相关的方法
方法
|
描述
|
| --- | --- |
| boolean isAltDown() | 如果这个鼠标事件的 Alt 键被按下,它将返回true。否则返回false。 |
| boolean isControlDown() | 如果这个鼠标事件的 Ctrl 键被按下,它将返回true。否则返回false。 |
| boolean isMetaDown() | 如果这个鼠标事件的 Meta 键被按下,它将返回true。否则返回false。 |
| boolean isShiftDown() | 如果这个鼠标事件的 Shift 键被按下,它将返回true。否则返回false。 |
| boolean isShortcutDown() | 如果针对这个鼠标事件按下了特定于平台的快捷键,它将返回true。否则返回false。快捷修饰键是 Windows 上的 Ctrl 键和 Mac 上的 Meta 键。 |
在边界上拾取鼠标事件
Node类有一个pickOnBounds属性来控制为节点选择(或生成)鼠标事件的方式。一个节点可以有任何几何形状,而它的边界总是定义一个矩形区域。如果属性设置为 true,则当鼠标位于节点的边界上或边界内时,将为节点生成鼠标事件。如果该属性设置为默认值 false,则当鼠标位于其几何形状的外围或内部时,将为该节点生成鼠标事件。一些节点,比如Text节点,将pickOnBounds属性的默认值设置为 true。
图 9-8 显示了一个圆的几何形状和边界的周长。如果圆形的pickOnBounds属性为 false,并且鼠标位于几何形状的周长和边界之间的四个角中的一个,则不会为圆形生成鼠标事件。
图 9-8
圆的几何形状和边界之间的差异
清单 9-7 包含显示一个Circle节点的pickOnBounds属性的效果的程序。显示如图 9-9 所示的窗口。程序给一个Group增加了一个Rectangle和一个Circle。请注意,Rectangle被添加到Circle之前的Group中,以保持前者在 Z 顺序上低于后者。
图 9-9
演示一个Circle节点的pickOnBounds属性的效果
Rectangle使用红色作为填充颜色,而浅灰色作为Circle的填充颜色。红色区域是几何图形的周界和Circle边界之间的区域。
您有一个控制圆的pickOnBounds属性的复选框。如果选中该属性,则该属性设置为 true。否则,它被设置为 false。
当你点击灰色区域时,Circle总是选择鼠标点击事件。当您在复选框未选中的情况下单击红色区域时,Rectangle会拾取该事件。当您在复选框被选中的情况下单击红色区域时,Circle会拾取该事件。输出显示了谁选择了鼠标点击事件。
// PickOnBounds.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class PickOnBounds extends Application {
private CheckBox pickonBoundsCbx = new CheckBox("Pick on Bounds");
Circle circle = new Circle(50, 50, 50, Color.LIGHTGRAY);
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.RED);
Group group = new Group();
group.getChildren().addAll(rect, circle);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(group, pickonBoundsCbx);
// Add MOUSE_CLICKED event handlers to the circle and
// rectangle
circle.setOnMouseClicked(e -> handleMouseClicked(e));
rect.setOnMouseClicked(e -> handleMouseClicked(e));
// Add an Action handler to the checkbox
pickonBoundsCbx.setOnAction(e -> handleActionEvent(e));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Pick on Bounds");
stage.show();
}
public void handleMouseClicked(MouseEvent e) {
String target = e.getTarget().getClass().getSimpleName();
String type = e.getEventType().getName();
System.out.println(type + " on " + target);
}
public void handleActionEvent(ActionEvent e) {
if (pickonBoundsCbx.isSelected()) {
circle.setPickOnBounds(true);
} else {
circle.setPickOnBounds(false);
}
}
}
Listing 9-7Testing the Effects of the pickOnBounds Property for a Circle Node
鼠标透明度
Node类有一个mouseTransparent属性来控制一个节点及其子节点是否接收鼠标事件。对比pickOnBounds和mouseTransparent属性:前者决定生成鼠标事件的节点区域,后者决定节点及其子节点是否生成鼠标事件,与前者的值无关。前者仅影响设置它的节点;后者影响设置它的节点及其所有子节点。
清单 9-8 中的代码展示了Circle的mouseTransparent属性的效果。这是清单 9-7 中程序的变体。它显示了一个与图 9-9 所示非常相似的窗口。当复选框MouseTransparency被选中时,它将圆的mouseTransparent属性设置为真。当复选框未被选中时,它将圆的mouseTransparent属性设置为 false。
当复选框被选中时,单击灰色区域中的圆圈,所有鼠标单击事件都将被传递到矩形中。这是因为圆圈是鼠标透明的,它让鼠标事件通过。取消选中该复选框,所有灰色区域中的鼠标单击都将传递到该圆。注意,单击红色区域总是将事件传递给矩形,因为默认情况下圆形的pickOnBounds属性设置为 false。输出显示了接收鼠标单击事件的节点。
// MouseTransparency.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class MouseTransparency extends Application {
private CheckBox mouseTransparentCbx =
new CheckBox("Mouse Transparent");
Circle circle = new Circle(50, 50, 50, Color.LIGHTGRAY);
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.RED);
Group group = new Group();
group.getChildren().addAll(rect, circle);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(group, mouseTransparentCbx);
// Add MOUSE_CLICKED event handlers to the circle
// and rectangle
circle.setOnMouseClicked(e -> handleMouseClicked(e));
rect.setOnMouseClicked(e -> handleMouseClicked(e));
// Add an Action handler to the checkbox
mouseTransparentCbx.setOnAction(e ->
handleActionEvent(e));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Mouse Transparency");
stage.show();
}
public void handleMouseClicked(MouseEvent e) {
String target = e.getTarget().getClass().getSimpleName();
String type = e.getEventType().getName();
System.out.println(type + " on " + target);
}
public void handleActionEvent(ActionEvent e) {
if (mouseTransparentCbx.isSelected()) {
circle.setMouseTransparent(true);
} else {
circle.setMouseTransparent(false);
}
}
}
Listing 9-8Testing the Effects of the mouseTransparent Property for a Circle Node
合成鼠标事件
可以使用多种类型的设备生成鼠标事件,如鼠标、跟踪板或触摸屏。触摸屏上的一些动作产生鼠标事件,这些事件被认为是合成鼠标事件。如果事件是使用触摸屏合成的,MouseEvent类的isSynthesized()方法返回true。否则返回false。
当手指在触摸屏上拖动时,它会生成滚动手势事件和鼠标拖动事件。可以在鼠标拖动事件处理程序中使用isSynthesized()方法的返回值来检测事件是通过在触摸屏上拖动手指还是通过拖动鼠标生成的。
处理鼠标进入和退出的事件
四种鼠标事件类型处理鼠标进入或退出节点时的事件:
-
MOUSE_ENTERED -
MOUSE_EXITED -
MOUSE_ENTERED_TARGET -
MOUSE_EXITED_TARGET
鼠标进入事件和鼠标退出事件有两组事件类型。一套包含两种类型,称为MOUSE_ENTERED和MOUSE_EXITED,另一套包含MOUSE_ENTERED_TARGET和MOUSE_EXITED_TARGET。两者都有共同点,比如什么时候触发。它们的传送机制不同。我将在本节中讨论所有这些问题。
当鼠标进入一个节点时,会产生一个MOUSE_ENTERED事件。当鼠标离开一个节点时,会生成一个MOUSE_EXITED事件。这些事件不会经历捕获和冒泡阶段。也就是说,它们被直接传递到目标节点,而不是它的任何父节点。
Tip
MOUSE_ENTERED和MOUSE_EXITED事件不参与捕获和冒泡阶段。然而,所有的事件过滤器和处理程序都是按照事件处理规则为目标执行的。
清单 9-9 中的程序展示了鼠标进入和鼠标退出事件是如何传递的。程序显示如图 9-10 所示的窗口。它在一个HBox内显示一个灰色填充的圆。鼠标进入和退出事件的事件处理程序被添加到HBox和Circle中。运行程序,将鼠标移进移出圆圈。当鼠标进入窗口的白色区域时,它的MOUSE_ENTERED事件被传递给HBox。当您将鼠标移进和移出圆圈时,输出显示MOUSE_ENTERED和MOUSE_EXITED事件仅传递给Circle,而不是HBox。请注意,在输出中,这些事件的源和目标总是相同的,这证明这些事件不会发生捕获和冒泡阶段。当您将鼠标移进和移出圆圈并保持在白色区域时,不会触发HBox的MOUSE_EXITED事件,因为鼠标停留在HBox上。要在HBox上触发MOUSE_EXITED事件,您需要将鼠标移动到场景区域之外,例如,在窗口之外或在窗口的标题栏上。
图 9-10
演示鼠标进入和鼠标退出事件
// MouseEnteredExited.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import static javafx.scene.input.MouseEvent.MOUSE_ENTERED;
import static javafx.scene.input.MouseEvent.MOUSE_EXITED;
public class MouseEnteredExited extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle (50, 50, 50);
circle.setFill(Color.GRAY);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(circle);
// Create a mouse event handler
EventHandler<MouseEvent> handler = e -> handle(e);
// Add mouse-entered and mouse-exited event handlers to
// the HBox
root.addEventHandler(MOUSE_ENTERED, handler);
root.addEventHandler(MOUSE_EXITED, handler);
// Add mouse-entered and mouse-exited event handlers to
// the Circle
circle.addEventHandler(MOUSE_ENTERED, handler);
circle.addEventHandler(MOUSE_EXITED, handler);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Mouse Entered and Exited Events");
stage.show();
}
public void handle(MouseEvent e) {
String type = e.getEventType().getName();
String source = e.getSource().getClass().getSimpleName();
String target = e.getTarget().getClass().getSimpleName();
System.out.println("Type=" + type +
", Target=" + target + ", Source=" + source);
}
}
Type=MOUSE_ENTERED, Target=HBox, Source=HBox
Type=MOUSE_ENTERED, Target=Circle, Source=Circle
Type=MOUSE_EXITED, Target=Circle, Source=Circle
Type=MOUSE_ENTERED, Target=Circle, Source=Circle
Type=MOUSE_EXITED, Target=Circle, Source=Circle
Type=MOUSE_EXITED, Target=HBox, Source=HBox
...
Listing 9-9Testing Mouse-Entered and Mouse-Exited Events
MOUSE_ENTERED和MOUSE_EXITED事件类型提供了大多数情况下所需的功能。有时,您需要这些事件经历正常的捕获和冒泡阶段,以便父节点可以应用过滤器并提供默认响应。MOUSE_ENTERED_TARGET和MOUSE_EXITED_TARGET事件类型提供了这些特性。他们参与事件捕获和冒泡阶段。
MOUSE_ENTERED和MOUSE_EXITED事件类型是MOUSE_ENTERED_TARGET和MOUSE_EXITED_TARGET事件类型的子类型。对其子节点的鼠标输入事件感兴趣的节点应该为MOUSE_ENTERED_TARGET类型添加事件过滤器和处理程序。子节点可以添加MOUSE_ENTERED、MOUSE_ENTERED_TARGET,或者同时添加事件过滤器和处理程序。当鼠标进入子节点时,父节点接收到MOUSE_ENTERED_TARGET事件。在事件被传递到子节点(事件的目标节点)之前,事件类型被改变为MOUSE_ENTERED类型。因此,在同一个事件处理中,目标节点接收MOUSE_ENTERED事件,而其所有父节点接收MOUSE_ENTERED_TARGET事件。因为MOUSE_ENTERED事件类型是MOUSE_ENTERED_TARGET类型的子类型,所以目标上的任一类型的事件处理程序都可以处理这个事件。这同样适用于鼠标退出事件及其相应的事件类型。
有时,在父事件处理程序内部,有必要区分触发MOUSE_ENTERED_TARGET事件的节点。当鼠标进入父节点本身或它的任何子节点时,父节点接收此事件。您可以在事件过滤器和处理程序中使用Event类的getTarget()方法检查目标节点引用是否与父节点的引用相等,以了解事件是否是由父节点触发的。
清单 9-10 中的程序展示了如何使用鼠标进入目标和鼠标离开目标事件。它给一个HBox增加了一个Circle和一个CheckBox。HBox被添加到Scene中。它向HBox添加鼠标进入目标和鼠标退出目标事件过滤器,并向Circle添加事件处理程序。它还向Circle添加了鼠标进入和鼠标退出的事件处理程序。当复选框被选中时,事件被HBox消费,因此它们不会到达Circle。以下是运行该程序时的一些观察结果:
-
不选中该复选框,当鼠标进入或离开
Circle时,HBox接收到MOUSE_ENTERED_TARGET和MOUSE_EXITED_TARGET事件。Circle接收MOUSE_ENTERED和MOUSE_EXITED事件。 -
选中复选框后,
HBox接收MOUSE_ENTERED_TARGET和MOUSE_EXITED_TARGET事件并消费它们。Circle不接收任何事件。 -
当鼠标进入或离开
HBox,窗口的白色区域时,HBox接收到MOUSE_ENTERED和MOUSE_EXITED事件,因为HBox是事件的目标。
通过移动鼠标,选择和取消选择复选框来玩应用程序。查看输出,了解这些事件是如何处理的。
// MouseEnteredExitedTarget.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseEvent;
import static javafx.scene.input.MouseEvent.MOUSE_ENTERED;
import static javafx.scene.input.MouseEvent.MOUSE_EXITED;
import static javafx.scene.input.MouseEvent.MOUSE_ENTERED_TARGET;
import static javafx.scene.input.MouseEvent.MOUSE_EXITED_TARGET;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class MouseEnteredExitedTarget extends Application {
private CheckBox consumeCbx = new CheckBox("Consume Events");
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Circle circle = new Circle(50, 50, 50);
circle.setFill(Color.GRAY);
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(circle, consumeCbx);
// Create mouse event handlers
EventHandler<MouseEvent> circleHandler = e ->
handleCircle(e);
EventHandler<MouseEvent> circleTargetHandler = e ->
handleCircleTarget(e);
EventHandler<MouseEvent> hBoxTargetHandler = e ->
handleHBoxTarget(e);
// Add mouse-entered-target and mouse-exited-target event
// handlers to HBox
root.addEventFilter(MOUSE_ENTERED_TARGET,
hBoxTargetHandler);
root.addEventFilter(MOUSE_EXITED_TARGET,
hBoxTargetHandler);
// Add mouse-entered-target and mouse-exited-target event
// handlers to the Circle
circle.addEventHandler(MOUSE_ENTERED_TARGET,
circleTargetHandler);
circle.addEventHandler(MOUSE_EXITED_TARGET,
circleTargetHandler);
// Add mouse-entered and mouse-exited event handlers to
// the Circle
circle.addEventHandler(MOUSE_ENTERED, circleHandler);
circle.addEventHandler(MOUSE_EXITED, circleHandler);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle(
"Mouse Entered Target and Exited Target Events");
stage.show();
}
public void handleCircle(MouseEvent e) {
print(e, "Circle Handler");
}
public void handleCircleTarget(MouseEvent e) {
print(e, "Circle Target Handler");
}
public void handleHBoxTarget(MouseEvent e) {
print(e, "HBox Target Filter");
if (consumeCbx.isSelected()) {
e.consume();
System.out.println(
"HBox consumed the " + e.getEventType() + " event");
}
}
public void print(MouseEvent e, String msg) {
String type = e.getEventType().getName();
String source = e.getSource().getClass().getSimpleName();
String target = e.getTarget().getClass().getSimpleName();
System.out.println(msg + ": Type=" + type +
", Target=" + target +
", Source=" + source);
}
}
Listing 9-10Using the Mouse-Entered-Target and Mouse-Exited-Target Events
处理关键事件
按键事件是一种表示击键发生的输入事件。它被传送到具有焦点的节点。在javafx.scene.input包中声明的KeyEvent类的一个实例代表一个键事件。按键、按键释放和按键输入是按键事件的三种类型。表 9-5 列出了KeyEvent类中的所有常量,它们代表关键事件类型。
表 9-5
在KeyEvent类中的常量代表关键事件类型
常量
|
描述
|
| --- | --- |
| ANY | 它是其他关键事件类型的超类型。 |
| KEY_PRESSED | 它在按键时发生。 |
| KEY_RELEASED | 它在释放一个键时发生。 |
| KEY_TYPED | 当输入 Unicode 字符时会出现这种情况。 |
Tip
形状(例如圆形或矩形)也可以接收按键事件,这一点可能并不明显。节点接收键事件的标准是节点应该有焦点。默认情况下,形状不是焦点遍历链的一部分,鼠标单击不会为它们带来焦点。Shape节点可以通过调用requestFocus()方法获得焦点。
与键入事件相比,按键和按键释放事件是较低级别的事件;它们分别在按键和释放时发生,并且取决于平台和键盘布局。
键类型事件是更高级别的事件。一般不依赖于平台和键盘布局。它在键入 Unicode 字符时发生。通常,按键会生成键入事件。然而,按键释放也可以生成按键类型的事件。例如,在 Windows 上使用 Alt 键和数字键盘时,释放 Alt 键会生成键入的事件,而不管在数字键盘上输入的击键次数。按键式事件也可以通过一系列按键和释放来生成。例如,通过按 Shift + A 输入字符 A,这包括两次按键(Shift 和 A)。在这种情况下,两次按键会生成一个键入事件。并非所有的按键或释放都会生成按键事件。例如,当您按下功能键(F1、F2 等。)或修饰键(Shift、Ctrl 等。),没有输入 Unicode 字符,因此不会生成键入的事件。
KeyEvent类维护三个变量来描述与事件相关的键:代码、文本和字符。这些变量可以使用表 9-6 中列出的KeyEvent类中的 getter 方法来访问。
表 9-6
返回关键细节的KeyEvent类中的方法
方法
|
有效期为
|
描述
|
| --- | --- | --- |
| KeyCode getCode() | KEY_PRESSED``KEY_RELEASED | KeyCode枚举包含一个常量来表示键盘上的所有键。该方法返回与被按下或释放的键相关联的KeyCode枚举常量。对于击键事件,它总是返回KeyCode.UNDEFINED,因为击键事件不一定由一次击键触发。 |
| String getText() | KEY_PRESSED``KEY_RELEASED | 它返回与按键和按键释放事件相关联的KeyCode的String描述。对于键类型的事件,它总是返回一个空字符串。 |
| String getCharacter() | KEY_TYPED | 它返回一个字符或一系列与键入事件相关的字符作为一个String。对于按键和按键释放事件,它总是返回KeyEvent.CHAR_UNDEFINED。 |
有趣的是,getCharacter()方法的返回类型是String,而不是char。这个设计是有意的。基本多语言平面之外的 Unicode 字符不能用一个字符表示。一些设备可以通过一次击键产生多个字符。getCharacter()方法的返回类型String涵盖了这些奇怪的情况。
KeyEvent类包含isAltDown()、isControlDown()、isMetaDown()、isShiftDown()和isShortcutDown()方法,这些方法可以让您检查当一个按键事件发生时,修饰键是否被按下。
处理按键和按键释放事件
简单地通过向KEY_PRESSED和KEY_RELEASED事件类型的节点添加事件过滤器和处理程序来处理按键和按键释放事件。通常,您使用这些事件来了解按下或释放了哪些键,并执行某个操作。例如,您可以检测 F1 功能键的按下,并显示焦点节点的自定义帮助窗口。
清单 9-11 中的程序展示了如何处理按键和按键释放事件。它显示一个Label和一个TextField。当你运行程序时,TextField有焦点。运行该程序时使用击键时,请注意以下几点:
-
按下并释放一些键。输出将显示事件发生时的详细信息。不是每个按键事件都会发生按键释放事件。
-
按键和按键释放事件之间的映射不是一一对应的。按键事件可能没有按键释放事件(参考下一项)。对于几个按键事件,可能有一个按键释放事件。长时间按住一个键会发生这种情况。有时,您这样做是为了多次键入同一个字符。按住 A 键一段时间,然后松开。这将生成几个按键事件和一个按键释放事件。
-
按 F1 键。它将显示帮助窗口。请注意,按下 F1 键不会为按键释放事件生成输出,即使在您释放按键之后也是如此。你能想到这是什么原因吗?在按键事件中,将显示帮助窗口,该窗口将获取焦点。主窗口上的
TextField不再有焦点。回想一下,关键事件被交付给具有焦点的节点,并且在 JavaFX 应用程序中只有一个节点可以具有焦点。因此,按键释放事件被传递到帮助窗口,而不是TextField。
// KeyPressedReleased.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class KeyPressedReleased extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Label nameLbl = new Label("Name:");
TextField nameTfl = new TextField();
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(nameLbl, nameTfl);
// Add key pressed and released events to the TextField
nameTfl.setOnKeyPressed(e -> handle(e));
nameTfl.setOnKeyReleased(e -> handle(e));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Key Pressed and Released Events");
stage.show();
}
public void handle(KeyEvent e) {
String type = e.getEventType().getName();
KeyCode keyCode = e.getCode();
System.out.println(type + ": Key Code=" +
keyCode.getName() +
", Text=" + e.getText());
// Show the help window when the F1 key is pressed
if (e.getEventType() == KEY_PRESSED &&
e.getCode() == KeyCode.F1) {
displayHelp();
e.consume();
}
}
public void displayHelp() {
Text helpText = new Text("Please enter a name.");
HBox root = new HBox();
root.setStyle("-fx-background-color: yellow;");
root.getChildren().add(helpText);
Scene scene = new Scene(root, 200, 100);
Stage helpStage = new Stage();
helpStage.setScene(scene);
helpStage.setTitle("Help");
helpStage.show();
}
}
Listing 9-11Handling Key-Pressed and Key-Released Events
处理键入的事件
键入的事件用于检测特定的击键。您不能使用它来阻止用户输入某些字符,为此,您可以使用格式化程序。这里我们不解释如何使用格式化程序,但是如果您需要使用这种功能,例如,TextField 控件的 API 文档中的setTextFormatter()方法描述会为您提供一个起点。
清单 9-12 中的程序显示了一个Label和一个TextField。它向TextField添加了一个按键类型的事件处理程序,该处理程序打印按键的一些信息。
// KeyTyped.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class KeyTyped extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Label nameLbl = new Label("Name:");
TextField nameTfl = new TextField();
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(nameLbl, nameTfl);
// Add key-typed event to the TextField
nameTfl.setOnKeyTyped(e -> handle(e));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Key Typed Event");
stage.show();
}
public void handle(KeyEvent e) {
String type = e.getEventType().getName();
System.out.println(type + ": Character=" +
e.getCharacter());
}
}
Listing 9-12Using the Key-Typed Event
处理窗口事件
当显示、隐藏或关闭窗口时,会发生窗口事件。javafx.stage包中的WindowEvent类的一个实例代表一个窗口事件。表 9-7 列出了WindowEvent类中的常量。
表 9-7
WindowEvent类中的常量来表示窗口事件类型
常量
|
描述
|
| --- | --- |
| ANY | 它是所有其他窗口事件类型的超类型。 |
| WINDOW_SHOWING | 它发生在窗口显示之前。 |
| WINDOW_SHOWN | 它发生在窗口显示之后。 |
| WINDOW_HIDING | 它发生在窗口隐藏之前。 |
| WINDOW_HIDDEN | 它发生在窗口隐藏之后。 |
| WINDOW_CLOSE_REQUEST | 当有关闭此窗口的外部请求时,就会发生这种情况。 |
窗口显示和窗口显示事件很简单。它们发生在窗口显示之前和之后。窗口显示事件的事件处理程序应该具有耗时的逻辑,因为它会延迟向用户显示窗口,从而降低用户体验。初始化一些窗口级别的变量是您需要在这个事件中编写的代码的一个很好的例子。通常,窗口显示事件为用户设置开始方向,例如,将焦点设置到窗口上的第一个可编辑字段,并向用户显示关于需要他们注意的任务的警告等。
窗口隐藏和窗口隐藏事件是窗口显示和窗口显示事件的对应物。它们发生在隐藏窗口之前和之后。
当存在关闭窗口的外部请求时,window-close-request 事件发生。使用上下文菜单中的关闭菜单或窗口标题栏中的关闭图标,或者在 Windows 上按 Alt + F4 组合键,都被视为关闭窗口的外部请求。注意,以编程方式关闭窗口,例如,使用Stage类的close()方法或Platform.exit()方法,不被认为是外部请求。如果使用了 window-close-request 事件,则不会关闭窗口。
清单 9-13 中的程序展示了如何使用所有的窗口事件。您可能会得到与代码下面所示不同的输出。它向主要阶段添加了一个复选框和两个按钮。如果未选中该复选框,则会消耗关闭窗口的外部请求,从而阻止窗口关闭。“关闭”按钮关闭窗口。“隐藏”按钮隐藏主窗口并打开一个新窗口,因此用户可以再次显示主窗口。
该程序将事件处理程序添加到窗口事件类型的主要阶段。当调用舞台上的show()方法时,会生成窗口显示和窗口显示事件。当您单击隐藏按钮时,将生成窗口隐藏和窗口隐藏事件。当您单击弹出窗口上的按钮以显示主窗口时,将再次生成窗口显示和窗口显示事件。尝试单击标题栏上的关闭图标来生成窗口关闭请求事件。如果未选中“可以关闭窗口”复选框,则不会关闭窗口。当您使用关闭按钮关闭窗口时,会生成窗口隐藏和窗口隐藏事件,但不会生成窗口关闭请求事件,因为它不是关闭窗口的外部请求。
// WindowEventApp.java
package com.jdojo.event;
import javafx.application.Application;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
public class WindowEventApp extends Application {
private CheckBox canCloseCbx = new CheckBox("Can Close Window");
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button closeBtn = new Button("Close");
closeBtn.setOnAction(e -> stage.close());
Button hideBtn = new Button("Hide");
hideBtn.setOnAction(e -> {
showDialog(stage); stage.hide(); });
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(
canCloseCbx, closeBtn, hideBtn);
// Add window event handlers to the stage
stage.setOnShowing(e -> handle(e));
stage.setOnShown(e -> handle(e));
stage.setOnHiding(e -> handle(e));
stage.setOnHidden(e -> handle(e));
stage.setOnCloseRequest(e -> handle(e));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Window Events");
stage.show();
}
public void handle(WindowEvent e) {
// Consume the event if the CheckBox is not selected
// thus preventing the user from closing the window
EventType<WindowEvent> type = e.getEventType();
if (type == WINDOW_CLOSE_REQUEST &&
!canCloseCbx.isSelected()) {
e.consume();
}
System.out.println(type + ": Consumed=" +
e.isConsumed());
}
public void showDialog(Stage mainWindow) {
Stage popup = new Stage();
Button closeBtn =
new Button("Click to Show Main Window");
closeBtn.setOnAction(e -> {
popup.close(); mainWindow.show();});
HBox root = new HBox();
root.setPadding(new Insets(20));
root.setSpacing(20);
root.getChildren().addAll(closeBtn);
Scene scene = new Scene(root);
popup.setScene(scene);
popup.setTitle("Popup");
popup.show();
}
}
WINDOW_SHOWING: Consumed=false
WINDOW_SHOWN: Consumed=false
WINDOW_HIDING: Consumed=false
WINDOW_HIDDEN: Consumed=false
WINDOW_SHOWING: Consumed=false
WINDOW_SHOWN: Consumed=false
WINDOW_CLOSE_REQUEST: Consumed=true
Listing 9-13Using Window Events
摘要
一般来说,术语“事件”用于描述感兴趣的事件。在 GUI 应用程序中,事件是用户与应用程序交互的发生,例如点击鼠标、按下键盘上的键等等。JavaFX 中的事件由javafx.event.Event类或其任何子类的对象表示。JavaFX 中的每个事件都有三个属性:事件源、事件目标和事件类型。
当应用程序中发生事件时,通常通过执行一段代码来执行一些处理。为响应事件而执行的这段代码称为事件处理程序或事件过滤器。当您想要处理 UI 元素的事件时,您需要向 UI 元素添加事件处理程序,例如,Window、Scene或Node。当 UI 元素检测到事件时,它会执行您的事件处理程序。
调用事件处理程序的 UI 元素是这些事件处理程序的事件源。当一个事件发生时,它会通过一连串的事件调度程序。事件的源是事件调度程序链中的当前元素。当事件通过事件调度程序链中的一个调度程序传递到另一个调度程序时,事件源会发生变化。事件目标是事件的目的地,它决定了事件在处理过程中的行进路线。事件类型描述发生的事件的类型。它们是以分层的方式定义的。每个事件类型都有一个名称和一个父类型。
当事件发生时,依次执行以下三个步骤:事件目标选择、事件路径构建和事件路径遍历。事件目标是基于事件类型选择的事件的目的节点。事件通过事件调度链中的事件调度程序传播。事件调度链是事件路由。事件的初始和默认路线由事件目标决定。默认事件路由由从阶段开始到事件目标节点的容器子路径组成。
事件路由遍历包括两个阶段:捕获和冒泡。一个事件在其路由中经过每个节点两次:一次在捕获阶段,一次在冒泡阶段。您可以为特定的事件类型向节点注册事件过滤器和事件处理程序。在捕获和冒泡阶段,当事件通过节点时,分别执行注册到节点的事件过滤器和事件处理程序。
在路由遍历期间,节点可以使用事件过滤器或处理程序中的事件,从而完成事件的处理。消费一个事件只需调用事件对象上的consume()方法。当一个事件被消费时,事件处理被停止,即使路由中的一些节点根本没有被遍历。
用户使用鼠标与 UI 元素的交互(如单击、移动或按下鼠标)会触发鼠标事件。MouseEvent类的一个对象代表一个鼠标事件。
按键事件表示击键的发生。它被传送到具有焦点的节点。KeyEvent类的一个实例代表一个按键事件。按键、按键释放和按键输入是按键事件的三种类型。
当显示、隐藏或关闭窗口时,会发生窗口事件。javafx.stage包中的WindowEvent类的一个实例代表一个窗口事件。
下一章讨论用作其他控件和节点的容器的布局窗格。