文章
Java
作者:James L. Weaver
使用 FX 标记语言的强大功能定义企业级应用程序的 UI。
下载:
:示例项目(Zip 格式)
JavaFX 2.0 是用于创建富互联网应用程序 (RIA) 的 API 和运行时。JavaFX 于 2007 年推出,2011 年 10 月发布了 2.0 版本。该版本的一个优点是可以在成熟、熟悉的工具中通过 Java 语言编写代码。
本文是该系列文章(共两部分)的第 2 部分,重点介绍了如何利用 FX 标记语言 (FXML,JavaFX 2.0 附带的一种工具)的功能快速定义企业级应用程序的用户界面。
FXML 支持使用 XML 呈现 UI。包含 FXML 功能的类位于 javafx.fxml 包中,其中包括 FXMLLoader、JavaFXBuilderFactory 以及名为 Initializable 的接口。本文在该系列第 1 部分的基础上对 SearchDemoFXML 示例进行了延伸,在其中包含了更多使用 FXML 创建企业应用程序的概念和技术。
在本系列的第 1 部分中,使用了 SearchDemoFXML 应用程序示例来帮助您了解如何在 JavaFX 2.0 应用程序中利用 FXML。在本文中,我们将为该应用程序提供另一种 UI,并在不修改 SearchDemoController.java 控制器类的情况下用一个新的 FXML 文件来描述它。
如图 1 和图 2 所示,该示例包含一些与原示例相同的 UI 组件:
此外,还增加了其他组件,并修改了布局和导航。而且,该 FXML 示例中还实施了级联样式表 (CSS) 和本地化。您将在下一节中下载 SearchDemoFXML 项目,其中包含该示例的代码,本文将着重探讨其中的部分代码。

图 1:SearchDemoFXML 应用程序的屏幕截图
当应用程序启动时,如果用户机器的区域设置与 SearchDemoFXML 应用程序所支持的区域设置相匹配,则 UI 控件中的文本将本地化。图 1 中的屏幕截图显示了当应用程序运行在区域设置为 en_US 的机器上时所显示的外观。当您在文本字段中输入标题或艺术家并单击按钮后,会弹出一个表,其中包含查询 iTunes 服务器所返回的一组歌曲,如图 1 所示。
从表中选择一首歌曲,然后选择 Detail 选项卡,此时会显示所选歌曲的专辑封面,如图 2 所示。单击 Preview 按钮在默认浏览器中打开新的选项卡,预览音频或视频剪辑:
图 2:SearchDemoFXML 在搜索过程中的屏幕截图
注:可以从 NetBeans 网站获取 NetBeans IDE。
图 3:在 NetBeans 中打开 SearchDemoFXML 项目
图 4:在 NetBeans 中运行 SearchDemoFXML 应用程序
SearchDemoFXML 应用程序会显示在一个窗口中,如前面图 1 中所示。继续使用该应用程序搜索歌曲、专辑和艺术家。当您准备好时,我们将继续介绍此程序版本中增加的 FXML 和相关特性。
在深入研究代码之前,我们先来分析图 5 所示的各个片段,这些片段共同组成了 SearchDemoFXML 应用程序。

图 5:SearchDemoFXML 应用程序示意图
从图 5 左上角开始,SearchDemo.java 是此 JavaFX 应用程序的主要模块,扩展了 Application 并包含 main() 和 start() 方法。它在 SearchDemoFXML 应用程序中的主要作用是创建一个场景并用场景图来填充它,而场景图需通过 FXMLLoader.load() 方法从 search_demo_w_tabs.fxml 文件或 search_demo.fxml 文件获得。
为了实现这一点,应用程序会在启动时检查名为 tabs 的命名参数 。如果 tabs 命名参数的值为 true,则加载 search_demo_w_tabs.fxml 文件,否则加载 search_demo.fxml 文件。这就生成了图 5 左下角显示的用户界面之一,具体取决于 tabs 命名参数。
图 5 顶部中间的 search_demo_w_tabs.fxml 和 search_demo.fxml 文件包含场景图的两种完全不同的 XML 表示。这两个文件还将 SearchDemoController.java 文件指定为控制器 类,它将 UI 组件发生的事件委托给控制器 中的处理器 方法。
图 5 底部中间位置的 SearchDemoController 控制器类保存 search_demo_w_tabs.fxml 和 search_demo.fxml 文件中对 UI 组件的引用,并处理这些组件发生的事件。
在图 5 右侧可以看到,SearchDemoController 调用 RestFX 类的方法来查询 iTuens 服务的 REST 接口。RestFX 是 JavaFX 2.0 外部的一个库,这里使用它来与 iTunes REST 端点通信,并分析端点的 JSON 响应。本文末尾的“另请参见”一节提供了 REST/FX 项目的链接。
如上一节所述,SearchDemo.java 是该 JavaFX 应用程序的主要模块。如清单 1 所示:
package demos.search;
import java.util.Locale;
import java.util.Map;
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 {
System.out.println("In SearchDemo#start()");
Object tabsParamObj;
Object languageParamObj;
Object countryParamObj;
String fxmlFile = "search_demo.fxml";
Locale locale = Locale.getDefault();
Map<String, String> namedParams = getParameters().getNamed();
if (namedParams != null) {
tabsParamObj = namedParams.get("tabs");
if (tabsParamObj != null) {
System.out.println("param tabs:"+ tabsParamObj);
if (((String)tabsParamObj).equalsIgnoreCase("true")) {
fxmlFile = "search_demo_w_tabs.fxml";
}
}
else {
System.out.println("param tabs not found");
}
languageParamObj = namedParams.get("language");
if (languageParamObj != null) {
System.out.println("param language:"+ languageParamObj);
}
else {
System.out.println("param language not found");
}
countryParamObj = namedParams.get("country");
if (countryParamObj != null) {
System.out.println("param country:"+ countryParamObj);
}
else {
System.out.println("param country not found");
}
if ((languageParamObj != null) &&
((String)languageParamObj).trim().length() > 0) {
if ((countryParamObj != null) &&
((String)countryParamObj).trim().length() > 0) {
locale = new Locale(((String)languageParamObj).trim(),
((String)countryParamObj).trim());
}
else {
locale = new Locale(((String)languageParamObj).trim());
}
}
}
else {
System.out.println("No params found for SearchDemo");
}
Parent root = FXMLLoader.load(getClass().getResource(fxmlFile),
ResourceBundle.getBundle("demos/search/search_demo", locale));
System.out.println("Locale set to:"+ locale);
primaryStage.setTitle("Search Demo");
primaryStage.setWidth(650);
primaryStage.setHeight(500);
primaryStage.setScene(
SceneBuilder.create()
.root(root)
.stylesheets("demos/search/myStyles.css")
.build()
);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
清单 1:SearchDemo.java
如清单 1 所示,已获取三个应用程序特定命名参数的值:tabs、language 和 country:
如清单 1 所示,此时会调用 FXMLLoader.load() 方法来加载所需的 FXML 文档和ResourceBundle。
要在 SearchDemoFXML 项目中尝试这些参数,您可以在 NetBeans 中将参数输入到 Project Properties 对话框的 Run 部分,如图 6 所示。
图 6:NetBeans 中 Project Properties 对话框中的 Run 部分
正如本系列第 1 部分所述,加载 FXML 文档之后,场景图将被实例化并分配给与阶段相关的场景的 root 属性,可通过 show() 方法显示。
如图 5 所示,search_demo_w_tabs.fxml 文档包含场景图的 XML 表示。继续查看清单 2,我们将介绍本文档中的新概念,而这些概念在第 1 部分并未提及:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.layout.*?> <?import javafx.scene.control.*?> <?import javafx.scene.image.*?> <?import javafx.scene.web.*?> <?import javafx.geometry.*?> <?import demos.search.*?> <BorderPane id="app-border" fx:controller="demos.search.SearchDemoController" xmlns:fx="http://javafx.com/fxml"> <top> <HBox> <children> <ImageView> <image> <Image url="@javafx-logo.png"/> </image> </ImageView> <Region HBox.hgrow="ALWAYS"/> <Label id="app-title" text="%appTitle"/> <Region> <HBox.hgrow>ALWAYS</HBox.hgrow> </Region> <HBox> <children> <HBox spacing="5" fillHeight="false"> <HBox.margin> <Insets top="20" right="10"/> </HBox.margin> <children> <TextField fx:id="searchTermTextField" prefColumnCount="11" promptText="%titleOrArtist" onAction="#handleSearchAction"/> <Button fx:id="searchButton" disable="false" onAction="#handleSearchAction"/> </children> </HBox> <Label fx:id="statusLabel"/> </children> </HBox> </children> </HBox> </top> <center> <TabPane> <tabs> <Tab text="%search" closable="false"> <content> <TableView fx:id="resultsTableView"> <fx:define> <ResultCellValueFactory fx:id="resultCellValueFactory"/> </fx:define> <columns> <ResultTableColumn key="itemName" text="%name" prefWidth="220" cellValueFactory="$resultCellValueFactory"/> <ResultTableColumn key="itemParentName" text="%album" prefWidth="220" cellValueFactory="$resultCellValueFactory"/> <ResultTableColumn key="artistName" text="%artist" prefWidth="220" cellValueFactory="$resultCellValueFactory"/> </columns> </TableView> </content> </Tab> <Tab text="%detail" closable="false"> <content> <VBox fillWidth="false" alignment="topCenter" spacing="6" style="-fx-padding: 10 0 0 0"> <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> </content> </Tab> </tabs> </TabPane> </center> <bottom> <HBox id="footer"> <children> <Label fx:id="statusLabel"/> </children> </HBox> </bottom> </BorderPane>
清单 2:search_demo_w_tabs.fxml
如以下代码段(节选自清单 2)所示,您可以使用 @后缀从与 FXML 文件相关的位置检索资源(例如,PNG 文件中的图像):
<ImageView> <image> <Image url="@javafx-logo.png"/> </image> </ImageView>
请注意,此处使用的技术与本示例中其他 ImageView 实例所使用的技术不同,因为在那些情况下,Image 对象由控制器来管理。
您可能已经注意到,如果您调整 SearchDemoFXML 应用程序(加载 search_demo_w_tabs.fxml 之后)的大小,应用程序标题会在标题中保持水平居中。这是由于清单 3(清单 2 中的代码段)所示的 Region 实例起到了水平“弹簧”的作用:
<HBox> <children> <ImageView> <image> <Image url="@javafx-logo.png"/> </image> </ImageView> <Region HBox.hgrow="ALWAYS"/> <Label id="app-title" text="%appTitle"/> <Region> <HBox.hgrow>ALWAYS</HBox.hgrow> </Region> <HBox> <children> <HBox spacing="5" fillHeight="false"> <HBox.margin> <Insets top="20" right="10"/> </HBox.margin> <children> <TextField fx:id="searchTermTextField" prefColumnCount="11" promptText="%titleOrArtist" onAction="#handleSearchAction"/> <Button fx:id="searchButton" disable="false" onAction="#handleSearchAction"/> </children> </HBox> <Label fx:id="statusLabel"/> </children> </HBox> </children> </HBox>
清单 3:Region 实例的作用就像弹簧
清单 3 中使用的 HBox.hgrow 表示静态属性,等同于以下 Java 代码(这些 Java 代码不是本应用程序示例的组成部分):
HBox.setHgrow(fooRegion, Priority.ALWAYS);
请注意,清单 3 中的 HBox.margin 同样使用了这一技术来设置 HBox 的边距,以容纳 TextField 和 Button。以下提供了等效的 Java 代码供您参考:
HBox.setMargin(fooHBox, new Insets(20, 10, 0, 0));
如下列代码段(节选自清单 2)所示,FXML 元素可以在与场景相关的级联样式表中引用规则:
<Label id="app-title" text="%appTitle"/>
在本例中,清单 4 中 #app-title 选择器表达的样式将应用于上述代码段中的 Label。请注意,使用 id 将元素与 CSS 规则相关联以及使用 fx:id 将元素映射到控制器中的实例变量,两者之间存在区别。
#app-border {
-fx-border-color:grey;
-fx-border-width: 3;
}
#footer {
-fx-border-color:grey;
-fx-border-width: 1;
-fx-padding: 5;
}
#app-title {
-fx-font-size:20pt;
-fx-font-weight:normal;
-fx-text-fill:grey;
-fx-padding: 15 0 0 0;
}
清单 4:myStyles.css
此外,正如本系列第 1 部分所述,FXMLLoader.load() 方法加载的 ResourceBundle 将会检索 Label 的文本,如清单 1 所示。例如,当区域设置为 pt_BR 时,Label 将显示从 search_demo_pt_BR.properties 文件检索的字符串“Demo de Busca”,如清单 5 所示。
name=Nome album=Álbum artist=Artista preview=Pré-visualização searching=Buscando... aborting=Abortando... cancelled=Solicitação cancelada resultCountFormat=Encontrados %1$d itens que correspondem a sua busca appTitle=Demo de Busca search=Busca detail=Detalhe titleOrArtist=Título ou Artista
清单 5:search_demo_pt_BR.properties
按照 Oracle“部署 JavaFX 应用程序”指南中的说明,SearchDemoFXML 示例已被部署为小程序并通过网页上的 Java Web Start 启动,如本文末尾的“另请参见”一节所述。“另请参见”一节还提供了“部署 JavaFX 应用程序”指南的链接。图 7 包含该网页的屏幕截图,其中显示了两个并行运行的应用程序实例,两者加载了不同的 FXML 文档。
此外,图 7 中的网页还显示了几个调用 SearchDemoFXML 应用程序的 Java Web Start 链接,每个链接所调用的应用程序均采用不同的区域设置。小程序和 Java Web Start 链接部署采用了之前 HTML 页面中讨论的命名参数和 Java Web Start 的 JNLP 文件中讨论的命名参数。
图 7:作为小程序和作为本地化 Java Web Start 链接运行的 SearchDemoFXML
FXML 是 JavaFX 2.0 附带的一种工具,该工具支持使用 XML 呈现 UI。除了本系列第一部分中介绍的概念,FXML 还能够在不修改控制器的情况下彻底更改 UI 。这可以通过加载不同的 FXML 文档、使用 JavaFX 级联样式表和创建本地化资源包来实现。通过在这些特性中使用命名参数,可以在启动时为应用程序提供相关信息。
Jim Weaver 是一位独立的 Java 和 JavaFX 开发人员、作者和演讲者,积极致力于促进富客户端 Java 和 JavaFX 成为新应用程序开发的首选技术。
Jim 撰写的著作包括《Inside Java》、、《Beginning J2EE》、《Pro JavaFX Platform》,后者已更新,涵盖了 JavaFX 2.0。他的专业背景包括 15 年的 EDS 系统架构师,以及同样年数的独立开发人员和软件开发公司老板。Jim 在许多国际软件技术会议上发表过演讲,包括在旧金山和圣保罗 JavaOne 2011 大会上的演讲和在俄勒冈州波特兰市 O’Reilly OSCON/Java 2011 大会上的主题演讲。
Jim 的博客为 http://javafxpert.com,Tweet 为 @javafxpert,电子邮件联系方式为 jim.weaver[at]javafxpert.com。