使用 JavaFX 2.0 FXML 呈现企业级应用程序 UI

作者:James L. Weaver

使用 FX 标记语言的强大功能定义企业级应用程序的 UI。

2012 年 6 月发布

下载:

下载Java FX

下载NetBeans IDE

下载示例项目(Zip 格式)

JavaFX 2.0 是一个用于创建富互联网应用程序 (RIA) 的 API 和运行时。JavaFX 于 2007 年推出,2011 年 10 月发布了 2.0 版本。该版本的一个优点是可以在成熟、熟悉的工具中通过 Java 语言编写代码。FX 标记语言 (FXML) 是 JavaFX 2.0 附带的一种工具,本文重点介绍了如何利用这种工具的功能快速定义企业级应用程序的用户界面。

FXML 支持使用 XML 呈现 UI。包含 FXML 功能的类位于 javafx.fxml 包中,其中包括 FXMLLoaderJavaFXBuilderFactory 以及一个名为 Initializable 的接口。本文提供了一个示例,演示了如何运用 FXML 和 JavaFX 2.0 的功能创建企业级应用程序。

SearchDemoFXML 应用程序概述

为帮助您了解如何在 JavaFX 2.0 应用程序中利用 FXML 功能,本文将使用一个名为 SearchDemoFXML 的示例应用程序。如图 1 所示,该应用程序包含以下内容:

  • 一个 TextField 和一个 Button,用于搜索 iTunes 中的媒体
  • 一个 TableView,用于显示搜索结果
  • 一个 ImageView,用于查看选所专辑的封面
  • 一个 Button,可以打开浏览器选项卡,预览所选标题的音频/视频剪辑

您将在下一节中下载 SearchDemoFXML 项目,其中包含该应用程序的代码,本文将着重探讨其中的部分代码。

图 1:搜索演示开始

图 1:SearchDemoFXML 应用程序的屏幕截图

如图 2 所示,在文本字段中输入歌曲名称、专辑名称或艺术家姓名并单击按钮之后,UI 右上角将出现一条提示信息,表示正在进行搜索。此外,该按钮的外观会发生变化,提示可以单击按钮取消搜索。在搜索过程中,文本字段将被禁用。

图 2:SearchDemoFXML 在搜索过程中的屏幕截图

图 2:SearchDemoFXML 在搜索过程中的屏幕截图

如图 3 所示,单击 Preview 按钮将在默认浏览器中打开一个新选项卡,预览音频或视频剪辑:

图 3:播放视频剪辑的浏览器选项卡

图 3:播放视频剪辑的浏览器选项卡

获取并运行 SearchDemoFXML 项目

  • 下载 NetBeans 项目文件,该文件中包含 SearchDemoFXML 项目。
  • 将该项目解压缩到您选择的目录。
  • 启动 NetBeans,选择 File -> Open Project
  • 在 Open Project 对话框中,转至所选目录后打开 SearchDemoFXML 项目,如图 4 所示。如果收到一个声明无法找到 jfxrt.jar 文件的消息,则单击 Resolve 按钮并转至 JavaFX 2.0 SDK 安装目录下面的 rt/lib 文件夹。

:可以从 NetBeans 网站获取 NetBeans IDE。

图 4:在 NetBeans 中打开 SearchDemoFXML 项目

图 4:在 NetBeans 中打开 SearchDemoFXML 项目

  • 要运行该应用程序,请单击工具栏上的 Run Project 图标或按下 F6 键。Run Project 图标外观类似媒体(例如,DVD)播放器上的 Play 按钮,如图 5 所示。

图 5:在 NetBeans 中运行 SearchDemoFXML 程序

图 5:在 NetBeans 中运行 SearchDemoFXML 程序

SearchDemoFXML 应用程序应显示在一个窗口中,如前面图 1 中所示。继续使用该应用程序,搜索歌曲、专辑和艺术家。完成上述操作后,我们就可以继续分析此应用程序,并探讨 FXML 相关代码。

分析 SearchDemoFXML 应用程序

在深入研究代码之前,我们先来分析图 6 所示的各个片段,这些片段共同组成了 SearchDemoFXML 应用程序。

图 6:SearchDemoFXML 应用程序示意图

图 6:SearchDemoFXML 应用程序示意图

让我们先从图 6 左上角的 SearchDemo.java 开始,它是这个 JavaFX 应用程序的主要模块,扩展了应用程序并且包含 main()start() 方法。在 SearchDemoFXML 应用程序中,该模块的用途是创建一个场景,通过 FXMLLoader.load() 方法从 search_demo.fxml 文件中获取场景图,并使用获取到的场景图填充场景。这就生成了图 6 左下角显示的 UI。

图 6 顶部中间位置的 search_demo.fxml 文件包含这个场景图的 XML 表示。此外,该文件还指定 SearchDemoController.java 文件是一个控制器 类,它将 UI 组件发生的事件委托给控制器 中的 handler 方法。

图 6 底部中间位置的 SearchDemoController 控制器类保存 search_demo.fxml 文件中对 UI 组件的引用,并处理这些组件发生的事件。

转到图 6 的右侧,SearchDemoController 调用 RestFX 类的方法来查询 iTuens 服务的 REST 接口。RestFX 是 JavaFX 2.0 外部的一个库,这里使用它来与 iTunes REST 端点通信,并分析端点的 JSON 响应。???本文末尾的“另请参见”一节提供了 REST/FX 项目的链接。

引导一个 FXML 应用程序

如上一节所述,SearchDemo.java 是这个 JavaFX 应用程序中的主要模块,如清单 1 所示:

package demos.search;

import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.SceneBuilder;
import javafx.stage.Stage;

public class SearchDemo extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("search_demo.fxml"),
ResourceBundle.getBundle("demos/search/search_demo"));

primaryStage.setTitle("Search Demo");
primaryStage.setWidth(650);
primaryStage.setHeight(500);
primaryStage.setScene(
SceneBuilder.create()
.root(root)
.build()
    );
primaryStage.show();
  }

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

清单 1:SearchDemo.java

清单 1 中的 FXMLLoader.load() 调用接受两个参数:

  • java.net.URL,代表 FXML 文件(本例中即 search_demo.fxml
  • ResourceBundle,包含了应用程序使用的字符串

需要注意的是,FXMLLoader.load() 方法还有其他签名,包括未指定 ResourceBundle 的签名。

加载 FXML 文件之后,场景图将被实例化,并分配给与阶段相关的场景的 root 属性,可通过 show() 方法显示。

使用 FXML 呈现 UI

如图 6 所示,search_demo.fxml 文件包含场景图的 XML 表示。该文件的内容如清单 2 所示:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import demos.search.*?>

<BorderPane fx:controller="demos.search.SearchDemoController" 
style="-fx-padding: 6 6 6 6" 
xmlns:fx="http://javafx.com/fxml">
<top>
<BorderPane>
<left>
<HBox spacing="6" style="-fx-padding: 0 0 6 0">
<children>
<TextField fx:id="searchTermTextField" prefColumnCount="18"
onAction="#handleSearchAction"/>
<Button fx:id="searchButton" disable="false" 
onAction="#handleSearchAction"/>
</children>
</HBox>
</left>
<right>
<Label fx:id="statusLabel"/>
</right>
</BorderPane>
</top>

<center>
<BorderPane>
<center>
<TableView fx:id="resultsTableView">
<fx:define>
<ResultCellValueFactory fx:id="resultCellValueFactory"/>
</fx:define>
<columns>
<ResultTableColumn 
key="itemName" text="%name" prefWidth="170" 
cellValueFactory="$resultCellValueFactory"/>
<ResultTableColumn 
key="itemParentName" text="%album" prefWidth="170" 
cellValueFactory="$resultCellValueFactory"/>
<ResultTableColumn 
key="artistName" text="%artist" prefWidth="170" 
cellValueFactory="$resultCellValueFactory"/>
</columns>
</TableView>
</center>
<right>
<VBox alignment="topCenter" spacing="6" style="-fx-padding: 0 0 0 6">
<children>
<StackPane prefWidth="120" prefHeight="120" 
style="-fx-border-color:#929292; -fx-border-width:1px">
<children>
<ImageView fx:id="artworkImageView"/>
</children>
</StackPane>
<Button fx:id="previewButton" text="%preview" 
onAction="#handlePreviewAction"/>
</children>
</VBox>
</right>
</BorderPane>
</center>
</BorderPane>

清单 2:search_demo.fxml

理解 FXML 文件的结构

如清单 2 所示,FXML 文件包含一些 import 处理指令和实例声明。import 处理指令类似于 Java import 语句,避免出现类名称由包名称限定的需要。

实例声明以分层的方式呈现场景图,包括布局容器、形状和 UI 控件等节点。请注意,根节点(本例中即 BorderPane)中包含一个名为 fx:controller 的属性。这指定 demos.search.SearchDemoController 类将作为此 FXML 文件的控制器。稍后,我们将详细介绍此控制器。

使用实例声明

实例声明的特点是,包含一个以大写字母开头的元素、多个以小写字母开头的可选属性,可选属性表示为以小写字母开头的内嵌元素。例如,下面来自清单 2 中的代码片段对 HBox 进行了实例化,将其 spacing 属性设置成 6style 属性设置成 -fx-padding:0 0 6 0。此外,这段代码段还将实例化一对 children(一个 TextField 和一个 Button),并将其包含在 HBox 中。

<HBox spacing="6" style="-fx-padding: 0 0 6 0">
<children>
<TextField fx:id="searchTermTextField" prefColumnCount="18"
onAction="#handleSearchAction"/>
<Button fx:id="searchButton" disable="false" 
onAction="#handleSearchAction"/>
</children>
</HBox>

注意,遵循 JavaBean 约定的任何类(例如,无参数构造函数和 get/set 方法)均可用于实例声明。

FXML 文件与控制器之间的映射

再看一下前面清单 2 中的代码片段,我们会发现 Button 实例声明中包含 fx:idonAction 属性。将 fx:id 属性赋予 "searchButton" 字符串,该字符串映射到 SearchDemoController 类中的 searchButton 实例变量。

onAction 属性赋予 "#handleSearchAction" 字符串,该字符串映射到 SearchDemoController 类中的 handleSearchAction() 方法。  

下面的代码片段摘自清单 4,展示了 searchButton 实例变量和 handleSearchAction() 方法(稍后的“定义控制器”一节将介绍相关内容)。

public class SearchDemoController implements Initializable {

  @FXML private Button searchButton;

  ...

  @FXML

  protected void handleSearchAction(ActionEvent event) {...}

  ...

}

该映射支持控制器管理实例在 FXML 文件中呈现的状态,同时处理实例中发生的事件。

将属性映射到 ResourceBundle 中的资源名称

清单 3 中的代码是清单 2 的另一个代码片段,演示了如何将一个实例声明中的属性值映射到 ResourceBundle 中的资源名称。如果为一个属性分配的值以百分比字符 (%) 开头,并且 FXMLLoader.load() 调用随 ResourceBundle 一起提供,则使用区域特定的值取代资源名称。

<TableView fx:id="resultsTableView">
<fx:define>
<ResultCellValueFactory fx:id="resultCellValueFactory"/>
</fx:define>
<columns>
<ResultTableColumn 
key="itemName" text="%name" prefWidth="170" 
cellValueFactory="$resultCellValueFactory"/>
<ResultTableColumn 
key="itemParentName" text="%album" prefWidth="170" 
cellValueFactory="$resultCellValueFactory"/>
<ResultTableColumn 
key="artistName" text="%artist" prefWidth="170" 
cellValueFactory="$resultCellValueFactory"/>
</columns>
</TableView>

清单 3:将属性映射到资源名称的示例

在这个示例中,TableView 列标题将从 search_demo.properties 文件中的资源名称 namealbumartist 开始检索。

创建场景图以外的对象

再次观察清单 3 中的代码片段,您将看到一个 fx:define 元素。该元素用于创建一个 ResultCellValueFactory 对象,并定义一个引用此对象的 resultCellValueFactory 变量。随后,每次 ResultTableColumn 实例化均使用此对象,$ 表示这是一个变量引用。

fx:define 元素的典型应用是创建一个不属于场景图节点的 ToggleGroup,并将 ToggleGroup 对象提供给多个表示互斥选择行为的单选按钮。

现在,让我们将注意力从 FXML 文件转移到控制器。

定义控制器

如前文所述,FXML 文件中声明的实例可以映射到控制器内的变量,而发生在这些实例中的事件则可以映射到控制器的处理程序。下面请观察清单 4,其中包含了控制器的代码。后面,我们将讨论更多关于 FXML 的概念。

package demos.search;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import restfx.web.GetQuery;
import restfx.web.Query;
import restfx.web.QueryListener;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

public class SearchDemoController implements Initializable {
@FXML private TextField searchTermTextField;
@FXML private Button searchButton;
@FXML private Label statusLabel;
@FXML private TableView<Map<String, Object>> resultsTableView;
@FXML private ImageView artworkImageView;
@FXML private Button previewButton;

private ResourceBundle resources = null;
private GetQuery getQuery = null;
public static final String QUERY_HOSTNAME = 
"ax.phobos.apple.com.edgesuite.net";
public static final String BASE_QUERY_PATH = 
"/WebObjects/MZStoreServices.woa/wa/itmsSearch";
public static final String MEDIA = "music";
public static final int LIMIT = 100;

public static final ImageView SEARCH_IMAGE_VIEW;
public static final ImageView CANCEL_IMAGE_VIEW;

static {
SEARCH_IMAGE_VIEW = new ImageView(new Image(SearchDemo.class
.getResourceAsStream("magnifier.png")));
CANCEL_IMAGE_VIEW = new ImageView(new Image(SearchDemo.class
.getResourceAsStream("bullet_cross.png")));
  }

@Override
@SuppressWarnings("rawtypes")
public void initialize(URL location, ResourceBundle resources) {
this.resources = resources;

// Initialize the search button content
searchButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
searchButton.setGraphic(SEARCH_IMAGE_VIEW);

// Add a selection change listener to the table view
resultsTableView.getSelectionModel().getSelectedCells()
.addListener(new ListChangeListener<TablePosition>() {
@Override
public void onChanged(Change<? extends TablePosition> change) {
while (change.next()) {
if (change.wasAdded()) {
updateArtwork(change.getAddedSubList().get(0).getRow());
          }
        }
      }
    });

// Do an example initial search so that the table is populated on startup
searchTermTextField.setText("Cheap Trick");
handleSearchAction(null);
  }

@FXML
protected void handleSearchAction(ActionEvent event) {
if (getQuery == null) {
String searchTerms = searchTermTextField.getText();

if (searchTerms.length() > 0) {
getQuery = new GetQuery(QUERY_HOSTNAME, BASE_QUERY_PATH);
getQuery.getParameters().put("term", searchTerms);
getQuery.getParameters().put("media", MEDIA);
getQuery.getParameters().put("limit", Integer.toString(LIMIT));
getQuery.getParameters().put("output", "json");

System.out.println(getQuery.getLocation());

statusLabel.setText(resources.getString("searching"));
updateActivityState();

getQuery.execute(new QueryListener<Object>() {
@Override
@SuppressWarnings("unchecked")
public void queryExecuted(Query<Object> task) {
if (task == getQuery) {
if (task.isCancelled()) {
statusLabel.setText(resources.getString("cancelled"));
searchTermTextField.requestFocus();
              } 
else {
Throwable exception = task.getException();
if (exception == null) {
Map<String, Object> value = 
(Map<String, Object>)task.getValue();
List<Object> results = (List<Object>)value.get("results");

// Update the table data
ObservableList<?> items = 
FXCollections.observableList(results);
resultsTableView.setItems(
(ObservableList<Map<String, Object>>)items);
statusLabel.setText(String.format(resources
.getString("resultCountFormat"), results.size()));

if (results.size() > 0) {
resultsTableView.getSelectionModel().select(0);
resultsTableView.requestFocus();
                  } 
else {
searchTermTextField.requestFocus();
                  }
                } 
else {
statusLabel.setText(exception.getMessage());
searchTermTextField.requestFocus();
                }
              }

getQuery = null;
searchButton.setDisable(false);

updateActivityState();
            }
          }
        });
      }
    } 
else {
getQuery.cancel(true);

searchButton.setDisable(true);
statusLabel.setText(resources.getString("aborting"));
    }
  }
@FXML
protected void handlePreviewAction(ActionEvent event) {
Map<String, Object> selectedResult = 
resultsTableView.getSelectionModel().getSelectedItem();

URL url;
try {
url = new URL((String)selectedResult.get("previewUrl"));
    } 
catch (MalformedURLException exception) {
throw new RuntimeException(exception);
    }

try {
java.awt.Desktop.getDesktop().browse(url.toURI());
    } 
catch (URISyntaxException exception) {
throw new RuntimeException(exception);
    } 
catch (IOException exception) {
throw new RuntimeException(exception);
    }
  }

private void updateActivityState() {
boolean active = (getQuery != null);
searchTermTextField.setDisable(active);
searchButton.setGraphic(active ?CANCEL_IMAGE_VIEW :SEARCH_IMAGE_VIEW);
  }

private void updateArtwork(int index) {
Map<String, Object> result = resultsTableView.getItems().get(index);
String artworkURL;
if (result == null) {
artworkURL = null;
previewButton.setDisable(true);
    } 
else {
artworkURL = (String)result.get("artworkUrl100");
System.out.println(result.get("itemName"));
previewButton.setDisable(false);
    }
artworkImageView.setImage(artworkURL == null ? 
null :new Image(artworkURL));
  }
}

清单 4:SearchDemoController.java

控制器初始化

如图 1 所示,SearchDemoFXML 程序启动时,首先搜索摇滚乐队 Cheap Trick 的歌曲,并使用搜索结果对表进行填充。这是通过清单 4 中所示的控制器的 initialize() 方法实现的,该方法由可选的 Initializable 接口指定。在控制器实施 Initializable 时,如果 FXMLLoader.load() 加载了一个引用,则实例化后将调用 initialize() 方法,并为该方法提供 FXML 文件的 URL 和对 ResourceBundle 的引用。???

如清单 5(来自清单 4 的代码片段)所示,这个 SearchDemoController 执行的另一个初始化操作如下:

  • 将所提供的 ResourceBundle 引用分配给一个实例变量
  • 修改 searchButton 引用的 Button 的外观
  • 向表中添加一个更改监听器,以确保表中的选定行发生变化时更新作品图像
public void initialize(URL location, ResourceBundle resources) {
this.resources = resources;

// Initialize the search button content
searchButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
searchButton.setGraphic(SEARCH_IMAGE_VIEW);

// Add a selection change listener to the table view
resultsTableView.getSelectionModel().getSelectedCells()
.addListener(new ListChangeListener<TablePosition>() {
@Override
public void onChanged(Change<? extends TablePosition> change) {
while (change.next()) {
if (change.wasAdded()) {
updateArtwork(change.getAddedSubList().get(0).getRow());
          }
        }
      }
    });

清单 5:SearchDemoController.java 执行的初始化

处理事件

在“FXML 文件与控制器之间的映射”一节中,我们了解了如何在 FXML 文件与控制器之间映射实例和事件处理。清单 6(清单 4 的另一个代码片段)中包含控制器事件处理程序的部分代码,用于处理用户单击 Search 按钮时的情况。

@FXML
protected void handleSearchAction(ActionEvent event) {
    ...
String searchTerms = searchTermTextField.getText();
      ...
if (searchTerms.length() > 0) {
statusLabel.setText(resources.getString("searching"));
                  ...
resultsTableView.setItems(
(ObservableList<Map<String, Object>>)items);
statusLabel.setText(String.format(resources
.getString("resultCountFormat"), results.size()));

if (results.size() > 0) {
resultsTableView.getSelectionModel().select(0);
resultsTableView.requestFocus();
                  } 
else {
searchTermTextField.requestFocus();
                  }

清单 6:来自 SearchDemoController.java 的事件处理程序的部分代码

清单 6 中的代码展示了控制器与节点(位于 FXML 文件呈现的场景图中)间的交互。例如,搜索词是从 searchTermTextField 中获取的,"searching" 资源名称的 statusLabel 文本设置为区域特定值。此外,屏幕中显示的表由搜索结果填充,该表的第一行被选中,并向系统请求了键盘焦点。

总结

FXML 是 JavaFX 2.0 附带的一种工具,该工具支持使用 XML 呈现 UI。javafx.fxml 包中含有能提供 FXML 功能的类。如果一个 JavaFX 应用程序采用了 FXML 功能,则当该应用程序启动时,它将使用 FXMLLoader.load() 方法载入 FXML 文件,并创建由该文件表示的场景图。FXML 文件指定一个控制器,该控制器所拥有的实例变量和处理程序方法分别映射到 FXML 文件所呈现的对象和事件中。

另请参见

关于作者

James L. (Jim) Weaver 是一位 Java 和 JavaFX 开发人员、作者和演讲者,积极致力于促进富客户端 Java 和 JavaFX 成为新应用程序开发的首选技术。Jim 撰写的著作包括《Inside Java》、《Beginning J2EE》和《Pro JavaFX 2》。他的专业背景丰富,有 15 年的 EDS 系统架构师经验和15年的独立开发经验。作为一名 Oracle Java 宣讲师,Jim 在许多国际软件技术会议上(包括在旧金山和圣保罗举办的 JavaOne 大会)发表过演讲。Jim 的博客地址为 http://javafxpert.com,Tweet 帐号为 @javafxpert,电子邮件联系方式为 james.weaver AT oracle.com