你所不知道的五件事情--改进Swing(译)
你所不知道的五件事情--改进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编程的更多知识。