JavaFX fxml 快速搭建入门

之前一直使用命令行或者Web技术当作用户交互界面的方式,而且刚开始学习编程的时候前人的经验都挺排斥图形界面编程的,以致于自己除了用Web页面方式之外,再也没有别的图形化编程经验。因为最近有些需要,语言要求是Java,在网上浏览之后,决定采用JavaFX而不是Swing或者awt。事实上,也有可能因为笔者Web项目的经验,而这些框架是基于MVC思想的,所以上手很快,就是得花点时间熟悉提供的工具特性。

环境支持

  1. JDK 1.8

笔者用的IDE是JetBrain IDEA Ultimate 2017.1,所以教程也是依据这个IDE提供的功能进行。

创建项目

打开IDEA,创建项目,在如图左侧选择JavaFX,一直下一步,项目名一如既往为test:

然后IDEA就会根据模板给我们创建好项目:

其中,src源码目录下会有一个sample的子目录,这个子目录在将来的工程开发即为包的名字,这个细节是后面的事了。sample目录下有三个文件:

  1. Controller.java,上面提到过,这些框架是基于MVC思想的,自然而然这个类就是项目的控制器了。当然也跟一般的web项目区别不大, JavaFX项目可以存在多个控制器。但是因为笔者不清楚JavaFX下是否有类似Spring这类IOC框架,所以控制器的实例化就需要手动操作了。
  2. Main.java,因为不是web项目,没有容器,所以JavaFX与一般项目一样,需要一个程序的入口,而这里的Main类就承担了这个责任。而且当存在启动参数设置时,尤其是需要传递到应用中,Main类则又承担了应用参数初始化的责任,这些系列将在后面演示。
  3. sample.fxml,这个就是GUI布局的文件,跟html有一些相似,却又不同,JavaFX指出两种建立GUI的方式,一种是在代码里面用代码的方式初始化GUI的布局,另一种就是用fxml的文件初始化布局。

FXML界面布局

这里介绍的是用fxml方式初始化界面布局,所谓的与html不同,是因为html提供了一些很直观的方式,例如当你放置一个tr在一个tr下面的时候,在没有css文件针对布局的时候,呈现出来的tr就一定是在tr下面,即编码的布局与显示的布局绑定了。在IDEA模板中,fxml里面的第一层是一个GridPane,将图形界面分为一个一个的格子。IDEA初始化的sample.fxml内容:

1
2
3
4
5
6
7
8
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
</GridPane>

既然是快速入门,先运行一遍项目,可以看到一个空白的窗口:

开始修改图形界面,以加入一个TextField为例,试试简单地在GridPane里面添加一句TextField:

1
2
3
4
5
6
7
8
9
10
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<TextField/>
</GridPane>

运行得到:

似乎没有什么问题,接下来尝试在这个TextField的左边加个显示内容为Input:Label以及一个Button,注意这时候就需要给这个GridPane内的元素添加GridPane.rowIndexGridPane.columnIndex属性来确定元素在里面的位置了,不然笔者猜测可能自动将添加的元素默认为GridPane.rowIndex=0GridPane.columnIndex=0然后按照出现的顺序将前面的元素覆盖了。修改后的sample.fxml:

1
2
3
4
5
6
7
8
9
10
11
12
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<Label text="Input:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Button GridPane.rowIndex="1" GridPane.columnIndex="1" text="Button"/>
</GridPane>

然后就可以得到一个简易的界面:

由于本文目的是快速构建一个GUI,例如想将两个元素放置入一个GridPane中(使用VBox)等涉及的更多的布局需求情况暂不在此展开讨论。

获取图形界面元素中的值

前面的步骤也只是建立了一个简单的图形界面,但是程序的后台无法获得图形中的界面发生的事件以及元素中的值。这一步将介绍如何获取窗口中Button的点击事件,以及获得TextField中的值。

首先打开Controller.java,IDEA给我们创建了空的内容:

1
2
3
4
package sample;

public class Controller {
}

因为我们是想获取TextField的内容的值,以及Button的点击事件,将代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class Controller {

@FXML
private TextField textField;

@FXML
public void handleButtonAction(ActionEvent actionEvent)
{
System.out.println(textField.getText());
}
}

有的教程里面会提示Controller需要实现javafx.fxml.Initializable接口才能正常使用Controller,因为在本过程中没有出现该情况,因而简做提示而跳过。

注意textField这个变量名,以及在Controller中成员以及函数添加的@FXML注解,都是为了让他们能被感知到。接着修改sample.fxml:

1
2
3
4
5
6
7
8
9
10
11
12
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<Label text="Input:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField GridPane.rowIndex="0" GridPane.columnIndex="1" fx:id="textField"/>
<Button GridPane.rowIndex="1" GridPane.columnIndex="1" text="Button" onAction="#handleButtonAction"/>
</GridPane>

注意到GridPane被IDEA模板创建的时候带有了一个fx:controller="sample.Controller",即该界面与Controller绑定,然后在TextField添加的fx:id="textField"即代表这个TextFieldController中的textField成员绑定。而Button则添加了一个onAction="#handleButtonAction"代表这个Button在发生点击事件的时候调用Controller中的handleButtonAction方法。

运行程序,在输入框中输入一些测试用的数据:

然后查看控制台输出:

证明Controller的成员成功地与图形界面中的元素成功对应。

设置传递非界面参数予Controller

前面介绍的基础内容已经足够一般的交互情况了,但是之前笔者涉及到读取程序启动参数并传入到Controller中,而且这个也可能涉及到程序的全局变量初始化问题,因而介绍一下。

假设我们需要设置Controller.textField成员初始显示的值,而且是从程序启动参数中获取,我们在Controller中添加一个String fromCmd并设置一个对应的setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class Controller {

@FXML
private TextField textField;

private String fromCmd;

@FXML
public void handleButtonAction(ActionEvent actionEvent)
{
System.out.println(textField.getText());
}

public void setFromCmd(String fromCmd) {
this.fromCmd = fromCmd;

textField.setText(fromCmd);
}
}

注意在此类类实例不知道什么时候初始化的情况里,不要用构造函数的方法初始化一些变量,之前尝试了初始化TableView并且在初始化中设置一些值,然而因为不清楚复杂的构造过程,无法成功构建。

原本的Main.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}


public static void main(String[] args) {
launch(args);
}
}

修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

private static String[] appArgs;

@Override
public void start(Stage primaryStage) throws Exception{

FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("sample.fxml"));

Parent root = fxmlLoader.load();

Controller controller = fxmlLoader.getController();

if(appArgs.length > 0)
{
controller.setFromCmd(appArgs[0]);
}
else
{
controller.setFromCmd("default");
}

primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}


public static void main(String[] args) {

appArgs = args;

launch(args);
}
}

这样的话我们就可以获取到程序启动参数并输入到Controller当中,当使用者提供启动参数的时候则设置到Controller.textField中,否则设置其值为default

运行结果,不输入参数:

在IDEA中设置程序启动参数:

加入参数后的运行结果:

从前面的Main.java获取Controller的过程来看,Controller的初始化是交给了FXMLLoader的,我们无法得知其初始化的时机,而且也之能够从FXMLLoader获取得帮定的控制器。

至此,一个基于fxml的快速构建应用介绍完毕。