你所不知道的五件事情--改進Swing
這是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,講述了關于改進Swing應用的一些竅門,值得大家學習。(2010.10.25最后更新)摘要:Swing已是一個比較老的工具集了,在美觀的用戶界面出來之前需要開發很長時間。它缺少一些你在開發富UI時所需的組件。幸運地是,像 Substance,SwingX及Java Look-and_Feel圖形倉庫這樣的開源項目使這一切變得不同。作者Steven Haines向你展示了如何無痛苦地向你的Swing UI中添加樹表,語法高亮,以及其它更多的東西。
在最近這些年里,用戶界面設計與開發已經發生了很大的改變,一些人可能會說Java平臺已經停滯不前了。發布于1997年的Swing仍然是在JVM中構建用戶界面的標準工具包。從好的方面說,相似的標準便于協作;從壞的方面說,它缺少富UI設計中已經普遍存在的特性。
在本期的5 things系列中,我會介紹四個免費的開源組件,你能用它們使Swing GUI更時髦。然后,我們所討論的內容將圍繞著你所不知道的Swing線程。
1. Substance
將Java應用程序與本地操作系統進行整合是困難的,主要是因為Swing要手工繪制它自己的組件。解決該問題的權宜之計之一就是Java外觀,它允許JVM將應用程序的組件外觀代理成本地外觀;當使用Mac外觀時,它們看起來就是像是Mac應用。
Swing提供標準的本地外觀,也提供它自己的獨立于平臺的外觀,叫作Metal。另外,Kirill Grouchnikov開發的Substance是一個開源的項目,它提供了更多的外觀皮膚。要想嘗試一下,可以從java.net下載Substance,然后:
1. 將substance.jar文件加到你的CLASSPATH中。
2. 將下面的系統配置加到應用程序中的啟動腳本中:
-Dswing.defaultlaf=org.jvnet.substance.skin.lookandfeelname
3. 在第二步中,對于lookandfeelname變量所處的位置,可嘗試下列任一值:
SubstanceAutumnLookAndFeel
SubstanceBusinessBlackSteelLookAndFeel
SubstanceBusinessBlueSteelLookAndFeel
SubstanceBusinessLookAndFeel
SubstanceChallengerDeepLookAndFeel
SubstanceCremeCoffeeLookAndFeel
SubstanceCremeLookAndFeel
SubstanceDustCoffeeLookAndFeel
SubstanceDustLookAndFeel
SubstanceEmeraldDuskLookAndFeel
SubstanceMagmaLookAndFeel
SubstanceMistAquaLookAndFeel
SubstanceMistSilverLookAndFeel
SubstanceModerateLookAndFeel
SubstanceNebulaBrickWallLookAndFeel
SubstanceNebulaLookAndFeel
SubstanceOfficeBlue2007LookAndFeel
SubstanceOfficeSilver2007LookAndFeel
SubstanceRavenGraphiteGlassLookAndFeel
SubstanceRavenGraphiteLookAndFeel
SubstanceRavenLookAndFeel
SubstanceSaharaLookAndFeel
SubstanceTwilightLookAndFeel
SubstanceBusinessBlackSteelLookAndFeel
SubstanceBusinessBlueSteelLookAndFeel
SubstanceBusinessLookAndFeel
SubstanceChallengerDeepLookAndFeel
SubstanceCremeCoffeeLookAndFeel
SubstanceCremeLookAndFeel
SubstanceDustCoffeeLookAndFeel
SubstanceDustLookAndFeel
SubstanceEmeraldDuskLookAndFeel
SubstanceMagmaLookAndFeel
SubstanceMistAquaLookAndFeel
SubstanceMistSilverLookAndFeel
SubstanceModerateLookAndFeel
SubstanceNebulaBrickWallLookAndFeel
SubstanceNebulaLookAndFeel
SubstanceOfficeBlue2007LookAndFeel
SubstanceOfficeSilver2007LookAndFeel
SubstanceRavenGraphiteGlassLookAndFeel
SubstanceRavenGraphiteLookAndFeel
SubstanceRavenLookAndFeel
SubstanceSaharaLookAndFeel
SubstanceTwilightLookAndFeel
圖1展示了使用默認Metal外觀的Java應用,而圖2則展示了使用Substance Raven外觀的應用:
圖1. Java平臺的Metal外觀

圖2. Substance的Raven外觀

2. SwingX
Swing框架包含了大部分你所需要的標準控件,包括樹,表,列表等等。但它缺少一些更現代的控件,像樹表。SwingX項目,它是SwingLabs的一部分,提供了一個富組件集,包括如下:
* Sorting, filtering, and highlighting for tables, trees, and lists
* Find/search
* Auto-completion
* Login/authentication framework
* TreeTable component
* Collapsible panel component
* Date picker component
* Tip-of-the-Day component
要嘗試的話,從SwingLabs中下載SwingX的JAR文件,然后把它加到CLASSPATH中,或者把下面的依賴加到Maven POM文件中:
<dependency>
<groupId>org.swinglabs</groupId>
<artifactId>swingx</artifactId>
<version>1.6</version>
</dependency>
<groupId>org.swinglabs</groupId>
<artifactId>swingx</artifactId>
<version>1.6</version>
</dependency>
圖3中的樹表就是SwingX組件的一個例子:
圖3. SwingX TreeTable組件

構建一個SwingX樹表
使用SwingX的JXTreeTable控件構建一個樹表是一件非常直接的事情。只要把表中的每一行看作既可能有列值,同時也可能有子節點。 SwingX提供了一個模型類,叫作org.jdesktop.swingx.treetable.AbstractTreeTableModel,對它進行擴展就可提供該功能。清單1展示樹表模型實現的一個樣例:
清單1. MyTreeTableModel.java
package com.geekcap.swingx.treetable;
import java.util.ArrayList;
import java.util.List;
import org.jdesktop.swingx.treetable.AbstractTreeTableModel;
public class MyTreeTableModel extends AbstractTreeTableModel
{
private MyTreeNode myroot;
public MyTreeTableModel()
{
myroot = new MyTreeNode( "root", "Root of the tree" );
myroot.getChildren().add( new MyTreeNode( "Empty Child 1",
"This is an empty child" ) );
MyTreeNode subtree = new MyTreeNode( "Sub Tree",
"This is a subtree (it has children)" );
subtree.getChildren().add( new MyTreeNode( "EmptyChild 1, 1",
"This is an empty child of a subtree" ) );
subtree.getChildren().add( new MyTreeNode( "EmptyChild 1, 2",
"This is an empty child of a subtree" ) );
myroot.getChildren().add( subtree );
myroot.getChildren().add( new MyTreeNode( "Empty Child 2",
"This is an empty child" ) );
}
@Override
public int getColumnCount()
{
return 3;
}
@Override
public String getColumnName( int column )
{
switch( column )
{
case 0: return "Name";
case 1: return "Description";
case 2: return "Number Of Children";
default: return "Unknown";
}
}
@Override
public Object getValueAt( Object node, int column )
{
System.out.println( "getValueAt: " + node + ", " + column );
MyTreeNode treenode = ( MyTreeNode )node;
switch( column )
{
case 0: return treenode.getName();
case 1: return treenode.getDescription();
case 2: return treenode.getChildren().size();
default: return "Unknown";
}
}
@Override
public Object getChild( Object node, int index )
{
MyTreeNode treenode = ( MyTreeNode )node;
return treenode.getChildren().get( index );
}
@Override
public int getChildCount( Object parent )
{
MyTreeNode treenode = ( MyTreeNode )parent;
return treenode.getChildren().size();
}
@Override
public int getIndexOfChild( Object parent, Object child )
{
MyTreeNode treenode = ( MyTreeNode )parent;
for( int i=0; i>treenode.getChildren().size(); i++ )
{
if( treenode.getChildren().get( i ) == child )
{
return i;
}
}
return 0;
}
public boolean isLeaf( Object node )
{
MyTreeNode treenode = ( MyTreeNode )node;
if( treenode.getChildren().size() > 0 )
{
return false;
}
return true;
}
@Override
public Object getRoot()
{
return myroot;
}
}
import java.util.ArrayList;
import java.util.List;
import org.jdesktop.swingx.treetable.AbstractTreeTableModel;
public class MyTreeTableModel extends AbstractTreeTableModel
{
private MyTreeNode myroot;
public MyTreeTableModel()
{
myroot = new MyTreeNode( "root", "Root of the tree" );
myroot.getChildren().add( new MyTreeNode( "Empty Child 1",
"This is an empty child" ) );
MyTreeNode subtree = new MyTreeNode( "Sub Tree",
"This is a subtree (it has children)" );
subtree.getChildren().add( new MyTreeNode( "EmptyChild 1, 1",
"This is an empty child of a subtree" ) );
subtree.getChildren().add( new MyTreeNode( "EmptyChild 1, 2",
"This is an empty child of a subtree" ) );
myroot.getChildren().add( subtree );
myroot.getChildren().add( new MyTreeNode( "Empty Child 2",
"This is an empty child" ) );
}
@Override
public int getColumnCount()
{
return 3;
}
@Override
public String getColumnName( int column )
{
switch( column )
{
case 0: return "Name";
case 1: return "Description";
case 2: return "Number Of Children";
default: return "Unknown";
}
}
@Override
public Object getValueAt( Object node, int column )
{
System.out.println( "getValueAt: " + node + ", " + column );
MyTreeNode treenode = ( MyTreeNode )node;
switch( column )
{
case 0: return treenode.getName();
case 1: return treenode.getDescription();
case 2: return treenode.getChildren().size();
default: return "Unknown";
}
}
@Override
public Object getChild( Object node, int index )
{
MyTreeNode treenode = ( MyTreeNode )node;
return treenode.getChildren().get( index );
}
@Override
public int getChildCount( Object parent )
{
MyTreeNode treenode = ( MyTreeNode )parent;
return treenode.getChildren().size();
}
@Override
public int getIndexOfChild( Object parent, Object child )
{
MyTreeNode treenode = ( MyTreeNode )parent;
for( int i=0; i>treenode.getChildren().size(); i++ )
{
if( treenode.getChildren().get( i ) == child )
{
return i;
}
}
return 0;
}
public boolean isLeaf( Object node )
{
MyTreeNode treenode = ( MyTreeNode )node;
if( treenode.getChildren().size() > 0 )
{
return false;
}
return true;
}
@Override
public Object getRoot()
{
return myroot;
}
}
清單2展示了一個定制的樹節點:
清單2. MyTreeNode.java
class MyTreeNode
{
private String name;
private String description;
private List<MyTreeNode> children = new ArrayList<MyTreeNode>();
public MyTreeNode()
{
}
public MyTreeNode( String name, String description )
{
this.name = name;
this.description = description;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getDescription()
{
return description;
}
public void setDescription(String description)
{
this.description = description;
}
public List<MyTreeNode> getChildren()
{
return children;
}
public String toString()
{
return "MyTreeNode: " + name + ", " + description;
}
}
{
private String name;
private String description;
private List<MyTreeNode> children = new ArrayList<MyTreeNode>();
public MyTreeNode()
{
}
public MyTreeNode( String name, String description )
{
this.name = name;
this.description = description;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getDescription()
{
return description;
}
public void setDescription(String description)
{
this.description = description;
}
public List<MyTreeNode> getChildren()
{
return children;
}
public String toString()
{
return "MyTreeNode: " + name + ", " + description;
}
}
如果你想使用這個樹表模式,你將需要創建一個它的實例,然后將該實例傳給JXTreeTable的構造器,就像這樣:
private MyTreeTableModel treeTableModel = new MyTreeTableModel();
private JXTreeTable treeTable = new JXTreeTable( treeTableModel );
private JXTreeTable treeTable = new JXTreeTable( treeTableModel );
現在你就可以把treeTable加入任一Swing容器,如JPanel或JFrame的內容面板。
3. RSyntaxTextArea
Swing絕不應該缺少的另一個組件就是有語法高亮功能的文本編輯器。如果你已經編寫過一個XML文檔,你就會知道以可視化的方式區分出標簽,屬性,屬性值及標簽值是多么的有用。FifeSoft的開發者已經構建了一組富組件,你可以在基于Swing的Java應用程序中使用它們,其中一個組件就是 RSyntaxTextArea。
RSyntaxTextArea支持大部分的開箱即用的編程語言,包括C,C++,Perl,PHP和Java,還有HTML,JavaScript,XML,甚至是SQL。
圖4是RSyntaxTextArea組件展示XML文件的一個截屏:
圖4. RSyntaxTextArea展示一個XML文件

在Swing應用中加入語法高亮
首先,從Sourceforge中下載RSyntaxTextArea的JAR文件。如果你使用Maven,你可能會想把它安裝到你的本地倉庫中,可使用如下的命令行:
mvn install:install-file -DgroupId=com.fifesoft -DartifactId=rsyntaxtextarea
-Dversion=1.0 -Dpackaging=jar -Dfile=/path/to/file
-Dversion=1.0 -Dpackaging=jar -Dfile=/path/to/file
一旦你在項目使用這個JAR文件,你就能在應用中創建RSyntaxTextArea的實例。如果你希望有滑動功能,就把它加入 RTestScrollPane中,然后調用setSyntaxEditingStyle()方法,并傳入一個SyntaxConstants作為該方法的參數。
清單3. Swing中的語法高亮
RSyntaxTextArea text = new RSyntaxTextArea();
add( new RTextScrollPane( text ) );
text.setSyntaxEditingStyle( SyntaxConstants.SYNTAX_STYLE_XML );
add( new RTextScrollPane( text ) );
text.setSyntaxEditingStyle( SyntaxConstants.SYNTAX_STYLE_XML );
4. Java外觀圖形倉庫
Microsoft作的很好的工作之一就是確保Windows應用都有著一致的外觀。如果你已經編寫過一個Java Swing應用,無論用了多長時間,你可能已經訪問過Oracle的Java外觀圖形倉庫。如果沒有,你會對它感滿意的。Java外觀圖形倉庫創建一組針對標準應用行為的圖標,例如File->New和Edit->Copy,還有更多的鮮為人知的命令,如媒體控件,瀏覽器導航功能,以及針對 Java開發員的編程工作。圖5展示了一個從Oracle網站上獲取的圖標的截屏:
圖5. Java外觀圖形倉庫圖標

如果Java外觀圖形倉庫只是提供預置的圖形,它也足夠好了,但它還提供了當你在構建和命名菜單,菜單欄,以及快捷鍵的標準規范。例如,復制功能應該有Ctrl-C快捷鍵,命名為Copy,并給一個Copy的提示。當它在菜單中,復制功能的助記符應為C,P,或至少是Y。
使用Java外觀圖形倉庫的圖標
嘗試圖5所示的一些預置圖形,要從Oracle網站上下載Java外觀圖形倉庫的JAR文件,并將它加到你的CLASSPATH中。你需要將JAR文件中圖標作為資源進行加載。這些圖標處于如下的格式:

toolbarButtonGraphics/general/Copy16.gif
toolbarButtonGraphics/general/Copy24.gif
toolbarButtonGraphics/general/Cut16.gif
toolbarButtonGraphics/general/Cut24.gif
toolbarButtonGraphics/general/Delete16.gif
toolbarButtonGraphics/general/Delete24.gif

所有的圖標都包含在toolbarButtonGraphics目錄中,被分割成圖5所示的類別。從這一分類中,我們可以從通用類中找到復制,剪切和刪除。名稱中的"16"和"24"表示圖標尺寸限制:16x16或24x24。你可以使用如下方法來創建一個ImageIcon到文件中:
Class class = this.getClass();
String urlString = "/toolbarButtonGraphics/general/Cut16.gif"
URL url = class.getResource( urlString );
ImageIcon icon = new ImageIcon( url );
String urlString = "/toolbarButtonGraphics/general/Cut16.gif"
URL url = class.getResource( urlString );
ImageIcon icon = new ImageIcon( url );
5. Swing線程
當啟動文中示例時,你可能會遇到一些看起來奇怪的運行時錯誤。如果是這樣,在你的Swing應用中,你可能會犯一個通常的線程錯誤。許多Java開發者不知道Swing應用程序希望運行在它們自己的線程中,而不是運行在主運行線程中。Swing不會原諒這方面的錯誤,但介紹過的許多組件目前還不會這樣。
為了幫助你在Swing應用自己的線程中啟動它自己,Java平臺提供了一個叫作SwingUtilties的類,它有一個invokeLater()方法,你應該使用它去啟動Swing應用。清單4展示了使用SwingUtilities.invokeLater()去啟動JXTreeTable:
清單4. SwingXExample.java
package com.geekcap.swingx;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import org.jdesktop.swingx.JXTreeTable;
import com.geekcap.swingx.treetable.MyTreeTableModel;
public class SwingXExample extends JFrame
{
private JTabbedPane tabs = new JTabbedPane();
private MyTreeTableModel treeTableModel = new MyTreeTableModel();
private JXTreeTable treeTable = new JXTreeTable( treeTableModel );
public SwingXExample()
{
super( "SwingX Examples" );
// Build the tree table panel
JPanel treeTablePanel = new JPanel( new BorderLayout() );
treeTablePanel.add( new JScrollPane( treeTable ) );
tabs.addTab( "JXTreeTable", treeTablePanel );
// Add the tabs to the JFrame
add( tabs );
setSize( 1024, 768 );
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
setLocation( d.width / 2 - 512, d.height/2 - 384 );
setVisible( true );
setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
}
public static void main( String[] args )
{
AppStarter starter = new AppStarter( args );
SwingUtilities.invokeLater( starter );
}
}
class AppStarter extends Thread
{
private String[] args;
public AppStarter( String[] args )
{
this.args = args;
}
public void run()
{
SwingXExample example = new SwingXExample();
}
}
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import org.jdesktop.swingx.JXTreeTable;
import com.geekcap.swingx.treetable.MyTreeTableModel;
public class SwingXExample extends JFrame
{
private JTabbedPane tabs = new JTabbedPane();
private MyTreeTableModel treeTableModel = new MyTreeTableModel();
private JXTreeTable treeTable = new JXTreeTable( treeTableModel );
public SwingXExample()
{
super( "SwingX Examples" );
// Build the tree table panel
JPanel treeTablePanel = new JPanel( new BorderLayout() );
treeTablePanel.add( new JScrollPane( treeTable ) );
tabs.addTab( "JXTreeTable", treeTablePanel );
// Add the tabs to the JFrame
add( tabs );
setSize( 1024, 768 );
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
setLocation( d.width / 2 - 512, d.height/2 - 384 );
setVisible( true );
setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
}
public static void main( String[] args )
{
AppStarter starter = new AppStarter( args );
SwingUtilities.invokeLater( starter );
}
}
class AppStarter extends Thread
{
private String[] args;
public AppStarter( String[] args )
{
this.args = args;
}
public void run()
{
SwingXExample example = new SwingXExample();
}
}
構造器設置JFrame的可視性為true,而如果運行在應用的主線程中,Swing是不允許這么做的。所以清單創建一個獨立的類,叫作 AppStarter,它繼承自Thread并會創建SwingXExample類的實例。main()方法創建AppStarter類的一個實例,并將它傳給SwingUtilities.invokeLater()方法以方便啟動應用。嘗試著養成這樣的習慣去運行Swing應用--不僅因為這是正確的方式,也因為如果你不這么做一些第三方的組件將無法工作。
結論
Swing是一個強大的類庫,它允許你在Java平臺上構建用戶界面,但它缺少一些你可能想引入的現代的組件。在本文中,為了美化(以及現代化)你的 Swing應用,我提供了一些小竅門。開源項目,如Substance,SwingX以及Java外觀圖形倉庫使在Java平臺上構建富用戶界面變得更容易。查看資源章節,以學習關于這些開源項目以及Swing編程的更多知識。