chapter 6 Understanding Nodes
什么是节点
节点和边界框的概念
节点的边界是根据包围节点的整个几何形状的矩形框定义的。
在二维空间中,minX和minY分别定义了边界框左上角的x坐标和y坐标,maxX和maxY分别定义了右下角的x坐标和y坐标。在2D空间中,边界框的z坐标值和深度为零。
了解节点的边界
场景图中的节点有三种类型的边界,定义为node类中的三个只读属性:
• layoutBounds • boundsInLocal • boundsInParent boundsInLocal和BoundsInParent被称为物理或视觉边界,因为它们对应于节点的视觉外观。layoutBounds也被称为逻辑边界,因为它不一定对应于节点的物理边界。
layoutBounds property
•对于可调整大小的节点(区域、控件和WebView),边界框左上角的坐标总是设置为(0,0)。例如,按钮的layoutBounds属性中的(minX, minY)值总是设置为(0,0)。
•对于不可调整大小的节点(形状、文本和组),边界框左上角的坐标是基于几何属性计算的。对于形状(矩形、圆形等)或Text,可以指定节点中特定点相对于该节点未转换的坐标空间的(x, y)坐标。例如,对于矩形,您可以指定左上角的(x, y)坐标,该坐标将变成由其layoutBounds属性描述的边界框的左上角的(x, y)坐标。对于圆,可以指定centerX、centerY和radius属性,其中centerX和centerY分别是圆心的x坐标和y坐标。圆圈的layoutBounds所描述的边界框左上角的(x, y)坐标计算为(centerX - radius, centerY - radius)。
容器根据其layoutBounds分配空间以布局子节点。
可调整大小的节点
要获得节点的实际大小,需要在node类中使用以下方法。注意,Node类没有定义任何与大小相关的属性。与大小相关的属性在区域、控制和其他类中定义。
• double prefWidth(double height) • double prefHeight(double width) • double minWidth(double height) • double minHeight(double width) • double maxWidth(double height) • double maxHeight(double width)
当一个节点的宽度取决于它的高度或反之亦然时,我们就说这个节点有内容偏差。如果节点的高度取决于其宽度,则该节点具有水平内容偏差。如果节点的宽度取决于其高度,则该节点具有垂直内容偏差。
Node类的getContentBias()方法返回节点的内容偏差。它的返回类型是javafx.geometry.Orientation枚举类型,它有两个常量:HORIZONTAL和VERTICAL。如果节点没有内容偏差,例如Text或ChoiceBox,则该方法返回null。
作为已标记类子类的所有控件(例如,Label、Button或CheckBox)在启用文本换行属性时都具有水平内容偏差。
对于那些有内容偏差的节点,需要传递有偏差的维度来获得另一个维度。例如,对于一个具有水平内容偏差的按钮,将传递-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);
在节点中存储用户数据
每个节点维护一个用户定义属性(键/值对)的可观察映射。属性的另一个用途是存储微帮助文本。当一个节点接收到焦点时,您可以读取它的微帮助属性并将其显示(例如在状态栏中),以帮助用户了解节点的使用情况。
Node类有两个方便的方法,setUserData(Object value)和getUserData(),用于将用户定义的值存储为节点的属性。
微帮助文本节点使用示例:
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); } } }
托管节点
Node类有一个托管属性,类型为BooleanProperty。默认情况下,所有节点都处于管理状态。托管节点的布局由其父节点管理。父节点在计算其自身大小时,会考虑其所有托管子节点的layoutBounds。父节点负责调整其托管的可调整大小的子节点的大小,并根据其布局策略对它们进行定位。当托管子节点的layoutBounds发生变化时,场景图的相关部分将被释放。
如果节点是非托管的,则应用程序单独负责布置它(计算它的大小和位置)。也就是说,父节点不布局其非托管子节点。非托管节点的layoutBounds中的更改不会触发其上面的布局。非托管父节点充当布局根节点。如果子节点调用Parent. requestlayout()方法,则只释放由非托管父节点根的分支。
将节点的管理属性与可见属性进行绑定,可以使得当节点不可见时改变页面的布局,比如下面的示例,当按钮B2不可见时,B3按钮会向左移动:
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(); } }
变换坐标空间之间的边界
可能需要将边界或点从一个坐标空间转换到另一个坐标空间,Node类包含几个方法来支持这一点。支持边界或点的以下转换:
• localToParent() 将节点的局部坐标空间中的边界或点转换为其父节点的坐标空间。 • localToScene() 将节点的局部坐标空间中的边界或点转换为其场景的坐标空间。 • ParentToLocal() 将节点的父节点坐标空间中的边界或点转换为该节点的本地坐标空间。 • SceneToLocal() 将节点场景坐标空间中的边界或点转换为该节点的本地坐标空间。
总结
场景图是一种树形数据结构。场景图中的每一项都称为一个节点。javafx.scene.Node类的实例表示场景图中的一个节点。一个节点可以有子项(也称为子节点),这样的节点称为分支节点。分支节点是Parent类的一个实例,它的具体子类是Group、Region和WebView。不能有子项的节点称为叶节点。
像Rectangle, Text, ImageView和MediaView这样的类的实例都是叶子节点的例子。每个场景图树中只有一个节点没有父节点,这个节点被称为根节点。一个节点可能在场景图的任何地方最多出现一次。
一个节点可以在任何线程上创建和修改,如果它还没有附加到一个场景。将节点附加到场景和随后的修改必须在JavaFX应用程序线程上进行。一个节点有几种类型的边界。边界是根据不同的坐标系确定的。场景图中的节点有三种类型的边界:layoutBounds, boundsInLocal和boundsInParent。
layoutBounds属性是根据节点在未转换的局部坐标空间中的节点的几何属性计算的。不包括效果、剪辑和转换。boundsInLocal属性是在节点的未转换坐标空间中计算的。它包括节点、效果和剪辑的几何属性。不包括应用于节点的转换。
节点的boundsInParent属性位于其父节点的坐标空间中。它包括节点、效果、剪辑和转换的几何属性。它很少直接在代码中使用。
分组的layoutBounds、boundsInLocal和boundsInParent的计算与节点的不同。Group接受其子节点的集合边界。可以对Group的每个子元素分别应用效果、剪辑和转换。还可以直接在Group上应用效果、剪辑和转换,并将它们应用于Group的所有子节点。Group的layoutBounds是它所有子元素的boundsInParent的联合。它包括直接应用于子元素的效果、剪辑和转换。它不包括直接应用于组的效果、剪辑和转换。Group的boundsInLocal是通过获取其layoutBounds并包括直接应用于Group的效果和剪辑来计算的。Group的boundsInParent是通过获取其boundsInLocal并包括直接应用于Group的转换来计算的。
每个节点维护一个用户定义属性(键/值对)的可观察映射。可以用它来存储任何有用的信息。节点可以被管理,也可以被不被管理。托管节点由其父节点布置,而应用程序负责布置非托管节点。