本文翻译自JavaFX 16官方文档:https://openjfx.io/javadoc/16/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html 原文Last updated: 01 May 2017 翻译:HyperQing 2021-03-23 关于翻译中遇到attribute 和property的说明:> https://zhuanlan.zhihu.com/p/70671215本文包含译者补充的注释,内容尽量结合实践观点给出,水平有限,仅供参考。黄色背景色标记为未经审校的内容。请尊重译者劳动,转载请保留出处。
翻译中的术语说明
场景图(Scene Graph)
文档中多次出现单词”Scene Graph”,没有对该词语进一步解释,根据上下文,译者理解为,软件的画面即为Scene Graph。在传统桌面软件开发中,理解为“画面场景”(stage、scene)、“用户界面”(user interface)、“视口”(viewport)、“窗口”(window)也都说得通。
成员属性(Property)
这个Property和下面的Attribute,是可能是本文翻译中最令人费解的地方。直译都可以用来表示“属性”的意思。译者看完全文,结合XML、HTML、JAVA、面向对象的相关语境和常用术语翻译。这里的“Property”将统一翻译为成员属性。在JAVA和面向对象中,成员属性就是我们在类里面声明public、private的那些变量。在本文中Property同样也是指这个意思。
例如下面这段代码:width是Box的成员属性,通过嵌套标记来表示向Box.width属性赋值30。
<Box>
<width>30</width>
</Box>
下文统一译作“成员属性”。结合原文的修饰定语,可能会看到“类实例的成员属性”或“静态成员属性”。
FXML属性(Attribute)
结合上下文,Attribute在这里理解为是XML文档中的“属性”。例如下面这段代码中的width属性“写法”就是本文所指的“Attribute”。在本文语境中,凡是出现在标记中,形如:“属性名=”属性值””的,统称为FXML属性,以便区分JAVA语境中的成员属性。
<Box width="30"></Box>
下文统一译作“FXML属性”。
如果有看到“property attribute”一词,表示的是类成员属性的FXML属性“写法”。
在FXML中,大部分情况下,成员属性写法和FXML写法可以互换,即是,既可以嵌套标记表示属性值,又能使用XML属性写法来表示属性值。
概览(Overview)
FXML是一种可编写脚本的、基于xml的标记语言,用于构造Java对象图(?)。
元素(Element)
在FXML中,一个XML元素包含以下内容:
- 类实例
- 类实例的成员属性
- 静态属性
- 声明块
- 脚本代码块
类实例,实例属性,静态属性,以及声明块将会在以下部分中进行讨论。而脚本编写部分将在后面的部分中进行。
类实例元素
在FXML中,类实例能够通过多种方法进行构造。最常见的就是通过实例来声明元素,不过就是通过一个名称来创建一个新的类实例对象。而其他闯将类实例对象的方法,包括引用已存在的变量,复制已存在的变量,以及包含外部的FXML文件。以上的这些都会在后面进行详细讨论。
实例声明
如果一个元素的标签被认为是一个实例声明,如果标签用大写英文(同时类要引入)(?),在JAVA里,意味着这是一个完全限定的类名(包括包的名)。当FXML加载器(这也会在后面进行介绍)遇到这些元素时,它将会创建这些类的实例。
使用import
处理指令(PI,processing instruction,处理指令,XML的基本组成部分)导入类。例如,下面这个处理指令将会导入javafx.scene.control.Label
类到当前的FXML文档的命名空间中。
<?import javafx.scene.control.Label?>
这个处理指令会从 javafx.scene.control包中导入所有的类到当前命名空间中:
<?import javafx.scene.control.*?>
任何附着到JavaBean构造器的类和属性名称命名惯例,能够使用FXML被解读为实例化的和配置完成的(?)。下面给出的是完整的示例,用来创建javafx.scene.control.Label
实例,并将它的“text”属性设置为“Hello world!”。
<?import javafx.scene.control.Label?>
<Label text="Hello, World!"/>
在这个例子中,Label的“text”成员属性是通过XML属性来设置的。属性也能通过嵌套(nested)属性元素来进行设置(?)。属性元素将会在后面的章节中进行讨论。成员属性也会在后面进行讨论。
在FXML中,没有遵循Bean惯例的类同样可以被加载,使用一个叫“builder”的对象即可。Builder构造器会在后面进行讨论。
Maps
在内部,FXML加载器使用com.sun.javafx.fxml.BeanAdapter
类来捆绑一个实例化的对象并调用它的设置器setter方法。这个私有类实现了java.util.Map
接口,并且允许调用者可以通过get和set方法,以key/value的形式设置Bean的属性值。
如果一个元素表示已实现Map
(例如java.util.HashMap
)类型,它不会被绑定,并且它的get()和put()方法会直接调用。例如,下面这段FXML,创建一个HashMap的实例,并设置“foo”和“bar”分别为“123”和“456”。
<HashMap foo="123" bar="456"/>
fx:value
fx:value
属性用来初始化一个类型的实例,没有默认构造函数但提供了一个静态valueOf(String)
方法。例如,java.lang.String
和每一个原始封包类型一样声明了一个valueOf()
方法,能够通过下面这段FXML进行构造:
<String fx:value="Hello, World!"/>
<Double fx:value="1.0"/>
<Boolean fx:value="false"/>
声明了valueOf(String)方法的自定义类的能够通过这种方式来构造。 :::info 译注: :::
fx:factory
fx:factory
属性是另一种用来构造实例的属性。适合用于没有默认构造函数的场合。属性的值是一个静态的、没有参数的生成类实例的工厂方法的名字。(?)
<FXCollections fx:factory="observableArrayList">
<String fx:value="A"/>
<String fx:value="B"/>
<String fx:value="C"/>
</FXCollections>
Builders
第三种没有遵循Bean惯例的创建类实例的方法就是构建器“builder”。这个构建器设计模式委派(代表)对象构造过程到一个可变的助手类(称为“builder”)(?)。负责产生不可变类型的实例。
在FXML中,Builder提供两个接口,javafx.util.Builder
接口声明了一个单例方法叫build(),这个用来负责构造实际的对象。
public interface Builder<T> {
public T build();
}
而javafx.util.BuilderFactory
负责产生指定类型的builder。
public interface BuilderFactory {
public Builder<?> getBuilder(Class<?> type);
}
javafx.fxml
包中提供了一个默认的builder工厂类:JavaFXBuilderFactory
。这个工厂类能够创建和配置大多数不可变的JavaFX类型。例如,下面这段标记使用了默认的builder来创建一个不可变的javafx.scene.paint.Color
类实例。
<Color red="1.0" green="0.0" blue="0.0"/>
注意,不同于Bean类型,当元素的开始标签被处理的时候,这会被构造,对象通过builder来构造时,是没实例化的,直到元素的结束标签。这是因为所有需要的参数可能不一定齐全,要等到整个元素完全处理好了才行。例如,前面提到的Color对象也可以写成这样:
<Color>
<red>1.0</red>
<green>0.0</green>
<blue>0.0</blue>
</Color>
需要将三个已知的颜色组件参数填好,才能正确地构造Color
实例。
当处理一个对象的标记时,将会通过builder来构造。Builder
实例会被处理像变量对象,如果Builder
类实现了Map
接口的话,put()
方法可以用来设置builder的属性值。另外,builder会被捆绑到BeanAdapter
,它的属性会被假定为通过标准Bean setter来暴露出去。
<fx:include>
用于引入另外一份FXML文件来创建对象。如下所示:
<fx:include source="filename"/>
filename是要引入的FXML文件路径。路径以正斜杠“/”开始的话,会认为是相对于项目classpath的路径。而没有以斜杠开始的话,就会认为是相对于当前文档的路径。
如下所示:
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml">
<children>
<fx:include source="my_button.fxml"/>
</children>
</VBox>
假设my_button.fxml包含以下内容:
<?import javafx.scene.control.*?>
<Button text="My Button"/>
结果就是,画面将会含有一个根对象VBox
,而单独的Button
作为其子节点。
:::info
译注:相当于下面代码展示的效果,原文只有一句带过。
:::
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml">
<children>
<Button text="My Button"/>
</children>
</VBox>
注意,这里使用了“fx”命名空间前缀,这是一个保留的前缀,它定义了一些用于FXML源文件内部处理的元素和属性。这通常会在FXML文档的根元素进行声明。关于通过“fx”命名空间提供的其他功能将会在后面的章节进行讨论。
<fx:include>
还支持resources参数来指定语言包resouce bundle的名称,使得引入的内容实现本地化。同样也可以给源文件指定encode文件编码参数。
:::info
译注:这里是指国际化I18N与本地化L10N,界面多语言等相关内容。
:::
<fx:include source="filename" resources="resource_file" charset="utf-8"/>
<fx:constant>
用来引用某个类的某个常量。例如下面这段标记,设置Button
实例的“minWidth”值为NEGATIVE_INFINITY
常量,该常量已经在java.lang.Double
类中声明。
<Button>
<minHeight><Double fx:constant="NEGATIVE_INFINITY"/></minHeight>
</Button>
<fx:reference>
用来引用已经存在的元素。任何地方这个标签出现,它都会有效地被source参数值对应的元素进行替换。这个通常和<fx:id>
联合使用或者与脚本变量连用,这些会在后面的部分进行讨论。<fx:reference>
的“source”属性用来指定对象的id名称。
例如:这段FXML表示,将前面已定义的id为“myImage”的Image
实例赋值给ImageView
控件的“image”属性。
:::info
译注:这里给一个更自然表述:假设前面已经定义了一个Image
实例并且id为“myImage”,然后使用Image
实例引用到此处,作为ImageView
控件的image属性的参数来使用。
:::
<ImageView>
<image>
<fx:reference source="myImage"/>
</image>
</ImageView>
注意,它特可以通过使用属性变量操作符来间接引用变量(这部分会在Attributes章节中讨论)。fx:reference
通常只有使用当一个引用值必须指定作为一个元素,如此当添加一个引用到集合中(?)。
<ArrayList>
<fx:reference source="element1"/>
<fx:reference source="element2"/>
<fx:reference source="element3"/>
</ArrayList>
<fx:copy>
用于创建已存在元素的副本。就像<fx:reference>
是通过fx:id
或者脚本变量来引用元素的。而这个,在source参数中指定需要进行复制的元素的id即可。source的类型必须定义一个复制构造器,这将会用来从源变量构造副本。
与此同时,非JavaFX平台的类提供一些副本构造器,以至于这些元素主要的提供通过应用开发者。这可能会在未来发布的版本中调整。
<fx:root>
用于引用一个先前定义的根元素。这个只能在FXML文档的根节点使用。<fx:root>
主要用于创建由FXML标记支持的自定义控件。这部分的详细内容会在FXMLLoader章节中讨论。
属性元素
“属性元素”是指:标签名以小写字母开头的,用来表示对象的成员属性的XML标签,称为“属性元素”。
:::info
译注:假设有一个Label
实例,现在向Label
的“text”成员属性赋值,写作:<Label><text>Hello, world!<text></Label>
。相当于伪代码:Label.text = "Hello, world!"
。这里出现的<text>
元素就是所谓的“属性元素”。
:::
“属性元素”有以下用途:
- 属性值的setter:用来设置成员属性的值。
- 只读List属性:用来设置List的内容,这个内容是不可修改的。
- 只读Map属性:用来设置Map内容,这个内容是不可修改的。
属性setter
如果这个元素表示属性的setter,则元素的内容(必须是文本节点或嵌套类实例元素)作为值传递给属性的setter。
例如:下面这段FXML,创建了一个Label类的实例,并将其“text”属性设置为“Hello, world!”。
<?import javafx.scene.control.Label?>
<Label>
<text>Hello, World!</text>
</Label>
这段代码和早期向大家介绍的一段代码一样,能够产生相同的效果,都是对“text”属性进行赋值。
<?import javafx.scene.control.Label?>
<Label text="Hello, World!"/>
属性元素通常用于,属性值是非常复杂的类型,且无法用简单的字符串属性值来表达的场合,或者说属性值的字符长度非常长,以至于在属性值里填写的话,会对可读性有负面的影响。
强制类型转换
FXML使用“强制类型转换”来根据需要转换属性值为合适的类型。“强制类型转换”是必须的,因为XML支持的数据类型只有“元素”、“文本”和“属性”(他们的值也是文本)。然而,Java支持许多不同的数据类型,包括内置的基本值类型和可扩展的引用类型。
FXML loader使用BeanAdapter
的coerce()
方法来执行任何需要的类型转换。该方法能够执行基本的基本类型转换,如String
到boolean
或者int
到double
,还能将String
转换为Class
或Enum
。额外的转换到目标类型的方法能够通过定义静态valueOf()
方法来实现。
只读List属性
只读List属性是一个Bean属性,它的getter返回java.util.List
实例,且没有对应的setter方法。只读List属性元素的内容在处理时会自动添加到List中。
例如:javafx.scene.Group
的“children”属性是只读的List属性,用来表示group的子节点。
<?import javafx.scene.*?>
<?import javafx.scene.shape.*?>
<Group xmlns:fx="http://javafx.com/fxml">
<children>
<Rectangle fx:id="rectangle" x="10" y="10" width="320" height="240"
fill="#ff0000"/>
...
</children>
</Group>
当<children>
的每个元素被读取,这些子节点元素它被添加到Group#getChildren()
返回的列表中。
只读Map属性
只读Map属性是一个Bean属性,它的getter返回java.util.Map
实例,且没有对应的setter方法。只读Map属性元素的内容在处理时会自动添加到Map中。
javafx.scene.Node
的“properties”属性就是个只读Map属性的例子。下面这段代码将Label实例的“foo”和“bar”参数分别设置为“123”和“456”。
<?import javafx.scene.control.*?>
<Button>
<properties foo="123" bar="456"/>
</Button>
注意,既不是List类型也不是Map类型的只读属性,将会被视为只读Map。getter方法的返回值将会被包装在BeanAdapter
中,并且可以与任何其他只读Map相同的方式使用。
默认属性
一个类可以通过javafx.beans
包中定义的@DefaultProperty
注解来定义一个“默认属性”。如果存在,则表示默认属性的子元素可以从标记中省略。
例如:javafx.scene.latout.pane
(javafx.scene.layout.VBox
的超类)定义了一个“children”默认属性,<children>
元素不是必须的;loader会自动添加VBox
的子元素到容器的“children”集合中:
<?import javafx.scene.*?>
<?import javafx.scene.shape.*?>
<VBox xmlns:fx="http://javafx.com/fxml">
<Button text="Click Me!"/>
...
</VBox>
注意:默认属性并不限于集合。如果一个元素的默认属性指向一个标量值,那么该元素的任何子元素都将被设置为该属性的值。
例如,由于javafx.scene.control.ScrollPane
定义了一个默认属性“content”,一个包含文本区域作为其内容的滚动窗格可以像下面这样指定参数:
<ScrollPane>
<TextArea text="Once upon a time..."/>
</ScrollPane>
静态属性
元素也可以用来表示“静态”属性(有时也会称为“附加属性”)。静态属性是仅在特定上下文中有意义的属性。它们不是所应用的类的固有属性,而是由另一个类来定义的(通常是控件的父容器)。
静态属性是以定义它们的类的名字作为前缀的。例如,下面这段FXML调用了静态GridPane
的“rowIndex”和“columnIndex”属性的setter:
<GridPane>
<children>
<Label text="My Label">
<GridPane.rowIndex>0</GridPane.rowIndex>
<GridPane.columnIndex>0</GridPane.columnIndex>
</Label>
</children>
</GridPane>
这段FXML粗略地用JAVA代码表示就像这样:
GridPane gridPane = new GridPane();
Label label = new Label();
label.setText("My Label");
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
调用GridPane#setRowIndex()
和GridPane#setColumnIndex()
方法附加有关index的设置信息到Label
实例中。之后,GridPane
会在布局过程中使用这些设置来适当地安排子元素的位置。在其他容器中,包括AnchorPane
,BorderPane
和StackPane
均定义了类似的属性。
与实例属性一样,当属性值不能有效地表示时,通常使用静态属性元素来表示。否则,静态属性的属性值(这会在后面的章节进行讨论)通常会产生更简洁、更易读的标记。
定义块(Define Blocks)
<fx:define>
元素用来创建存在于对象层次结构之外但可能需要在其他地方引用的对象。
例如,使用单选按钮时,通常要定义一个ToggleGroup
组件来管理按钮的选择状态。这个组不是场景图的一部分,所以不应该直接添加知道按钮的父容器中。于是,这里使用定义块来创建按钮组来完成这件事,而不会影响FXML文档的整个结构。
<VBox>
<fx:define>
<ToggleGroup fx:id="myToggleGroup"/>
</fx:define>
<children>
<RadioButton text="A" toggleGroup="$myToggleGroup"/>
<RadioButton text="B" toggleGroup="$myToggleGroup"/>
<RadioButton text="C" toggleGroup="$myToggleGroup"/>
</children>
</VBox>
在定义块中,元素通常要赋值一个ID,用来在后面的元素的属性值中引用这个刚刚定义好的元素。关于ID的更多信息将会在后面的章节进行讨论。
FXML属性(Attributes)
一个FXML属性可以用来表示以下内容:
- 一个类实例的成员属性
- 静态成员属性
- 事件处理器
实例的成员属性
就像成员属性元素那样,FXML属性也能它用来设置类实例对象的成员属性。例如,下面这段代码创建了Button实例,并且将其text成员属性设置为“Click Me!”:
<?import javafx.scene.control.*?>
<Button text="Click Me!"/>
和成员属性元素一样,成员属性的FXML属性支持强制类型转换。当下面的标记进行处理时,“x”,“y”,“width”和“height”属性值会换成成double类型,而“fill”属性则会转换为Color
实例。
<Rectangle fx:id="rectangle" x="10" y="10" width="320" height="240"
fill="#ff0000"/>
与成员属性元素不同的是,FXML属性在处理时生效应用,而成员属性的FXML属性在到达各自元素的结束标记时才应用。这样做主要时为了方便这样一些情况:FXML属性值依赖某个信息,而这些信息只有元素的内容被完全处理之后才会可用(例如:TabPane
的当前选中的索引,只有添加了所有选项卡后,才能进行设置)。
在FXML中,成员属性的FXML属性写法和成员属性元素的关键区别在于,FXML属性写法支持大量可以用来扩展功能的“解析操作符”。FXML属性值支持以下操作符,这部分将在后面的章节讨论。
- 位置解析
- 资源解析
- 变量解析
位置解析
作为字符串,XML不能原生地表示类型化的位置信息。然而,通常有必要在标记中指定一些用来表示位置的值:例如表示一张图片资源的源地址。位置解析操作符(在FXML属性值字符串前加“@”前缀来表示)用来指定应该将属性值视为相对于当前文件的位置,而不是简单的字符串。
例如,下面这段代码创建了一个ImageView图片视图,以及使用my_image.png图片数据来填充这个图片视图,我们假设这张图片位于当前FXML文件的路径:
<ImageView>
<image>
<Image url="@my_image.png"/>
</image>
</ImageView>
因为Image是不可变对象,需要一个builder来构造它。另外,如果Image要定义一个valueOf(URL)
工厂方法,可以用如下方式来填充图片视图。
<ImageView image="@my_image.png"/>
“image”FXML属性值会通过FXML loader转换为URL对象,然后使用valueOf()方法强制转换为图像。
注意:URL中的空格会被转义编码,为了表示这样的文件名“My Image.png”,在FXML文档中应该这样书写:
<Image url="@My%20Image.png"/>
而不是:
<Image url="@My Image.png"/>
资源解析
在FXML中,资源替换可以在加载时执行,以达到本地化的目的。当提供java.util.ResourceBundle
实例时,FXML loader将用它们特定于区域设置的值替换资源名称的实例。资源名使用“%”前缀来标识。如下所示:
<Label text="%myText"/>
如果向loader提供这样的资源包定义:
myText = This is the text!
FXML loader的输出就会是一个包含“This is the text”字样的Label实例。
变量解析
FXML文档定义了一个变量命名空间,其中命名的元素和脚本变量可以用来唯一地标识。变量解析操作符允许调用者在调用相应setter方法之前用命名对象的实例替换属性值。变量引用由“$”前缀标识。如下所示:
<fx:define>
<ToggleGroup fx:id="myToggleGroup"/>
</fx:define>
...
<RadioButton text="A" toggleGroup="$myToggleGroup"/>
<RadioButton text="B" toggleGroup="$myToggleGroup"/>
<RadioButton text="C" toggleGroup="$myToggleGroup"/>
将fx:id
值赋值给一个元素会在文档的命名空间中创建一个变量,这个变量以后可以由变量解引用属性引用,比如上面显示的“toggleGroup”FXML属性,或者在后面章节讨论的脚本代码中引用。此外如果对象的类型定义了一个“id”属性,则该值也将传递给对象的setId()
方法。
转义序列
如果FXML属性值以一个资源解析前缀之一来开头,则可以通过在其前面加上反斜杠来(“\”)字符来转义字符。例如,下面的标记创建了一个Label
实例,它的文本是“$10.00”。
<Label text="\$10.00"/>