我已經使用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 row
GroupPanel {
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插件、上下文敏感的代碼自動完成功能使工作變得輕松。