Java 语言的声望和它在桌面应用程序(GUI 程序)所取得的成就显然极不相符,至今仍然很少能看到非常成功 Java 桌面程序。虽然有 JBuilder,Netbean,JProbe 等大型软件作为代表,但这仍不能证明 Java 的 GUI 程序是成功的:它们的外观总是和同一操作系统平台下的其它软件显得格格不入。对机器配置的需求也似乎永无止境,这使得它们只能被一些总是拥有当前最高性能 PC 的程序员们所容忍,或是那些不在乎金钱和时间的专业用户所接受。对绝大多数计算机使用者来说,AWT 或 SWING 代表着怪异的界面和无法接受的速度。Standard Widget Toolkit(SWT)或许是 Java 这一噩梦的终结者,广大 Java 程序员终于可以开发出高效率的 GUI 程序,它们拥有标准的外观,几乎没有人能看出你的程序是用 Java 写出来的,更为重要的是,这些程序是跨平台的。
SWT 本身仅仅是 Eclipse 组织为了开发 Eclipse IDE 环境所编写的一组底层图形界面 API。或许是无心插柳,或是有意为之,至今为止,SWT 无论是在性能和外观上,都超越了 SUN 公司提供的 AWT 和 SWING。目前 Eclipse IDE 已经开发到了 2.1 版本,SWT 已经十分稳定。这里指的稳定应该包含两层意思:
一是指性能上的稳定,其中的关键是源于 SWT 的设计理念。SWT 最大化了操作系统的图形构件 API,就是说只要操作系统提供了相应图形的构件,那么 SWT 只是简单应用 JNI 技术调用它们,只有那些操作系统中不提供的构件,SWT 才自己去做一个模拟的实现。可以看出 SWT 的性能上的稳定大多时候取决于相应操作系统图形构件的稳定性。
另一个稳定是指 SWT API 包中的类、方法的名称和结构已经少有改变,程序员不用担心由于 Eclipse 组织开发进度很快(Eclipse IDE 每天都会有一个 Nightly 版本的发布),而导致自己的程序代码变化过大。从一个版本的 SWT 更新至另一版本,通常只需要简单将 SWT 包换掉就可以了。
下面让我们开始一个 SWT 程序。(注意:以下的例子和说明主要针对 Windows 平台,其它的操作系统应该大同小异)。首先要在 Eclipse 安装文件中找到 SWT 包,Eclipse 组织并不提供单独的 SWT 包下载,必须下载完整的 Eclipse 开发环境才能得到 SWT 包。SWT 是作为 Eclipse 开发环境的一个插件形式存在,可以在 $ {你的 eclipse 安装路径} \plugins 路径下的众多子目录下去搜索 SWT.JAR 文件,在找到的 JAR 文件中包含了 SWT 全部的 Java 类文件。因为 SWT 应用了 JNI 技术,因此同时也要找到相对应的 JNI 本地化库文件,由于版本和操作平台的不同,本地化库文件的名称会有些差别,比如 SWT-WIN32-2116.DLL 是 Window 平台下 Eclipse Build 2116 的动态库,而在 Unix 平台相应版本的库文件的扩展名应该是 .so,等等。注意的是,Eclipse 是一个开放源代码的项目,因此你也可以在这些目录中找到 SWT 的源代码,相信这会对开发很有帮助。
下面是一段打开空窗口的代码 ( 只有 main 方法 )。
import com.e2one.example;
public class OpenShell{
public static void main(String [] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.open();
// 开始事件处理循环,直到用户关闭窗口
while (!shell.isDisposed()) {
if (!display.readAndDispatch())
display.sleep();
}
display.dispose();
}
}
|
确信在 CLASSPATH 中包括了 SWT.JAR 文件,先用 Javac 编译例子程序。编译无错后可运行 java -Djava.library.path=${ 你的 SWT 本地库文件所在路径 } com.e2one.example.OpenShell,比如 SWT-WIN32-2116.DLL 件所在的路径是 C:\swtlib,运行的命令应该是 java -Djava.library.path=c:\swtlib com.e2one.example.OpenShell。成功运行后,系统会打开了一个空的窗口。
下面再让我们进一步分析 SWT API 的组成。所有的 SWT 类都用 org.eclipse.swt 做为包的前缀,下面为了简化说明,我们用 * 号代表前缀 org.eclipse.swt,比如 *.widgets 包,代表的是 org.eclipse.swt.widgets 包。
我们最常用的图形构件基本都被包括在 *.widgets 包中,比如 Button,Combo,Text,Label,Sash,Table 等等。其中两个最重要的构件当数 Shell 和 Composite。Shell 相当于应用程序的主窗口框架,上面的例子代码中就是应用 Shell 构件打开一个空窗口。Composite 相当于 SWING 中的 Panel 对象,充当着构件容器的角色,当我们想在一个窗口中加入一些构件时,最好到使用 Composite 作为其它构件的容器,然后再去 *.layout 包找出一种合适的布局方式。SWT 对构件的布局也采用了 SWING 或 AWT 中 Layout 和 Layout Data 结合的方式,在 *.layout 包中可以找到四种 Layout 和与它们相对应的布局结构对象(Layout Data)。在 *.custom 包中,包含了对一些基本图形构件的扩展,比如其中的 CLabel,就是对标准 Label 构件的扩展,上面可以同时加入文字和图片,也可以加边框。StyledText 是 Text 构件的扩展,它提供了丰富的文本功能,比如对某段文字的背景色、前景色或字体的设置。在 *.custom 包中也可找到一个新的 StackLayout 布局方式。
SWT 对用户操作的响应,比如鼠标或键盘事件,也是采用了 AWT 和 SWING 中的 Observer 模式,在 *.event 包中可以找到事件监听的 Listener 接口和相应的事件对象,例如常用的鼠标事件监听接口 MouseListener,MouseMoveListener 和 MouseTrackListener,及对应的事件对象 MouseEvent。
*.graphics 包中可以找到针对图片、光标、字体或绘图的 API。比如可通过 Image 类调用系统中不同类型的图片文件。通过 GC 类实现对图片、构件或显示器的绘图功能。
对不同平台,Eclipse 还开发了一些富有针对性的 API。例如,在 Windows 平台,可以通过 *.ole.win32 包很容易的调用 ole 控件,这使 Java 程序内嵌 IE 浏览器或 Word、Excel 等程序成为可能!
要进一步了解 SWT 的情况,可以在 Eclipse IDE 的帮助文档中找到 SWT 的 JavaDoc 说明。当然最深入的了解莫过于去读 SWT 的源代码,这也正是开放源代码项目对程序员的魅力所在 !
下面让我们展示一个比上面例子更加复杂一些的程序。这个程序拥有一个文本框和一个按键,当用户点击按键的时候,文本框显示一句欢迎信息。
为了文本框和按键有比较合理的大小和布局,这里采用了 GradLayout 布局方式。这种布局是 SWT 中最常用也是最强大的布局方式,几乎所有的格式都可能通过 GradLayout 去达到。下面的程序也涉及到了如何应用系统资源 (Color),以及如何释放系统资源。
private void initShell(Shell shell) {
// 为 Shell 设置布局对象
GridLayout gShellLay = new GridLayout();
shell.setLayout(gShellLay);
// 构造一个 Composite 构件作为文本框和按键的容器
Composite panel = new Composite(shell,SWT.NONE);
// 为 Panel 指定一个布局结构对象。这里让 Panel 尽可能的占满 Shell,
// 也就是全部应用程序窗口的空间。
GridData gPanelData = new GridData(GridData.GRAB_HORIZONTAL|
GridData.GRAB_VERTICAL|GridData.FILL_BOTH);
panel.setLayoutData(gPanelData);
// 为 Panel 也设置一个布局对象。文本框和按键将按这个布局对象来显示。
GridLayout gPanelLay = new GridLayout();
panel.setLayout(gPanelLay);
// 为 Panel 生成一个背景色
final Color bkColor = new Color(Display.getCurrent(),200,0,200);
panel.setBackground(bkColor);
// 生成文本框
final Text text = new Text(panel,SWT.MULTI|SWT.WRAP);
// 为文本框指定一个布局结构对象,这里让文本框尽可能的占满 Panel 的空间。
GridData gTextData = new GridData(GridData.GRAB_HORIZONTAL|
GridData.GRAB_VERTICAL|GridData.FILL_BOTH);
text.setLayoutData(gTextData);
// 生成按键
Button butt = new Button(panel,SWT.PUSH);
butt.setText("Push");
// 为按键指定鼠标事件
butt.addMouseListener(new MouseAdapter(){
public void mouseDown(MouseEvent e){
// 当用户点击按键的时候,显示信息
text.setText("Hello SWT");
}
});
// 当主窗口关闭时,会触发 DisposeListener。这里用来释放 Panel 的背景色。
shell.addDisposeListener(new DisposeListener(){
public void widgetDisposed(DisposeEvent e) {
bkColor.dispose();
}
});
}
|
把这段代码中的方法 initShell() 加入到第一个打开空窗口的例子中,得到的是一段能成功运行的完整 GUI 应用程序。运行方法可参考第一个例子。
在一个图形化的操作系统中开发程序,都要调用系统中的资源,如图片、字体、颜色等。通常这些资源都是有限的,程序员务必非常小心的使用这些资源:当不再使用它们时,就请尽快释放,不然操作系统迟早会油尽灯枯,不得不重新启动,更严重的会导致系统崩溃。
SWT 是用 Java 开发的,Java 语言本身的一大优势就是 JVM 的"垃圾回收机制",程序员通常不用理会变量的释放,内存的回收等问题。那么对 SWT 而言,系统资源的操作是不是也是如此?答案是一个坏消息,一个好消息。
坏消息是 SWT 并没采用 JVM 的垃圾回收机制去处理操作系统的资源回收问题,一个关键的因素是因为 JVM 的垃圾回收机制是不可控的,也就是说程序员不能知道,也不可能做到在某一时刻让 JVM 回收资源!这对系统资源的处理是致命的,试想你的程序希望在一个循环语句中去查看数万张图片,常规的处理方式是每次调入一张,查看,然后就立即释放该图片资源,而后在循环调入下一张图片,这对操作系统而言,任何时刻程序占用的仅仅是一张图片的资源。但如果这个过程完全交给 JVM 去处理,也许会是在循环语句结束后,JVM 才会去释放图片资源,其结果可能是你的程序还没有运行结束,操作系统已经宕掉。
但下面的好消息也许会让这个坏消息变得无关紧要。对于 SWT,只需了解两条简单的"黄金"法则就可以放心的使用系统资源!之所以称为黄金法则,一是因为少,只有两条,二是因为它们出奇的简单。第一条是"谁占用,谁释放",第二条是"父构件被销毁,子构件也同时被销毁"。第一条原则是一个无任何例外的原则,只要程序调用了系统资源类的构造函数,程序就应该关心在某一时刻要释放这个系统资源。比如调用了
Font font = new Font (display, "Courier", 10, SWT.NORMAL);
那么就应该在不在需要这个 Font 的时候调用
font.dispose();
对于第二个原则,是指如果程序调用某一构件的 dispose() 方法,那么所有这个构件的子构件也会被自动调用 dispose() 方法而销毁。通常这里指的子构件与父构件的关系是在调用构件的构造函数时形成的。比如,
Shell shell = new Shell();
Composite parent = new Composite(shell,SWT.NULL);
Composite child = new Composite(parent,SWT.NULL);
|
其中 parent 的父构件是 shell,而 shell 则是程序的主窗口,所以没有相应的父构件,同时 parent 又包括了 child 子构件。如果调用 shell.dispose() 方法,应用第二条法则,那么 parent 和 child 构件的 dispose() 方法也会被 SWT API 自动调用,它们也随之销毁。
对于这两个法则,参考资料 2 中的文章有更加深入的介绍。
在任何操作平台的 GUI 系统中,对构件或一些图形 API 的访问操作都要被严格同步并串行化。例如,在一个图形界面中的按键构件可被设成可用状态 (enable) 或禁用状态 (disable),正常的处理方式是,用户对按键状态设置操作都要被放入到 GUI 系统的事件处理队列中(这意味着访问操作被串行化),然后依次处理(这意味着访问操作被同步)。想象当按键可用状态的设置函数还没有执行结束的时候,程序就希望再设置该按键为禁用状态,势必会引起冲突。实际上,这种操作在任何 GUI 系统都会触发异常。
Java 语言本身就提供了多线程机制,这种机制对 GUI 编程来说是不利的,它不能保证图形构件操作的同步与串行化。SWT 采用了一种简单而直接的方式去适应本地 GUI 系统对线程的要求:在 SWT 中,通常存在一个被称为"用户线程"的唯一线程,只有在这个线程中才能调用对构件或某些图形 API 的访问操作。如果在非用户线程中程序直接调用这些访问操作,那么 SWTExcepiton 异常会被抛出。但是 SWT 也在 *.widget.Display 类中提供了两个方法可以间接的在非用户线程的进行图形构件的访问操作,这是通过的 syncExec(Runnable) 和 asyncExec(Runnable) 这两个方法去实现的。例如:
// 此时程序运行在一个非用户线程中,并且希望在构件 panel 上加入一个按键。
Display.getCurrent().asyncExec(new Runnable() {
public void run() {
Button butt = new Button(panel,SWT.PUSH);
butt.setText("Push");
}
}); |
方法 syncExec() 和 asyncExec() 的区别在于前者要在指定的线程执行结束后才返回,而后者则无论指定的线程是否执行都会立即返回到当前线程。
JFace 与 SWT 的关系好比 Microsoft 的 MFC 与 SDK 的关系,JFace 是基于 SWT 开发,其 API 比 SWT 更加易于使用,但功能却没 SWT 来的直接。比如下面的代码应用 JFace 中的 MessageDialog 打开一个警告对话框:
MessageDialog.openWarning(parent,"
Warning","
Warning message");
如果只用 SWT 完成以上功能,语句不会少于 30 行 !
JFace 原本是为更加方便的使用 SWT 而编写的一组 API,其主要目的是为了开发 Eclipse IDE 环境,而不是为了应用到其它的独立应用程序。因此在 Eclipse 2.1 版本之前,很难将 JFace API 完整的从 Eclipse 的内核 API 中剥离出来,总是要多多少少导入一些非 JFace 以外的 Eclipse 核心代码类或接口才能得到一个没有任何编译错误的 JFace 开发包。但目前 Eclipse 组织似乎已经逐渐意识到了 JFace 在开发独立应用程序起到的重要作用,在正在开发的 2.1 版本中,JFace 也开始变成了和 SWT 一样的完整独立的开发包,只是这个开发包还在变动中 ( 笔者写本文时,应用的 Eclipse2.1M3 版本 )。JFace 开发包的包前缀是以 org.eclipse.jface 开头。JAR 包和源代码也和 SWT 一样,也在 $ {你的 eclipse 安装路径} \plugins 路径下去找。
对开发人员来说,在开发一个图形构件的时候,比较好的方式是先到 JFace 包去找一找,看是不是有更简洁的实现方法,如果没有再用 SWT 包去自己实现。比如 JFace 为对话框提供了很好的支持,除了各种类型的对话框 ( 比如上面用的 MessageDialog,或是带有 Title 栏的对话框 ),如要实现一个自定义的对话框也最好从 JFace 中的 Dialog 类继承,而不是从 SWT 中的 *.widget.Dialog 继承。
应用 JFace 中的 Preference 包中的类很容易为自己的软件做出一个很专业的配置对话框。对于 Tree、Table 等图形构件,它们在显示的同时也要和数据关联,例如 Table 中显示的数据,在 JFace 中的 View 包中为此类构件提供了 Model-View 方式的编程方法,这种方法使显示与数据分开,更加利于开发与维护。JFace 中提供最多的功能就是对文本内容的处理。可以在 org.eclipse.jface.text.* 包中找到数十个与文本处理相关类。
Java 程序通常是以 class 文件的方式发布的,运行 class 需要 JRE 或 JDK 的支持。这又是 Java GUI 程序的另一个致命的弱点,想象对一个面向广大用户的应用程序来说,无论你的程序功能有多简单,或是你的代码十分的精简,你都不得不让用户去下载一个 7、8M 的 JRE,那是多么令人沮丧的一件事。而且对程序员来说,Class 通常意味着源代码的暴露,反编译的工具让那些居心叵测的人轻易得到你的源代码。虽然有很多对 Class 的加密方法,但那总是以牺牲性能为代价的。好在我们还有其它的方式可用:把 Class 编译成 exe 文件 !
参考资料 3 提供了应用 GCJ 将 SWT 编译成 exe 文件的详细方法。作为对那篇文章的补充,就是目前也可以通过下载 Cygwin 得到 Windows 下的 GCJ。 Excelsior 也是笔者曾经用过的一款很好的编译器,唯一的遗憾就是它的价格 !
通过 SWT 开发包,简单、跨平台、可靠等这些 Java 语言本身所具有的优点正渐渐融合到图形界面的应用程序开发中去。因此,我相信 Java 语言的另一扇成功之门正在逐渐打开。
-
http://www.e2one.com极一软件工作室应用 SWT&JFace API 开发出的企业实时通讯 IM 软件,是 SWT&JFace 开发 Standalone 程序的范例。
-
http://www.eclipse.org/articles/swt-design-2/swt-design-2.html这篇文章中详细论述了 SWT 开发者应该如何管理系统资源。
-
http://www.ibm.com/developerworks/cn/linux/guitoolkit/j-nativegui/index.shtml描述了如何将 SWT 程序应用 GCJ 编译成本机应用程序文件。
-
http://www.ibm.com/developerworks/cn/java/l-eclipse/index.shtml介绍了 Eclipse 的使用及简单的插件开发过程。
-
www.eclipse.org所有的关于 Eclipse, SWT, JFace 的问题都可以在这里找到答案。也是下载 Eclipse 开发环境的网站。
-
http://www.cygwin.com/可以得得到 Windows 版本的 GCJ。
倪大鹏,有多年的软件开发经验,其中近四年的时间集中在Java相关技术的研究。可以通过e-mail: ndp@e2one.com与他联系。