文章
Java
作者:James L. Weaver
本文是由两部分组成的系列文章的第 2 部分,重点介绍如何在 JavaFX 2.0 中使用最佳实践来开发企业应用程序。
下载:
:Java FX
:NetBeans IDE
:示例应用程序 (Zip)
JavaFX 2.0 是一个用于创建富互联网应用程序 (RIA) 的 API 和运行时。JavaFX 于 2007 年推出,2011 年 10 月发布了 2.0 版本。JavaFX 2.0 的一个优点是可以使用成熟、熟悉的工具用 Java 语言编写代码。
本文是由两部分组成的系列文章的第 2 部分,重点介绍如何在 JavaFX 2.0 中使用最佳实践来开发企业应用程序。
注:有关本系列文章第 1 部分的内容,请查看 JavaFX 2.0 企业应用程序最佳实践(第 1 部分)。为说明在 Java 2.0 中开发企业应用程序时用到的更多技巧和最佳实践,我们将继续研究 TweetBrowser 应用程序。该应用程序的屏幕截图如图 1 所示。
您将在下一节中下载 TweetBrowser 项目,其中包含该应用程序的代码,本文将着重探讨其中的部分代码。

图 1:TweetBrowser 应用程序启动时的屏幕截图
回顾第 1 部分,当您单击 #hashtag 或 @screenname 时,UI 的右上角将出现一个旋转的进度指示器,指示正在进行搜索。您也可以在文本域键入一个 #hashtag、@screenname 或文字,然后按 Enter 键或单击 Search 按钮。无论使用哪种方式,Search 按钮的外观都将变成 X,指示您可以通过单击该按钮取消搜索并禁用大部分 UI。
单击某推文中的 Web 链接将打开一个弹出式窗口,其中包含所选页面的 WebView。
注:可以从 NetBeans 网站获取 NetBeans IDE。

图 2:在 NetBeans 中打开 TweetBrowser 项目

图 3:在 NetBeans 中运行 TweetBrowser 应用程序
TweetBrowser 应用程序应显示在一个窗口中,如前面图 1 中所示。继续使用该应用程序,浏览屏幕名称、hashtag 和 Web 链接。下面我们将分析该应用程序并探讨其中一些代码。
在深入研究代码之前,我们先来了解一下图 4 中显示的各个片段,这些片段共同组成了 TweetBrowser 应用程序。

图 4:TweetBrowser 应用程序示意图
首先看一下图 4 的右上角,用户可以通过单击 TweetBrowser 主页上的大眼鸟图标来启动应用程序。如果尚未运行 TweetBrowser 实例,系统将会通过 Java Web Start 调用应用程序。有关 TweetBrowser 主页的 URL,请参考“另请参见”一节。
图 4 左下角代表的是 javafxpert.tweetbrowser.ui 软件包(为节省空间在示意图中缩写为 tweetbrowser.ui),其中包含 2 个 Java 类和一个 JavaFX 级联样式表 (CSS):
图 4 中下部代表的是 javafxpert.tweetbrowser.model 软件包,其中包含 3 个 Java 类:
如图 4 所示,javafxpert.tweetbrowser.ui 软件包中的类将调用 javafxpert.tweetbrowser.model 软件包中类的方法。此外,javafxpert.tweetbrowser.ui 软件包中的类还会将模型的状态呈现到 UI,这主要利用 JavaFX 的绑定功能来实现。
在本系列文章的第 1 部分中,我们已经讨论过 TweetBrowser 应用程序用到的一些技巧和最佳实践,即下面这些内容:
在本文中,我们将讨论 TweetBrowser 应用程序将会用到的下列其他技巧和最佳实践:
首先从列表中的第一项开始:利用 JavaFX 级联样式表。
JavaFX 具有许多非常强大的特性,其中一个特性就是使用 JavaFX CSS 来修改应用程序的外观。要将样式表和 TweetBrowser 应用程序的 Scene 关联起来,请使用 SceneBuilder 类的 stylesheets() 方法。如清单 1 中的代码所示,这些代码节选自 TweetBrowserMain.java。
Scene scene = SceneBuilder.create()
.width(1000)
.height(660)
.stylesheets("javafxpert/tweetbrowser/ui/tweetbrowser.css")
.root(
BorderPaneBuilder.create()
.top(createToolBar())
.center(createListView())
.build()
)
.build(); 清单 1:将样式表和 Scene 关联起来
作为样式表的使用示例,请留意图 1 右上角中以粗体显示的标签周围的外观和填充形式。这些外观和填充形式部分取决于 tweetbrowser.css 样式表中的 #tokenLabel 选择器,如清单 2 所示。
#backButton {
-fx-padding: 3 3 3 3;
}
#searchButton {
-fx-padding: 4 4 4 4;
}
#tokenLabel {
-fx-font-size: 14pt;
-fx-font-weight: bold;
-fx-label-padding: 0 5 0 15
}
#screenNameHyperlink {
-fx-font-weight: bold;
}
#createdAtLabel {
-fx-font-size: 10pt;
}
#popupButtonBar {
-fx-padding: 0 0 8 0
} 清单 2:tweetbrowser.css 样式表
通过使用 id 属性,#tokenLabel 将与样式表中的规则关联起来,如清单 3 末尾处所示。
private ToolBar createToolBar() {
Region strut = new Region();
Region spring = new Region();
ImageView searchImageView = ImageViewBuilder.create()
.image(new Image(getClass()
.getResourceAsStream("images/search-16.png")))
.build();
ImageView cancelImageView = ImageViewBuilder.create()
.image(new Image(getClass()
.getResourceAsStream("images/bullet_cross.png")))
.build();
ToolBar toolBar = ToolBarBuilder.create()
.items(
backButton = ButtonBuilder.create()
.id("backButton")
.graphic((new ImageView(
new Image(getClass()
.getResourceAsStream("images/nav-back-16.png"))))
)
.onAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent e) {
if (!TweetBrowserModel.instance.historyStack
.oneRemainingProperty.get()) {
TweetBrowserModel.instance.historyStack.pop();
}
String peekedStr = TweetBrowserModel.instance.historyStack
.peek();
invokeSearch(peekedStr, false);
}
})
.build(),
HBoxBuilder.create()
.spacing(5)
.children(
searchTextField = TextFieldBuilder.create()
.prefColumnCount(15)
.onAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent e) {
invokeSearch(searchTextField.getText(), true);
searchTextField.setText("");
}
})
.build(),
searchButton = ButtonBuilder.create()
.id("searchButton")
.onAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent e) {
invokeSearch(searchTextField.getText(), true);
searchTextField.setText("");
}
})
.build()
)
.build(),
strut,
currentTokenLabel = LabelBuilder.create()
.id("tokenLabel")
.build(),
spring,
GroupBuilder.create()
.children(
progressIndicator = ProgressIndicatorBuilder.create()
.scaleX(0.7)
.scaleY(0.7)
.progress(-1.0)
.build()
)
.build()
)
.build();
backButton.disableProperty().bind(TweetBrowserModel.instance.historyStack
.oneRemainingProperty.or(TweetBrowserModel.instance.queryActive
.or(TweetBrowserModel.instance.webViewPopupVisible)));
currentTokenLabel.textProperty().bind(TweetBrowserModel.instance
.historyStack.topProperty);
searchButton.graphicProperty().bind(
new When(TweetBrowserModel.instance.queryActive)
.then(cancelImageView)
.otherwise(searchImageView));
searchButton.disableProperty()
.bind(searchTextField.textProperty()
.isEqualTo("").and(TweetBrowserModel.instance.queryActive.not()));
strut.setPrefWidth(200);
strut.setMinWidth(Region.USE_PREF_SIZE);
strut.setMaxWidth(Region.USE_PREF_SIZE);
HBox.setHgrow(spring, Priority.ALWAYS);
return toolBar;
} 清单 3:TweetBrowserMain.java createToolBar() 方法
有关使用 JavaFX CSS 的更多信息,请参阅本文末尾处的“另请参见”一节。
JavaFX 在用户界面布局方面具有非常强大的特性。利用这些特性,无论何种尺寸的场景或何种类型的平台,应用程序都能按照您所希望的方式显示出来。
其中一个特性吸收了名为 springs 和 struts 的概念,该概念可追溯到跨平台开发的早期阶段。TweetBrowser 应用程序在其工具栏中实现了这个概念,以便在水平调整工具栏大小时,控制工具栏 3 个节点之间的水平间距。图 5 对此作了说明,如图所示,在最右侧按钮和标签之间有一个固定的水平 strut,在标签和进度指示器之间有一个可变的水平 spring。

图 5:实现 Springs 和 Struts
清单 4 节选了清单 3 中的一些代码段,这些代码段表明 strut 和 spring 均为 Region 实例。
Region strut = new Region();
Region spring = new Region();
...
ToolBar toolBar = ToolBarBuilder.create()
.items(
...
strut,
currentTokenLabel = LabelBuilder.create()
.id("tokenLabel")
.build(),
spring,
...
)
.build();
...
strut.setPrefWidth(200);
strut.setMinWidth(Region.USE_PREF_SIZE);
strut.setMaxWidth(Region.USE_PREF_SIZE);
HBox.setHgrow(spring, Priority.ALWAYS); 清单 4:strut 和 spring 均为 Region 实例
在清单 4 中,strut 的首选宽度设置为 200,最小和最大宽度设置为使用首选宽度。当 spring 的容器(在本例中指的是工具栏)的大小调整为大于首选宽度时,spring 将优先享有为其分配的额外水平间距。
在本系列文章的第 1 部分中,我们讨论过如何将 UI 绑定到模型,并列举了几个相关示例。在本文中,我们将重点讲解较为复杂的类似于三元运算符的绑定表达式。
下面的代码节选自清单 3,代码中包含一个有关此绑定技巧的示例,该技巧以使用 When 类为特征:
searchButton.graphicProperty().bind(
new When(TweetBrowserModel.instance.queryActive)
.then(cancelImageView)
.otherwise(searchImageView));
此绑定表达式的结果是,当该模型的 queryActive 属性为 true 时,searchButton 的 graphicProperty 将为 cancelImageView;否则,它将为 searchImageView。
上面的一段代码演示了如何使用 JavaFX API 中的一些属性,例如,Button 的 graphic 属性(继承自 Labeled 类)。这个代码片段通常用于定义应用程序中的用户自定义属性,例如 TweetBrowser 应用程序中的 Tweet 类就属于这种情况。清单 5 显示了 Tweet 类的定义,该定义遵循 JavaFX 属性方法命名约定。
package javafxpert.tweetbrowser.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* A JavaFX property that represents a tweet in the TweetBrowser program
*/
public final class Tweet {
private StringProperty id;
final public void setId(String value) {
idProperty().set(value);
}
final public String getId() {
return idProperty().get();
}
final public StringProperty idProperty() {
if (id == null) {
id = new SimpleStringProperty(this, "id");
}
return id;
}
private StringProperty text;
final public void setText(String value) {
textProperty().set(value);
}
final public String getText() {
return textProperty().get();
}
final public StringProperty textProperty() {
if (text == null) {
text = new SimpleStringProperty(this, "text");
}
return text;
}
private StringProperty profileImageUrl;
final public void setProfileImageUrl(String value) {
profileImageUrlProperty().set(value);
}
final public String getProfileImageUrl() {
return profileImageUrlProperty().get();
}
final public StringProperty profileImageUrlProperty() {
if (profileImageUrl == null) {
profileImageUrl = new SimpleStringProperty(this, "profileImageUrl");
}
return profileImageUrl;
}
private StringProperty userName;
final public void setUserName(String value) {
userNameProperty().set(value);
}
final public String getUserName() {
return userNameProperty().get();
}
final public StringProperty userNameProperty() {
if (userName == null) {
userName = new SimpleStringProperty(this, "userName");
}
return userName;
}
private StringProperty screenName;
final public void setScreenName(String value) {
screenNameProperty().set(value);
}
final public String getScreenName() {
return screenNameProperty().get();
}
final public StringProperty screenNameProperty() {
if (screenName == null) {
screenName = new SimpleStringProperty(this, "screenName");
}
return screenName;
}
private StringProperty createdAt;
final public void setCreatedAt(String value) {
createdAtProperty().set(value);
}
final public String getCreatedAt() {
return createdAtProperty().get();
}
final public StringProperty createdAtProperty() {
if (createdAt == null) {
createdAt = new SimpleStringProperty(this, "createdAt");
}
return createdAt;
}
public Tweet(String id, String text, String profileImageUrl,
String screenName, String userName, String createdAt) {
setId(id);
setText(text);
setProfileImageUrl(profileImageUrl);
setScreenName(screenName);
setUserName(userName);
setCreatedAt(createdAt);
}
public String toString() {
return text.getValue();
}
} 清单 5:定义 Tweet.java 中的属性
Tweet 类封装了 6 个与检索自 Twitter API 的推文有关的字符串属性。使用 JavaFX 属性方法命名约定来定义该类,可使该类与需要 JavaFX 属性的类很好地协作并加入绑定表达式中。有关 JavaFX 属性和绑定的更多信息,请参阅“另请参见”一节。
如上所述,当用户单击推文中的 Web 链接时,屏幕将出现一个弹出窗口,其中包含所选页面的 WebView。清单 6 列出了用于填充和实例化 Popup 的代码。
private void createWebViewPopup() {
WebView webView;
webView = WebViewBuilder.create()
.prefWidth(920)
.prefHeight(560)
.build();
TweetBrowserModel.instance.webViewPopupWebEngine = webView.getEngine();
Button okButton;
webViewPopup = PopupBuilder.create()
.content(
StackPaneBuilder.create()
.children(
RectangleBuilder.create()
.width(930)
.height(620)
.arcWidth(20)
.arcHeight(20)
.fill(Color.WHITE)
.stroke(Color.GREY)
.strokeWidth(2)
.build(),
BorderPaneBuilder.create()
.center(
GroupBuilder.create()
.children(webView)
.build()
)
.bottom(
HBoxBuilder.create()
.id("popupButtonBar")
.alignment(Pos.CENTER)
.spacing(10)
.children(
ButtonBuilder.create()
.id("backButton")
.graphic((new ImageView(
new Image(getClass()
.getResourceAsStream("images/nav-back-16.png"))))
)
.onAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent e) {
TweetBrowserModel.instance.webViewPopupWebEngine
.executeScript("history.go(-1)");
}
})
.build(),
okButton = ButtonBuilder.create()
.text("OK")
.onAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent e) {
TweetBrowserModel.instance.webViewPopupVisible
.setValue(false);
}
})
.build()
)
.build()
)
.build()
)
.build()
)
.build();
BorderPane.setAlignment(okButton, Pos.CENTER);
BorderPane.setMargin(okButton, new Insets(10, 0, 10, 0));
TweetBrowserModel.instance.webViewPopupVisible
.addListener(new ChangeListener() {
public void changed(ObservableValue ov, Object oldVal, Object newVal) {
if ((Boolean)newVal) {
webViewPopup.show(stage,
(stage.getWidth() - webViewPopup.getWidth()) / 2 + stage.getX(),
(stage.getHeight() - webViewPopup.getHeight()) / 2 + stage.getY());
}
else {
webViewPopup.hide();
}
}
});
}
} 清单 6:TweetBrowserMain.java createWebViewPopup() 方法
createWebViewPopup() 在应用程序中仅调用一次:即在启动 start() 方法时调用。因此,仅存在一个实例,其可见性将通过是否将清单 6 中显示的 ChangeListener 添加到模型的 webViewPopupVisible 属性来控制。当 webViewPopupVisible 变为 true 时,系统将调用 Popup 实例的 show() 方法,并传递一个和应用程序窗口相关的位置。当 webViewPopupVisible 变为 false 时,将调用 Popup 实例的 hide() 方法。
再次查看清单 6,您会发现在单击 OK 按钮之后,webViewPopupVisible 设置为 false。清单 7 节选自 TweetCell.java,它演示了在没有按住 Shift 键的情况下单击包含 Web 链接的 Hyperlink 时,webViewPopupVisible 将设置为 true。
else if (word.startsWith("http") && word.length() > 12) {
wordNode = HyperlinkBuilder.create()
.text(word)
.onMouseClicked(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent me) {
if (me.isShiftDown()) {
URL url;
try {
url = new URL(word);
}
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);
}
}
else {
TweetBrowserModel.instance.webViewPopupWebEngine.load(null);
TweetBrowserModel.instance.webViewPopupWebEngine.load(word);
TweetBrowserModel.instance.webViewPopupVisible
.setValue(true);
}
}
})
.build();
} 清单 7:创建和处理包含 Web 链接的 Hyperlink
请注意,清单 7 还演示了如何使用 java.awt.Desktop 类将默认浏览器指向所需 URL。
清单 7 显示调用了模型中的 webViewPopupWebEngine 变量引用的 WebEngine 的 load() 方法。第一次调用时,通过 null 参数清空 WebView,第二次调用时使用所需的 URL。
第一次调用的目的是让 WebView 在加载所需页时不保留其上次可见时显示的内容。请注意,下面的代码节选自清单 6,其结果是 webViewPopupWebEngine 变量将引用 WebEngine 实例:
TweetBrowserModel.instance.webViewPopupWebEngine = webView.getEngine();
有许多技巧和最佳实践可应用于 JavaFX 应用程序,其中包括利用 JavaFX 级联样式表、在 UI 中实现 Springs 和 Struts、在绑定表达式中使用三元运算符、定义 JavaFX 属性、利用 Popup 实现一个对话框,以及使用 WebView 显示一个网页。
另外,在此还应重复一下在本系列文章第 1 部分中重点讲解的技巧:从应用程序主页通过 Java Web Start 调用应用程序、确保只启动应用程序的一个实例、将 UI 绑定到模型。
这些当然不是完整清单,通过继续自学 TweetBrowser 和其他 JavaFX 应用程序,您将整理出许多其他技巧和最佳实践。例如,有关是否在应用程序中使用 REST/FX 库来调用 REST 服务这个问题,学习 TweetBrowserModel 类将有助于您做出正确决定。此外,日后如果遇到如何在应用程序中实现 Back 按钮导航这方面的问题,学习 HistoryStack 类及其在 TweetBrowser 中的用法可能会对您有所启发。