JavaFX17 学习手册(三)
五、创建场景
在本章中,您将学习:
-
JavaFX 应用程序中的场景和场景图是什么
-
关于场景图形的不同渲染模式
-
如何为场景设置光标
-
如何确定场景中的焦点所有者
-
如何使用
Platform和HostServices类
本章的例子在com.jdojo.scene包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.scene to javafx.graphics, javafx.base;
...
什么是场景?
一个场景代表一个舞台的视觉内容。javafx.scene包中的Scene类表示 JavaFX 程序中的一个场景。一个Scene对象一次最多只能连接到一个载物台。如果已经附加的场景被附加到另一个舞台,它将首先与前一个舞台分离。一个舞台在任何时候最多只能附加一个场景。
场景包含由可视节点组成的场景图。在这个意义上,场景充当了场景图的容器。场景图是一个树形数据结构,其元素被称为节点。场景图中的节点形成父子层次关系。场景图中的节点是javafx.scene.Node类的一个实例。节点可以是分支节点或叶节点。分支节点可以有子节点,而叶节点则不能。场景图中的第一个节点称为根节点。根节点可以有子节点;但是,它从来没有父节点。图 5-1 显示了场景图中节点的排列。分支节点显示在圆角矩形中,叶节点显示在矩形中。
图 5-1
场景图中节点的排列
JavaFX 类库提供了许多类来表示场景图中的分支和叶节点。javafx.scene包中的Node类是场景图中所有节点的超类。图 5-2 显示了代表节点的类的部分类图。
图 5-2
javafx.scene.Node类的部分类图
场景总是有一个根节点。如果根节点是可调整大小的,例如一个Region或一个Control,它跟踪场景的大小。也就是说,如果调整了场景的大小,可调整大小的根节点会调整自身的大小以填充整个场景。基于根节点的策略,当场景的大小改变时,场景图可以被再次布局。
Group是一个不可调整大小的Parent节点,它可以被设置为场景的根节点。如果一个Group是一个场景的根节点,那么场景图的内容会被场景的大小裁剪掉。如果调整了场景的大小,场景图形将不会重新布局。
Parent是一个抽象类。它是场景图中所有分支节点的基类。如果要将分支节点添加到场景图形,请使用其具体子类之一的对象,例如,Group、Pane、HBox或VBox。作为Node类而不是Parent类的子类的类表示叶节点,例如Rectangle、Circle、Text、Canvas或ImageView。场景图的根节点是一个特殊的分支节点,它是最顶端的节点。这就是创建Scene对象时使用Group或VBox作为根节点的原因。我将在第 10 和 12 章详细讨论表示分支和叶节点的类。表 5-1 列出了Scene类的一些常用属性。
表 5-1
Scene类的常用属性
类型
|
名字
|
属性和描述
|
| --- | --- | --- |
| ObjectProperty<Cursor> | cursor | 它为Scene定义了鼠标光标。 |
| ObjectProperty<Paint> | fill | 它定义了Scene的背景填充。 |
| ReadOnlyObjectProperty<Node> | focusOwner | 它定义了Scene中拥有焦点的节点。 |
| ReadOnlyDoubleProperty | height | 它定义了Scene的高度。 |
| ObjectProperty<Parent> | root | 它定义了场景图的根Node。 |
| ReadOnlyDoubleProperty | width | 它定义了Scene的宽度。 |
| ReadOnlyObjectProperty<Window> | window | 它为Scene定义了Window。 |
| ReadOnlyDoubleProperty | x | 它定义了Scene在窗口上的水平位置。 |
| ReadOnlyDoubleProperty | y | 它定义了Scene在窗口上的垂直位置。 |
图形渲染模式
场景图在屏幕上呈现 JavaFX 应用程序的内容时起着至关重要的作用。通常,有两种类型的 API 用于在屏幕上呈现图形:
-
即时模式 API
-
保留模式 API
在即时模式 API 中,当屏幕上需要一个框架时,应用程序负责发出绘制命令。图形直接画在屏幕上。当屏幕需要重新绘制时,应用程序需要向屏幕重新发出绘制命令。Java2D 是即时模式图形渲染 API 的一个例子。
在保留模式 API 中,应用程序创建图形对象并将其附加到图形。图形库,而不是应用程序代码,将图形保存在内存中。需要时,图形库会将图形呈现在屏幕上。应用程序只负责创建图形对象——“什么”部分;图形库负责存储和渲染图形,即“何时”和“如何”部分。保留模式呈现 API 将开发人员从编写呈现图形的逻辑中解放出来。例如,通过使用高级 API 从图形中添加或移除图形对象,从屏幕中添加或移除图形的一部分是简单的;图形库负责剩下的工作。与即时模式相比,保留模式 API 使用更多的内存,因为图形存储在内存中。JavaFX 场景图使用保留模式 API。
您可能认为使用即时模式 API 总是比使用保留模式 API 更快,因为前者直接在屏幕上呈现图形。然而,使用保留模式 API 打开了类库优化的大门,这在即时模式下是不可能的,在即时模式下,每个开发人员负责编写关于应该呈现什么以及何时呈现的逻辑。
图 5-3 和 5-4 分别说明了立即模式和保留模式 API 是如何工作的。它们展示了如何使用这两个 API 在屏幕上绘制文本、Hello 和六边形。
图 5-4
保留模式 API 的示例
图 5-3
即时模式 API 的示例
为场景设置光标
javafx.scene.Cursor类的一个实例代表一个鼠标光标。Cursor类包含许多常量,例如,HAND,CLOSED_HAND,DEFAULT,TEXT,NONE,WAIT,用于标准鼠标光标。以下代码片段为场景设置WAIT光标:
Scene scene;
...
scene.setCursor(Cursor.WAIT);
您也可以为场景创建和设置自定义光标。如果指定的name是一个标准光标的名字,Cursor类的cursor(String name)静态方法返回一个标准光标。否则,它将指定的name视为光标位图的 URL。下面的代码片段从一个名为mycur.png的位图文件中创建一个光标,该文件假定位于CLASSPATH中:
// Create a Cursor from a bitmap
URL url = getClass().getClassLoader().getResource("mycur.png");
Cursor myCur = Cursor.cursor(url.toExternalForm());
scene.setCursor(myCur);
// Get the WAIT standard cursor using its name
Cursor waitCur = Cursor.cursor("WAIT")
scene.setCursor(waitCur);
场景中的焦点所有者
场景中只有一个节点可以是焦点所有者。Scene类的focusOwner属性跟踪拥有焦点的Node类。注意focusOwner属性是只读的。如果您希望场景中的特定节点成为焦点所有者,您需要调用Node类的requestFocus()方法。
您可以使用Scene类的getFocusOwner()方法来获取场景中具有焦点的节点的引用。一个场景可能没有焦点所有者,在这种情况下,getFocusOwner()方法返回null。例如,场景在创建时没有焦点所有者,但没有附加到窗口。
理解焦点所有者和拥有焦点的节点之间的区别很重要。每个场景可能有一个焦点所有者。比如打开两个窗口,就有两个场景,可以有两个焦点拥有者。但是,一次只能有两个焦点所有者中的一个拥有焦点。活动窗口的焦点所有者将获得焦点。要检查焦点所有者节点是否也有焦点,您需要使用Node类的focused属性。下面的代码片段显示了使用焦点所有者的典型逻辑:
表 5-2
平台类的方法
|方法
|
描述
|
| --- | --- |
| void exit() | 它终止一个 JavaFX 应用程序。 |
| boolean isFxApplicationThread() | 如果调用线程是 JavaFX 应用程序线程,则返回true。否则返回false。 |
| boolean isImplicitExit() | 它返回应用程序的隐式implicitExit属性的值。如果它返回true,意味着应用程序将在最后一个窗口关闭后终止。否则,你需要调用这个类的exit()方法来终止应用程序。 |
| boolean isSupported(ConditionalFeature feature) | 如果平台支持指定的条件特性,则返回true。否则返回false。 |
| void runLater(Runnable runnable) | 它在 JavaFX 应用程序线程上执行指定的Runnable。执行的时间没有规定。该方法将Runnable发送到事件队列并立即返回。如果使用这种方法提交了多个Runnables,它们将按照提交到队列的顺序执行。 |
| void setImplicitExit(boolean value) | 它将implicitExit属性设置为指定的值。 |
Scene scene;
...
Node focusOwnerNode = scene.getFocusOwner();
if (focusOwnerNode == null) {
// The scene does not have a focus owner
}
else if (focusOwnerNode.isFocused()) {
// The focus owner is the one that has the focus
}
else {
// The focus owner does not have the focus
}
了解平台类
javafx.application包中的Platform类是用于支持平台相关功能的实用程序类。它由所有静态方法组成,这些方法在表 5-2 中列出。
runLater()方法用于向事件队列提交一个Runnable任务,因此它在 JavaFX 应用程序线程上执行。JavaFX 允许开发人员只在 JavaFX 应用程序线程上执行一些代码。清单 5-1 在init()方法中创建一个在 JavaFX 启动器线程上调用的任务。它使用Platform.runLater()方法提交稍后要在 JavaFX 应用程序线程上执行的任务。
Tip
使用Platform.runLater()方法执行在 JavaFX 应用程序线程之外的线程上创建的任务,但该任务需要在 JavaFX 应用程序线程上运行。
// RunLaterApp.java
package com.jdojo.scene;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class RunLaterApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void init() {
System.out.println("init(): " +
Thread.currentThread().getName());
// Create a Runnable task
Runnable task = () ->
System.out.println("Running the task on the "
+ Thread.currentThread().getName());
// Submit the task to be run on the JavaFX Application
// Thread
Platform.runLater(task);
}
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(new Group(), 400, 100));
stage.setTitle("Using Platform.runLater() Method");
stage.show();
}
}
init(): JavaFX-Launcher
Running the task on the JavaFX Application Thread
Listing 5-1Using the Platform.runLater() Method
JavaFX 实现中的一些特性是可选的(或有条件的)。它们可能无法在所有平台上使用。在不支持可选功能的平台上使用该功能不会导致错误;可选特性被简单地忽略了。可选特性被定义为javafx.application包中ConditionalFeature枚举的枚举常量,如表 5-3 所列。
表 5-3
在ConditionalFeature枚举中定义的常量
枚举常量
|
描述
|
| --- | --- |
| EFFECT | 指示滤镜效果的可用性,例如,倒影、阴影等。 |
| INPUT_METHOD | 指示文本输入法的可用性。 |
| SCENE3D | 指示 3D 功能的可用性。 |
| SHAPE_CLIP | 指示可以针对任意形状裁剪节点。 |
| TRANSPARENT_WINDOW | 指示全窗口透明度的可用性。 |
假设您的 JavaFX 应用程序根据用户需求使用 3D GUI。您可以编写启用 3D 功能的逻辑,如以下代码所示:
import javafx.application.Platform;
import static javafx.application.ConditionalFeature.SCENE3D;
...
if (Platform.isSupported(SCENE3D)) {
// Enable 3D features
}
else {
// Notify the user that 3D features are not available
}
了解主机环境
javafx.application包中的HostServices类提供与托管 JavaFX 应用程序的启动环境(本书的桌面)相关的服务。您不能直接创建HostServices类的实例。Application类的getHostServices()方法返回HostServices类的一个实例。以下是如何在从Application类继承的类中获取HostServices实例的示例:
HostServices host = getHostServices();
HostServices类包含以下方法:
-
String getCodeBase() -
String getDocumentBase() -
String resolveURI(String base, String relativeURI) -
void showDocument(String uri)
getCodeBase()方法返回应用程序的代码库统一资源标识符(URI)。在独立模式下,它返回包含用于启动应用程序的 JAR 文件的目录的 URI。如果应用程序是使用类文件启动的,它将返回一个空字符串。
getDocumentBase()方法返回文档库的 URI。它返回以独立模式启动的应用程序的当前目录的 URI。
resolveURI()方法根据指定的基本 URI 解析指定的相对 URI,并返回解析后的 URI。
方法在新的浏览器窗口中打开指定的 URI。视浏览器偏好而定,它可能会在新标签页中打开 URI。以下代码片段打开 Yahoo!主页:
getHostServices().showDocument("http://www.yahoo.com");
清单 5-2 中的程序使用了HostServices类的所有方法。它显示了一个带有两个按钮和主机详细信息的阶段。一键打开雅虎!另一个显示一个警告框。根据应用程序的启动方式,舞台上显示的输出会有所不同。
// KnowingHostDetailsApp.java
package com.jdojo.scene;
import java.util.HashMap;
import java.util.Map;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class KnowingHostDetailsApp extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
String yahooURL = "http://www.yahoo.com";
Button openURLButton = new Button("Go to Yahoo!");
openURLButton.setOnAction(e →
getHostServices().showDocument(yahooURL));
Button showAlert = new Button("Show Alert");
showAlert.setOnAction(e -> showAlert());
VBox root = new VBox();
// Add buttons and all host related details to the VBox
root.getChildren().addAll(openURLButton, showAlert);
Map<String, String> hostdetails = getHostDetails();
for(Map.Entry<String, String> entry :
hostdetails.entrySet()) {
String desc = entry.getKey() + ": " +
entry.getValue();
root.getChildren().add(new Label(desc));
}
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Knowing the Host");
stage.show();
}
protected Map<String, String> getHostDetails() {
Map<String, String> map = new HashMap<>();
HostServices host = this.getHostServices();
String codeBase = host.getCodeBase();
map.put("CodeBase", codeBase);
String documentBase = host.getDocumentBase();
map.put("DocumentBase", documentBase);
String splashImageURI =
host.resolveURI(documentBase, "splash.jpg");
map.put("Splash Image URI", splashImageURI);
return map;
}
protected void showAlert() {
Stage s = new Stage(StageStyle.UTILITY);
s.initModality(Modality.WINDOW_MODAL);
Label msgLabel = new Label("This is an FX alert!");
Group root = new Group(msgLabel);
Scene scene = new Scene(root);
s.setScene(scene);
s.setTitle("FX Alert");
s.show();
}
}
Listing 5-2Knowing the Details of the Host Environment for a JavaFX Application
摘要
场景代表舞台的视觉内容。javafx.scene包中的Scene类表示 JavaFX 程序中的一个场景。一个Scene对象一次最多被附加到一个阶段。如果已经附加的场景被附加到另一个舞台,它将首先与前一个舞台分离。一个舞台在任何时候最多只能附加一个场景。
场景包含由可视节点组成的场景图。在这个意义上,场景充当了场景图的容器。场景图是一种树形数据结构,其元素称为节点。场景图中的节点形成父子层次关系。场景图中的节点是javafx.scene.Node类的一个实例。节点可以是分支节点或叶节点。分支节点可以有子节点,而叶节点则不能。场景图中的第一个节点称为根节点。根节点可以有子节点;但是,它从来没有父节点。
javafx.scene.Cursor类的一个实例代表一个鼠标光标。Cursor类包含许多常量,例如,HAND,CLOSED_HAND,DEFAULT,TEXT,NONE,WAIT,用于标准鼠标光标。您可以使用Scene类的setCursor()方法为场景设置光标。
场景中只有一个节点可以是焦点所有者。Scene类的只读属性focusOwner跟踪拥有焦点的节点。如果您希望场景中的特定节点成为焦点所有者,您需要调用Node类的requestFocus()方法。每个场景可能有一个焦点所有者。例如,如果你打开两个窗口,你将有两个场景,你可能有两个焦点所有者。但是,一次只能有两个焦点所有者中的一个拥有焦点。活动窗口的焦点所有者将获得焦点。要检查焦点所有者节点是否也有焦点,您需要使用Node类的focused属性。
javafx.application包中的Platform类是用于支持平台相关功能的实用程序类。它包含终止应用程序、检查正在执行的代码是否在 JavaFX 应用程序线程上执行等方法。
javafx.application包中的HostServices类提供与托管 JavaFX 应用程序的启动环境(本书的桌面)相关的服务。您不能直接创建HostServices类的实例。Application类的getHostServices()方法返回HostServices类的一个实例。
下一章将详细讨论节点。
六、了解节点
在本章中,您将学习:
-
JavaFX 中的节点是什么
-
关于笛卡尔坐标系
-
关于节点的边界和边界框
-
如何设置节点的大小以及如何定位节点
-
如何在节点中存储用户数据
-
什么是受管节点
-
如何在坐标空间之间转换节点的边界
本章的例子在com.jdojo.node包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.node to javafx.graphics, javafx.base;
...
什么是节点?
第五章向你介绍了场景和场景图。场景图是一种树形数据结构。场景图中的每一项都称为一个节点。javafx.scene.Node类的一个实例表示场景图中的一个节点。注意,Node类是一个抽象类,存在几个具体的类来表示特定类型的节点。
一个节点可以有子项(也称为子节点),这些节点称为分支节点。分支节点是Parent的一个实例,它的具体子类是Group、Region和WebView。不能有子项的节点称为叶节点。诸如Rectangle、Text、ImageView和MediaView之类的实例是叶节点的例子。每个场景图树中只有一个节点没有父节点,称为根节点。一个节点在场景图中的任何地方最多出现一次。
如果节点尚未附加到场景,则可以在任何线程上创建和修改节点。将节点附加到场景中以及随后的修改必须发生在 JavaFX 应用程序线程上。
一个节点有几种类型的边界。界限是相对于不同的坐标系确定的。下一节将讨论一般的笛卡尔坐标系;以下部分解释了如何使用笛卡尔坐标系来计算 JavaFX 中节点的边界。
笛卡尔坐标系
如果你在高中的坐标几何课上学习过(并且还记得)笛卡尔坐标系,你可以跳过这一节。
笛卡尔坐标系是唯一定义 2D 平面上每个点的方法。有时,它也被称为直角坐标系。它由两条垂直的直线组成,即 x 轴和 y 轴。两轴相交的点称为原点。
2D 平面中的一个点由两个值定义,即它的 x 和 y 坐标。一个点的 x 和 y 坐标分别是它与 y 轴和 x 轴的垂直距离。沿着轴,距离在原点的一侧测量为正,在另一侧测量为负。原点有(x,y)坐标,比如(0,0)。这些轴将平面分成四个象限。注意,2D 平面本身是无限的,四个象限也是无限的。笛卡尔坐标系中所有点的集合定义了该系统的坐标空间。
图 6-1 显示了笛卡尔坐标系的图解。它显示了具有 x1 和 y1 的 x 和 y 坐标的点 P。它显示了每个象限中 x 和 y 坐标的值的类型。例如,右上象限显示(+、+),这意味着该象限中所有点的 x 和 y 坐标都为正值。
图 6-1
坐标几何中使用的二维笛卡尔坐标系
变换是坐标空间中的点到同一坐标空间的映射,保留一组预定义的几何属性。几种类型的变换可以应用于坐标空间中的点。变换类型的一些例子是平移、旋转、缩放和剪切。
在平移变换中,一对固定的数字被添加到所有点的坐标中。假设您想通过(a,b)将平移应用于坐标空间。如果一个点在平移之前具有坐标(x,y ),那么它在平移之后将具有坐标(x + a,y + b)。
在旋转变换中,轴围绕坐标空间中的轴心点旋转,并且点的坐标被映射到新的轴。图 6-2 显示了平移和旋转变换的例子。
图 6-2
平移和旋转变换的示例
在图 6-2 中,变换前的轴用实线表示,变换后的轴用虚线表示。注意,点 P 在(4,3)处的坐标在平移和旋转的坐标空间中保持不变。但是,该点相对于原始坐标空间的坐标在变换后会发生变化。原始坐标空间中的点以纯黑色填充颜色显示,而在转换后的坐标空间中,该点没有填充颜色。在旋转变换中,您已经使用原点作为轴心点。因此,原始坐标空间和变换坐标空间的原点是相同的。
节点的笛卡尔坐标系
场景图中的每个节点都有自己的坐标系。节点使用由 x 轴和 y 轴组成的笛卡尔坐标系。在计算机系统中,x 轴上的值向右增加,y 轴上的值向下增加,如图 6-3 所示。通常,当显示节点的坐标系时,x 轴和 y 轴的负边不会显示,即使它们总是存在。图 6-3 的右部显示了简化版坐标系。一个节点可以有负的 x 和 y 坐标。
图 6-3
节点的坐标系
在典型的 GUI 应用程序中,节点被放置在它们的父节点中。根节点是所有节点的最终父节点,它位于场景内部。场景放置在舞台内,舞台放置在屏幕内。组成一个窗口的每个元素,从节点到屏幕,都有自己的坐标系,如图 6-4 所示。
图 6-4
构成 GUI 窗口的所有元素的坐标系
最外面的矩形区域是屏幕,带有粗黑边框。剩下的是一个 JavaFX stage,带有一个区域和一个矩形。该区域的背景颜色为浅灰色,矩形的背景颜色为蓝色。该区域是矩形的父区域。这个简单的窗口使用五个坐标空间,如图 6-4 所示。我只标注了 x 轴。所有 y 轴都是垂直线,在原点与各自的 x 轴相交。
矩形左上角的坐标是什么?问题不完整。点的坐标是相对于坐标系定义的。如图 6-4 所示,你有五个坐标系,因此有五个坐标空间。因此,必须指定要知道矩形左上角坐标的坐标系。在一个节点的坐标系中,它们是(10,15);在父母的坐标系中,它们是(40,45);在一个场景的坐标系中,它们是(60,55);在一个阶段的坐标系中,它们是(64,83);在屏幕的坐标系中,它们是(80,99)。
边界和包围盒的概念
每个节点都有一个几何形状,它位于一个坐标空间中。节点的大小和位置统称为其边界。节点的边界是根据包围该节点的整个几何形状的边界矩形框来定义的。图 6-5 显示了一个三角形、一个圆形、一个圆角矩形和一个带实线边框的矩形。用虚线边框显示的矩形是这些形状(节点)的边界框。
图 6-5
定义节点几何形状的边界矩形框
由节点的几何形状及其边界框覆盖的面积(2D 空间中的面积和 3D 空间中的体积)可以不同。比如图 6-5 中的前三个节点,从左边数,节点的面积和它们的包围盒是不一样的。然而,对于没有圆角的最后一个矩形,其面积和其边界框的面积是相同的。
javafx.geometry.Bounds类的一个实例代表一个节点的边界。Bounds类是一个抽象类。BoundingBox类是Bounds类的具体实现。Bounds类被设计用来处理 3D 空间中的边界。它用边界框中的最小深度以及边界框的宽度、高度和深度封装左上角的坐标。方法getMinX()、getMinY()和getMinZ()用于获取坐标。使用getWidth()、getHeight()和getDepth()方法访问边界框的三个维度。Bounds类包含getMaxX()、getMaxY()和getMaxZ()方法,这些方法返回边界框中右下角最大深度的坐标。
在 2D 空间中,minX和minY分别定义边界框左上角的 x 和 y 坐标,maxX和maxY分别定义右下角的 x 和 y 坐标。在 2D 空间中,边界框的 z 坐标值和深度值为零。图 6-6 显示了 2D 坐标空间中边界框的细节。
图 6-6
2D 空间中边界框的制作
Bounds类包含isEmpty()、contains()和intersects()实用方法。如果一个Bounds的三个维度(宽度、高度或深度)中的任何一个是负数,isEmpty()方法返回true。contains()方法允许您检查一个Bounds是否包含另一个Bounds、一个 2D 点或一个 3D 点。intersects()方法允许您检查一个Bounds的内部是否与另一个Bounds、2D 点或 3D 点的内部相交。
知道节点的边界
到目前为止,我已经讨论了与节点相关的坐标系统、边界和边界框等主题。那个讨论是为了让你为这一节做准备,这一节是关于知道一个节点的边界。您可能已经猜到(虽然不正确)了Node类应该有一个getBounds()方法来返回节点的边界。要是这么简单就好了!在这一节中,我将讨论不同类型的节点边界的细节。在下一节中,我将带您看一些例子。
图 6-7 显示了一个带有三种形式文本“关闭”的按钮。
图 6-7
有和没有效果和变形的按钮
第一个,从左边开始,没有效果或变换。第二个有投影效果。第三个有投影效果和旋转变换。图 6-8 显示了代表这三种形式的按钮边界的边界框。暂时忽略坐标,您可能会注意到按钮的边界会随着效果和变换的应用而改变。
图 6-8
具有和不具有效果的按钮以及具有边界框的变换
场景图中的节点有三种类型的边界,在Node类中定义为三个只读属性:
-
layoutBounds -
boundsInLocal -
boundsInParent
当你试图理解一个节点的三种界限时,你需要寻找三个点:
图 6-9
影响节点大小的因素
-
(
minX,minY)值是如何定义的。它们定义了由Bounds对象描述的边界框左上角的坐标。 -
请记住,点的坐标总是相对于坐标空间来定义的。因此,请注意在第一步中描述的定义坐标的坐标空间。
-
特定类型的边界中包含节点的哪些属性(几何图形、描边、效果、剪辑和变换)。
图 6-9 显示了构成节点边界的节点属性。它们按顺序从左到右应用。一些节点类型(例如,Circle、Rectangle)可能具有非零笔画。非零笔划被认为是节点几何的一部分,用于计算其边界。
表 6-1 列出了有助于特定类型节点边界的属性以及定义边界的坐标空间。节点的boundsInLocal和boundsInParent也被称为其物理边界,因为它们对应于节点的物理属性。节点的layoutBounds被称为逻辑边界,因为它不一定绑定到节点的物理边界。当一个节点的几何图形改变时,所有的边界都被重新计算。
表 6-1
为节点边界提供属性
|界限类型
|
坐标空间
|
贡献者
|
| --- | --- | --- |
| layoutBounds | 节点(未转换) | 节点的几何形状非零笔划 |
| boundsInLocal | 节点(未转换) | 节点的几何形状非零笔划效果夹子 |
| boundsInParent | 父母 | 节点的几何形状非零笔划效果夹子转换 |
Tip
boundsInLocal和BoundsInParent被称为物理或可视边界,因为它们对应于节点的可视外观。layoutBounds也被称为逻辑边界,因为它不一定对应于节点的物理边界。
布局绑定属性
layoutBounds属性是基于节点在未变换的局部坐标空间中的几何属性来计算的。不包括效果、剪辑和变换。根据节点的可调整行为,使用不同的规则来计算由layoutBounds描述的边界框左上角的坐标:
-
对于一个可调整大小的节点(一个
Region、一个Control和一个WebView),边界框左上角的坐标总是被设置为(0,0)。例如,对于一个按钮,layoutBounds属性中的(minX,minY))值总是(0,0)。 -
对于一个不可调整大小的节点(一个
Shape,一个Text,和一个Group,边界框左上角的坐标是基于几何属性计算的。对于一种形状(矩形、圆形等。)或者一个Text,可以指定节点中特定点相对于该节点未变换坐标空间的(x,y)坐标。例如,对于一个矩形,您可以指定左上角的(x,y)坐标,该坐标成为由其layoutBounds属性描述的边界框的左上角的(x,y)坐标。对于一个圆,可以指定centerX、centerY和radius属性,其中centerX和centerY分别是圆心的 x 和 y 坐标。由layoutBounds描述的圆形边界框左上角的(x,y)坐标计算如下(centerX–半径,centerY–半径)。
layoutBounds中的宽度和高度是节点的宽度和高度。有些节点允许您设置它们的宽度和高度;但是有些会自动为您计算它们,并让您覆盖它们。
在哪里使用节点的layoutBounds属性?容器根据它们的layoutBounds分配空间来布局子节点。让我们看一个如清单 6-1 所示的例子。它在一个VBox中显示四个按钮。第一个按钮有投影效果。第三个按钮有投影效果和 30 度旋转变换。第二个和第四个按钮没有效果或变形。产生的屏幕如图 6-10 所示。输出显示,不管效果和变换如何,所有按钮都具有相同的layoutBounds值。所有按钮的layoutBounds对象的大小(宽度和高度)由按钮的文本和字体决定,这对于所有按钮都是一样的。在您的平台上,输出可能有所不同。
图 6-10
layoutBounds属性不包括效果和变换
// LayoutBoundsTest.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class LayoutBoundsTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button b1 = new Button("Close");
b1.setEffect(new DropShadow());
Button b2 = new Button("Close");
Button b3 = new Button("Close");
b3.setEffect(new DropShadow());
b3.setRotate(30);
Button b4 = new Button("Close");
VBox root = new VBox();
root.getChildren().addAll(b1, b2, b3, b4);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Testing LayoutBounds");
stage.show();
System.out.println("b1=" + b1.getLayoutBounds());
System.out.println("b2=" + b2.getLayoutBounds());
System.out.println("b3=" + b3.getLayoutBounds());
System.out.println("b4=" + b4.getLayoutBounds());
}
}
b1=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b2=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b3=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b4=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
Listing 6-1Accessing the layoutBounds of Buttons with and Without Effects
有时,您可能希望在节点的layoutBounds中包含显示节点效果和变换所需的空间。解决这个问题的方法很简单。您需要将节点包装在一个Group中,将Group包装在一个容器中。现在,容器将向Group查询它的layoutBounds。一个Group的layoutBounds是其所有子节点的boundsInParent的并集。回想一下(见表 6-1 )节点的boundsInParent包括显示效果和节点变换所需的空间。如果你改变陈述
root.getChildren().addAll(b1, b2, b3, b4);
在清单 6-1 中为
root.getChildren().addAll(new Group(b1), b2, new Group(b3), b4);
产生的屏幕如图 6-11 所示。这一次,VBox为第一组和第三组分配了足够的空间,以考虑应用于包装按钮的效果和变换。
图 6-11
使用Group为节点的效果和变换分配空间
Tip
基于节点的几何属性来计算节点的layoutBounds。因此,您不应该将节点的这种属性绑定到包含节点的layoutBounds的表达式。
boundsInLocal 属性
boundsInLocal属性是在节点的未变换坐标空间中计算的。它包括节点、效果和剪辑的几何属性。不包括应用于节点的变换。
清单 6-2 打印一个按钮的layoutBounds和boundsInLocal。boundsInLocal属性包括按钮周围的阴影效果。注意,layoutBounds定义的边界框左上角的坐标是(0.0,0.0),boundsInLocal的坐标是(–9.0,–9.0)。不同平台上的输出可能会有所不同,因为节点的大小是根据运行程序的平台自动计算的。
// BoundsInLocalTest.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class BoundsInLocalTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button b1 = new Button("Close");
b1.setEffect(new DropShadow());
VBox root = new VBox();
root.getChildren().addAll(b1);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Testing LayoutBounds");
stage.show();
System.out.println("b1(layoutBounds)=" +
b1.getLayoutBounds());
System.out.println("b1(boundsInLocal)=" +
b1.getBoundsInLocal());
}
}
b1(layoutBounds)=BoundingBox [minX:0.0, minY:0.0, minZ:0.0, width:57.0, height:23.0, depth:0.0, maxX:57.0, maxY:23.0, maxZ:0.0]
b1(boundsInLocal)=BoundingBox [minX:-9.0, minY:-9.0, minZ:0.0, width:75.0, height:42.0, depth:0.0, maxX:66.0, maxY:33.0, maxZ:0.0]
Listing 6-2Accessing the boundsInLocal Property of a Node
什么时候使用节点的boundsInLocal?当你需要包含一个节点的效果和剪辑时,你可以使用boundsInLocal。假设你有一个带反射的Text节点,你想让它垂直居中。如果你使用Text节点的layoutBounds,它只会将节点的文本部分居中,而不会包括反射。如果您使用boundsInLocal,它将使文本与其倒影居中。另一个例子是检查有影响的球的碰撞。如果两个球之间发生碰撞,当一个球在另一个球的边界内移动,包括它们的效果,使用球的boundsInLocal。如果碰撞只发生在它们的几何边界相交时,使用layoutBounds。
boundsInParent 属性
节点的boundsInParent属性位于其父节点的坐标空间中。它包括节点、效果、剪辑和变换的几何属性。它很少直接用在代码中。
群的界限
一个Group的layoutBounds、boundsInLocal和boundsInParent的计算不同于一个节点的计算。一个Group承担其子节点的集合边界。您可以对每个Group子对象分别应用效果、剪辑和变换。您还可以直接在Group上应用效果、剪辑和变换,它们会应用到它的所有子节点。
一个Group的layoutBounds是其所有子节点的boundsInParent的并集。它包括直接应用到子对象的效果、剪辑和变换。它不包括直接应用于Group的效果、剪辑和变换。Group的boundsInLocal是通过取其layoutBounds并包括直接应用于Group的效果和剪辑来计算的。Group的boundsInParent通过取其boundsInLocal并包括直接应用于Group的变换来计算。
当您想要为应该包括效果、剪辑和变换的节点分配空间时,您需要尝试将节点包装在Group中。假设您有一个带有效果和变换的节点,并且您只想为它的效果而不是它的变换分配布局空间。您可以通过在节点上应用效果并将其包装在一个Group中,然后在Group上应用变换来实现这一点。
一个关于界限的详细例子
在这一节中,我将通过一个例子向您展示如何计算节点的边界。在本例中,您将使用一个矩形及其不同的属性、效果和变换。
请考虑下面的代码片段,它创建了一个 50 x 20 的矩形,并将其放置在矩形的局部坐标空间中的(0,0)处。生成的矩形如图 6-12 所示,其中显示了父节点的轴和节点未变换的局部轴(本例中为矩形),此时是相同的:
图 6-12
一个 50 x 20 的矩形,放置在(0,0)处,没有任何效果和变换
Rectangle r = new Rectangle(0, 0, 50, 20);
r.setFill(Color.GRAY);
矩形的三种边界是相同的,如下所示:
layoutBounds[minX=0.0, minY=0.0, width=50.0, height=20.0]
boundsInLocal[minX=0.0, minY=0.0, width=50.0, height=20.0]
boundsInParent[minX=0.0, minY=0.0, width=50.0, height=20.0]
让我们修改矩形,将其放置在(75,50)处,如下所示:
Rectangle r = new Rectangle(75, 50, 50, 20);
结果节点如图 6-13 所示。
图 6-13
放置在(75,50)处的 50 乘 20 的矩形,没有效果和变换
父节点和节点的轴仍然相同。所有界限都是相同的,如下所示。所有边界框的左上角已经移动到(75,50),宽度和高度都相同:
layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInParent[minX=75.0, minY=50.0, width=50.0, height=20.0]
让我们修改矩形,并给它一个阴影效果,如下所示:
Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
结果节点如图 6-14 所示。
图 6-14
放置在(75,50)处的一个 50 x 20 的矩形,带有投影,没有变换
父节点和节点的轴仍然相同。现在,layoutBounds没有改变。为了适应投影效果,boundsInLocal和boundsInParent已经改变,它们具有相同的值。回想一下,boundsInLocal是在节点的未变换坐标空间中定义的,而boundsInParent是在父节点的坐标空间中定义的。在这种情况下,两个坐标空间是相同的。因此,两个边界的相同值定义了相同的边界框。界限的值如下:
layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=66.0, minY=41.0, width=68.0, height=38.0]
让我们修改前面的矩形,使其具有(150,75)的(x,y)平移,如下所示:
Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
r.getTransforms().add(new Translate(150, 75));
结果节点如图 6-15 所示。转换(在本例中是平移)转换了节点的坐标空间,结果,您看到的是被转换的节点。在这种情况下,您需要考虑三个坐标空间:父节点的坐标空间以及节点的未转换和已转换坐标空间。layoutBounds和boundsInParent是相对于节点未变换的局部坐标空间。boundsInParent是相对于父对象的坐标空间。图 6-15 显示了游戏中的所有坐标空间。界限的值如下:
图 6-15
一个 50 x 20 的矩形,放置在(75,50)处,带有投影和(150,75)平移
layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=216.0, minY=116.0, width=68.0, height=38.0]
让我们修改矩形,使其具有(150,75)的(x,y)平移和 30 度顺时针旋转:
Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
r.getTransforms().addAll(new Translate(150, 75), new Rotate(30));
产生的节点如图 6-16 所示。请注意,平移和旋转已应用于矩形的局部坐标空间,矩形出现在相对于其变换后的局部坐标轴的相同位置。layoutBounds和boundsInLocal保持不变,因为你没有改变矩形的几何形状和效果。boundsInParent已经改变,因为你添加了一个旋转。界限的值如下:
图 6-16
一个 50 x 20 的矩形,放置在(75,50)处,带有投影,平移(150,75),顺时针旋转 30 度
layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=167.66, minY=143.51, width=77.89, height=66.91]
作为最后一个示例,您将向矩形添加缩放和剪切变换:
Rectangle r = new Rectangle(75, 50, 50, 20);
r.setEffect(new DropShadow());
r.getTransforms().addAll(new Translate(150, 75), new Rotate(30),
new Scale(1.2, 1.2), new Shear(0.30, 0.10));
结果节点如图 6-17 所示。
图 6-17
一个放置在(75,50)处的 50 乘 20 的矩形,带有投影,一个(150,75)平移,一个 30 度顺时针旋转,一个 1.2 英寸的 x 和 y 缩放,以及一个 0.30 x 剪切和 0.10 y 剪切
请注意,只有boundsInParent发生了变化。界限的值如下:
layoutBounds[minX=75.0, minY=50.0, width=50.0, height=20.0]
boundsInLocal[minX=66.0, minY=41.0, width=68.0, height=38.0]
boundsInParent[minX=191.86, minY=171.45, width=77.54, height=94.20]
对于初学者来说,掌握节点不同类型界限背后的概念并不容易。初学者是第一次学习某样东西的人。我开始是一个初学者,学习边界。在学习过程中,另一个美丽的概念以及它在 JavaFX 程序中的实现出现了。这个程序是一个非常详细的演示应用程序,它帮助您直观地理解改变节点的状态是如何影响边界的。您可以保存带有所有坐标轴的场景图形。您可以运行清单 6-3 中所示的NodeBoundsApp类来查看本节中的所有示例。
// NodeBoundsApp.java
package com.jdojo.node;
...
public class NodeBoundsApp extends Application {
// The code for this class is not included here as it is very big.
// Please refer to the source code. You can download the source code
// for all programs in this book from
// http://www.apress.com/source-code
}
Listing 6-3Computing the Bounds of a Node
使用布局和布局定位节点
如果您不理解所有与布局相关的属性背后的细节和原因,那么在 JavaFX 中布置节点是非常令人困惑的。Node类有两个属性,layoutX和layoutY,分别定义其坐标空间沿 x 轴和 y 轴的平移。Node类有做同样事情的translateX和translateY属性。节点坐标空间的最终平移是两者之和:
finalTranslationX = layoutX + translateX
finalTranslationY = layoutY + translateY
为什么有两个属性来定义同类翻译?原因很简单。它们的存在是为了在不同的情况下获得相似的结果。使用layoutX和layoutY定位稳定布局的节点。使用translateX和translateY为动态布局定位一个节点,例如在动画过程中。
记住layoutX和layoutY属性不指定节点的最终位置是很重要的。它们是应用于节点的坐标空间的平移。当您计算layoutX和layoutY的值以将节点定位在特定位置时,您需要考虑layoutBounds的minX和minY值。要将节点边界框的左上角定位在finalX和finalY,请使用以下公式:
layoutX = finalX - node.getLayoutBounds().getMinX()
layoutY = finalY - node.getLayoutBounds().getMinY()
Tip
Node类有一个方便的方法relocate(double finalX, double finalY),将节点定位在(finalX, finalY)位置。该方法计算并正确设置layoutX和layoutY值,考虑layoutBounds的minX和minY值。为了避免错误和节点的错位,我更喜欢使用relocate()方法,而不是setLayoutX()和setLayoutY()方法。
有时,设置节点的layoutX和layoutY属性可能无法将它们定位在其父节点内的所需位置。如果遇到这种情况,请检查父类型。大多数父母是Region类的子类,他们使用自己的定位策略,忽略孩子的layoutX和layoutY设置。比如HBox和VBox使用自己的定位策略,他们会忽略layoutX和layoutY的值给孩子。
下面的代码片段将忽略两个按钮的layoutX和layoutY值,因为它们被放在使用自己的定位策略的VBox中。最终布局如图 6-18 所示。
图 6-18
两个按钮使用layoutX和layoutY属性并放置在一个VBox内
Button b1 = new Button("OK");
b1.setLayoutX(20);
b1.setLayoutY(20);
Button b2 = new Button("Cancel");
b2.setLayoutX(50);
b2.setLayoutY(50);
VBox vb = new VBox();
vb.getChildren().addAll(b1, b2);
如果您想完全控制一个节点在其父节点中的位置,请使用Pane或Group。一个Pane是一个Region,不定位其子节点。您需要使用layoutX和layoutY属性来定位孩子。下面的代码片段将布局两个按钮,如图 6-19 所示,其中显示了坐标网格,线之间相隔 10px:
图 6-19
使用layoutX和layoutY属性的两个按钮,放置在Group或Pane中
Button b1 = new Button("OK");
b1.setLayoutX(20);
b1.setLayoutY(20);
Button b2 = new Button("Cancel");
b2.setLayoutX(50);
b2.setLayoutY(50);
Group parent = new Group(); //Or. Pane parent = new Pane();
parent.getChildren().addAll(b1, b2);
设置节点的大小
每个节点都有一个大小(宽度和高度),可以更改。也就是说,每个节点都可以调整大小。有两种类型的节点:可调整大小的节点和不可调整大小的节点。前面两句话不矛盾吗?答案是肯定的,也是否定的。的确,每个节点都有调整大小的潜力。但是,可调整大小的节点意味着在布局过程中,节点可以由其父节点调整大小。例如,按钮是可调整大小的节点,矩形是不可调整大小的节点。当一个按钮被放置在一个容器中时,例如,在一个HBox中,HBox决定了按钮的最佳大小。HBox根据按钮显示所需的空间和HBox可用的空间来调整按钮的大小。当一个矩形被放置在一个HBox中时,HBox并不决定它的大小;相反,它使用应用程序指定的矩形大小。
Tip
在布局过程中,可调整大小的节点可以由其父节点调整大小。在布局过程中,不可调整大小的节点不会被其父节点调整大小。如果要调整不可调整大小的节点的大小,需要修改影响其大小的属性。例如,要调整矩形的大小,您需要更改它的width和height属性。Region、Control和WebView是可调整大小的节点的例子。Group、Text和Shape是不可调整大小的节点的例子。
如何知道一个节点是否可以调整大小?Node类中的isResizable()方法为可调整大小的节点返回true;对于不可调整大小的节点,它返回false。
清单 6-4 中的程序显示了布局期间可调整大小和不可调整大小的节点的行为。它向一个HBox添加一个按钮和一个矩形。运行程序后,缩短载物台的宽度。当按钮显示省略号(…)时,它会变得更小。矩形始终保持相同的大小。图 6-20 显示了调整尺寸过程中三个不同点的载物台。
图 6-20
调整舞台大小后,以全尺寸显示的按钮和矩形
// ResizableNodeTest.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ResizableNodeTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button btn = new Button("A big button");
Rectangle rect = new Rectangle(100, 50);
rect.setFill(Color.WHITE);
rect.setStrokeWidth(1);
rect.setStroke(Color.BLACK);
HBox root = new HBox();
root.setSpacing(20);
root.getChildren().addAll(btn, rect);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Resizable Nodes");
stage.show();
System.out.println("btn.isResizable(): " +
btn.isResizable());
System.out.println("rect.isResizable(): " +
rect.isResizable());
}
}
btn.isResizable(): true
rect.isResizable(): false
Listing 6-4A Button and a Rectangle in an HBox
可调整大小的节点
可调整大小的节点的实际大小由两个因素决定:
-
放置节点的容器的大小调整策略
-
由节点本身指定的大小范围
每个容器都有一个针对其子容器的调整大小策略。我将在第十章讨论容器的尺寸调整策略。一个可调整大小的节点可以指定其大小的范围(宽度和高度),这应该被一个用于布局节点的荣誉容器所考虑。可调整大小的节点指定构成其大小范围的三种类型的大小:
-
首选尺寸
-
最小尺寸
-
最大尺寸
节点的首选大小是显示其内容的理想宽度和高度。例如,根据图像、文本、字体和文本换行等当前属性,一个按钮的首选大小足以显示其所有内容。节点的最小尺寸是它想要的最小宽度和高度。例如,最小尺寸的按钮足以显示图像和文本的省略号。节点的最大尺寸是它想要的最大宽度和高度。在按钮的情况下,按钮的最大大小与其首选大小相同。有时,您可能希望将节点扩展到无限大小。在这些情况下,最大宽度和高度被设置为Double.MAX_VALUE。
大多数可调整大小的节点根据其内容和属性设置自动计算其首选、最小和最大大小。这些尺寸被称为它们的内在尺寸。Region和Control类定义了两个常量,作为节点固有大小的标记值。这些常量是
-
USE_COMPUTED_SIZE -
USE_PREF_SIZE
两个常量都是double类型。USE_COMPUTED_SIZE和USE_PREF_SIZE的值分别为–1 和Double.NEGATIVE_INFINITY。没有记载为什么相同的常量被定义了两次。也许设计者不想将它们在类层次结构中上移,因为它们不适用于所有类型的节点。
如果节点的大小设置为 sentinel 值USE_COMPUTED_SIZE,节点将根据其内容和属性设置自动计算该大小。USE_PREF_SIZE标记值用于设置最小和最大尺寸,如果它们与首选尺寸相同的话。
Region和Control类有六个DoubleProperty类型的属性来定义它们的宽度和高度的首选值、最小值和最大值:
-
prefWidth -
prefHeight -
minWidth -
minHeight -
maxWidth -
maxHeight
默认情况下,这些属性被设置为标记值USE_COMPUTED_SIZE。这意味着节点会自动计算这些大小。您可以设置这些属性之一来覆盖节点的固有大小。例如,您可以将按钮的首选、最小和最大宽度设置为 50 像素,如下所示:
Button btn = new Button("Close");
btn.setPrefWidth(50);
btn.setMinWidth(50);
btn.setMaxWidth(50);
前面的代码片段将按钮的首选宽度、最小宽度和最大宽度设置为相同的值,使按钮在水平方向不可调整大小。
以下代码片段将按钮的最小和最大宽度设置为首选宽度,其中首选宽度本身是内部计算的:
Button btn = new Button("Close");
btn.setMinWidth(Control.USE_PREF_SIZE);
btn.setMaxWidth(Control.USE_PREF_SIZE);
Tip
在大多数情况下,节点的首选、最小和最大大小的内部计算值是合适的。仅当内部计算的大小不满足应用程序的需要时,才使用这些属性来重写内部计算的大小。如果您需要将一个节点的大小绑定到一个表达式,您将需要绑定prefWidth和prefHeight属性。
如何获得节点的实际首选、最小和最大大小?您可能会猜测您可以使用getPrefWidth()、getPrefHeight()、getMinWidth()、getMinHeight()、getMaxWidth()和getMaxHeight()方法来获得它们。但是您不应该使用这些方法来获取节点的实际大小。这些大小可以设置为 sentinel 值,节点将在内部计算实际大小。这些方法返回标记值或覆盖值。清单 6-5 创建了两个按钮,并将其中一个按钮的首选固有宽度覆盖为 100 像素。产生的屏幕如图 6-21 所示。以下输出证明,这些方法对于了解用于布局目的的节点的实际大小不是很有用。
图 6-21
按钮使用 sentinel 并覆盖其宽度值
// NodeSizeSentinelValues.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class NodeSizeSentinelValues 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");
// Override the intrinsic width of the cancel button
cancelBtn.setPrefWidth(100);
VBox root = new VBox();
root.getChildren().addAll(okBtn, cancelBtn);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Overriding Node Sizes");
stage.show();
System.out.println("okBtn.getPrefWidth(): " +
okBtn.getPrefWidth());
System.out.println("okBtn.getMinWidth(): " +
okBtn.getMinWidth());
System.out.println("okBtn.getMaxWidth(): " +
okBtn.getMaxWidth());
System.out.println("cancelBtn.getPrefWidth(): " +
cancelBtn.getPrefWidth());
System.out.println("cancelBtn.getMinWidth(): " +
cancelBtn.getMinWidth());
System.out.println("cancelBtn.getMaxWidth(): " +
cancelBtn.getMaxWidth());
}
}
okBtn.getPrefWidth(): -1.0
okBtn.getMinWidth(): -1.0
okBtn.getMaxWidth(): -1.0
cancelBtn.getPrefWidth(): 100.0
cancelBtn.getMinWidth(): -1.0
cancelBtn.getMaxWidth(): -1.0
Listing 6-5Using getXXXWidth() and getXXXHeight() Methods of Regions and Controls
要获得节点的实际大小,需要在Node类中使用以下方法。请注意,Node类没有定义任何与大小相关的属性。与尺寸相关的属性在Region、Control和其他类中定义。
-
double prefWidth(double height) -
double prefHeight(double width) -
double minWidth(double height) -
double minHeight(double width) -
double maxWidth(double height) -
double maxHeight(double width)
在这里,您可以看到获取节点实际大小的另一个变化。您需要传递它的高度值来获得它的宽度,反之亦然。对于 JavaFX 中的大多数节点,宽度和高度是独立的。但是,对于某些节点,高度取决于宽度,反之亦然。当一个节点的宽度依赖于它的高度时,或者反之亦然,该节点被称为具有内容偏差。如果一个节点的高度取决于它的宽度,那么这个节点有一个水平内容偏差。如果一个节点的宽度取决于它的高度,那么这个节点有一个垂直内容偏差。请注意,一个节点不能同时具有水平和垂直内容偏好,这将导致循环依赖。
Node类的getContentBias()方法返回一个节点的内容偏差。它的返回类型是javafx.geometry.Orientation枚举类型,有两个常量:HORIZONTAL和VERTICAL。如果一个节点没有内容偏向,例如Text或ChoiceBox,该方法返回null。
所有属于Labeled类的子类的控件,例如Label、Button或CheckBox,当它们启用了文本换行属性时,都有一个HORIZONTAL内容偏好。对于某些节点,它们的内容偏向取决于它们的方向。比如一个FlowPane的方位是HORIZONTAL,它的内容偏置是HORIZONTAL;如果它的方向是VERTICAL,那么它的内容偏差就是VERTICAL。
您应该使用上面列出的六种方法来获得用于布局目的的节点的大小。如果一个节点类型没有内容偏向,您需要将–1 作为另一个维度的值传递给这些方法。例如,ChoiceBox没有内容偏好,您将获得其首选大小,如下所示:
ChoiceBox choices = new ChoiceBox();
...
double prefWidth = choices.prefWidth(-1);
double prefHeight = choices.prefHeight(-1);
对于那些有内容偏向的节点,您需要传递偏向的维度来获得另一个维度。例如,对于一个按钮,它有一个HORIZONTAL内容偏差,您可以传递–1 来获得它的宽度,并且可以传递它的宽度值来获得它的高度,如下所示:
Button b = new Button("Hello JavaFX");
// Enable text wrapping for the button, which will change its
// content bias from null (default) to HORIZONTAL
b.setWrapText(true);
...
double prefWidth = b.prefWidth(-1);
double prefHeight = b.prefHeight(prefWidth);
如果按钮没有启用文本换行属性,您可以将–1 传递给方法prefWidth()和prefHeight(),因为它没有内容偏向。
获取用于布局目的的节点的宽度和高度的一般方法概述如下。该代码显示了如何获取首选的宽度和高度,该代码类似于获取节点的最小和最大宽度和高度:
Node node = get the reference of the node;
...
double prefWidth = -1;
double prefHeight = -1;
Orientation contentBias = b.getContentBias();
if (contentBias == HORIZONTAL) {
prefWidth = node.prefWidth(-1);
prefHeight = node.prefHeight(prefWidth);
} else if (contentBias == VERTICAL) {
prefHeight = node.prefHeight(-1);
prefWidth = node.prefWidth(prefHeight);
} else {
// contentBias is null
prefWidth = node.prefWidth(-1);
prefHeight = node.prefHeight(-1);
}
现在您知道了如何获得节点的首选、最小和最大大小的指定值和实际值。这些值表示节点大小的范围。当一个节点被放置在一个容器中时,容器会尝试给这个节点一个自己喜欢的大小。但是,根据容器的策略和指定的节点大小,节点可能无法获得其首选大小。相反,一个荣誉容器会给一个节点一个在其指定范围内的大小。这被称为电流大小。如何获得一个节点的当前大小?Region和Control类定义了两个只读属性width和i ght,它们保存了节点的当前宽度和高度值。
现在让我们看看所有这些方法的实际应用。清单 6-6 将一个按钮放在一个HBox中,为按钮打印不同类型的尺寸,更改一些属性,并再次打印按钮的尺寸。以下输出显示,随着按钮的首选宽度变小,其首选高度变大。
// NodeSizes.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class NodeSizes extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button btn = new Button("Hello JavaFX!");
HBox root = new HBox();
root.getChildren().addAll(btn);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Sizes of a Node");
stage.show();
// Print button's sizes
System.out.println("Before changing button properties:");
printSizes(btn);
// Change button's properties
btn.setWrapText(true);
btn.setPrefWidth(80);
stage.sizeToScene();
// Print button's sizes
System.out.println(
"\nAfter changing button properties:");
printSizes(btn);
}
public void printSizes(Button btn) {
System.out.println("btn.getContentBias() = " +
btn.getContentBias());
System.out.println("btn.getPrefWidth() = " +
btn.getPrefWidth() +
", btn.getPrefHeight() = " + btn.getPrefHeight());
System.out.println("btn.getMinWidth() = " +
btn.getMinWidth() +
", btn.getMinHeight() = " + btn.getMinHeight());
System.out.println("btn.getMaxWidth() = " +
btn.getMaxWidth() +
", btn.getMaxHeight() = " + btn.getMaxHeight());
double prefWidth = btn.prefWidth(-1);
System.out.println("btn.prefWidth(-1) = " + prefWidth +
", btn.prefHeight(prefWidth) = " +
btn.prefHeight(prefWidth));
double minWidth = btn.minWidth(-1);
System.out.println("btn.minWidth(-1) = " + minWidth +
", btn.minHeight(minWidth) = " +
btn.minHeight(minWidth));
double maxWidth = btn.maxWidth(-1);
System.out.println("btn.maxWidth(-1) = " + maxWidth +
", btn.maxHeight(maxWidth) = " +
btn.maxHeight(maxWidth));
System.out.println("btn.getWidth() = " + btn.getWidth() +
", btn.getHeight() = " + btn.getHeight());
}
}
Before changing button properties:
btn.getContentBias() = null
btn.getPrefWidth() = -1.0, btn.getPrefHeight() = -1.0
btn.getMinWidth() = -1.0, btn.getMinHeight() = -1.0
btn.getMaxWidth() = -1.0, btn.getMaxHeight() = -1.0
btn.prefWidth(-1) = 107.0, btn.prefHeight(prefWidth) = 22.8984375
btn.minWidth(-1) = 37.0, btn.minHeight(minWidth) = 22.8984375
btn.maxWidth(-1) = 107.0, btn.maxHeight(maxWidth) = 22.8984375
btn.getWidth() = 107.0, btn.getHeight() = 23.0
After changing button properties:
btn.getContentBias() = HORIZONTAL
btn.getPrefWidth() = 80.0, btn.getPrefHeight() = -1.0
btn.getMinWidth() = -1.0, btn.getMinHeight() = -1.0
btn.getMaxWidth() = -1.0, btn.getMaxHeight() = -1.0
btn.prefWidth(-1) = 80.0, btn.prefHeight(prefWidth) = 39.796875
btn.minWidth(-1) = 37.0, btn.minHeight(minWidth) = 22.8984375
btn.maxWidth(-1) = 80.0, btn.maxHeight(maxWidth) = 39.796875
btn.getWidth() = 80.0, btn.getHeight() = 40.0
Listing 6-6Using Different Size-Related Methods of a Node
获取或设置可调整大小的节点的方法还不止这些。有一些方便的方法可以用来执行与本节中讨论的方法相同的任务。表 6-2 列出了与尺寸相关的方法及其定义类别和用法。
表 6-2
可调整大小的节点的大小相关方法
|方法/属性
|
定义类别
|
使用
|
| --- | --- | --- |
| 属性:prefWidth``prefHeight``minWidth``minHeight``maxWidth``maxHeight | Region,Control | 它们定义了首选、最小和最大尺寸。默认情况下,它们被设置为标记值。使用它们来覆盖默认值。 |
| 方法:double prefWidth(double h)``double prefHeight(double w)``double minWidth(double h)``double minHeight(double w)``double maxWidth(double h)``double maxHeight(double w) | Node | 使用它们来获得节点的实际大小。如果节点没有内容偏向,则传递–1 作为参数。如果节点有内容偏差,则将另一维的实际值作为参数传递。请注意,这些方法没有对应的属性。 |
| 属性:width``height | Region,Control | 这些是只读的属性,保存可调整大小的节点的当前宽度和高度。 |
| 方法:void setPrefSize(double w, double h)``void setMinSize(double w, double h)``void setMaxSize(double w, double h) | Region,Control | 这些是覆盖节点的默认计算宽度和高度的方便方法。 |
| 方法:void resize(double w, double h) | Node | 它将节点调整到指定的宽度和高度。它由节点的父节点在布局期间调用。您不应该在代码中直接调用此方法。如果您需要设置节点的大小,请使用setMinSize()、setPrefSize()或setMaxSize()方法。此方法对不可调整大小的节点无效。 |
| 方法:void autosize() | Node | 对于可调整大小的节点,它将布局边界设置为其当前首选的宽度和高度。它会处理内容偏差。此方法对不可调整大小的节点无效。 |
不可调整的节点
在布局过程中,不可调整大小的节点不会被其父节点调整大小。但是,您可以通过更改它们的属性来更改它们的大小。不可调整大小的节点(例如,所有形状)具有决定其大小的不同属性。例如,矩形的宽度和高度、圆的半径以及直线的(startX、startY、endX、endY)决定了它们的大小。
在Node类中定义了几个与大小相关的方法。当在不可调整大小的节点上调用这些方法或它们返回当前大小时,这些方法不起作用。例如,在不可调整大小的节点上调用Node类的resize(double w, double h)方法没有任何效果。对于不可调整大小的节点,Node类中的prefWidth(double h)、minWidth(double h)和maxWidth(double h)方法返回其layoutBounds宽度;而prefHeight(double w)、minHeight(double w)和maxHeight(double w)方法返回其layoutBounds高度。不可调整大小的节点没有内容偏见。将–1 作为其他维度的参数传递给所有这些方法。
在节点中存储用户数据
每个节点都维护一个用户定义属性(键/值对)的可见映射。你可以用它来储存任何有用的信息。假设您有一个TextField让用户操作一个人的名字。您可以将最初从数据库中检索到的人名存储为TextField的属性。您可以稍后使用该属性来重置名称,或者生成一个UPDATE语句来更新数据库中的名称。属性的另一个用途是存储微帮助文本。当节点获得焦点时,您可以读取它的 micro help 属性并显示它,例如,在状态栏中,以帮助用户理解节点的用法。
Node类的getProperties()方法返回一个ObservableMap<Object, Object>,您可以在其中添加或删除节点的属性。以下代码片段将带有值"Advik"的属性"originalData"添加到TextField节点:
TextField nameField = new TextField();
...
ObservableMap<Object, Object> props = nameField.getProperties();
props.put("originalData", "Advik");
以下代码片段从nameField节点读取"originalData"属性的值:
ObservableMap<Object, Object> props = nameField.getProperties();
if (props.containsKey("originalData")) {
String originalData = (String)props.get("originalData");
} else {
// originalData property is not set yet
}
Node类有两个方便的方法,setUserData(Object value)和getUserData(),用来存储用户定义的值作为节点的属性。在setUserData()方法中指定的value使用相同的ObservableMap来存储getProperties()方法返回的数据。Node类使用内部的Object作为键来存储值。您需要使用getUserData()方法来获取使用setUserData()方法存储的值,如下所示:
nameField.setUserData("Saved"); // Set the user data
...
String userData = (String)nameField.getUserData(); // Get the user data
Tip
除非使用getUserData()方法,否则不能直接访问节点的用户数据。因为它存储在由getProperties()方法返回的同一个ObservableMap中,所以您可以通过迭代该映射中的值来间接访问它。
Node类有一个hasProperties()方法。您可以使用它来查看是否为该节点定义了任何属性。
什么是受管节点?
Node类有一个托管属性,它的类型是BooleanProperty。默认情况下,所有节点都被管理。受管节点的布局由其父节点管理。一个Parent节点在计算自己的大小时会考虑到它所有被管理的子节点的layoutBounds。一个Parent节点负责调整其托管的可调整大小的子节点的大小,并根据其布局策略定位它们。当被管理子节点的layoutBounds发生变化时,场景图的相关部分被重新显示。
如果一个节点是非托管的,应用程序单独负责布局(计算它的大小和位置)。也就是说,Parent节点不布局它的非托管子节点。非托管节点的layoutBounds中的变化不会触发其上的重新布局。非托管的Parent节点充当布局根。如果一个子节点调用了Parent.requestLayout()方法,那么只有以非托管Parent节点为根的分支才会被重发。
Tip
对比Node类的visible属性和它的managed属性。出于布局目的,Parent节点会考虑其所有不可见子节点的layoutBounds,并忽略非托管子节点。
什么时候使用非托管节点?通常,您不需要在应用程序中使用非托管节点,因为它们需要您做额外的工作。然而,只要知道它们的存在,如果需要的话,你就可以使用它们。
当您想在容器中显示一个节点而不考虑它的layoutBounds时,您可以使用一个非托管节点。您需要自己调整节点的大小和位置。清单 6-7 演示了如何使用非托管节点。当一个节点获得焦点时,它使用一个非托管的Text节点来显示一个微帮助。该节点需要有一个名为"microHelpText"的属性。当显示微帮助时,整个应用程序的布局不会被打乱,因为显示微帮助的Text节点是一个非托管节点。您在focusChanged()方法中将节点放置在适当的位置。该程序向场景的focusOwner属性注册了一个更改监听器,因此当场景内的焦点发生变化时,您可以显示或隐藏微帮助Text节点。当两个不同的节点具有焦点时,产生的屏幕如图 6-22 所示。注意,在这个例子中,定位Text节点很容易,因为所有节点都在同一个父节点GridPane中。如果节点放在不同的父节点中,定位Text节点的逻辑变得复杂。
图 6-22
使用非托管的Text节点显示微帮助
// MicroHelpApp.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class MicroHelpApp extends Application {
// An instance variable to store the Text node reference
private Text helpText = new Text();
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
TextField fName = new TextField();
TextField lName = new TextField();
TextField salary = new TextField();
Button closeBtn = new Button("Close");
closeBtn.setOnAction(e -> Platform.exit());
fName.getProperties().put("microHelpText",
"Enter the first name");
lName.getProperties().put("microHelpText",
"Enter the last name");
salary.getProperties().put("microHelpText",
"Enter a salary greater than $2000.00.");
// The help text node is unmanaged
helpText.setManaged(false);
helpText.setTextOrigin(VPos.TOP);
helpText.setFill(Color.RED);
helpText.setFont(Font.font(null, 9));
helpText.setMouseTransparent(true);
// Add all nodes to a GridPane
GridPane root = new GridPane();
root.add(new Label("First Name:"), 1, 1);
root.add(fName, 2, 1);
root.add(new Label("Last Name:"), 1, 2);
root.add(lName, 2, 2);
root.add(new Label("Salary:"), 1, 3);
root.add(salary, 2, 3);
root.add(closeBtn, 3, 3);
root.add(helpText, 4, 3);
Scene scene = new Scene(root, 300, 100);
// Add a change listener to the scene, so you know when
// the focus owner changes and display the micro help
scene.focusOwnerProperty().addListener(
(ObservableValue<? extends Node> value,
Node oldNode, Node newNode)
-> focusChanged(value, oldNode, newNode));
stage.setScene(scene);
stage.setTitle("Showing Micro Help");
stage.show();
}
public void focusChanged(ObservableValue<? extends Node> value,
Node oldNode, Node newNode) {
// Focus has changed to a new node
String microHelpText =
(String)newNode.getProperties().get("microHelpText");
if (microHelpText != null &&
microHelpText.trim().length() > 0) {
helpText.setText(microHelpText);
helpText.setVisible(true);
// Position the help text node
double x = newNode.getLayoutX() +
newNode.getLayoutBounds().getMinX() –
helpText.getLayoutBounds().getMinX();
double y = newNode.getLayoutY() +
newNode.getLayoutBounds().getMinY() +
newNode.getLayoutBounds().getHeight() -
helpText.getLayoutBounds().getMinX();
helpText.setLayoutX(x);
helpText.setLayoutY(y);
helpText.setWrappingWidth(
newNode.getLayoutBounds().getWidth());
}
else {
helpText.setVisible(false);
}
}
}
Listing 6-7Using an Unmanaged Text Node to Show Micro Help
有时,如果某个节点变得不可见,您可能希望使用该节点所使用的空间。假设你有一个有几个按钮的HBox。当其中一个按钮变得不可见时,您希望从右向左滑动所有按钮。可以在VBox中实现上滑效果。通过将节点的managed属性绑定到visible属性,很容易在HBox和VBox(或任何其他具有相对定位的容器)中实现滑动效果。清单 6-8 展示了如何在HBox中实现向左滑动的特性。它显示四个按钮。第一个按钮用于使第三个按钮b2可见和不可见。b2按钮的托管属性绑定到它的 visible 属性:
b2.managedProperty().bind(b2.visibleProperty());
当b2按钮变得不可见时,它就变得不受管理,并且HBox在计算它自己的layoutBounds时不使用它的layoutBounds。这使得b3按钮向左滑动。图 6-23 显示了应用程序运行时的两个屏幕截图。
图 6-23
模拟 B2 按钮的向左滑动功能
// SlidingLeftNodeTest.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.beans.binding.When;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class SlidingLeftNodeTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Button b1 = new Button("B1");
Button b2 = new Button("B2");
Button b3 = new Button("B3");
Button visibleBtn = new Button("Make Invisible");
// Add an action listener to the button to make
// b2 visible if it is invisible and invisible if it
// is visible
visibleBtn.setOnAction(e ->
b2.setVisible(!b2.isVisible()));
// Bind the text property of the button to the visible
// property of the b2 button
visibleBtn.textProperty().bind(
new When(b2.visibleProperty())
.then("Make Invisible")
.otherwise("Make Visible"));
// Bind the managed property of b2 to its visible
// property
b2.managedProperty().bind(b2.visibleProperty());
HBox root = new HBox();
root.getChildren().addAll(visibleBtn, b1, b2, b3);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Sliding to the Left");
stage.show();
}
}
Listing 6-8Simulating the Slide-Left Feature Using Unmanaged Nodes
变换坐标空间之间的界限
我已经介绍了节点使用的坐标空间。有时,您可能需要将一个Bounds或一个点从一个坐标空间转换到另一个坐标空间。Node类包含几个方法来支持这一点。支持以下Bounds或点的变换:
-
本地到父
-
本地到场景
-
父到本地
-
场景到本地
localToParent()方法将节点的本地坐标空间中的一个Bounds或一个点转换到其父节点的坐标空间。localToScene()方法将节点的局部坐标空间中的一个Bounds或一个点转换到其场景的坐标空间。parentToLocal()方法将节点的父节点的坐标空间中的一个Bounds或一个点转换到该节点的局部坐标空间。sceneToLocal()方法将节点的场景坐标空间中的Bounds或点转换到该节点的局部坐标空间。所有方法都有三个重载版本;一个版本将一个Bounds作为参数,并返回转换后的Bounds;另一个版本将一个Point2D作为参数,并返回转换后的Point2D;另一个版本获取一个点的 x 和 y 坐标,并返回转换后的Point2D。
这些方法足以将一个坐标空间中的点的坐标变换到场景图形中的另一个坐标空间。有时,您可能需要将节点的局部坐标空间中的点的坐标转换到舞台或屏幕的坐标空间。您可以使用Scene和Stage类的x和y属性来实现这一点。场景的(x,y)属性定义了场景左上角在其舞台坐标空间中的坐标。舞台的(x,y)属性定义了屏幕坐标空间中舞台左上角的坐标。例如,如果(x1,y1)是场景坐标空间中的一个点,(x1 + x2,y1 + y2)定义了舞台坐标空间中的同一点,其中 x2 和 y2 分别是舞台的x和y属性。应用相同的逻辑来获得屏幕坐标空间中的点的坐标。
让我们看一个使用节点、其父节点及其场景的坐标空间之间的变换的例子。一个场景有三个Label和三个TextField放置在不同的父对象下。一个红色的小圆圈被放置在具有焦点的节点的边界框的左上角。随着焦点的改变,需要计算圆的位置,该位置与当前节点左上角相对于圆的父节点的位置相同。圆心需要与具有焦点的节点的左上角重合。图 6-24 显示焦点在名和姓节点的阶段。清单 6-9 有完整的程序来实现这一点。
图 6-24
使用坐标空间变换将圆移动到焦点节点
该节目有一个由三个Label和TextField组成的场景,一对Label和一对TextField被放置在一个HBox中。所有的HBox都放在一个VBox中。一个非托管的Circle被放置在VBox中。该程序向场景的focusOwner属性添加了一个变化监听器来跟踪焦点变化。当焦点改变时,圆被放置在具有焦点的节点的左上角。
placeMarker()包含主逻辑。它获取局部坐标空间中焦点节点边界框左上角的(x,y)坐标:
double nodeMinX = newNode.getLayoutBounds().getMinX();
double nodeMinY = newNode.getLayoutBounds().getMinY();
它将节点左上角的坐标从局部坐标空间转换到场景的坐标空间:
Point2D nodeInScene = newNode.localToScene(nodeMinX, nodeMinY);
现在节点左上角的坐标从场景的坐标空间转换到圆的坐标空间,程序中命名为marker:
Point2D nodeInMarkerLocal = marker.sceneToLocal(nodeInScene);
最后,节点左上角的坐标被转换到圆的父节点的坐标空间:
Point2D nodeInMarkerParent = marker.localToParent(nodeInMarkerLocal);
此时,nodeInMarkerParent是相对于圆的父点的点(焦点节点的左上角)。如果将圆重新定位到此点,则将圆边界框的左上角放置到焦点节点的左上角:
marker.relocate(nodeInMarkerParent.getX(), nodeInMarkerParent.getY())
如果要将圆心放在焦点节点的左上角,则需要相应地调整坐标:
// CoordinateConversion.java
package com.jdojo.node;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class CoordinateConversion extends Application {
// An instance variable to store the reference of the circle
private Circle marker;
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
TextField fName = new TextField();
TextField lName = new TextField();
TextField salary = new TextField();
// The Circle node is unmanaged
marker = new Circle(5);
marker.setManaged(false);
marker.setFill(Color.RED);
marker.setMouseTransparent(true);
HBox hb1 = new HBox();
HBox hb2 = new HBox();
HBox hb3 = new HBox();
hb1.getChildren().addAll(
new Label("First Name:"), fName);
hb2.getChildren().addAll(new Label("Last Name:"), lName);
hb3.getChildren().addAll(new Label("Salary:"), salary);
VBox root = new VBox();
root.getChildren().addAll(hb1, hb2, hb3, marker);
Scene scene = new Scene(root);
// Add a focus change listener to the scene
scene.focusOwnerProperty().addListener(
(prop, oldNode, newNode) -> placeMarker(newNode));
stage.setScene(scene);
stage.setTitle("Coordinate Space Transformation");
stage.show();
}
public void placeMarker(Node newNode) {
double nodeMinX = newNode.getLayoutBounds().getMinX();
double nodeMinY = newNode.getLayoutBounds().getMinY();
Point2D nodeInScene =
newNode.localToScene(nodeMinX, nodeMinY);
Point2D nodeInMarkerLocal =
marker.sceneToLocal(nodeInScene);
Point2D nodeInMarkerParent =
marker.localToParent(nodeInMarkerLocal);
// Position the circle approperiately
marker.relocate(
nodeInMarkerParent.getX()
+ marker.getLayoutBounds().getMinX(),
nodeInMarkerParent.getY()e
+ marker.getLayoutBounds().getMinY());
}
}
Listing 6-9Transforming the Coordinates of a Point from One Coordinate Space to Another
marker.relocate(
nodeInMarkerParent.getX() + marker.getLayoutBounds().getMinX(),
nodeInMarkerParent.getY() + marker.getLayoutBounds().getMinY());
摘要
场景图是一种树形数据结构。场景图中的每个项目称为一个节点。javafx.scene.Node类的一个实例表示场景图中的一个节点。一个节点可以有子项(也称为子节点),这样的节点称为分支节点。分支节点是Parent类的一个实例,它的具体子类是Group、Region和WebView。不能有子项的节点称为叶节点。诸如Rectangle、Text、ImageView和MediaView之类的实例是叶节点的例子。每个场景图树中只有一个节点没有父节点,它被称为根节点。一个节点在场景图中的任何地方最多出现一次。
如果节点尚未附加到场景,则可以在任何线程上创建和修改节点。将节点附加到场景中以及随后的修改必须发生在 JavaFX 应用程序线程上。一个节点有几种类型的边界。界限是相对于不同的坐标系确定的。场景图中的节点有三种类型的边界:layoutBounds、boundsInLocal和boundsInParent。
layoutBounds属性是基于节点在未变换的局部坐标空间中的几何属性来计算的。不包括效果、剪辑和变换。boundsInLocal属性是在节点的未变换坐标空间中计算的。它包括节点、效果和剪辑的几何属性。不包括应用于节点的变换。节点的boundsInParent属性位于其父节点的坐标空间中。它包括节点、效果、剪辑和变换的几何属性。它很少直接用在代码中。
一个Group的layoutBounds、boundsInLocal和boundsInParent的计算不同于一个节点的计算。一个Group承担其子节点的集合边界。您可以对每个Group子对象分别应用效果、剪辑和变换。您还可以直接在Group上应用效果、剪辑和变换,它们会应用到它的所有子节点。一个Group的layoutBounds是其所有子节点boundsInParent的并集。它包括直接应用到子对象的效果、剪辑和变换。它不包括直接应用于Group的效果、剪辑和变换。Group的boundsInLocal通过取其layoutBounds并包括直接应用于Group的效果和剪辑来计算。Group的boundsInParent通过取其boundsInLocal并包括直接应用于Group的变换来计算。
每个节点都维护一个用户定义属性(键/值对)的可见映射。你可以用它来储存任何有用的信息。节点可以是托管的,也可以是非托管的。托管节点由其父节点布局,而应用程序负责布局非托管节点。
下一章将讨论如何在 JavaFX 中使用颜色。
七、玩转颜色
在本章中,您将学习:
-
JavaFX 中如何表示颜色
-
有哪些不同的颜色图案
-
如何使用图像模式
-
如何使用线性颜色渐变
-
如何使用径向颜色渐变
本章的例子在com.jdojo.color包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.color to javafx.graphics, javafx.base;
...
这是我们第一次使用resources文件夹中的文件。为了简化对资源文件的访问,我们在包com.jdojo.util中引入了一个实用程序类:
package com.jdojo.util;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class ResourceUtil {
// Where the resources directory is, seen from current working
// directory. This differs from build tool to build tool, and
// from IDE to IDE, so you might have to adapt this.
private final static String RSRC_PATH_FROM_CURRENT_DIR = "bin";
public static URL getResourceURL(String inResourcesPath) {
var fStr = (RSRC_PATH_FROM_CURRENT_DIR +
"/resources/" +
inResourcesPath).replace("/", File.separator);
try {
return new File(fStr).getCanonicalFile().toURI().toURL();
} catch (IOException e) {
System.err.println("Cannot fetch URL for '" +
inResourcesPath + "'");
System.err.println("""
If the path is correct, try to adapt the
RSRC_PATH_FROM_CURRENT_DIR constant in class
ResourceUtil""".stripIndent());
e.printStackTrace(System.err);
return null;
}
}
public static String getResourceURLStr(String inResourcesPath) {
return getResourceURL(inResourcesPath).toString();
}
public static String getResourcePath(String inResourcesPath) {
var fStr = (RSRC_PATH_FROM_CURRENT_DIR +
"/resources/" +
inResourcesPath).replace("/", File.separator);
return new File(fStr).getAbsolutePath();
}
}
理解颜色
在 JavaFX 中,可以为文本指定颜色,为区域指定背景颜色。您可以将颜色指定为统一颜色、图像图案或颜色渐变。统一颜色使用相同的颜色填充整个区域。图像图案允许您用图像图案填充区域。颜色渐变定义了一种颜色模式,其中颜色沿着一条直线从一种颜色变化到另一种颜色。颜色梯度的变化可以是线性的或放射状的。在这一章中,我将给出使用所有颜色类型的例子。图 7-1 显示了 JavaFX 中颜色相关类的类图。所有的类都包含在javafx.scene.paint包中。
图 7-1
JavaFX 中颜色相关类的类图
Paint类是一个抽象类,它是其他颜色类的基类。它只包含一个静态方法,该方法接受一个String参数并返回一个Paint实例。返回的Paint实例属于Color、LinearGradient或RadialGradient类,如以下代码所示:
public static Paint valueOf(String value)
你不会直接使用Paint类的valueOf()方法。它用于转换从 CSS 文件的String中读取的颜色值。下面的代码片段从String创建了Paint类的实例:
// redColor is an instance of the Color class
Paint redColor = Paint.valueOf("red");
// aLinearGradientColor is an instance of the LinearGradient class
Paint aLinearGradientColor = Paint.valueOf("linear-gradient(to bottom right, red, black)" );
// aRadialGradientColor is an instance of the RadialGradient class
Paint aRadialGradientColor = Paint.valueOf("radial-gradient(radius 100%, red, blue, black)");
均匀颜色、图像图案、线性颜色渐变和径向颜色渐变分别是Color、ImagePattern、LinearGradient和RadialGradient类的实例。在处理颜色渐变时使用了Stop类和CycleMethod枚举。
Tip
通常,设置节点颜色属性的方法将Paint类型作为参数,允许您使用四种颜色模式中的任何一种。
使用颜色类
Color类表示 RGB 颜色空间中的纯色统一颜色。每种颜色都有一个定义在 0.0 到 1.0 或 0 到 255 之间的 alpha 值。alpha 值为 0.0 或 0 表示颜色完全透明,alpha 值为 1.0 或 255 表示颜色完全不透明。默认情况下,alpha 值设定为 1.0。有三种方式可以拥有一个Color类的实例:
-
使用构造器
-
使用工厂方法之一
-
使用在
Color类中声明的颜色常量之一
Color类只有一个构造器,让你在范围[0.0;1.0]:
public Color(double red, double green, double blue, double opacity)
以下代码片段创建了完全不透明的蓝色:
Color blue = new Color(0.0, 0.0, 1.0, 1.0);
您可以在Color类中使用以下静态方法来创建Color对象。双精度值需要介于 0.0 和 1.0 之间,而int值需要介于 0 和 255 之间;
-
Color color(double red, double green, double blue) -
Color color(double red, double green, double blue, double opacity) -
Color hsb(double hue, double saturation, double brightness) -
Color hsb(double hue, double saturation, double brightness, double opacity) -
Color rgb(int red, int green, int blue) -
Color rgb(int red, int green, int blue, double opacity)
通过valueOf()和web()工厂方法,您可以从 web 颜色值格式的字符串中创建Color对象。以下代码片段使用不同的字符串格式创建蓝色Color对象:
Color blue = Color.valueOf("blue");
Color blue = Color.web("blue");
Color blue = Color.web("#0000FF");
Color blue = Color.web("0X0000FF");
Color blue = Color.web("rgb(0, 0, 255)");
Color blue = Color.web("rgba(0, 0, 255, 0.5)"); // 50% transparent blue
Color类定义了大约 140 个颜色常量,例如RED、WHITE、TAN和BLUE等等。由这些常量定义的颜色是完全不透明的。
使用 ImagePattern 类
图像图案允许您用图像填充形状。图像可以填充整个形状,也可以使用平铺模式。以下是获取图像模式的步骤:
-
使用文件中的图像创建一个
Image对象。 -
相对于要填充的形状的左上角定义一个矩形,称为定位矩形。
图像显示在锚定矩形中,然后调整大小以适合锚定矩形。如果要填充的形状的边框比锚定矩形的边框大,则带有图像的锚定矩形会以平铺模式在形状内重复。
您可以使用ImagePattern的一个构造器创建它的一个对象:
-
ImagePattern(Image image) -
ImagePattern(Image image, double x, double y, double width, double height, boolean proportional)
第一个构造器用不带任何图案的图像填充整个边界框。第二个构造器允许您指定定位矩形的 x 和 y 坐标、宽度和高度。如果proportional argument为真,则根据单位正方形,相对于要填充的形状的边界框指定锚定矩形。如果proportional参数为 false,则在形状的局部坐标系中指定定位矩形。以下对两个构造器的两次调用将产生相同的结果:
ImagePatterm ip1 = new ImagePattern(anImage);
ImagePatterm ip2 = new ImagePattern(anImage, 0.0, 0.0, 1.0, 1.0, true);
对于此处的示例,您将使用图 7-2 中所示的图像。它是一个 37px 25px 的蓝色圆角矩形。可以在源代码文件夹下的resources/picture/blue_rounded_rectangle.png文件中找到。
图 7-2
蓝色圆角矩形
使用该文件,让我们使用以下代码创建一个图像模式:
Image img = create the image object...
ImagePattern p1 = new ImagePattern(img, 0, 0, 0.25, 0.25, true);
ImagePattern构造器中的最后一个参数设置为true,使得锚定矩形的边界 0、0、0.25 和 0.25 被解释为与要填充的形状的大小成比例。图像模式将在要填充的形状的(0,0)处创建一个锚定矩形。它的宽度和高度将是要填充形状的 25%。这将使锚定矩形水平重复四次,垂直重复四次。如果将下面的代码与前面的图像模式一起使用,将会产生一个如图 7-3 所示的矩形:
图 7-3
用图像图案填充矩形
Rectangle r1 = new Rectangle(100, 50);
r1.setFill(p1);
如果您使用相同的图像模式用下面的代码片段填充一个三角形,得到的三角形将如图 7-4 所示:
图 7-4
用图像图案填充三角形
Polygon triangle = new Polygon(50, 0, 0, 50, 100, 50);
triangle.setFill(p1);
在没有拼贴图案的情况下,如何用图像完全填充形状?您需要使用一个参数设置为 true 的ImagePattern。锚点矩形的中心应该在(0,0)处,其宽度和高度应该设置为 1,如下所示:
// An image pattern to completely fill a shape with the image
ImagePatterm ip = new ImagePattern(yourImage, 0.0, 0.0, 1.0, 1.0, true);
清单 7-1 中的程序展示了如何使用图像模式。产生的屏幕如图 7-5 所示。它的init()方法将图像加载到一个Image对象中,并将其存储在一个实例变量中。如果在CLASSPATH中没有找到图像文件,它会打印一条错误信息并退出。
图 7-5
用图像图案填充不同的形状
// ImagePatternApp.java
package com.jdojo.color;
import com.jdojo.util.ResourceUtil;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.HBox;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ImagePatternApp extends Application {
private Image img;
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void init() {
// Create an Image object
final String imgPath = ResourceUtil.getResourceURLStr(
"picture/blue_rounded_rectangle.png");
img = new Image(imgPath);
}
@Override
public void start(Stage stage) {
// An anchor rectangle at (0, 0) that is 25% wide and 25% tall
// relative to the rectangle to be filled
ImagePattern p1 = new ImagePattern(img, 0, 0, 0.25, 0.25, true);
Rectangle r1 = new Rectangle(100, 50);
r1.setFill(p1);
// An anchor rectangle at (0, 0) that is 50% wide and 50% tall
// relative to the rectangle to be filled
ImagePattern p2 = new ImagePattern(img, 0, 0, 0.5, 0.5, true);
Rectangle r2 = new Rectangle(100, 50);
r2.setFill(p2);
// Using absolute bounds for the anchor rectangle
ImagePattern p3 = new ImagePattern(img, 40, 15, 20, 20, false);
Rectangle r3 = new Rectangle(100, 50);
r3.setFill(p3);
// Fill a circle
ImagePattern p4 = new ImagePattern(img, 0, 0, 0.1, 0.1, true);
Circle c = new Circle(50, 50, 25);
c.setFill(p4);
HBox root = new HBox();
root.getChildren().addAll(r1, r2, r3, c);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Using Image Patterns");
stage.show();
}
}
Listing 7-1Using an Image Pattern to Fill Different Shapes
了解线性颜色渐变
使用称为渐变线的轴来定义线性颜色渐变。渐变线上的每个点都有不同的颜色。垂直于渐变线的直线上的所有点都具有相同的颜色,即两条线的交点的颜色。渐变线由起点和终点定义。沿渐变线的颜色是在渐变线上的一些点定义的,这些点被称为停止色点(或停止点)。使用插值法计算两个停止点之间的颜色。
渐变线有方向,是从起点到终点。垂直于渐变线并通过停止点的线上的所有点将具有停止点的颜色。例如,假设您用颜色 C1 定义了一个停止点 P1。如果你画一条垂直于穿过 P1 点的渐变线的线,该线上的所有点都将具有 C1 的颜色。
图 7-6 显示了构成线性颜色渐变的元素的细节。它显示了一个用线性颜色渐变填充的矩形区域。从左侧到右侧定义渐变线。起点为白色,终点为黑色。在矩形的左侧,所有点都是白色,在右侧,所有点都是黑色。在左侧和右侧之间,颜色在白色和黑色之间变化。
图 7-6
线性颜色渐变的细节
使用 LinearGradient 类
在 JavaFX 中,LinearGradient类的一个实例表示线性颜色渐变。该类有以下两个构造器。他们最后的争论类型是不同的:
-
LinearGradient(double startX, double startY, double endX, double endY, boolean proportional, CycleMethod cycleMethod, List<Stop> stops) -
LinearGradient(double startX, double startY, double endX, double endY, boolean proportional, CycleMethod cycleMethod, Stop... stops)
startX和startY参数定义了渐变线起点的 x 和 y 坐标。endX和endY参数定义了渐变线终点的 x 和 y 坐标。
proportional参数影响起点和终点坐标的处理方式。如果为真,则起点和终点相对于单位正方形处理。否则,它们将被视为局部坐标系中的绝对值。这个论点的用法需要多一点解释。
通常,颜色渐变用于填充区域,例如矩形。有时候,你知道区域的大小,有时候你不会。此参数的值允许您以相对或绝对形式指定渐变线。在相对形式中,该区域被视为一个单位正方形。也就是说,左上角和右下角的坐标分别是(0.0,0.0)和(1.0,1.0)。区域中的其他点的 x 和 y 坐标将在 0.0 和 1.0 之间。假设你指定起点为(0.0,0.0),终点为(1.0,0.0)。它定义了一条从左到右的水平渐变线。(0.0,0.0)和(0.0,1.0)的起点和终点定义了一条从上到下的垂直渐变线。(0.0,0.0)和(0.5,0.0)的起点和终点定义了从区域左侧到中间的水平渐变线。
当proportional参数为假时,起点和终点的坐标值被视为相对于局部坐标系的绝对值。假设你有一个宽 200 高 100 的矩形。(0.0,0.0)和(200.0,0.0)的起点和终点定义了一条从左到右的水平渐变线。(0.0,0.0)和(200.0,100.0)的起点和终点定义了一条从左上角到右下角的倾斜渐变线。
cycleMethod参数定义了由起点和终点定义的颜色渐变边界之外的区域应该如何填充。假设您将比例参数设置为true的起点和终点分别定义为(0.0,0.0)和(0.5,0.0)。这只覆盖了该区域的左半部分。区域的右半部分应该如何填充?您可以使用cycleMethod参数来指定这种行为。其值是在CycleMethod枚举中定义的枚举常量之一:
-
CycleMethod.NO_CYCLE -
CycleMethod.REFLECT -
CycleMethod.REPEAT
NO_CYCLE的循环方法用终端颜色填充剩余区域。如果您已将颜色定义为仅从区域左侧到中间的停止点,则右半部分将用为区域中间定义的颜色填充。假设您只为区域的中间一半定义了颜色渐变,而左侧的 25%和右侧的 25%未定义。NO_CYCLE方法将使用距离左侧 25%处定义的颜色填充左侧 25%的区域,使用距离右侧 25%处定义的颜色填充右侧 25%的区域。中间 50%的颜色将由颜色停止点决定。
REFLECT的循环方法通过从最近的填充区域开始到结束和结束到开始反映颜色渐变来填充剩余的区域。REPEAT的循环方法重复颜色渐变填充剩余区域。
stops参数定义了沿渐变线的颜色停止点。一个颜色停止点由一个Stop类的实例表示,它只有一个构造器:
Stop(double offset, Color color)
偏移值介于 0.0 和 1.0 之间。它定义了从起点开始沿渐变线的停止点的相对距离。例如,偏移 0.0 是起点,偏移 1.0 是终点,偏移 0.5 在起点和终点的中间,依此类推。您可以用两种不同的颜色定义至少两个停止点,以获得颜色渐变。您可以为颜色渐变定义的停止点数量没有限制。
以上是对LinearGradient构造器参数的解释。所以让我们来看一些如何使用它们的例子。
以下代码片段用线性颜色渐变填充一个矩形,如图 7-7 所示:
图 7-7
具有两个停止点的水平线性颜色渐变:起点为白色,终点为黑色
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
你有两个颜色停止点。起点的停止点是白色的,终点的停止点是黑色的。起点(0,0)和终点(1,0)定义了从左到右的水平渐变。proportional参数被设置为true,这意味着坐标值被解释为相对于单位正方形。设置为NO_CYCLE的循环方法参数在这种情况下不起作用,因为渐变边界覆盖了整个区域。在前面的代码中,如果您想将proportional参数值设置为false,以达到相同的效果,您可以如下创建LinearGradient对象。请注意,使用 200 作为终点的 x 坐标来表示矩形宽度的终点:
LinearGradient lg = new LinearGradient(0, 0, 200, 0, false, NO_CYCLE, stops);
让我们看另一个例子。运行以下代码片段后得到的矩形如图 7-8 所示:
图 7-8
有两个停止点的水平线性颜色渐变:起点为白色,中点为黑色
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.5, 0, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
在这段代码中,您做了一点小小的修改。您定义了一条水平渐变线,该线从矩形的左侧开始,在中间结束。注意使用(0.5,0)作为终点的坐标。这使得矩形的右半部分没有颜色渐变。在这种情况下,循环方法是有效的,因为它的工作是填充未填充的区域。矩形中间的颜色是黑色,由第二个停止点定义。NO_CYCLE值使用终端黑色填充矩形的右半部分。
让我们看一下前一个例子的一个微小的变体。您将循环方法从NO_CYCLE更改为REFLECT,如以下代码片段所示,这将生成如图 7-9 所示的矩形。请注意,右半部分区域(具有未定义梯度的区域)是左半部分的反射:
图 7-9
带有两个停止点的水平线性颜色渐变:起点为白色,中点为黑色,循环方法为REFLECT
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.5, 0, true, REFLECT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
让我们对前面的例子做一点小小的改变,这样终点坐标只覆盖了矩形宽度的十分之一。代码如下,生成的矩形如图 7-10 所示。矩形右边的 90%使用REFLECT循环方法填充,交替使用首尾相连和首尾相连的颜色模式:
图 7-10
带有两个停止点的水平线性颜色渐变:起点为白色,十分之一点为黑色,循环方法为REFLECT
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.1, 0, true, REFLECT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
现在我们来看看使用REPEAT循环法的效果。下面的代码片段使用了一个位于矩形宽度中间的结束点和一个循环方法REPEAT。这产生了如图 7-11 所示的矩形。在本例中,如果将终点设置为宽度的十分之一,就会得到如图 7-12 所示的矩形。
图 7-12
带有两个停止点的水平线性颜色渐变:起点为白色,十分之一点为黑色,循环方法为REPEAT
图 7-11
带有两个停止点的水平线性颜色渐变:起点为白色,中点为黑色,循环方法为REPEAT
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.5, 0, true, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
您还可以定义两个以上的停止点,如下面的代码片段所示。它将渐变线上起点和终点之间的距离分为四段,每段占宽度的 25%。第一段(从左开始)的颜色介于红色和绿色之间,第二段介于绿色和蓝色之间,第三段介于蓝色和橙色之间,第四段介于橙色和黄色之间。产生的矩形如图 7-13 所示。如果你正在阅读这本书的印刷本,你可能看不到颜色。
图 7-13
具有五个停止点的水平线性颜色渐变
Stop[] stops = new Stop[]{new Stop(0, Color.RED),
new Stop(0.25, Color.GREEN),
new Stop(0.50, Color.BLUE),
new Stop(0.75, Color.ORANGE),
new Stop(1, Color.YELLOW)};
LinearGradient lg = new LinearGradient(0, 0, 1, 0, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
您不仅限于定义水平颜色渐变。您可以使用任意角度的渐变线来定义颜色渐变。下面的代码片段创建了一个从左上角到右下角的渐变。请注意,当比例参数为真时,(0,0)和(1,1)定义了区域左上角和右下角的(x,y)坐标:
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 1, 1, true, NO_CYCLE, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
以下代码片段定义了(0,0)和(0.1,0.1)点之间的渐变线。它使用REPEAT循环方法来填充剩余的区域。产生的矩形如图 7-14 所示。
图 7-14
带有两个停止点的倾斜线性颜色渐变:起点(0,0)为白色,终点(0.1,0.1)为黑色,使用REPEAT作为循环方法
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
LinearGradient lg = new LinearGradient(0, 0, 0.1, 0.1, true, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(lg);
使用字符串格式定义线性颜色渐变
您还可以使用LinearGradient类的静态方法valueOf(String colorString)指定字符串格式的线性颜色渐变。通常,字符串格式用于在 CSS 文件中指定线性颜色渐变。它具有以下语法:
linear-gradient([gradient-line], [cycle-method], color-stops-list)
方括号([和])中的参数是可选的。如果不指定可选参数,后面的逗号也需要排除。渐变线参数的默认值是“到底”循环方法参数的默认值是NO_CYCLE。可以用两种方式指定渐变线:
-
使用两点——起点和终点
-
使用侧面或角落
对渐变线使用两点的语法是
from point-1 to point-2
这些点的坐标可以以面积的百分比或以像素的实际测量值来指定。对于 200 像素宽 100 像素高的矩形,可以通过以下两种方式指定水平渐变线:
from 0% 0% to 100% 0%
或者
from 0px 0px to 200px 0px
使用边或角的语法是
to side-or-corner
边值或角值可以是上、左、下、右、左上、左下、右下或右上。当使用边或角定义坡线时,只需指定终点。起点推断。例如,值“到顶部”将起点推断为“从底部”,值“到右下角”将起点推断为“从左上角”,依此类推。如果缺少渐变线值,则默认为“到底部”
cycle-method的有效值是repeat和reflect。如果缺少,则默认为NO_CYCLE。将cycle-method参数的值指定为NO_CYCLE是一个运行时错误。如果您希望它是NO_CYCLE,只需从语法中省略cycle-method参数。
color-stops-list参数是一个色标列表。色标由一个 web 颜色名称和一个位置(可选)组成,该位置以像素或起点百分比为单位。色标列表的示例有
-
white, black -
white 0%, black 100% -
white 0%, yellow 50%, blue 100% -
white 0px, yellow 100px, red 200px
当您没有指定第一个和最后一个色标的位置时,第一个色标的位置默认为 0%,第二个色标的位置默认为 100%。因此,颜色停止列表"white, black"和"white 0%, black 100%"基本上是相同的。
如果您没有为列表中的任何颜色停止点指定位置,它们将被分配位置,使它们均匀地位于起点和终点之间。以下两个色标列表是相同的:
-
white, yellow, black, red, green -
white 0%, yellow 25%, black 50%, red 75%, green 100%
您可以为列表中的某些色标指定位置,而不为其他色标指定位置。在这种情况下,没有位置的色标均匀分布在前面和后面有位置的色标之间。以下两个色标列表是相同的:
-
white, yellow, black 60%, red, green -
white 0%, yellow 30%, black 50%, red 80%, green 100%
如果列表中某个色标的位置设置小于为任何先前色标指定的位置,则其位置将设置为等于为先前色标设置的最大位置。以下色标列表将第三个色标设置为 10%,小于第二个色标的位置(50%):
white, yellow 50%, black 10%, green
这将在运行时更改为使用 50%的第三个颜色停止,如下所示:
white 0%, yellow 50%, black 50%, green 100%
现在我们来看一些例子。下面的字符串将创建一个从上到下的线性渐变,使用NO_CYCLE作为循环方法。顶部和底部的颜色分别是白色和黑色:
linear-gradient(white, black)
该值与相同
linear-gradient(to bottom, white, black)
下面的代码片段将创建一个如图 7-15 所示的矩形。它定义了一个水平颜色渐变,其终点位于矩形宽度的中间。它使用repeat作为循环方法:
图 7-15
使用字符串格式创建线性颜色渐变
String value = "from 0px 0px to 100px 0px, repeat, white 0%, black 100%";
LinearGradient lg2 = LinearGradient.valueOf(value);
Rectangle r2 = new Rectangle(200, 100);
r2.setFill(lg2);
以下线性颜色渐变的字符串值将创建一个从左上角到右下角的对角线渐变,用白色和黑色填充该区域:
"to bottom right, white 0%, black 100%"
了解径向颜色渐变
在径向颜色渐变中,颜色从一个点开始,以圆形或椭圆形向外平滑过渡。这个形状,比如说一个圆,是由一个中心点和一个半径定义的。颜色的起点被称为渐变的焦点。颜色沿着一条线变化,从渐变的焦点开始,向各个方向变化,直到到达形状的外围。使用三个组件定义径向颜色渐变:
-
渐变形状(渐变圆的中心和半径)
-
具有渐变的第一种颜色的焦点
-
颜色停止
渐变的焦点和渐变形状的中心点可能不同。图 7-16 显示了径向颜色渐变的组成部分。该图显示了两个径向梯度:在左侧,焦点和中心点位于同一位置;在右侧,焦点位于形状中心点的水平右侧。
图 7-16
定义径向颜色渐变的元素
聚焦点由聚焦角度和聚焦距离定义,如图 7-17 所示。焦点角度是穿过形状中心点的水平线和连接中心点和焦点的线之间的角度。焦距是形状的中心点和渐变的焦点之间的距离。
图 7-17
在径向颜色渐变中定义焦点
色标列表确定渐变形状内部某一点的颜色值。焦点定义了色标的 0%位置。圆周上的点定义了色标的 100%位置。如何确定渐变圆内某一点的颜色?你可以画一条穿过该点和焦点的线。将使用线中该点每侧最近的颜色停止点对该点的颜色进行插值。
使用径向梯度级
RadialGradient类的一个实例代表一种径向颜色渐变。该类包含以下两个构造器,它们的最后一个参数的类型不同:
-
RadialGradient(double focusAngle, double focusDistance, double centerX, double centerY, double radius, boolean proportional, CycleMethod cycleMethod, List<Stop> stops) -
RadialGradient(double focusAngle, double focusDistance, double centerX, double centerY, double radius, boolean proportional, CycleMethod cycleMethod, Stop... stops)
focusAngle参数定义焦点的聚焦角度。正聚焦角从穿过中心点的水平线和连接中心点和焦点的线开始顺时针测量。逆时针测量负值。
focusDistance参数用圆半径的百分比来表示。该值固定在–1 和 1 之间。也就是说,焦点总是在渐变圆内。如果焦点距离将焦点设置在渐变圆的外围之外,则使用的焦点是圆的外围与连接中心点和设置的焦点的线的交点。
聚焦角度和焦距可以有正值和负值。图 7-18 说明了这一点:它显示了位于距离中心点 80%处的四个焦点,正和负,正和负成 60 度角。
图 7-18
利用焦点角度和焦距定位焦点
centerX和centerY参数分别定义中心点的 x 和 y 坐标,半径参数是渐变圆的半径。这些参数可以相对于单位平方(在 0.0 和 1.0 之间)或以像素为单位来指定。
proportional参数影响中心点和半径坐标值的处理方式。如果这是真的,它们相对于单位正方形被处理。否则,它们将被视为局部坐标系中的绝对值。关于使用proportional参数的更多细节,请参考本章前面的“使用 LinearGradient 类”一节。
Tip
JavaFX 允许您创建圆形的径向渐变。但是,当要由径向颜色渐变填充的区域具有非方形边界框(例如,矩形)并且您相对于要填充的形状的大小指定渐变圆的半径时,JavaFX 将使用椭圆形径向颜色渐变。这在RadialGradient类的 API 文档中没有记载。我将很快给出一个这样的例子。
cycleMethod和stops参数与前面使用LinearGradient类一节中描述的含义相同。在径向颜色渐变中,停止点是沿着连接焦点和渐变圆外围点的线定义的。焦点定义 0%停止点,圆周上的点定义 100%停止点。
让我们看一些使用RadialGradient类的例子。下面的代码片段为一个圆形产生一个径向颜色渐变,如图 7-19 所示:
图 7-19
具有相同中心点和焦点的径向颜色渐变
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg = new RadialGradient(0, 0, 0.5, 0.5, 0.5, true, NO_CYCLE, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);
焦点角度和焦点距离的零值将焦点定位在渐变圆的中心。true proportional参数将中心点坐标(0.5,0.5)解释为 50 乘 50 的圆形矩形边界的(25px,25px)。半径值 0.5 被解释为 25px,这将渐变圆的中心放置在与要填充的圆的中心相同的位置。在这种情况下,NO_CYCLE的循环方法不起作用,因为渐变圆填充了整个圆形区域。在焦点处的色阶是白色的,在渐变圆的外围是黑色的。
以下代码片段将渐变圆的半径指定为要填充的圆的 0.2 倍。这意味着它将使用 10px (0.2 乘以 50px,这是要填充的圆的半径)的渐变圆。产生的圆如图 7-20 所示。由于循环方法被指定为NO_CYCLE,超出半径 0.2 的圆区域被填充为黑色:
图 7-20
具有相同中心点和焦点的径向颜色渐变具有半径为 0.20 的渐变圆
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg = new RadialGradient(0, 0, 0.5, 0.5, 0.2, true, NO_CYCLE, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);
现在让我们使用前面代码片段中的循环方法REPEAT。最终的圆如图 7-21 所示。
图 7-21
中心点和焦点相同的径向颜色渐变,半径为 0.20 的渐变圆,循环方式为REPEAT
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg = new RadialGradient(0, 0, 0.5, 0.5, 0.2, true, REPEAT, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);
所以现在让我们使用一个不同的中心点和焦点。使用 60 度聚焦角度和 0.2 倍半径作为焦距,如以下代码所示。产生的圆如图 7-22 所示。请注意将焦点从中心点移开所获得的 3D 效果。
图 7-22
使用不同中心和焦点的径向颜色渐变
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
new RadialGradient(60, 0.2, 0.5, 0.5, 0.2, true, REPEAT, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);
现在让我们用径向颜色渐变填充一个矩形区域(非正方形)。该效果的代码如下,生成的矩形如图 7-23 所示。注意 JavaFX 使用的椭圆渐变形状。您已经将渐变的半径指定为 0.5,并将proportional参数指定为true。由于您的矩形宽 200 像素,高 100 像素,因此会产生两个半径:一个沿 x 轴,一个沿 y 轴,从而产生一个椭圆。沿 x 轴和 y 轴的半径分别为 100 像素和 50 像素。
图 7-23
用径向颜色渐变填充的矩形,其比例参数值为 true
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
new RadialGradient(0, 0, 0.5, 0.5, 0.5, true, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(rg);
如果你想用圆形而不是椭圆形的颜色渐变填充矩形,你应该将proportional参数指定为false,半径值将以像素为单位。以下代码片段生成一个矩形,如图 7-24 所示:
图 7-24
用径向颜色渐变填充的矩形,其比例参数值为 false
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
new RadialGradient(0, 0, 100, 50, 50, false, REPEAT, stops);
Rectangle r = new Rectangle(200, 100);
r.setFill(rg);
如何用径向颜色渐变填充三角形或任何其他形状?径向梯度的形状,圆形或椭圆形,取决于几个条件。表 7-1 显示了决定径向颜色渐变形状的标准组合。
表 7-1
用于确定径向颜色渐变形状的标准
|比例论点
|
填充区域的边界框
|
梯度形状
| | --- | --- | --- | | 真实的 | 平方 | 圆 | | 真实的 | 非方形 | 椭圆 | | 错误的 | 平方 | 圆 | | 错误的 | 非方形 | 圆 |
我应该在这里强调,在前面的讨论中,我谈论的是要填补的区域的界限,而不是该区域。例如,假设您想要用径向颜色渐变填充一个三角形。三角形的边界将由其宽度和高度决定。如果三角形有相同的宽度和高度,它的边界是一个正方形区域。否则,它的边界采用矩形区域。
以下代码片段用顶点(0.0,0.0),(0.0,100.0)和(100.0,100.0)填充一个三角形。请注意,这个三角形的边界框是一个 100 像素乘 100 像素的正方形。由此产生的三角形是图 7-25 中的左图。
图 7-25
用圆形和椭圆形的径向颜色渐变填充三角形
Stop[] stops = new Stop[]{new Stop(0, Color.WHITE), new Stop(1, Color.BLACK)};
RadialGradient rg =
new RadialGradient(0, 0, 0.5, 0.5, 0.2, true, REPEAT, stops);
Polygon triangle = new Polygon(0.0, 0.0, 0.0, 100.0, 100.0, 100.0);
triangle.setFill(rg);
图 7-25 右侧的三角形使用了一个 200px 乘 100px 的矩形边界框,由下面的代码片段生成。请注意,渐变使用了椭圆形状:
Polygon triangle = new Polygon(0.0, 0.0, 0.0, 100.0, 200.0, 100.0);
最后,我们来看一个使用多个色标的例子,焦点在圆的外围,如图 7-26 。产生这种效果的代码如下:
图 7-26
在径向颜色渐变中使用多个色标
Stop[] stops = new Stop[]{
new Stop(0, Color.WHITE),
new Stop(0.40, Color.GRAY),
new Stop(0.60, Color.TAN),
new Stop(1, Color.BLACK)};
RadialGradient rg =
new RadialGradient(-30, 1.0, 0.5, 0.5, 0.5, true, REPEAT, stops);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);
以字符串格式定义径向颜色渐变
您还可以使用RadialGradient类的静态方法valueOf(String colorString)指定字符串格式的径向颜色渐变。通常,字符串格式用于在 CSS 文件中指定径向颜色渐变。它具有以下语法:
radial-gradient([focus-angle], [focus-distance], [center], radius, [cycle-method], color-stops-list)
方括号中的参数是可选的。如果没有指定可选参数,后面的逗号也需要排除。
focus-angle和focus-distance的默认值为 0。您可以用度、弧度、梯度和圈数来指定焦点角度。焦距被指定为半径的百分比。例子如下:
-
focus-angle 45.0deg -
focus-angle 0.5rad -
focus-angle 30.0grad -
focus-angle 0.125turn -
focus-distance 50%
center和radius参数以相对于被填充区域的百分比或绝对像素来指定。不能将一个参数指定为百分比,而将另一个参数指定为像素。两者必须以相同的单位指定。中心的默认值是(0,0)单位。例子如下:
-
center 50px 50px, radius 50px -
center 50% 50%, radius 50%
cycle-method参数的有效值是repeat和reflect。如果未指定,则默认为NO_CYCLE。
使用颜色及其位置来指定颜色色标列表。位置被指定为从焦点到渐变形状外围的直线上的距离的百分比。有关更多详细信息,请参考前面关于在线性颜色渐变中指定颜色停止点的讨论。例子如下:
-
white, black -
white 0%, black 100% -
red, green, blue -
red 0%, green 80%, blue 100%
下面的代码片段会产生一个圆,如图 7-27 所示:
图 7-27
使用字符串格式指定径向颜色渐变
String colorValue =
"radial-gradient(focus-angle 45deg, focus-distance 50%, " +
"center 50% 50%, radius 50%, white 0%, black 100%)";
RadialGradient rg = RadialGradient.valueOf(colorValue);
Circle c = new Circle(50, 50, 50);
c.setFill(rg);
摘要
在 JavaFX 中,您可以为区域指定文本颜色和背景颜色。您可以将颜色指定为统一颜色、图像图案或颜色渐变。统一颜色使用相同的颜色填充整个区域。图像图案允许您用图像图案填充区域。颜色渐变定义了一种颜色模式,其中颜色沿着一条直线从一种颜色变化到另一种颜色。颜色梯度的变化可以是线性的或放射状的。所有的类都包含在javafx.scene.paint包中。
Paint类是一个抽象类,它是其他颜色类的基类。统一颜色、图像图案、线性颜色渐变和径向颜色渐变分别是Color、ImagePattern、LinearGradient和RadialGradient类的实例。使用颜色渐变时会用到Stop类和CycleMethod枚举。您可以使用这些类之一的实例或字符串形式来指定颜色。当使用 CSS 样式化节点时,使用字符串形式指定颜色。
图像图案允许您用图像填充形状。图像可以填充整个形状,也可以使用平铺模式。
使用称为渐变线的轴来定义线性颜色渐变。渐变线上的每个点都有不同的颜色。垂直于渐变线的直线上的所有点都具有相同的颜色,即两条线的交点的颜色。渐变线由起点和终点定义。沿渐变线的颜色是在渐变线上的一些点定义的,这些点称为停止颜色点(或停止点)。使用插值法计算两个停止点之间的颜色。渐变线有方向,是从起点到终点。垂直于穿过停止点的渐变线的线上的所有点将具有停止点的颜色。例如,假设您用颜色 C1 定义了一个停止点 P1。如果你画一条垂直于穿过 P1 点的渐变线的线,该线上的所有点都将具有 C1 的颜色。
在径向颜色渐变中,颜色从一个点开始,以圆形或椭圆形向外平滑过渡。该形状由中心点和半径定义。颜色的起点被称为渐变的焦点。颜色沿着一条线变化,从渐变的焦点开始,向各个方向变化,直到到达形状的外围。
下一章将向你展示如何使用 CSS 样式化场景图中的节点。