【IT168 技术文档】
Java 世界的人似乎一直都对 Java 的桌面应用程序十分不满,从 AWT 到 SWING,从默认的 Theme到第三方的产品,不是太难看(AWT)就是在某些平台有 BUG(SWING,Quaqua--一个Windows平台下的仿Mac的主题包),再不就是对中文支持不好(某些第三方 LookAndFeel)。于是,如果想要获得和本机平台一致的用户界面和比较稳定的性能,SWT就成了一个不可忽视的选择。SWT 是一个独立于平台的,可以脱离 Eclipse 框架单独使用的图形组件,用JNI技术提供与本机系统同样的用户界面组件的观感,较好的运行效率,稳定的平台表现。
当然,虽然这个专题名叫"全接触",但毕竟不可能面面俱到,在一篇文章中兼收并蓄SWT的全部内容也不现实。但不管怎么说,我都将尽力展示SWT的使用细节,希望能为那些对SWT感兴趣的人提供一些帮助。
SWT-"Standard Widget Toolkit",它是一个Java平台下开放源码的Native GUI组件库,也是Eclipse平台的UI组件之一。从功能上来说,SWT与AWT/SWING是基本等价的。SWT以方便有效的方式提供了便携式的(即Write Once,Run Away)带有本地操作系统观感的UI组件:
由于widget系统的固有复杂性以及平台之间微妙的差异,即使在理想情况下,能够达到工业标准的跨平台的widget类库也是很难编写和维护的。最早的AWT组件现在被认为是样貌丑陋的,而且存在很多问题;SWING组件虽然也是缺点多多,但是随着JDK版本的不断升高,它仍在不断进行着改进。我认为,SWT在功能上与AWT/SWING不相伯仲,但是组件更为丰富,平台表现稳定,BUG也相对较少。如果你的应用程序真的需要在多个平台上运行,需要更为美观的界面,又不那么依赖于其他基于AWT/SWING的图形库,那么SWT或许是一个比AWT/SWING更好的选择。
一如介绍其他程序的起始,我们都需要来一个HelloWorld来帮助我们入门,SWT的HelloWorld如下:
import org.eclipse.swt.widgets.*; public class HelloWorld { public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("Hello World"); shell.setSize(200, 100); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep (); } display.dispose (); } }
运行这个程序就会得到如下结果:
下面我讲逐一介绍这个程序所包含的内容。
- Display
这是一个顶层容器组件,类似于Container或Component的功能,它主要负责与底层的窗口系统之间的连接。在具体含义上,它代表"屏幕"。
一个Display可以包含多个Shell(也是容器组件,下面会介绍到)。
通常情况下,一个应用程序只含一个Display,即Display通常是一个单例组件(Singleton)。 - Shell
它表示位于"屏幕"上面的"窗口",是Composite组件和Control组件构成的组件树的根。
在我们的HelloWorld程序中,我们可以设置标题(setText()),设置大小(setSize()),然后通过open()方法来显示这个窗口。怎么样,感觉很像JFrame吧?其实功能上差不多。 - Composite
可以包含其它Composite和Control的容器 - Control
这是一个重量级(HeavyWeight)系统对象。像按钮(Button),标签(Label),表格,工具栏和树形结构这些组件都是Control的子类,Conposite和Shell也不例外。
2.1.1 消息循环
我们可以看到,上面的代码中有这样的语句:
while (!shell.isDisposed())
{
if (!display.readAndDispatch())
display.sleep ();
}
|
如果你像我一样是由Java语言起步的,那么你会对这个消息循环的代码感到比较陌生,毕竟在SWING中我们主要利用事件驱动模型而不这样利用类似于Windows程序设计中的消息循环的方法来处理事件。但是这段代码意义还算简单明了,就是反复的读取和分派(dispatch)事件,并在没有事件的时候把控制权还给CPU。
2.1.2 资源的释放
最后一条语句是display.dispose ();,这告诉我们操作系统的资源是由程序员显示释放的。资源的释放遵循以下两条规则:
1. 如果你创建了某个资源,那么你就有责任释放它。
2. 释放父组件资源的同时也释放了其子组件的资源。
2.1.3 标准构造函数
窗口组件被创建的时候必须伴随一个他的上层组件,例如,我要建立一个按钮就可以采用如下方法:Button button = new Button(shell, SWT.PUSH);
其中,Button的父组件Shell是必不可少的,这样就限定了我们生成组件的顺序。
第二个参数被称为"Style Bit",表示了这个组件的显示特性,每种特性占一位,如下例所示:
Text test=new Text(group, SWT.SINGLE|SWT.BORDER);
|
这条代码生成了一个单一的,有边框的文本框。这显然又与习惯了JavaBeans模型,总是用setXXX()来设置属性的我们不太适应--毕竟是IBM的东西啊,秉承了其产品不易上手的传统。
2.1.4 错误与异常
SWTError指的是不能修复的错误,以及一些操作系统错误。
SWTException指的是一些可恢复的错误以及无效的线程访问之类的错误。
IllegalArgumentException指可修复的错误或参数为null之类的错误。
2.1.5 Item
Item类是一个轻量级的系统对象,总是作为基本的单位元素与其他一些类配合使用。比如Tree中的元素即为TreeItem,Table的单位元素则是TableItem,而MenuItem就是Menu的基本单位元素了。
2.1.6 SWT的类阶层体系结构
最后让我们来整体认识一下整个SWT窗口组件的层次结构,如下所示:以上的部分给我们以整体的认识,即一个SWT引用程序应该怎么创建,其基本的运行规则和相关类的体系结构。我想我就不用再对每一个控件的API或使用方面费唇舌了,熟悉这些东西是体力劳动,而网上有很多例子可供参考。下面一节我将详细介绍有关SWT布局的相关知识。
相信对于组件的布局(Layout)大家都不会太陌生,它的存在就是提供给我们一种可以在组件位置移动或更改大小时重新绘制组件的机制。设置组件的布局我们可以采用Composite.setLayout()方法来实现。
每种布局都有其相应的数据(Layout Data),可以通过Control.setLayoutData()方法来进行关联。以下是一些布局类及其显示效果:
- FillLayout:让所有子组件等大小的"填满"整个面板空间。
FillLayout是最简单的一个布局类,它将所有窗口组件放置到一行或一列中,并强制他们的大小也相等。FillLayout不能外覆(wrap),也不能定制边框和距离。很显然这样的限制让这个布局类最适合作类似于计算器面板的布局,或者为Taskbar和Toolbar上面的按钮作布局使用。

RowLayout:类似于AWT中的FlowLayout,让所有组件按行排列,一行排不下就放到下一行。
RowLayout比FillLayout用得更广泛一些,原因很简单,就是RowLayout支持FillLayout所部支持的功能,例如能够外覆,能够修改边框和间距等等。另外,每一个位于RowLayout中的窗口组件都可以通过设定一个RowData类来指定其在RowLayout中的宽度和高度。
GridLayout: GridLayout是3个标准布局类中最有用的,但同时也是最复杂的--没办法,强大的功能必定伴随着一定程度的复杂性。通过GridLayout,一个Composite的子窗口组件被放置在一个网格(Grid)之中。GridLayout有很多配置字段,并且和RowLayout一样,每一个布局于其中的窗口组件都可以有一个与之相关联的布局数据类,称为GridData。GridLayout的强大功能是通过对于每一个窗口组件的GridData的灵活控制来实现的。
鉴于GridLayout的复杂性(原本我就怀疑它根本就不是为手工书写代码而设计的),我并不建议各位直接手动书写GridData,最好借助可视化的工具(如VI)来帮助我们完成用GridLayout进行的界面设计。这样我们只需要书写少量控制代码,就可以获得复杂的界面布局了。
FormLayout:如图所示
StackLayout:几乎完全等同于CardLayout的功能。
在SWT中,位置和大小的变化并非自动发生的。应用程序既可以在Composite子类的构造函数中指定初始位置和大小,也可以在一个改变窗口大小的监听器中用布局类来定位和改变Composite子类的大小。
下面的一幅图包含了我们将要讨论的有关布局的大部分细节。一个Composite类的可显示区域分为三个部分,分别是Location,clientArea和trim。Composite的大小就是clientArea和trim的区域之和。一个布局类(Layout)的主要功能就是管理Composite子组件的大小和位置。通过布局类,我们可以管理子组件之间的距离-即间距(Spaceing),子组件与布局边缘之间的距离-即边距(margin)。布局的大小同时也是Composite的clientArea的大小。
至此,关于SWT的基础部分就告一段落,希望能够给大家以一个对于SWT的总体认识。下面的部分将主要介绍SWT的弱项-绘图。JGraph的一个作者就表达了对SWT/JFace/Draw2D的不满,认为SWT在执行效率上并没有什么改善,而且缺乏一些有用的API实现。话虽如此,但SWT的基本绘图功能还是不错的,如果有足够的时间和耐心的话还是可以绘出想要的图形的。下面就让我们看看SWT如何绘制2D和3D图形。
用SWT绘图通常由两种方法,一种是借助Graphics Context,另一种是利用Draw2D。然而Draw2D是一个基于SWT Composite的轻量级组件,于是在效率上,它无法体现出SWT的Native Code的速度优势。故其虽然强大,但仅适用于绘图工作不是系统瓶颈的应用程序。所以我在这里只介绍第一种方法。
我们可以在任何实现了org.eclipse.swt.graphics.Drawable接口的类上绘制图形,这包括一个控件,一幅图像,一个显示设备或一个打印设备。类org.eclipse.swt.graphics.GC是一个封装了所有可执行的绘图操作的图形上下文(Graphics Context)。两种使用GC的方式我们已经在本节前言中提过,稍后会作详细说明。
下面一段代码创建了一个带有图像的GC并在上面绘制了两条线:
Image image = new Image(display,"C:/music.gif"); GC gc = new GC(image); Rectangle bounds = image.getBounds(); gc.drawLine(0,0,bounds.width,bounds.height); gc.drawLine(0,bounds.height,bounds.width,0); gc.dispose(); image.dispose();

一旦你创建了一个GC,你就有责任通过它的dispose方法释放它的资源。一个由应用程序创建的GC需要立即被绘制,然后尽快释放掉。这是因为每个GC都需要一个底层的系统资源,而在某些操作系统中这些资源是稀缺的,像Win98就只允许同时创建五个GC对象。
类org.eclipse.swt.widgets.Control是可绘制的,所以你可以用像在图像上一样的方式来绘制图形。而和在图像上绘制所不同的是,如果你使用GC在一个Control上绘制图形,你需要知道当操作系统自身要绘制这个control的时候,它将覆盖掉你的改动。所以在一个Control上绘制图形的正确方法是加入其绘制事件的监听器。监听器类为org.eclipse.swt.events.PaintListener,其回调函数的参数是一个org.eclipse.swt.events.PaintEvent类的实例。这个PaintEvent实例中包含一个GC的引用,你可以向这个GC发送消息。下面的代码示例说明了如何建立这种类型的绘图:
Shell shell = new Shell(display); shell.addPaintListener(new PaintListener(){ public void paintControl(PaintEvent e){ Rectangle clientArea = shell.getClientArea(); e.gc.drawLine(0,0,clientArea.width,clientArea.height); } }); shell.setSize(150,150)

.4 剪切(Clipping)
GC的剪切域是可见绘图发生的部分。在缺省情况下,一个GC是一个被构造的可视部分边界。改变一个GC的剪切域可以让我们构造出各种图形效果。其中的一个例子是如果你想填充一个缺失了边缘的矩形。一种方法是绘制多边形矩形来组成所需要的图形,另一种方法就是剪切GC,然后对其剪切部分进行填充。
shell.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
Rectangle clientArea = shell.getClientArea();
int width = clientArea.width;
int height = clientArea.height;
e.gc.setClipping(20,20,width - 40, height - 40);
e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN));
e.gc.fillPolygon(new int[] {0,0,width,0,width/2,height});
}
});
|
这段代码在Shell上的显示的过程效果如下:
虽然任何Control都可以通过自身的paintEvent来绘制图形,但其子类org.eclipse.swt.widgets.Canvas是专门被设计用来进行图形操作的特殊的绘图类。我们既可以使用一个Canvas,再加入一个绘图监听器来实现绘图,也可以通过继承来建立一个可重用的自定义Control。Canvas有很多style bit,可以在绘图发生时产生作用。
我们有很多方法可以在一个GC上画线,包括在两点之间,一系列离散的点之间或一个预定义的图形上都可以。直线是以GC的前景色来绘制的,我们可以通过GC绘制拥有不同厚度的各式直线。对于一个Paint事件,GC有着与Control组件一样的属性,即激发事件且缺省的直线样式固定为1个像素宽。
GC.drawLine(int x1, int y1, int x2, int y2);这条语句在可绘制的面板上的两点间花了一条直线,起始点为(x1,y1),终止点为(x2,y2)。终止点包含在画好的直线中。如果起始点等于终止点的话,将会有一个独立的象素点被绘制出来。
GC.drawPolyline(int[] pointArray);这条语句绘制了一系列互相连接的线段,作为参数的数组用于描述点的位置。语句gc.drawPolyline(new int[] { 25,5,45,45,5,45 });绘制了如下的图形:
GC.drawPolygon(int[] pointArray);与drawPolyline(int[])是类似的,唯一区别在于最后一个点和低一个点是连接的。gc.drawPolygon(new int[] { 25,5,45,45,5,45 });将会获得与上图一样的结果。
GC.drawRectangle(int x, int y, int width, int height);这条语句从左上角的(X,Y)点,用参数中的宽和高画出了一个矩形。gc.drawRectangle(5,5,90,45);将会绘制出如下图形:
GC.drawRoundedRectangle(int x,int y,int width,int height,int arcWidth,int arcHeight);一个圆矩形与标准矩形的区别就在于其四个角是圆的。圆矩形的每一个角都可以被想象成为1/4个椭圆,并且arcWidth和arcHeight由完整的椭圆的宽和高决定。gc.drawRoundedRectangle(5,5,90,45,25,15);绘制了一个左上角位置为5.5的圆矩形,右边的图形是放大后的效果:
GC.drawOval(int x, int y, int width, int height);一个椭圆是由其相对应的矩形的左上角的位置(x,y)来确定绘制位置的,其宽和高即为对应矩形的宽和高。对于圆形来说,只需要另宽和高相等即可。
GC.drawArc(int x, int y, int width, int height, int startAngle, int endAngle);曲线的绘制也是与一个相应的矩形有关,即其左上角的位置与宽和高都是相应矩形的属性。StartAntle是从横向的X开始计算的,所以0度指向的是东而不是北。曲线的绘制是从StartAngle到endAngle以逆时针方向执行。gc.drawArc(5,5,90,45,90,200);所绘制的图形如下:
文本可以被绘制在一个GC上, 字形是用GC的前景色和字体来绘制的,并且它所占用的区域是用GC背景色绘制的。要绘制文本,你需要定义要绘制文本的左上角,宽度和高度。有两组方法可以用来绘制文本,第一组方法的名字里都带有一个Text,并将会处理直线定界符和制表符。第二组API方法集的名字里都带有String,它们没有制表符或回车的处理,并主要用于控制像Eclipse的Java编辑器StyledText这样复杂的Control。
GC.drawText(String text, int x, int y);
Font font = new Font(display,"Arial",14,SWT.BOLD | SWT.ITALIC);
// ...
gc.drawText("Hello World",5,5);
gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE));
gc.setFont(font);
gc.drawText("Hello\tThere\nWide\tWorld",5,25);
// ...
font.dispose();
|

drawText API将控制字符\t处理为制表符,将\n处理为回车符。
GC.drawString(String text, int x, int y);
Font font = new Font(display,"Arial",14,SWT.BOLD | SWT.ITALIC);
// ...
gc.drawString("Hello World",5,5);
gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE));
gc.setFont(font);
gc.drawString("Hello\tThere\nWide\tWorld",5,25);
// ...
font.dispose()
|

当使用drawString时,制表符和回车符将不会被处理。
在一个GC上绘制字符的时候,一个字符串所占用的大小取决于它的内容以及GC的字体。想要确定一个字符串在被绘制之后所占用的区域可以使用方法:GC.stringExtent(String text), 或 GC.textExtent(String text)。这两个方法都返回一个Point类,这个Point的X和Y是渲染参数字符串所需要的宽和高。