JavaFX 2.0 企业应用程序最佳实践(第 2 部分)

作者:James L. Weaver

本文是由两部分组成的系列文章的第 2 部分,重点介绍如何在 JavaFX 2.0 中使用最佳实践来开发企业应用程序。

2012 年 5 月发布

下载:

下载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 部分)


TweetBrowser 应用程序概述

为说明在 Java 2.0 中开发企业应用程序时用到的更多技巧和最佳实践,我们将继续研究 TweetBrowser 应用程序。该应用程序的屏幕截图如图 1 所示。

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

图 1:TweetBrowser 应用程序启动时的屏幕截图

图 1:TweetBrowser 应用程序启动时的屏幕截图

回顾第 1 部分,当您单击 #hashtag@screenname 时,UI 的右上角将出现一个旋转的进度指示器,指示正在进行搜索。您也可以在文本域键入一个 #hashtag@screenname 或文字,然后按 Enter 键或单击 Search 按钮。无论使用哪种方式,Search 按钮的外观都将变成 X,指示您可以通过单击该按钮取消搜索并禁用大部分 UI。

单击某推文中的 Web 链接将打开一个弹出式窗口,其中包含所选页面的 WebView

获取并运行 TweetBrowser 项目

  • 下载包含 TweetBrowser 项目的 NetBeans 项目文件
  • 将该项目解压缩到您选择的目录。
  • 启动 NetBeans,选择 File -> Open Project
  • 在 Open Project 对话框中,导航至所选目录后打开 TweetBrowser 项目,如图 2 所示。如果屏幕上出现一条消息,表明无法找到 jfxrt.jar 文件,则单击 Resolve 按钮并导航至 JavaFX 2.0 SDK 安装目录下的 rt/lib 文件夹。

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

图 2:在 NetBeans 中打开 TweetBrowser 项目

图 2:在 NetBeans 中打开 TweetBrowser 项目

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

图 3:在 NetBeans 中运行 TweetBrowser 程序

图 3:在 NetBeans 中运行 TweetBrowser 应用程序

TweetBrowser 应用程序应显示在一个窗口中,如前面图 1 中所示。继续使用该应用程序,浏览屏幕名称、hashtag 和 Web 链接。下面我们将分析该应用程序并探讨其中一些代码。

了解 TweetBrowser 应用程序

在深入研究代码之前,我们先来了解一下图 4 中显示的各个片段,这些片段共同组成了 TweetBrowser 应用程序。

图 4:TweetBrowser 应用程序示意图

图 4:TweetBrowser 应用程序示意图

首先看一下图 4 的右上角,用户可以通过单击 TweetBrowser 主页上的大眼鸟图标来启动应用程序。如果尚未运行 TweetBrowser 实例,系统将会通过 Java Web Start 调用应用程序。有关 TweetBrowser 主页的 URL,请参考“另请参见”一节。

图 4 左下角代表的是 javafxpert.tweetbrowser.ui 软件包(为节省空间在示意图中缩写为 tweetbrowser.ui),其中包含 2 个 Java 类和一个 JavaFX 级联样式表 (CSS):

  • TweetBrowserMain 是 JavaFX 应用程序的主类,它将扩展 Application,并且包含 main()start() 方法。其在 TweetBrowser 应用程序中的角色是创建一个场景并向其中填充图 1 所示的 ToolBarListView 。此外,TweetBrowserMain 还将创建和有条件显示一个包含 WebView 的弹出窗口,该窗口将在单击推文中的 Web 链接时弹出。
  • TweetCell 是一个 ListCell 扩展类,它将呈现推文的表现形式,如图 1 所示。每条推文都有一个 TweetCellTweetCell 实例包含在 ListView 中。
  • tweetbrowser.css 是一个 JavaFX CSS,它规定了应用程序中各个节点的样式,这些节点包括 Back 和 Search 按钮以及推文编写者的屏幕名称。

图 4 中下部代表的是 javafxpert.tweetbrowser.model 软件包,其中包含 3 个 Java 类:

  • TweetBrowserModel 是应用程序的主模型类,它包含表示应用程序状态的属性。它还包含使用 REST/FX 库(如图 4 右下角所示)查询 Twitter API 的方法。REST/FX 是 JavaFX 2.0 的一个外部库,此处用于与 Twitter REST 端点通信并分析其 JSON 响应。本文末尾的“另请参见”一节提供了 REST/FX 项目的链接。
  • Tweet 是用于表示推文的类,它遵循为 JavaFX 属性规定的约定。例如,Tweet 包含一个名为 idStringProperty,该属性可通过 setId()getId()idProperty() 方法访问。
  • HistoryStack 实现了一个 FIFO 栈,用于管理 hashtag、文字和屏幕名称的导航历史。Back 按钮使用此历史记录来确定当其被单击时将显示哪个 hashtag、文字或屏幕名称。

如图 4 所示,javafxpert.tweetbrowser.ui 软件包中的类将调用 javafxpert.tweetbrowser.model 软件包中类的方法。此外,javafxpert.tweetbrowser.ui 软件包中的类还会将模型的状态呈现到 UI,这主要利用 JavaFX 的绑定功能来实现。

TweetBrowser 应用程序中用到的更多技巧和最佳实践

在本系列文章的第 1 部分中,我们已经讨论过 TweetBrowser 应用程序用到的一些技巧和最佳实践,即下面这些内容:

  • 从应用程序主页通过 Java Web Start 调用应用程序
  • 确保只启动应用程序的一个实例
  • 将 UI 绑定到模型

在本文中,我们将讨论 TweetBrowser 应用程序将会用到的下列其他技巧和最佳实践:

  • 利用 JavaFX 级联样式表
  • 在 UI 中实现 springsstruts
  • 在绑定表达式中使用三元运算符
  • 定义 JavaFX 属性
  • 利用 Popup 实现一个对话框
  • 使用 WebView 显示一个网页

首先从列表中的第一项开始:利用 JavaFX 级联样式表。

利用 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 的更多信息,请参阅本文末尾处的“另请参见”一节。

在 UI 中实现 Springs 和 Struts

JavaFX 在用户界面布局方面具有非常强大的特性。利用这些特性,无论何种尺寸的场景或何种类型的平台,应用程序都能按照您所希望的方式显示出来。

其中一个特性吸收了名为 springsstruts 的概念,该概念可追溯到跨平台开发的早期阶段。TweetBrowser 应用程序在其工具栏中实现了这个概念,以便在水平调整工具栏大小时,控制工具栏 3 个节点之间的水平间距。图 5 对此作了说明,如图所示,在最右侧按钮和标签之间有一个固定的水平 strut,在标签和进度指示器之间有一个可变的水平 spring

图 5:实现 Springs 和 Struts

图 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:strutspring 均为 Region 实例

在清单 4 中,strut 的首选宽度设置为 200,最小和最大宽度设置为使用首选宽度。当 spring 的容器(在本例中指的是工具栏)的大小调整为大于首选宽度时,spring 将优先享有为其分配的额外水平间距。

在绑定表达式中使用三元运算符

在本系列文章的第 1 部分中,我们讨论过如何将 UI 绑定到模型,并列举了几个相关示例。在本文中,我们将重点讲解较为复杂的类似于三元运算符的绑定表达式。

下面的代码节选自清单 3,代码中包含一个有关此绑定技巧的示例,该技巧以使用 When 类为特征:

    searchButton.graphicProperty().bind(

            new When(TweetBrowserModel.instance.queryActive)

                     .then(cancelImageView)

                     .otherwise(searchImageView));

此绑定表达式的结果是,当该模型的 queryActive 属性为 true 时,searchButtongraphicProperty 将为 cancelImageView;否则,它将为 searchImageView

定义 JavaFX 属性

上面的一段代码演示了如何使用 JavaFX API 中的一些属性,例如,Buttongraphic 属性(继承自 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 属性和绑定的更多信息,请参阅“另请参见”一节。

利用 Popup 实现一个对话框

如上所述,当用户单击推文中的 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。

使用 WebView 显示一个网页

清单 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 中的用法可能会对您有所启发。

另请参见