JavaFX学习曲线日记-2:声明用户接口

我已经使用Java语言定义用户接口近十年了,当我第一次体验JavaFX脚本时便马上感到 了这两种不同环境之间的差异。尽管程序员在Java语言中使用过程式代码来定义用户接口, 而在JavaFX脚本语言中你能够使用声明语句来定义用户接口。这是两者之间最大的不同,要 适应后者的确需要花费一定的时间和精力。

为了学习这种创建UI的全新声明风格, 我决定将一个从前使用Java语言实现的应用UI移植到JavaFX脚本上。于是我挑选了一个在 Java语言中心的Swingworker教学中使用的图片浏览应用。原始应用演示了如何在JavaSE 6.0中使用Swingworker类,正因为其UI本身非常简单,所以我将它作为移植的“原料 ”。

现存的用户接口

现存的应用为用户提供了查询、列表、从Flickr 站点下载并显示图片的功能。用户可以输出查询关键字,应用调用REST API来查询Flickr以 获取匹配的缩略图片;而且用户还可以从缩略图片中进行选择,以便查看更大更细致的图片 。现存应用的查询结果如下图:

图 1. 带有查询结果的应用UI

这个UI由下列组件构成,按照从上至下的顺序:

• 主框架窗体容器

• 查询标签和查询文本输入栏

• 查询匹配标签和处理进度条

• 与简短描述(检索关键字)相匹配的缩略图列表

• 选择标签和处理进度条

• 显示被选择图片的标签

此UI 由以下常见的Swing组件构成:JFrame、Jlabel、JprogressBar、JscrollPane、JList。 JList组件具有自定义渲染器,它能够显示缩略图和相应的简短描述。

但这还是一个相当简单的UI,我们用它来研究如何使用JavaFX脚本描述UI。下一步,我 打算尝试使用JavaFX实现整个应用;但是目前,只要完成一个对现存UI的近似实现就可以了 。下面展示了一个毫无生气的UI,它代表了我使用JavaFX脚本进行UI描述来实现的最初目标 :

图 2. 应用UI

我使用NetBeans IDE和它的Matisse GUI实现了这个原始的UI。所以源代码都可从 Swingworker教学中下载;下面列出了用于生成UI的主要代码。它告诉了我们如何在 NetBeans中使用GroupLayout来创建UI。

private void initComponents() {   lblSearch = new javax.swing.JLabel();   txtSearch = new javax.swing.JTextField();   lblImageList = new javax.swing.JLabel();   scrollImageList = new javax.swing.JScrollPane();   listImages = new JList(listModel);   lblSelectedImage = new javax.swing.JLabel();   lblImage = new javax.swing.JLabel();   progressMatchedImages = new javax.swing.JProgressBar();   progressSelectedImage = new javax.swing.JProgressBar();   setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);   setTitle("Image Search");   lblSearch.setText("Search");   lblImageList.setText("Matched Images");   // ...   // event listeners, models, and cell renderers removed for this example   //   lblSelectedImage.setText("Selected Image");   lblImage.setBorder(javax.swing.BorderFacTory.createLineBorder(       new java.awt.Color(204, 204, 204)));   lblImage.setFocusable(false);   lblImage.setMaximumSize(new java.awt.Dimension(500, 500));   lblImage.setMinimumSize(new java.awt.Dimension(250, 250));   lblImage.setOpaque(true);   lblImage.setPreferredSize(new java.awt.Dimension(500, 250));   javax.swing.GroupLayout layout = new javax.swing.GroupLayout (getContentPane());   getContentPane().setLayout(layout);   layout.setHorizontalGroup(     layout.createParallelGroup (javax.swing.GroupLayout.Alignment.LEADING)     .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()       .addContainerGap()       .addGroup(layout.createParallelGroup (javax.swing.GroupLayout.Alignment.TRAILING)         .addComponent(lblImage, javax.swing.GroupLayout.Alignment.LEADING,             javax.swing.GroupLayout.DEFAULT_SIZE, 462, Short.MAX_VALUE)         .addComponent(scrollImageList, javax.swing.GroupLayout.DEFAULT_SIZE,             462, Short.MAX_VALUE)         .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()           .addGroup(layout.createParallelGroup (javax.swing.GroupLayout.Alignment.LEADING)             .addComponent(lblImageList)             .addComponent(lblSelectedImage))           .addPreferredGap (javax.swing.LayoutStyle.ComponentPlacement.RELATED)           .addGroup(layout.createParallelGroup (javax.swing.GroupLayout.Alignment.LEADING)             .addComponent(progressMatchedImages, javax.swing.GroupLayout.DEFAULT_SIZE,                 350, Short.MAX_VALUE)             .addComponent(progressSelectedImage, javax.swing.GroupLayout.DEFAULT_SIZE,                 350, Short.MAX_VALUE)))         .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()           .addComponent(lblSearch)           .addPreferredGap (javax.swing.LayoutStyle.ComponentPlacement.RELATED)           .addComponent(txtSearch, javax.swing.GroupLayout.DEFAULT_SIZE,               411, Short.MAX_VALUE)))       .addContainerGap())   );   layout.setVerticalGroup(     layout.createParallelGroup (javax.swing.GroupLayout.Alignment.LEADING)     .addGroup(layout.createSequentialGroup()       .addContainerGap()       .addGroup(layout.createParallelGroup (javax.swing.GroupLayout.Alignment.BASELINE)         .addComponent(lblSearch)         .addComponent(txtSearch, javax.swing.GroupLayout.PREFERRED_SIZE,             javax.swing.GroupLayout.DEFAULT_SIZE,             javax.swing.GroupLayout.PREFERRED_SIZE))       .addPreferredGap (javax.swing.LayoutStyle.ComponentPlacement.RELATED)       .addGroup(layout.createParallelGroup (javax.swing.GroupLayout.Alignment.LEADING)         .addComponent(lblImageList)         .addComponent(progressMatchedImages, javax.swing.GroupLayout.PREFERRED_SIZE,             javax.swing.GroupLayout.DEFAULT_SIZE,             javax.swing.GroupLayout.PREFERRED_SIZE))       .addPreferredGap (javax.swing.LayoutStyle.ComponentPlacement.RELATED)       .addComponent(scrollImageList, javax.swing.GroupLayout.PREFERRED_SIZE, 235,           javax.swing.GroupLayout.PREFERRED_SIZE)       .addPreferredGap (javax.swing.LayoutStyle.ComponentPlacement.RELATED)       .addGroup(layout.createParallelGroup (javax.swing.GroupLayout.Alignment.LEADING)         .addComponent(lblSelectedImage)         .addComponent(progressSelectedImage, javax.swing.GroupLayout.PREFERRED_SIZE,             javax.swing.GroupLayout.DEFAULT_SIZE,             javax.swing.GroupLayout.PREFERRED_SIZE))       .addPreferredGap (javax.swing.LayoutStyle.ComponentPlacement.RELATED)       .addComponent(lblImage, javax.swing.GroupLayout.DEFAULT_SIZE, 305, Short.MAX_VALUE)       .addContainerGap())   );   pack();}

使用NetBeans IDE进行UI布局非常简单——只需要鼠标的拖、拽操作便可。在本示例中 ,NetBeans通过使用javax.swing.GroupLayout生成了UI代码,javax.swing.GroupLayout这 个类为我们提供了跨平台的正确大小、位置、组件之间的间隔。虽然生成的代码并不便于阅 读,但从工具支持性来讲它确实是非常优秀的,而且无需任何人工编写代码。

声明JavaFX脚本接口

尽管JavaFX脚本并没有提供GUI工具,NetBeans也没有提供支持JavaFX脚本语言的UI设计 界面,但我们可以使用JavaFXPad这个OpenJFX站点提供的演示应用。通过使用JFXPad,你能 够人工输入UI代码,并马上见到代码所呈现的效果。尽管它是一个演示应用,但的确是一个 超级好用的简单工具。

本接口所需的所有GUI组件都在javafx.ui包中。JavaFX脚本和Java语言一样支持包 (package)结构和导入(import)语句。我在学习过程中并不是导入整个包,而是每次导 入要用到的组件类。这样可以让我们在查看代码时能够准确地识别出使用了哪些组件。

JavaFX脚本组件具有height、width、text、content等属性。其中content属性可以包含 子组件,每个content属性可以包含被声明为数组的多个组件。在声明图片检索应用的用户 接口时,我选用并使用了适合的JavaFX脚本组件,并设置了它们的属性。

例如,下面简短的脚本定义了一个不含有任何内容的空白框架。

import javafx.ui.Frame;Frame. {   title: "JFX Image Search"   height: 500   width: 500   visible: true}

在JavaFX脚本中与Swing的Jframe等价的是javafx.ui.Frame。这里的title属性声明了 window的标题。Height和width属性则以像素为单元定义了整个框架的大小。最后,visible 属性声明了框架是否可见,这与在Swing中的Jframe类的setVisible方法类似。

FlowPanels 和 Boxes

javafx.ui包中包含了Label、SimpleLabel、Box、FlowPanel、ProgressBar、 ScrollPane和ListBox这些组件。它们的名称听起来与Swing组件非常相似,因此我首先尝试 使用它们来实现目标UI。

尽管JavaFX脚本支持GroupLayout和GroupPanel组件,但我还是尽量在首次尝试时避开这 些组件。我并不认为GroupLayout有多好,因为在Java语言中使用它时你不得不人工编写代 码,从这一点出发我对在JavaFX脚本中使用GroupLayout存在着同样的顾虑。于是我选择了 更加简单的FlowPanel和Box组件来创建下面的JavaFXImageSearchUI1.fx代码:

package com.sun.demo.jfx;import javafx.ui.Frame;import javafx.ui.Box;import javafx.ui.FlowPanel;import javafx.ui.SimpleLabel;import javafx.ui.Label;import javafx.ui.TextField;import javafx.ui.ScrollPane;import javafx.ui.ListBox;import javafx.ui.ProgressBar;import javafx.ui.LineBorder;import javafx.ui.EmptyBorder;import javafx.ui.Color;Frame. {   title: "JFX Image Search"   content: Box {     border: EmptyBorder {       top: 10       left: 10       right: 10       bottom: 10     }     orientation: VERTICAL     content: [     FlowPanel {       alignment: LEADING       content: [       SimpleLabel {         text: "Search"       },       TextField {         preferredSize: {width: 425}       }       ]     },     FlowPanel {       alignment: LEADING       content: [       SimpleLabel {         text: "Matched Images"       },       ProgressBar {         preferredSize: {width: 378}       }       ]     },     ListBox {       preferredSize: {height: 200 }     },     FlowPanel {       alignment: LEADING       content: [       SimpleLabel {         text: "Selected Image"       },       ProgressBar {         preferredSize: {width: 382 }       }       ]     },     Label {      opaque: true       preferredSize: {         height: 250       }       border: LineBorder     }     ]   }   visible: true}

在JavaFXPad中输入以上内容后,上方的window中将显示相应的执行结果。如果使用 NetBeans IDE创建项目时,请将项目配置为使用FXShell类执行JFXImageSearchUI1.fx文件 ;这样我们所期望的窗体框架将会被显示出来。虽然我硬性编码了文本输入栏和进度条的宽 度以使其看起来和原始UI一样大小,但对于第一次尝试用JavaFX创建UI来讲,其效果还算称 得上成功。:-)

图 3. 复制好的UI

框架的内容是一个Box组件。这个组件的orientation(朝向)属性是VERTICAL,这意味 着在Box中的内容将被垂直放置,而不是水平放置。Box组件具有一个content属性。你可以 在其content属性中放置多个组件。如果你插入了多个组件,那么则必须将这些组件以数组 的形式写在方括号中,而在数组中的组件之间使用逗号分割:

content: [   SimpleLabel {     text: "Search"   },   TextField {     preferredSize: {width: 425}   }]

FlowPanel组件常常用于创建一对Label组件。我使用多个FlowPanel来组织Label和其它 的组件,例如TextField或者ProgressBar。当将FlowPanel的alignment属性设置为LEADING 时,框架中的面板将向左侧对齐,这样UI看上去最漂亮。

尽管这个UI看上去已经很不错了,但其布局仍然需要硬性编写进度条的宽度,以使其充 满整个框架。不幸的是,我们完全不可能非常精确地将进度条对齐到框架的右侧。你可以从 上面的UI中发觉查询文本框和匹配图片进度条并没有完全对齐。

GroupPanel 和 GroupLayout

为了实现能够自动设置组件大小并使其充满容器空间的布局,我决定尝试 javafx.ui.GroupPanel接口。这个接口使用了Swing的GroupLayout,因此它能够实现更加精 确地表现布局。

GroupPanel组件使用行和列来定位在表格中的组件。它能够自动在组件和其容器之间提 供平台特定(platform-specific)的间隔,以达到布局的目的。另外,它能够很好地对齐 组件。GroupPanel简化了Swing的GroupLayout,使其更加易于编写。

下面是UI的第二个版本JFXImageSearchUI.fx:

package com.sun.demo.jfx;import javafx.ui.Frame;import javafx.ui.GroupPanel;import javafx.ui.Row;import javafx.ui.Column;import javafx.ui.SimpleLabel;import javafx.ui.Label;import javafx.ui.TextField;import javafx.ui.ProgressBar;import javafx.ui.LineBorder;import javafx.ui.ListBox;Frame. {   title: "JavaFX Image Search"   content:     // main panel within the frame     GroupPanel {     // define the five rows and main column     var searchRow = new Row     var matchedProgressRow = new Row     var thumbNailRow = new Row {resizable: true}     var selectedProgressRow = new Row     var imageRow = new Row {resizable: true}     var mainCol = new Column {resizable: true}     // declare the five rows and the column     rows: [searchRow, matchedProgressRow, thumbNailRow, selectedProgressRow, imageRow]     columns: mainCol     // provide the array of components in the frame     content: [     // search text row     GroupPanel {       autoCreateContainerGaps: false       row: searchRow       column: mainCol       var row = new Row       var searchLabelCol = new Column       var searchTextFieldCol = new Column {         resizable: true       }       rows: row       columns: [searchLabelCol, searchTextFieldCol]       content: [       SimpleLabel {         text: "Search:"         row: row         column: searchLabelCol       },       TextField {         row: row         column: searchTextFieldCol         columns: 50       }       ]     },     // matching images progress panel row     GroupPanel {       autoCreateContainerGaps: false       row: matchedProgressRow       column: mainCol       var row = new Row       var lblCol = new Column       var progressBarCol = new Column {resizable: true}       rows: row       columns: [lblCol, progressBarCol]       content: [       SimpleLabel {         text: "Matched Images"         row: row         column: lblCol       },       ProgressBar {         row: row         column: progressBarCol       }       ]     },     // thumbnail list row     ListBox {       preferredSize: {height: 200, width: 400}       row: thumbNailRow       column: mainCol     },     // selected image progress row     GroupPanel {       autoCreateContainerGaps: false       row: selectedProgressRow       column: mainCol       var row = new Row       var lblCol = new Column       var progressBarCol = new Column {resizable: true}       rows: row       columns: [lblCol, progressBarCol]       content: [       SimpleLabel {         text: "Selected Image"         row: row         column: lblCol       },       ProgressBar {         row: row         column: progressBarCol       }       ]     },     // selected image display row     Label {       opaque: true       preferredSize: {height: 300, width: 400}       row: imageRow       column: mainCol       border: LineBorder     }     ]   }   visible: true}

这段代码生成了更加完美的布局。框架中的组件之间进行了很好的分割,并且与框架的 左右两侧分别对齐。

图 4. JFX图片查询UI

虽然在这个框架中使用的组件前一个例子中相同,但这里使用了GroupPanel,而不是 FlowPanel和Box。当你查看原始的UI时,你会发现五个不同的行:检索行、匹配图片进度行 、列表行、选择图片进度行、被选择图片行。在这些组件被顺序放置在一个居中的列内。

而我的第二个版本也具有五行和一列。框架的主要content是一个GroupPanel,这个组件 包含了几个GroupPanel和其它组件。下面便让我们看一下在GroupPanel中是如何实现这五行 和一列的:

content: GroupPanel {   var searchRow = new Row   var matchedProgressRow = new Row   var thumbNailRow = new Row {resizable: true}   var selectedProgressRow = new Row   var imageRow = new Row {resizable: true}   var mainCol = new Column {resizable: true}   rows: [searchRow, matchedProgressRow, thumbNailRow, selectedProgressRow, imageRow]   columns: mainCo

l

第一个GroupPanel包括一个组件的数组和一个用于检索关键字输入的GroupPanel(这里 称为第二个GroupPanel)。下面就是用于检索关键字输入的GroupPanel的行列属性设置:

// search text rowGroupPanel {   autoCreateContainerGaps: false   row: searchRow   column: mainCol

GroupPanel具有两个非常重要的属性:autoCreateGaps和autoCreateContainerGaps,它 们定义了如何在组件和容器之间创建间隔。这两个属性默认值为true,但由于这里已经在组 件之间创建了间隔,在第一个GroupPanel和其中包含的第二个GroupPanel之间不需要额外的 间隔,因此这里将其autoCreateContainerGaps设置为false来取掉额外的间隔。否则,检索 文本输入行将被插入不必要的边缘。要使检索文本行所在的GroupPanel组件填充父容器的相 应行、列,我们需要设置它的行和列属性为searchRow和mainCol。

检索文本行所在的GroupPanel定义了它自己的行和列,在其中包含了检索标签和文本输 入框:

var row = new Row       var searchLabelCol = new Column       var searchTextFieldCol = new Column {         resizable: true       }       rows: row       columns: [searchLabelCol, searchTextFieldCol]

行、列定义被创建好后,让我们继续使用声明式语法定义content的数组:

content: [   SimpleLabel {     text: "Search:"     row: row     column: searchLabelCol   },   TextField {     row: row     column: searchTextFieldCol     columns: 50   }]

余下的代码都遵循相应的模式。包含多个组件的行被封装在一个GroupPanel中。具有单 个组件的行,例如下拉列表和图片标签,则使用row和column属性与外部的GroupPanel相关 联。

尽管我最初对使用GroupPanel很担心,但JavaFX脚本将GroupLayout封装后使其变得非常 易用,我在尝试GroupPanel后便打消了担忧。另外NetBeans IDE的编辑器插件和JavaFXPad 演示程序提供了上下文敏感的代码自动完成功能,它可以根据输入内容弹出相关可用的属性 。通过使用简单的行列布局和代码自动完成,本人感觉使用JavaFX脚本的GroupPanel并没有 想象中那样困难。下面的图片展示了在IDE或者JavaFXPad中按下CTRL+SPACE出现的弹出选项 。

图 5. 弹出选项

总结

为了探索在创建UI过程中如何使用声明式语法,我将现有应用的UI进行了大胆的移植。 原始应用的UI使用了Swing的GroupLayout来定位、对齐组件。尽管NetBeans IDE没有提供用 于JavaFX脚本的图形化设计工具,但通过编写JavaFX代码进行布局并非我所想象的那样困难 。通过使用GroupPanel和其它组件,我实现了和原始应用完全相同的UI。方便的GroupPanel 组合加上NetBeans IDE插件、上下文敏感的代码自动完成功能使工作变得轻松。

我就想是一只草原中被牧童遗忘的羊,

JavaFX学习曲线日记-2:声明用户接口

相关文章:

你感兴趣的文章:

标签云: