JavaFX 是 Java 的一个实现客户端窗口的框架,该框架运行用户将 UI 设计和代码逻辑分开来,还提供了 SceneBuilder 可视化拖动绘制 UI 并自动生成 Controller 骨架结构。
Controller
controller 用于处理页面的标记和业务逻辑处理,可以在里面写一些 handler 、设置监听等。controller 通过 fxml 文件的 fx:controller 属性设置。
处理事件的 Handler
在 fxml 文件中定义的节点,声明的点击事件就在 controller 中给出对应的实现方法,示例如下
<VBox fx:controller="com.foo.MyController"
xmlns:fx="http://javafx.com/fxml">
<children>
<Button text="Click Me!" onAction="#handleButtonAction"/>
</children>
</VBox>
其中 button 的点击事件声明的 handleButtonAction 实现如下
public class MyController {
public void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
}
}
Initialize() 方法
很多情况下,这有助于简化在样式中的声明事件处理方法。当 controller 中有更多的行为控制和需要操作的元素时,可以在 controller 中定义 initialize() 方法,这个会在与其相关联的 fxml 文件完全加载完成之后调用一次。
实现这个方法可以对内容进行各种必要的异步处理。这同样使得 controller 可以访问用于加载资源的文件和解析相对路径在文档中的位置(通常等同于本文档的位置)。
如例,下面的代码定义了 initialize 方法,其中实现了一个button 的 action 事件处理方法,而不是像之前的例子一样是通过事件处理属性定义的。这个 button 实例变量是在文件被加载时被 loader 注入的。
<VBox fx:controller="com.foo.MyController"
xmlns:fx="http://javafx.com/fxml">
<children>
<!-- 没有属性 -->
<Button fx:id="button" text="Click Me!"/>
</children>
</VBox>
public class MyController implements Initializable {
public Button button;
@Override
public void initialize(URL location, Resources resources)
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("You clicked me!");
}
});
}
}
FXML 注解
请注意,在之前的例子中,controller 中的成员变量和方法都是被声明为 public 的所以它们能够被 loader 调用。在实践中,这通常不会有什么问题,因为通常一个 controller 只会对创建它的 FXML loader 可见。但是如果你是对controller字段和 handler 方法可见性有着严格要求的程序员,可以考虑使用 @FXML
注解。这个注解可用于标注允许 FXML 访问的类成员。
之前的例子可以被改写为:
public class MyController {
@FXML
private void handleButtonAction(ActionEvent event) {
System.out.println("You clicked me!");
}
}
public class MyController implements Initializable {
@FXML private Button button;
@FXML
protected void initialize()
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("You clicked me!");
}
});
}
}
嵌套 Controller
使用嵌套 Controller 的 FXML 文件通过 fx:include 属性可以直接定位到其中的成员 controller,这允许开发者很容易的访问定义在 include 中节点的 controller 中的方法。例如:
<VBox fx:controller="com.foo.MainController">
<fx:define>
<fx:include fx:id="dialog" source="dialog.fxml"/>
</fx:define>
...
</VBox>
外部 controller 中使用内部 include 节点的 controller
public class MainController extends Controller {
//include 的根节点
@FXML private Window dialog;
//嵌套的 controller 作为成员变量
@FXML private DialogController dialogController;
...
}
当 initialize 方法被调用时,dialog 字段就会持有 dialog.fxml 的根节点,dialogController 就会持有内部节点的 controller。这样 MainController 就可以调用内部 controller 中的方法来发布和展示出 dialog 界面了。请注意,直接使用 fx:include 引入文件中内容的话,它将成为 main_window_content 布局的一部分,所以必须使用 fx:define 将 fx:include 包裹起来,将这两个窗口的布局分隔开。
FXMLLoader
FXMLLoader 类实际负责加载 fxml 源文件并返回结果对象图。比如,下面段代码从相对于加载类的类路径上加载了一个 fxml 文件并使用名为“com.example.foo”的资源包将其本地化。这个根节点的类型是javafx.scene.layout.Pane
的子类,并且这个文件中定义了一个 MyController 类型的控制器。
URL location = getClass().getResource("example.fxml");
ResourceBundle resources = ResourceBundle.getBundle("com.foo.example");
FXMLLoader fxmlLoader = new FXMLLoader(location, resources);
Pane root = (Pane)fxmlLoader.load();
MyController controller = (MyController)fxmlLoader.getController();
注意,load 方法返回的在文件中实际命名类组成的层次结构实例,而不是org.w3c.dom 节点代表的那些类。FxmlLoader 内部使用 javax.xml.streamAPI(也可被用于处理 xml 或者 stax) 来加载 fxml 文件。StAX 是一个极其高效的基于事件处理的 xml 解析 API ,概念上和它的 w3c 前身 SAX 类似。它可以一次性解析完 fxml 文件,而不是先加载一个 DOM 中间结构后,再去异步的处理。
自定义组件
FXMLLoader 中的 setRoot() 和 setController() 方法允许调用方将文件的root和controller 分别注入到文件的命名空间中,而不用委托 Loader 自己来完成这些值的创建。这使得开发者可以创建通过内部 xml 语言实现的可重用控件,但是从 API 的角度看起来和编程实现的控件一样。
比如,下下面这段 xml 就定义了一个简单的自定义控件结构,包含一个 TextField 和一个 Button 实例。其根容器被声明为一个 VBox 实例。
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml">
<TextField fx:id="textField"/>
<Button text="Click Me" onAction="#doSomething"/>
</fx:root>
fx:root 标签会创建一个已经定义过的根元素的引用,这个元素的值是通过调用 Fxml 中的 getRoot() 方法获取的。在调用 load() 之前,调用方必须先通过调用 setRoot() 设置过 root 的值。调用者同样也会通过调用 setController() 为文件的 controller 设置值,并将在文件被加载时作为该文件的 controller 使用。在创建自定 FXML 组件时,这两个方法通常会被放在一起使用。
一下这个示例,CustomControl 继承自 VBox ,并在它的构造函数中设置了 FXML 文件的 root 和 controller 。当这个类被加载时,CustomControl 中的内容将会根据之前的 FXML 文件中的内容进行填充(CustomControl 将会根据之前定义的 FXML 文件中的内容进行加载)。
public class CustomControl extends VBox {
@FXML private TextField textField;
public CustomControl() {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("custom_control.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
public String getText() {
return textProperty().get();
}
public void setText(String value) {
textProperty().set(value);
}
public StringProperty textProperty() {
return textField.textProperty();
}
@FXML
protected void doSomething() {
System.out.println("The button was clicked!");
}
}
现在,我们就可以在代码或者 fxml 文件中使用这个自定义组件的示例了,就像使用其他的组件一样。
HBox hbox=new HBox();
CustomControl custom=new CustomControl();
custom.setText("hello");
hbox.getChildren().add(custom);
Fxml中使用
<HBox>
<CustomControl text="Hello World!"/>
</HBox>