13: 创建窗口与Applet

来源:互联网 发布:深度新闻 知乎 编辑:程序博客网 时间:2024/04/28 14:47

14: 创建窗口与Applet
设计的宗旨是"能轻松完成简单的任务,有办法完成复杂的任务"。[74]

最初设计Java 1.0的GUI类库时,Sun的目标是,要让程序员能在所有平台下都能做出非常漂亮的界面。但是这个目标没能实现。相反用Java 1.0的Abstract Window Toolkit(AWT) 作出来的GUI,在所有平台上都显得那么平庸。而且它的功能也非常有限;只能用四种字体,不能访问操作系统所提供的更复杂的GUI元素。此外Java 1.0 AWT用的是非常笨拙的,非面向对象的编程模式。有一次,我课上的一个学员(在Java初创时期,他曾在Sun干过)向我解释了原委:AWT从构思到设计直至最后实现总共只花了一个月。效率虽然很高,但做出来的东西却成了证明设计重要性的反面教员了。

Java 1.1 AWT的事件模型让情况有了些改观。它用了思路更为清晰的面向对象的解决方案,此外还引入了JavaBean。JavaBean是一种为可视编程环境服务的(visual programming environment)组件编程模式。最终Java 2(JDK 1.2)的Java Foundation Classes (JFC)全面替换了Java 1.0 AWT,并完成了这次转型。JFC的GUI部分被称为"Swing"。这是一组易学易用的,能拖放的(当然也可以手写代码)的JavaBean,用它创建的GUI还是能令人满意的。看来软件业的"第三版"定律(只有到第三版,产品才会成熟)也适用于编程语言。

本章只介绍Java 2的Swing类库,并且合理假定Swing是Java GUI类库的发展方向。[75]如果出于某种原因,你需要使用"旧的"AWT(或许你要维护旧代码,或者浏览器有限制),可以到www.BruceEckel.com去下载本书第一版(CD-ROM里也有)。注意,Java还保留了一些AWT的组件,有时你还不得不用它们。

本章的开头部分会讲,用Swing创建applet与创建应用程序有什么不同,以及怎样创建一个既能当applet在浏览器里运行,又能当普通的应用程序,在命令行下运行程序。书里的绝大部分GUI程序都能这样运行。

请注意,本书既不是Swing组件的词汇表,也不准备事无巨细地去讲解这些类和它们的方法。因此这里有意地做了简化。Swing类库的体系庞大,而本章的目的也只是想让你从基础开始理解并且熟悉这些概念。如果你有更高的要求,只要肯花精力研究,Swing大概都能做到。

我假定你已经从java.sun.com下载并安装了JDK类库的HTML文档,并且知道能在javax.swing的类里找Swing类库的详细资料。Swing的结构很简单,因此通常情况下,这些资料已经足以解决问题了。此外还有数不胜数的(同时也是巨厚无比的)Swing方面的专著。如果你想了解得更深,或者要修改Swing的缺省行为,可以去读这种书。

你越了解Swing,你就越能体会到:

与其它语言或开发环境相比,Swing是一个好得多的编程模型。而JavaBeans (本章的临近结尾的地方会作介绍)则是专为这个构架服务的类库。
就整个Java开发环境来讲,"GUI builders" (可视化编程环境)只是一个"社交"的层面。当你用图形工具把组件放到窗体上的时候,实际上是GUI builder在调用JavaBeans和Swing为你编写代码。这样不仅能可以加快GUI的开发速度,而且能让你做更多实验。这样你就可以尝试更多的方案,得到更好的效果了。
Swing的简单易用与设计合理,使你即便用GUI builder也能得到可读性颇佳的代码。这一点解决了GUI builder的一个老问题,那就是代码的可读性。
Swing囊括了所有比较时髦的用户界面元素:从带图像的按钮到树型控件和表格控件,应有尽有。考虑到类库的规模,其复杂性还是比较理想的。如果要做的东西比较简单,代码就不会很多,如果项目很复杂,那么代码就会相应地变得复杂。也就是说入门很容易,但是如果有必要,它也可以变得很强大。

对Swing的好感,很大程度上源于其"使用的正交性"。也就是说,一旦领会了这个类库的精神,你就可以把这种概念应用到任何地方。这一点首先就缘于其标准的命名规范。写本章的例程的时候,我常常靠猜来写方法的名字,而且经常一猜即中,这样就能省去翻文档了。很显然,这是一个设计良好的类库所应具备的特点。此外,通常情况下,如果想把组件嵌套到其它组件里,直接插就行了。

为了照顾运行速度,组件都是"轻量级"的。为了能跨平台,Swing是完全用Java写的。

键盘支持是内置的;运行Swing应用的时候可以完全不用鼠标,这一点,不需要额外的编程。加滚动条,很容易,只要把控件直接嵌到JScrollPane里就行了。加Tooltip也只要一行代码。

Swing还提供一种更前卫的,被称为"pluggable look and feel(可插接式外观)"的功能,也就是说用户界面的外观可以根据操作系统或用户的习惯动态地改变。你甚至可以自己发明一套外观(当然是很难的)。

基本的applet
Java能创建applet,也就是一种能在Web浏览器里运行的小程序。Applet必须安全,所以它的功能很有限。但是applet是一种很强大的客户端编程工具,而后者是Web开发的一个大课题。

Applet的限制
applet编程的限制是很多的,所以也经常被称作关在"沙箱"里。因为时时刻刻都有一个人——也就是Java的运行时安全系统——在监视着你。

不过你也可以走出沙箱,编写普通的应用程序而不是applet,这样就可以访问操作系统的其它功能了。迄今为止,我们写的都是这种应用程序,只不过它们都是些没有图形界面的控制台程序罢了。Swing也可以写GUI界面的应用程序。

大体上说,要想知道applet能做什么,最好先去了解一下,为什么要有applet:用它来扩展浏览器里的Web页面的功能。因为上网的人是不可能真正知道这个页面是不是来自于一个无恶意的网站的,但是你又必须确保所有运行的代码都是安全的。所以你会发现,它的最大的限制是:

Applet不能访问本地磁盘。也就是说不能读写,因为你总不会希望applet能不经同意就读取你的私人信息,然后通过Internet把它传出去吧。当然写也要禁掉,否则就是开门欢迎病毒了。Java为applet提供了数字签名。你可以选择让有数字签名(由可信的开发商签署)的applet访问你的硬盘,这样applet的很多限制就被解除了。本章的后面在讲Java Web Start的时候,会介绍一个这样的例子的。Web Start是一种通过Internet将应用程序安全地送到客户端的方案。
Applet的启动时间很长,因为每次都得下载所有东西,而且每下载一个类都要向服务器发一个请求。或许浏览器会作缓存,但这并不是一定的。所以一定要把applet的全部组件打成一个JAR的(Java ARchive)卷宗(除了.class文件之外,还包括图像,声音),这样只要发一个请求就可以下载整个applet了。JAR卷宗里的东西可以逐项地"数字签名"。
Applet的优势
如果你不在意这些限制,applet还是有明显优势的,特别是在构建client/server 或是其他网络应用的时候:

没有安装的问题。Applet是真正平台无关的(包括播放音频文件) ,所以你用不着去为不同的平台修改程序,用户也用不着安装完了之后再作调整。实际上每次载入有applet的Web页面时,安装就自动完成了。因此软件的更新可以不惊动客户自动地完成。为传统的client/server系统构建和安装一个新版的软件,通常都是一场恶梦。
由于Java语言和applet内置了安全机制,因此你不用担心错误代码会破坏别人的机器。有了这两个优势,Java就能在intranet的client/server应用里大展身手了。所谓intranet的client/server应用,是指仅存在于公司内部的,或者可以限定和控制用户环境的(Web浏览器和插件)特殊场合的client/server应用。
由于applet是自动集成到HTML里面的,因此你就有了一种与平台无关的,能支持applet的文档系统了(译者注:指HTML)。这真是太有趣了,因为我们通常都认为文档是程序的一部分,而不是相反。

应用框架
类库通常按功能进行分类。有些类库是拿来直接用的,比如Java标准类库里面的String和ArrayList。有些类库则是用来创建其它类的。此外还有一种被称为应用框架(application framework)的类库。它的目的是,提供一个或一组具备某些基本功能的类,帮助程序员创建应用程序。而这些基本功能,是这类应用程序所必备的。于是你写应用程序的时候,只要继承这个类,然后再根据需要,覆写几个你感兴趣的方法,定制一下它的行为就可以了。应用框架的默认控制机制会在适当的时机,调用那些你写的方法。应用框架是一种"将会变和不会变的东西分开来"的绝好的例子。它的设计思想是,通过覆写方法把程序的个性化部分留在本地。[76]

Applet是用应用框架创建的。你只要继承JApplet类,再覆写几个方法就可以了。下面几个方法可以控制Web页面上的applet的创建和执行:

方法 操作
init( ) applet初始化的时候会自动调用,其任务包括装载组件的布局。必须覆写。
start( ) 在Web浏览器上显示applet的时候调用。显示完毕之后,applet才开始正常工作,(特别是那些用stop( )关闭的applet)。(此外,应用框架)调用完init( )之后也会调用这个方法。
stop( ) 让applet从Web浏览器上消失的时候调用,这样它就能关闭一些很耗资源的操作了。此外(应用框架调用)destroy( )之前也会先调用这个方法。
destroy( ) 当(浏览器)不再需要这个applet了,要把它从页面里卸载下来的时候,就会调用这个方法以释放资源了。

知道这些之后,你就可以写一个简单的applet了:

//: c14:Applet1.java
// Very simple applet.
import javax.swing.*;
import java.awt.*;
public class Applet1 extends JApplet {
  public void init() {
    getContentPane().add(new JLabel("Applet!"));
  }
} ///:~
注意applet不需要main()。它已经包括在应用框架里了;你只要把启动代码放到init( )里面就行了。

这个程序只做了一件事情,就是用JLabel将一个文本标签放到applet里面(AWT已经把Label和其他组件的名字给占了,所以Swing的 组件通常会以"J"打头的)。JLabel的构造函数需要一个String作参数。在上面这个程序里,标签是放在表单上的。

init( )方法负责将组件add( )到表单上。或许你会觉得,应该能直接调用它自己(JApplet)的add( )方法。实际上AWT就是这么做的。Swing要求你将所有的组件都加到表单的"内容面板(content pane)"上,所以add( )的时候,必须先调用getContentPane( )。

在Web浏览器里运行applet
要想运行程序,先得把applet放到Web页面里,然后用一个能运行Java的Web浏览器打开页面。你得用一种特殊的标记[77]把applet放到Web页面里,然后让它来告诉页面该怎样装载并运行这个applet。

这个步骤曾经非常简单。那时Java自己就很简单,所有人都用同一个Java,浏览器里的Java也都一样。所以你只要稍微改一下HTML就行了,就像这样:

<applet code=Applet1 width=100 height=50>
</applet>
但是随后而来的浏览器和语言大战使我们(不仅是程序员,还包括最终用户)都成了输家。不久,Sun发现再也不能指望靠浏览器来支持风味醇正的Java了,唯一的解决方案是利用浏览器的扩展机制来提供"插件(add-on)"。通过这个办法(要想禁掉Java,除非把所有第三方的插件全都禁掉,但是为了获取竞争优势,没有一个浏览器的提供商会这么做的),Sun确保了Java不会被敌对的浏览器提供商给禁掉。

对于Internet Explorer,这种扩展机制就是ActiveX的控件,而Netscape的则是plugin。你做页面时必须为这两个浏览器各写一套标记,不过JDK自带了一个能自动生成标记的HTMLconverter工具。下面就是用HTMLconverter处理过的Applet1的HTML页面:

<!--"CONVERTED_APPLET"-->
<!-- HTML CONVERTER -->
<OBJECT
    classid = "clsid:CAFEEFAC-0014-0001-0000-ABCDEFFEDCBA"
    codebase = "http://java.sun.com/products/plugin/autodl/jinstall-1_4_1-windows-i586.cab#Version=1,4,1,0"
    WIDTH = 100 HEIGHT = 50 >
    <PARAM NAME = CODE VALUE = Applet1 >
    <PARAM NAME = "type" VALUE = "application/x-java-applet;jpi-version=1.4.1">
    <PARAM NAME = "scriptable" VALUE = "false">
    <COMMENT>
      <EMBED
          type = "application/x-java-applet;jpi-version=1.4.1"
          CODE = Applet1
          WIDTH = 100
          HEIGHT = 50 
          scriptable = false
          pluginspage = "http://java.sun.com/products/plugin/index.html#download">
          <NOEMBED>
          </NOEMBED>
      </EMBED>
    </COMMENT>
</OBJECT>
<!--
<APPLET CODE = Applet1 WIDTH = 100 HEIGHT = 50>
</APPLET>
-->
<!--"END_CONVERTED_APPLET"-->
 
有几行实在太长了,只能折起来,否则就放不下了。不过源代码里的程序(可以从www.BruceEckel.com下载)都能正常运行,不用担心换行的问题。

code的值是applet所处的.class文件的名字,width和height则表示applet的初始尺寸(和前面一样,以象素为单位)。此外applet标记里面还可以放一些东西:到哪里去找.class文件(codebase),怎样对齐(align),applet相互通讯的时候要用的标识符(name),以及提供给applet的参数。参数的格式是这样的:

<param name="identifier" value = "information">
你可以根据需要,加任意多个参数。

本书源代码(www.BruceEckel.com提供免费下载)为每个applet准备了一个HTML页面,由此也提供了很多怎样使用applet标记的范例。这些页面是由本章的index.html控制的。此外你可以参考java.sun.com ,上面有怎样往网页里插applet的最新和最全的资料。

Appletviewer的用法
Sun的JDK包含一个名为Appletviewer的工具,它可以根据<applet>标记在HTML文件里找出applet,然后不显示HTML文本,直接运行这个applet。由于Appletviewer忽略了除applet标记之外的所有其他东西,因此你可以直接把applet标记当作注释放到Java的源文件里:

// <applet code=MyApplet width=200 height=100></applet>
这样你就可以用"appletviewer MyApplet.java"来启动applet了,不用再写HTML的测试文件了。比方说,只要在Applet1.java里加上这样几个的HTML标记:

//: c14:Applet1b.java
// Embedding the applet tag for Appletviewer.
// <applet code=Applet1b width=100 height=50></applet>
import javax.swing.*;
import java.awt.*;
public class Applet1b extends JApplet {
  public void init() {
    getContentPane().add(new JLabel("Applet!"));
  }
} ///:~
你就可以用下面这条命令来启动applet了

appletviewer Applet1b.java
本书会用这种简易方法来测试applet。此外,你马上就会看到另外一种方法。它可以不用Appletviewer,直接用命令行启动applet。

Applet的测试
如果只是想简单的测试一下,那么根本不用上网。直接启动Web浏览器,打开含applet标记的HTML文件就可以了。浏览器装载HTML的时候会发现applet标记,并且按照code的值去找相应的.class文件。当然,是在CLASSPATH里面找。如果CLASSPATH里没有,它就在浏览器的状态栏里给一个错误信息,告诉你找不到.class文件。

如果要在Web网站上测试applet,问题就有些复杂了。首先,你得先有一个网站。对绝大多数人来讲,网站就是第三方的ISP(Internet服务提供商)机器上的某个目录。由于applet只是几个文件,因此ISP不会提供特殊支持,所以你得自己想办法把HTML文件和.class文件传到指定的ISP的机器上的目录里。这一步通常是由FTP完成的。好在FTP的工具很多,有的是freeware,有的是shareware。由此看来,似乎在网站上测试applet,就是用FTP把文件传到ISP的机器上,然后再连到网站,用浏览器打开页面,等到applet跳出来就大功告成了。真的是这样吗?

这里就是会让你栽跟头的地方了。如果客户端的浏览器不能在服务器上找到.class文件,它会到客户机的CLASSPATH里面去找。这样一来,就有可能发生,浏览器在服务器上找不到.class文件,因此用你机器上的文件启动了applet的情形了。于是在你看来一切都很正常,而其他人则什么都看不见了。所以测试之前,一定要把本地的.class文件(或.jar文件)全部删了,只有这样才能知道程序是不是呆在正确的服务器目录里。

我就曾经在这里栽过一个很冤枉的跟头。有一次,我上载HTML和applet的时候搞错了package的名字,因此把applet放错了目录。但是我的浏览器还能在本地的CLASSPATH里找到程序,所以我成了唯一一个能正常运行这个applet的人了。所以给applet标记的CODE参数赋值的时候,一定要给全名,这一点非常重要。很多公开发表的applet都没有打包,但是实际工作中,最好还是打个包。

用命令行启动applet
有时你还会希望让GUI程序能做一些别的事情。比方说在保留Java引以为豪的跨平台性的前提下,让它做一些"普通"程序能作的事。在本书的前面章节中,我们一直在写命令行应用程序,但是有些操作系统(比如Macintosh)是没有命令行的。所以我们会有无数条理由,要用Java来写一些非applet的GUI程序。这当然是一种很合理的需求。

你可以用Swing类库写出与当前操作系统的外观风格完全一致的应用程序。如果你要编写GUI程序,先看看是不是能最新版的Java及与之相关的工具,只有是,那才值得去写。[78]因为只有这样,才能写出不会让用户觉得讨厌的程序。如果出于某种原因,你不得不用旧版的Java来写一个重要得GUI程序,那么在接收任务之前,先仔细地考虑一下。

你肯定会希望能编写出既能在命令行下启动,又能当applet运行的程序。这样测试applet的时候,会非常方便。因为通常从命令行启动要比从Web浏览器和Appletviewer启动更快,也更方便。

要想创建能用命令行启动的applet,只要在类里加一个main( ),让它把这个applet的实例嵌到JFrame里面就行了。[79]我们就以Applet1b.java为例,看看该怎样修改才能让它既能当应用程序,又能当applet运行。

//: c14:Applet1c.java
// An application and an applet.
// <applet code=Applet1c width=100 height=50></applet>
import javax.swing.*;
import java.awt.*;
public class Applet1c extends JApplet {
  public void init() {
    getContentPane().add(new JLabel("Applet!"));
  }
  // A main() for the application:
  public static void main(String[] args) {
    JApplet applet = new Applet1c();
    JFrame frame = new JFrame("Applet1c");
    // To close the application:
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.getContentPane().add(applet);
    frame.setSize(100,50);
    applet.init();
    applet.start();
    frame.setVisible(true);
  }
} ///:~
我们只加了一个main( ),其它什么都没动。main( )创建了applet的实例,并把它加到JFrame里面,这样我们就能看到它了。

你会看到main( )明确地调用了applet的init( )和start( )。这是因为,这两步原先是交给浏览器做的。当然这并不能完全替代浏览器,因为前者还会调用stop( )和destroy( ),不过大致说来,这个办法还是可行的。如果还有问题,你可以亲自去调用。[80]

注意,要是没有这行:

frame.setVisible(true);
那么你什么都看不见。

一个专门用来显示Applet的框架
虽然将applet转换成应用程序的代码是非常有用的,但写多了就既浪费纸张又让人生厌了。所以接下来我们会用下面这个框架来显示Swing的例子。

//: com:bruceeckel:swing:Console.java
// Tool for running Swing demos from the
// console, both applets and JFrames.
package com.bruceeckel.swing;
import javax.swing.*;
import java.awt.event.*;
public class Console {
  // Create a title string from the class name:
  public static String title(Object o) {
    String t = o.getClass().toString();
    // Remove the word "class":
    if(t.indexOf("class") != -1)
      t = t.substring(6);
    return t;
  }
  public static void
  run(JFrame frame, int width, int height) {
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setSize(width, height);
    frame.setVisible(true);
  }
  public static void
  run(JApplet applet, int width, int height) {
    JFrame frame = new JFrame(title(applet));
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.getContentPane().add(applet);
    frame.setSize(width, height);
    applet.init();
    applet.start();
    frame.setVisible(true);
  }
  public static void
  run(JPanel panel, int width, int height) {
    JFrame frame = new JFrame(title(panel));
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.getContentPane().add(panel);
    frame.setSize(width, height);
    frame.setVisible(true);
  }
} ///:~
这是一个人人都用得着的工具,所以我把它收进了com.bruceeckel.swing。Console是一个完全由static方法所组成的类。第一个方法负责提取对象所属的类的名字(利用RTTI)。由于getClass( )通常会在类的名字后面加上个"class",所以这里用indexOf( )来判断名字里是不是有"class"。如果有,就先用substring( )把它剥掉,然后再返回这个字符串。这个名字会出现在run( )所显示的窗口的标题栏上。

setDefaultCloseOperation( )的作用是,告诉程序,一旦JFrame被关掉了,程序也应该退出了。默认情况下关掉JFrame并不意味着退出程序,所以除非你调用setDefaultCloseOperation( )或者写一些类似的代码,否则程序是不会中止的。

run( )分别为JApplet,JPanel和JFrame作了三次重载。注意只有对JApplet才需要调用init( )和start( )。

现在只要为applet创建一个main( ),然后把下边这行加进去,就可以在控制台上运行了:

Console.run(new MyClass(), 500, 300);
最后这两个参数分别表示宽和高。下面我们用Console来修改Applet1c.java:

//: c14:Applet1d.java
// Console runs applets from the command line.
// <applet code=Applet1d width=100 height=50></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Applet1d extends JApplet {
  public void init() {
    getContentPane().add(new JLabel("Applet!"));
  }
  public static void main(String[] args) {
    Console.run(new Applet1d(), 100, 50);
  }
} ///:~
这样就不用再去敲同样的代码了。

制作按钮
做按钮很简单:想让按钮显示什么,就拿它作参数去调用JButton的构造函数。下面我们会做一些更精彩的东西,比如在按钮上放个图标什么的。

通常按钮是类的一个字段,这样就能用它来表示按钮了。

JButton是一个组件——它有自己的小窗口——更新的时候会自动刷新。也就是说,你不用明确地指示该如何显示按钮或是其他什么控件;只要把它们放到表单上,它们就能自己把自己给画出来了。所以你得在init( )把按钮放到表单上:

//: c14:Button1.java
// Putting buttons on an applet.
// <applet code=Button1 width=200 height=50></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Button1 extends JApplet {
  private JButton
    b1 = new JButton("Button 1"),
    b2 = new JButton("Button 2");
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(b1);
    cp.add(b2);
  }
  public static void main(String[] args) {
    Console.run(new Button1(), 200, 50);
  }
} ///:~
这里有点新的内容:在往content pane里面加东西之前,我们先把它的"布局管理器(layout manager)"设成FlowLayout。布局管理器的作用是告诉面板将控件放在表单上的哪个位置。这里我们不能用applet默认的BorderLayout,这是因为(本章后面在讲表单的布局的时会做详细的解释)如果用它,一旦你往表单上加了新的控件,旧控件就会被全部遮住了。而FlowLayout则会让控件均匀地散布在表单上,从左到右,从上到下。

捕捉事件
如果你编译运行上面那个applet的时候,你会发现,点完按钮什么也没有发生。这就是你的活了。你得写代码告诉它该做些什么。而编写事件驱动的程序是GUI编程的一个重要重要组成部分,而驱动程序的基本思路就是,将事件与代码联系起来。

Swing的思路是,将接口(图形组件)和实现(当组件发生了某个事件之后,你要运行的代码)明明白白地分开来。Swing组件能通报在它身上可以发生什么事件,以及发生了什么事件。所以,如果你对某个事件不感兴趣,比如鼠标从按钮的上方经过,你完全可以不去理会这个事件。用这种方法能非常简洁优雅地解决事件驱动的问题,一旦你理解了其基本概念,你甚至可以去直接使用过去从未看到过的Swing组件。实际上这个模型也适用于JavaBean(本章后面会再作探讨)。

我们还是先来关心一下我们要用的这个控件的主要事件。就这个例子而言,这个控件JButton,而"我们感兴趣的事件"就是按下按钮。要想表示你对按钮被按下感兴趣,可以调用addActionListener( )。这个方法需要一个ActionListener参数。ActionListener是一个接口,它只有一个方法,actionPerformed( )。所以要想让JButton执行任务,你就得实现先定制一个ActionListener,然后用addActionListener方法把这个类的实例注册给JButton。当按钮被按下的时候,这个方法就会启动了。(这通常被称作回调(callback))

那么按下按钮之后,它又该做些什么呢?我们希望能在屏幕上看到变化,所以这里引入一个新的Swing组件:JTextField。这个组件可以显示文本。它的文本既可以是用户输入的,也可以是程序插的。虽然创建JTextField的方法有很多,但是最简单的还是把宽度直接传给JTextField的构造函数。等到JTextField被放上了表单,你就可以用setText( )来修改内容了(JTextField还有很多方法,请参阅JDK文档)。下面就是修改后的程序:

//: c14:Button2.java
// Responding to button presses.
// <applet code=Button2 width=200 height=75></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Button2 extends JApplet {
  private JButton
    b1 = new JButton("Button 1"),
    b2 = new JButton("Button 2");
  private JTextField txt = new JTextField(10);
  class ButtonListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String name = ((JButton)e.getSource()).getText();
      txt.setText(name);
    }
  }
  private ButtonListener bl = new ButtonListener();
  public void init() {
    b1.addActionListener(bl);
    b2.addActionListener(bl);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(b1);
    cp.add(b2);
    cp.add(txt);
  }
  public static void main(String[] args) {
    Console.run(new Button2(), 200, 75);
  }
} ///:~
JTextField的创建和摆放的步骤与JButton或其它Swing组件的完全相同。不同之处在于,这段程序创建了一个实现ActionListener的ButtonListener类。actionPerformed( )需要一个ActionEvent参数,它的作用是提供事件的具体信息和来源。这里我们想让它显示你按了哪个按钮。我用getSource( )获取按钮上的文字,然后把它放到JTextField里面,这样就能证明你点击按钮的时候代码真的是被调用了。

我们在init( )里面用addActionListener( )为这两个按钮注册了ButtonListener对象。

考虑到每个listener通常只需要一个实例,用匿名内部类来实现ActionListener会更方便。下面我们就用匿名内部类来改写Button2.java

//: c14:Button2b.java
// Using anonymous inner classes.
// <applet code=Button2b width=200 height=75></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Button2b extends JApplet {
  private JButton
    b1 = new JButton("Button 1"),
    b2 = new JButton("Button 2");
  private JTextField txt = new JTextField(10);
  private ActionListener bl = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      String name = ((JButton)e.getSource()).getText();
      txt.setText(name);
    }
  };
  public void init() {
    b1.addActionListener(bl);
    b2.addActionListener(bl);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(b1);
    cp.add(b2);
    cp.add(txt);
  }
  public static void main(String[] args) {
    Console.run(new Button2b(), 200, 75);
  }
} ///:~
本书会优先使用匿名内部类的办法(除非不得已)。

JTextArea
与JTextField相比,JTextArea能容纳多行文字,提供更多的功能,除此之外它们很相似。append( )方法很有用;你可以用它把程序的输出直接导到JTextArea里,这样你就能用Swing程序来改进我们先前写的,往标准输出上打印的命令行程序了(因为你能用滚动条看到前面的输出了)。下面这段程序用到了我们在第十一章讲的geography生成器,我们用它的输出来填充JTextArea:

//: c14:TextArea.java
// Using the JTextArea control.
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import java.util.*;
import com.bruceeckel.swing.*;
import com.bruceeckel.util.*;
public class TextArea extends JFrame {
  private JButton
    b = new JButton("Add Data"),
    c = new JButton("Clear Data");
  private JTextArea t = new JTextArea(20, 40);
  private Map m = new HashMap();
  public TextArea() {
    // Use up all the data:
    Collections2.fill(m, Collections2.geography,
      CountryCapitals.pairs.length);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        Iterator it = m.entrySet().iterator();
        while(it.hasNext()) {
          Map.Entry me = (Map.Entry)(it.next());
          t.append(me.getKey() + ": "+ me.getValue()+"/n");
        }
      }
    });
    c.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        t.setText("");
      }
    });
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(new JScrollPane(t));
    cp.add(b);
    cp.add(c);
  }
  public static void main(String[] args) {
    Console.run(new TextArea(), 475, 425);
  }
} ///:~
由于要读取本地磁盘,因此这个类应该是一个JFrame而不是JApplet,所以它就不能作为applet在HTML页面里运行了。

我们在init( )里面把国家和首都装进Map。注意这两个按钮,在创建和添加ActionListener的过程中,我们没有使用中间变量。这是因为程序的其它部分根本用不着这两个监听器(listener)。"Add Data"按钮负责数据的排列和添加,而"Clear Data"按钮则则负责用setText( )方法,把JTextArea里的文字清空。

在把JTextArea加入applet的时候,我们先把它嵌入JScrollPane,这样一旦文本多了出来,滚动条就会自动显现了。装滚动条就这么简单。相比其它GUI编程环境下的类似控件,JScrollPane的简洁与优雅真是令人折服。

控制布局
用Java把组件放到表单上的方法可能与你见过的其他GUI系统的都不同。首先,它全部都是代码,根本没有"资源"这个故事。其次,组件在表单上的位置不是由绝对定位来控制的,而是由一个被称为"布局管理器"的对象来控制的。布局管理器的作用是根据组件add( )的顺序,决定其摆放的位置。组件的大小,形状,摆放的位置会随布局管理器的不同而有明显的差别。此外布局管理器会根据applet或应用程序的窗口大小,自动调整组件的位置,这样一旦窗口的大小改变了,组件的尺寸,形状,摆放位置也会相应的作出调整。

JApplet,JFrame ,JWindow以及JDialog,这几个组件都有一个getContentPane( )方法。这个方法能返回一个Container,而这个Container的作用是安置(contain)和显示Component。Container有一个setLayout( )方法,你就是用它来选择布局管理器的。其它类,比如JPanel,能直接安置和显示组件,所以你得跳过内容面板(content pane)直接设置它的布局管理器。

这里,我们会用往面板上加按钮的方法来考察各种布局管理器(这么做最简单)。我们没写事件处理程序,因为这里的程序只是用来显示按钮的摆放位置的。

BorderLayout
Applet的默认布局管理器是BorderLayout (前几个例子里,我们把它改成了FlowLayout)。如果没有其它指令,BorderLayout会把所有控件全都放到表单的正中,并且拉伸到最大。

好在BorderLayout的功能还不止这些。它还有四个边界以及中央的概念。当你往BorderLayout的面板上加控件加时,你还可以选择重载过的add( )方法。这时要给它一个常量。这个常量可以是下边五个中的一个:

BorderLayout. NORTH 顶边
BorderLayout. SOUTH 底边
BorderLayout. EAST 右边
BorderLayout. WEST 左边
BorderLayout.CENTER 填满当中,除非碰到其它组件或表单的边缘

如果你不指明摆放对象的区域,默认它就使用CENTER。

下面我们来举一个简单的例子。由于JApplet默认就使用BorderLayout,因此我们就不设布局了:

//: c14:BorderLayout1.java
// Demonstrates BorderLayout.
//<applet code=BorderLayout1 width=300 height=250></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class BorderLayout1 extends JApplet {
  public void init() {
    Container cp = getContentPane();
    cp.add(BorderLayout.NORTH, new JButton("North"));
    cp.add(BorderLayout.SOUTH, new JButton("South"));
    cp.add(BorderLayout.EAST, new JButton("East"));
    cp.add(BorderLayout.WEST, new JButton("West"));
    cp.add(BorderLayout.CENTER, new JButton("Center"));
  }
  public static void main(String[] args) {
    Console.run(new BorderLayout1(), 300, 250);
  }
} ///:~
除了CENTER,其它控件都会在一个方向上压缩到最小,在另一个方向上拉伸到最大。而CENTER则会往两个方向上拉伸,直至占满整个中间区域。

FlowLayout
它会让组件直接"流"到表单上,从左到右,从上到下,一行满了再换一行。

下面我们来举一个FlowLayout的例子。你会发现,用了FlowLayout之后,组件就会冻结在它的"固有"尺寸上。比如JButton就会根据字符串的长度来安排大小。

//: c14:FlowLayout1.java
// Demonstrates FlowLayout.
// <applet code=FlowLayout1 width=300 height=250></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class FlowLayout1 extends JApplet {
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < 20; i++)
      cp.add(new JButton("Button " + i));
  }
  public static void main(String[] args) {
    Console.run(new FlowLayout1(), 300, 250);
  }
} ///:~
由于FlowLayout面板上的控件会被自动地压缩到最小,因此时常会出现一些意外。比方说,JLabel是根据字符串来决定控件的大小的,所以当你对FlowLayout面板上的JLable控件的文本作右对齐时,显示效果不会有任何变化。

GridLayout
GridLayout的意思是,把表单视作组件的表格,当你往里加东西的时候,它会按从左到右,从上到下的顺序把组件放到格子里。创建布局管理器的时候,你得在构造函数里指明行和列的数目,这样它就能帮你把表单划分成相同大小的格子了。

//: c14:GridLayout1.java
// Demonstrates GridLayout.
// <applet code=GridLayout1 width=300 height=250></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class GridLayout1 extends JApplet {
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(7,3));
    for(int i = 0; i < 20; i++)
      cp.add(new JButton("Button " + i));
  }
  public static void main(String[] args) {
    Console.run(new GridLayout1(), 300, 250);
  }
} ///:~
这里我们创建了20个按钮但却准备了21个槽(slot)。由于GridLayout没有所谓的"平衡"机制,所以最后一个槽会空着。

GridBagLayout
GridBagLayout的作用是,当窗口的大小发生变化时,你能用它来准确地控制窗口各部分的反应。但与此同时,它也是最难和最复杂的布局管理器。它主要是供GUI builder生成代码用的(或许GUI builder用的不是绝对定位而是GridBagLayout)。如果你的设计非常复杂,以至于不得不用GridBagLayout,那么建议你使用GUI builder。如果你认为有必要理解其内部的细节,建议你去读Horstmann和Cornell的Core Java 2, Volume 1, (Prentice Hall, 2001),或者看Swing方面的专著。

绝对定位
还可以这样对图形组件进行绝对定位:

将Container的布局管理器设成null:setLayout(null)。
往面板上加组件的时候,先调用setBounds( )或reshape( )方法(根据各个版本)为组件设一个以象素为单位的矩形。这个步骤可以放在构造函数里,也可以放在paint( )里面,看你的需要。
有些GUI builder主要用的就是这个方法,但是通常情况下,这并不是最好的方法。

BoxLayout
由于GridBagLayout实在是太难学难用了,所以Swing引入了一个新的BoxLayout。它保留了GridBagLayout的很多优点,但是却没那么复杂。所以当你想手工控制控件的布局时,可以优先考虑用它(再重申一遍,如果设计非常复杂,最好还是用GUI builder)。BoxLayout能让你在垂直和水平两个方向上控制组件的摆放,它用一些被称为支柱(struts)和胶水(glue)的东西来控制组件间的距离。下面我们先用实验其他布局管理器的方法来试试它:

//: c14:BoxLayout1.java
// Vertical and horizontal BoxLayouts.
// <applet code=BoxLayout1 width=450 height=200></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class BoxLayout1 extends JApplet {
  public void init() {
    JPanel jpv = new JPanel();
    jpv.setLayout(new BoxLayout(jpv, BoxLayout.Y_AXIS));
    for(int i = 0; i < 5; i++)
      jpv.add(new JButton("jpv " + i));
    JPanel jph = new JPanel();
    jph.setLayout(new BoxLayout(jph, BoxLayout.X_AXIS));
    for(int i = 0; i < 5; i++)
      jph.add(new JButton("jph " + i));
    Container cp = getContentPane();
    cp.add(BorderLayout.EAST, jpv);
    cp.add(BorderLayout.SOUTH, jph);
  }
  public static void main(String[] args) {
    Console.run(new BoxLayout1(), 450, 200);
  }
} ///:~
BoxLayout的构造函数同其它布局管理器稍有些不同——它的第一个参数是这个BoxLayout将要控制的Container,第二个参数是布局的方向。

为了简化起鉴,Swing还提供了一种内置BoxLayout的特殊容器,Box(译者注:这里的容器是指java.awt.Container类)。Box有两个能创建水平和垂直对齐的box的static的方法,下面我们就用它来排列组件。

//: c14:Box1.java
// Vertical and horizontal BoxLayouts.
// <applet code=Box1 width=450 height=200></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Box1 extends JApplet {
  public void init() {
    Box bv = Box.createVerticalBox();
    for(int i = 0; i < 5; i++)
      bv.add(new JButton("bv " + i));
    Box bh = Box.createHorizontalBox();
    for(int i = 0; i < 5; i++)
      bh.add(new JButton("bh " + i));
    Container cp = getContentPane();
    cp.add(BorderLayout.EAST, bv);
    cp.add(BorderLayout.SOUTH, bh);
  }
  public static void main(String[] args) {
    Console.run(new Box1(), 450, 200);
  }
} ///:~
有了Box之后,再要往content pane里加组件的时候,你就可以把它成第二的参数了。

支柱(struts)会把组件隔开,它是大小是按象素算的。用的时候,只要把它当作空格加在两个组件的中间就行了。

//: c14:Box2.java
// Adding struts.
// <applet code=Box2 width=450 height=300></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Box2 extends JApplet {
  public void init() {
    Box bv = Box.createVerticalBox();
    for(int i = 0; i < 5; i++) {
      bv.add(new JButton("bv " + i));
      bv.add(Box.createVerticalStrut(i * 10));
    }
    Box bh = Box.createHorizontalBox();
    for(int i = 0; i < 5; i++) {
      bh.add(new JButton("bh " + i));
      bh.add(Box.createHorizontalStrut(i * 10));
    }
    Container cp = getContentPane();
    cp.add(BorderLayout.EAST, bv);
    cp.add(BorderLayout.SOUTH, bh);
  }
  public static void main(String[] args) {
    Console.run(new Box2(), 450, 300);
  }
} ///:~
与固定间距的struts不同,glue(胶水)会尽可能地将组件隔开。所以更准确的说,它应该是"弹簧(spring)"而不是"胶水"(再加上这种设计是基于被称为"springs and struts"的思想的,因此这个术语就显得有些奇怪了)。

//: c14:Box3.java
// Using Glue.
// <applet code=Box3 width=450 height=300></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Box3 extends JApplet {
  public void init() {
    Box bv = Box.createVerticalBox();
    bv.add(new JLabel("Hello"));
    bv.add(Box.createVerticalGlue());
    bv.add(new JLabel("Applet"));
    bv.add(Box.createVerticalGlue());
    bv.add(new JLabel("World"));
    Box bh = Box.createHorizontalBox();
    bh.add(new JLabel("Hello"));
    bh.add(Box.createHorizontalGlue());
    bh.add(new JLabel("Applet"));
    bh.add(Box.createHorizontalGlue());
    bh.add(new JLabel("World"));
    bv.add(Box.createVerticalGlue());
    bv.add(bh);
    bv.add(Box.createVerticalGlue());
    getContentPane().add(bv);
  }
  public static void main(String[] args) {
    Console.run(new Box3(), 450, 300);
  }
} ///:~
strut只控制一个方向上的距离,而rigid area(刚性区域)则在两个方向上固定组件间的距离。

//: c14:Box4.java
// Rigid areas are like pairs of struts.
// <applet code=Box4 width=450 height=300></applet>
import javax.swing.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class Box4 extends JApplet {
  public void init() {
    Box bv = Box.createVerticalBox();
    bv.add(new JButton("Top"));
    bv.add(Box.createRigidArea(new Dimension(120, 90)));
    bv.add(new JButton("Bottom"));
    Box bh = Box.createHorizontalBox();
    bh.add(new JButton("Left"));
    bh.add(Box.createRigidArea(new Dimension(160, 80)));
    bh.add(new JButton("Right"));
    bv.add(bh);
    getContentPane().add(bv);
  }
  public static void main(String[] args) {
    Console.run(new Box4(), 450, 300);
  }
} ///:~
应该告诉你,对rigid area的评价不是那么的统一。实际上它就是绝对定位,因此有些人认为,它根本就是多余的。

最佳方案?
Swing的功能非常强大,短短几行代码就能做很多事。本书的代码都很简单,而且为了教学目的,它们都是用手写的。实际上你完全可以把这几种布局结合起来,完成一些比较复杂的任务。但是,有些时候用手写代码创建GUI表单就不那么明智了;首先是太复杂,其次也不值得。Java和Swing的设计者们是想用语言和类库去支持GUI builder工具,然后让你用工具来简化编程。实际上只要知道布局是怎么一回事,以及事件该怎么处理(下面讲)就行了,懂不懂手写代码控制控件的摆放,其实并不重要。你完全可以选一个趁手的工具(毕竟Java的初衷就是想让你提高编程的效率)。

Swing的事件模型
在Swing的事件模型中,组件可以发起(或"射出")事件[译注1]。各种事件都是类。当有事件发生时,一个或多个"监听器(listener)"会得到通知,并做出反应。这样事件的来源就同它的处理程序分隔开来了。一般说来,程序员是不会去修改Swing组件的,他们写的都是些事件处理程序,当组件收到事件[译注1]时,会自动调用这些代码,因此Swing的事件模型可称得上是将接口与实现分隔开来的绝好范例了。

[译注1]这段话前面讲组件发出事件,后面讲组件收到事件。就我的理解,前一个事件是指组件在调用listener的方法时传给它的参数,后一个事件是指用户的操作。拿前面Button2.java举例,前一个指组件在调用ButtonListener的actionPerformed方法时传给它的ActionEvent e,后一个指用户按下按钮这个事件。

实际上事件监听器(event listener)就是一个实现listener接口的对象。所以,程序员要做的就是创建一个listener对象,然后向发起事件的组件注册这个对象。注册的过程就是调用组件的addXXXListener( )方法,这里"XXX"表示组件所发起的事件的类型。只要看一眼"addListener"方法的名字就能知道组件能处理哪种事件了,所以如果你让它听错了事件,那么编译就根本通不过。到后面你就会看到,JavaBean在决定它能处理哪些事件时,也遵循"addListener"的命名规范。

事务逻辑都应该封装成listener。创建listener的唯一的条件是,它必须实现接口。你完全可以创建一个"全局的listener(global listener)",但是内部类或许更合适。这么做不仅是因为要根据UI或事务逻辑对listener进行逻辑分组,更重要的是(你很快就会看到),要利用内部类可以引用宿主类对象的特性,这样就能跨越类或子系统的边界进行调用了。

我们前面的例子里已经涉及到Swing的事件模型了,下面我们把这个模型的细节补充完整。

事件与监听器种类
所有Swing组件都有addXXXListener( )和removeXXXListener( )方法,因此组件都能添加和删除监听器。你会发现,这里的"XXX"也正好是方法的参数,例如addMyListener(MyListener m)。下面这张表列出了基本的事件,监听器和方法,以及与之相对应的,提供了addXXXListener( )和removeXXXListener( )方法的组件。要知道,这个事件模型在设计时就强调了扩展性,因此你很可能会遇到这张表里没有讲到过的事件和监听器。

事件,listener接口以及add和remove方法 支持这一事件的组件
ActionEvent
ActionListener
addActionListener( )
removeActionListener( )
 JButton, JList, JTextField, JMenuItem 以及它们的派生类JCheckBoxMenuItem, JMenu,和JpopupMenu
AdjustmentEvent
AdjustmentListener
addAdjustmentListener( )
removeAdjustmentListener( )
 JScrollbar以及实现Adjustable接口的组件
ComponentEvent
ComponentListener
addComponentListener( )
removeComponentListener( )
 *Component及其派生类JButton, JCheckBox, JComboBox, Container, JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar, JTextArea,和JTextField
ContainerEvent
ContainerListener
addContainerListener( )
removeContainerListener( )
 Container及其派生类JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog,和JFrame 
FocusEvent
FocusListener
addFocusListener( )
removeFocusListener( )
 Component及其"派生类(derivatives*)"
KeyEvent
KeyListener
addKeyListener( )
removeKeyListener( )
 Component及其"派生类(derivatives*)"
MouseEvent(包括点击和移动)
MouseListener
addMouseListener( )
removeMouseListener( )
 Component及其"派生类(derivatives*)"
MouseEvent[81](包括点击和移动)
MouseMotionListener
addMouseMotionListener( )
removeMouseMotionListener( )
 Component及其"派生类(derivatives*)"
WindowEvent
WindowListener
addWindowListener( )
removeWindowListener( )
 Window及其派生类JDialog, JFileDialog,和JFrame 
ItemEvent
ItemListener
addItemListener( )
removeItemListener( )
 JCheckBox, JCheckBoxMenuItem, JComboBox, JList, 以及实现了ItemSelectableinterface的组件
TextEvent
TextListener
addTextListener( )
removeTextListener( )
 JTextComponent的派生类,包括JTextArea和JTextField 

你会发现一个组件只支持几个事件。所以要搞清楚所有组件的事件是一件非常困难的事情。有一个简单的办法,就是修改第10章的ShowMethods.java,让它告诉你组件都支持哪些事件。

第10章,我们介绍了reflection,并且给出了一个能显示类的方法的程序 —— 既可以是所有方法,也可以根据参数,列出其中的一部分。reflection的神奇之处在于,不用遍历类的继承体系,它就能得到类的所有方法。因此对编程来说,这是一个很有价值的,能节省时间的工具。由于绝大多数Java方法的名字都起得很直观,因此你可以选一个你感兴趣的词让它去找,找到之后再去查JDK文档。

但是第10章的时候,我们还没讲到Swing,因此我们写了个命令行的程序。这里我们要写一个更实用的GUI程序,只是它是专门用来找Swing组件的"addListener"方法的。

//: c14:ShowAddListeners.java
// Display the "addXXXListener" methods of any Swing class.
// <applet code=ShowAddListeners
// width=500 height=400></applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import java.util.regex.*;
import com.bruceeckel.swing.*;
public class ShowAddListeners extends JApplet {
  private JTextField name = new JTextField(25);
  private JTextArea results = new JTextArea(40, 65);
  private static Pattern addListener =
    Pattern.compile("(add//w+?Listener//(.*?//))");
  private static Pattern qualifier =
    Pattern.compile("//w+//.");
  class NameL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String nm = name.getText().trim();
      if(nm.length() == 0) {
        results.setText("No match");
        return;
      }
      Class klass;
      try {
        klass = Class.forName("javax.swing." + nm);
      } catch(ClassNotFoundException ex) {
        results.setText("No match");
        return;
      }
      Method[] methods = klass.getMethods();
      results.setText("");
      for(int i = 0; i < methods.length; i++) {
        Matcher matcher =
          addListener.matcher(methods[i].toString());
        if(matcher.find())
          results.append(qualifier.matcher(
            matcher.group(1)).replaceAll("") + "/n");
      }
    }
  }
  public void init() {
    NameL nameListener = new NameL();
    name.addActionListener(nameListener);
    JPanel top = new JPanel();
    top.add(new JLabel("Swing class name (press ENTER):"));
    top.add(name);
    Container cp = getContentPane();
    cp.add(BorderLayout.NORTH, top);
    cp.add(new JScrollPane(results));
    // Initial data and test:
    name.setText("JTextArea");
    nameListener.actionPerformed(
      new ActionEvent("", 0 ,""));
  }
  public static void main(String[] args) {
    Console.run(new ShowAddListeners(), 500,400);
  }
} ///:~
只要在JTextField 里面输入你想查询的Swing组件的名字,它就会用正则表达式把结果提取出来,然后交给JTextArea显示。

你会发现表单上没有类似按钮之类的,能让你表示开始查询的控件。这是因为JTextField已经有一个ActionListener了。当你做完修改按下ENTER的时候,这份清单就会立即更新。(过程是这样的,)如果JTextField不为空,ActionListener就根据这个字符串用Class.forName( )来查找这个类。如果名字不正确,Class.forName( )就会抛出一个异常,然后让异常处理程序在JTextArea里面显示"No match"。如果名字正确(注意是大小写敏感的),Class.forName( )就会正常返回,接着getMethods( )会返回Method对象的数组。

这里有两个正则表达式。第一个,也就是addListener,负责匹配"add"加若干个单词字符(译者注:原文为word character,表示字母,数字,以及下划线)加"Listener",加上括号括起来的任意个字符。注意,我们用括号把整个正则表达式给括了起来,也就是说如果匹配成功,我们会把它当作一个group来用。NameL.ActionPerformed( )方法会把Method对象逐个传给Pattern.matcher( ),由后者创建Matcher对象。如果匹配成功,find( )会返回true,这样你就能用group(1)来选择整个表达式的第一个带括号的group了。这时,字符串前面还有限定词。所以我们要照着c09:ShowMethods.java那样用qualifier 把这个限定词剥掉了。

最后,我们在init( )方法里设置了name的初始值并运行了事件。

用这个程序来查看Swing组件真是太简单。一旦你知道了组件所支持的事件,你就用不着再去查文档了。你只要:

把event类的名字里的"Event"去掉,加上"Listener",这就是你要实现的接口的名字了。
实现上述接口,想捕捉哪个事件就实现它的接口。比方说,如果你对鼠标的移动感兴趣,你可以去实现MouseMotionListener接口的mouseMoved( )方法。(你必须实现这个接口的全套方法,但是这种情况下,通常都会有捷径,过一会就会看到了。)
创建一个listener的对象。在接口的名字前面加一个"add",然后用这个方法向组件注册。比如,addMouseMotionListener( )。
下面是部分listener接口的方法:

Listener接口/Adapter 接口所定义的方法
ActionListener actionPerformed(ActionEvent)
AdjustmentListener adjustmentValueChanged(AdjustmentEvent)
ComponentListener
ComponentAdapter
 componentHidden(ComponentEvent)
componentShown(ComponentEvent)
componentMoved(ComponentEvent)
componentResized(ComponentEvent)
 
ContainerListener
ContainerAdapter
 componentAdded(ContainerEvent)
componentRemoved(ContainerEvent)
 
FocusListener
FocusAdapter
 focusGained(FocusEvent)
focusLost(FocusEvent)
 
KeyListener
KeyAdapter
 keyPressed(KeyEvent)
keyReleased(KeyEvent)
keyTyped(KeyEvent)
 
MouseListener
MouseAdapter
 mouseClicked(MouseEvent)
mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent)
 
MouseMotionListener
MouseMotionAdapter
 mouseDragged(MouseEvent)
mouseMoved(MouseEvent)
 
WindowListener
WindowAdapter
 windowOpened(WindowEvent)
windowClosing(WindowEvent)
windowClosed(WindowEvent)
windowActivated(WindowEvent)
windowDeactivated(WindowEvent)
windowIconified(WindowEvent)
windowDeiconified(WindowEvent)
 
ItemListener itemStateChanged(ItemEvent)

之所以这张表不是很全,部分是因为事件模型允许你创建自己的事件及相关的listener。所以你时常会碰到一些在事件类型方面自成体系的类库,而你在本章所学到的知识会帮助你学会使用这些事件。

用listener的adapter简化编程
可以看到上面那张表里的一些listener只有一个方法。实现这种接口的工作量并不大,因为写完方法,接口也就实现了。但是如果你要使用有多个方法的listener的话,事情就不那么轻松愉快了。举例来说,如果你要捕捉鼠标点击的话(button就不会替你捕捉这个事件),你就必须写一个mouseClicked( )方法。但是由于MouseListener是一个interface,所以即使不用,你也得实现其所有的方法。这可真是烦人。

为了解决这个问题,有些(但不是全部)多方法的listener接口提供了适配器(adapter)。从上面那张表已经列出了它们的名字。适配器会为接口提供默认的空方法。这样,你只要继承适配器,根据需要覆写方法就可以了。比如下面这个最常见的MouseListener的用法:

class MyMouseListener extends MouseAdapter {
  public void mouseClicked(MouseEvent e) {
    // Respond to mouse click...
  }
}
adapter就是用来简化listener的创建的。

但是适配器这种东西也有缺点。假设你写了一个和上面一样的MouseAdapter:

class MyMouseListener extends MouseAdapter {
  public void MouseClicked(MouseEvent e) {
    // Respond to mouse click...
  }
}
它不但不起作用,反而会把你给逼疯了。它能正常编译,运行也不报错,但是按鼠标就是没反应。能看出问题吗?答案就在方法的名字里:你把mouseClicked( )写成了MouseClicked( )了。只是大小写方面的一个疏漏,它就成为一个新方法了。还好,这还不是关闭窗口时会调用的方法,所以最坏的结果也只是不能实现预期的效果。虽然有种种不方便,但接口却能保证让你实现所有应该实现的方法。

跟踪多个事件
为了证明事件真的会被激发出来,同时也作为一个很有趣的实验,我们准备创建一个能跟踪JButton的其它行为的(也就是除被按下以外的其它事件的)applet。此外这里还演示了怎样用继承来定制你自己的按钮对象,因为你感兴趣的事件最后都要作用到这个对象上。要作到这一点,只要直接继承JButton就行了。[82]

MyButton是TrackEvent的内部类,所以MyButton可以访问其宿主窗口,并控制宿主窗口里的text field。如果要把状态信息做成宿主类的字段,这种能力就是必须的了。当然这个解决方案也有其局限性,MyButton只能和TrackEvent配合使用。这类代码常被称为"高度耦合的(highly coupled)":

//: c14:TrackEvent.java
// Show events as they happen.
// <applet code=TrackEvent width=700 height=500></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
public class TrackEvent extends JApplet {
  private HashMap h = new HashMap();
  private String[] event = {
    "focusGained", "focusLost", "keyPressed",
    "keyReleased", "keyTyped", "mouseClicked",
    "mouseEntered", "mouseExited", "mousePressed",
    "mouseReleased", "mouseDragged", "mouseMoved"
  };
  private MyButton
    b1 = new MyButton(Color.BLUE, "test1"),
    b2 = new MyButton(Color.RED, "test2");
  class MyButton extends JButton {
    void report(String field, String msg) {
      ((JTextField)h.get(field)).setText(msg);
    }
    FocusListener fl = new FocusListener() {
      public void focusGained(FocusEvent e) {
        report("focusGained", e.paramString());
      }
      public void focusLost(FocusEvent e) {
        report("focusLost", e.paramString());
      }
    };
    KeyListener kl = new KeyListener() {
      public void keyPressed(KeyEvent e) {
        report("keyPressed", e.paramString());
      }
      public void keyReleased(KeyEvent e) {
        report("keyReleased", e.paramString());
      }
      public void keyTyped(KeyEvent e) {
        report("keyTyped", e.paramString());
      }
    };
    MouseListener ml = new MouseListener() {
      public void mouseClicked(MouseEvent e) {
        report("mouseClicked", e.paramString());
      }
      public void mouseEntered(MouseEvent e) {
        report("mouseEntered", e.paramString());
      }
      public void mouseExited(MouseEvent e) {
        report("mouseExited", e.paramString());
      }
      public void mousePressed(MouseEvent e) {
        report("mousePressed", e.paramString());
      }
      public void mouseReleased(MouseEvent e) {
        report("mouseReleased", e.paramString());
      }
    };
    MouseMotionListener mml = new MouseMotionListener() {
      public void mouseDragged(MouseEvent e) {
        report("mouseDragged", e.paramString());
      }
      public void mouseMoved(MouseEvent e) {
        report("mouseMoved", e.paramString());
      }
    };
    public MyButton(Color color, String label) {
      super(label);
      setBackground(color);
      addFocusListener(fl);
      addKeyListener(kl);
      addMouseListener(ml);
      addMouseMotionListener(mml);
    }
  }
  public void init() {
    Container c = getContentPane();
    c.setLayout(new GridLayout(event.length + 1, 2));
    for(int i = 0; i < event.length; i++) {
      JTextField t = new JTextField();
      t.setEditable(false);
      c.add(new JLabel(event[i], JLabel.RIGHT));
      c.add(t);
      h.put(event[i], t);
    }
    c.add(b1);
    c.add(b2);
  }
  public static void main(String[] args) {
    Console.run(new TrackEvent(), 700, 500);
  }
} ///:~
MyButton的构造函数通过setBackground( )方法来设置按钮的颜色。listener的安装也相当简单。

TrackEvent类里的HashMap是用来存放事件类型及与之相对应的JTextField用的。当然,你可以静态地创建这些对象而不把它们放到HashMap里,但是我认为这种方法更简单也更方便修改,我想对此你也和我持同样的意见。特别是,如果你要往TrackEvent里面加或减一个新的事件时,你可以直接往event数组里加或减一个字符串——剩下的它会自动完成。

report( )需要事件的名字以及事件的parameter string做参数,然后根据事件的名字在Hashmap h里找到JTextField,再把parameter string写进去。

这个程序运行起来很有意思,因为它让你很直观地看到程序是怎样处理事件的。

Swing组件的一览表
现在你已经知道布局管理器和事件模型了,接下来就要学习怎样使用Swing组件了。这部分只是一个大致的介绍,我们讲的都是常用的Swing组件及其特性。我们有意把例程作得比较小,这样你就能把这些代码搬到自己的程序里了。

记住:

要运行例程很简单,只要打开源代码里的HTML页面就可以了。(到www.BruceEckel.com去下载)
JDK文档里有所有Swing组件的资料(这里只讲了一小部分)。
Swing事件的命名规范比较合理,所以要猜该怎样编写和安装事件处理程序也比较简单。用我们前面讲的ShowAddListeners.java来检查组件。
如果事情开始变得复杂了,那么恭喜你毕业了,该用GUI Builder了。
Button
Swing收录了很多Button,包括各种按钮,check box, radio button,甚至连菜单项(menu item)都是继承AbstractButton的(鉴于菜单项也牵涉进来了,(AbstractButton)可能还是叫"AbstractSelector"或其它什么名字更好一些)。我们很快就会看到菜单项的用法了,不过首先还是来看一下Swing的按钮:

//: c14:Buttons.java
// Various Swing buttons.
// <applet code=Buttons width=350 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.plaf.basic.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;
public class Buttons extends JApplet {
  private JButton jb = new JButton("JButton");
  private BasicArrowButton
    up = new BasicArrowButton(BasicArrowButton.NORTH),
    down = new BasicArrowButton(BasicArrowButton.SOUTH),
    right = new BasicArrowButton(BasicArrowButton.EAST),
    left = new BasicArrowButton(BasicArrowButton.WEST);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(jb);
    cp.add(new JToggleButton("JToggleButton"));
    cp.add(new JCheckBox("JCheckBox"));
    cp.add(new JRadioButton("JRadioButton"));
    JPanel jp = new JPanel();
    jp.setBorder(new TitledBorder("Directions"));
    jp.add(up);
    jp.add(down);
    jp.add(left);
    jp.add(right);
    cp.add(jp);
  }
  public static void main(String[] args) {
    Console.run(new Buttons(), 350, 100);
  }
} ///:~
这段程序先演示了javax.swing.plaf.basic的BasicArrowButton,接着是其它形形色色的按钮。程序运行的时候你会发现,toggle button能保存是否被按下的状态。而check box和radio button的行为则完全相同,只要点击就能打开或关闭(它们是JToggleButton的派生类)。

Button组
如果想让radio button以"几选一"的方式运行(译者注:原文为exclusive or,字面的意思是"排他性的逻辑与"),你就必须把他们加到一个"button组(button group)"里。但正如下面这段程序所展示的,只要是AbstractButton,都可以加进ButtonGroup。

为了避免代码的重复,我们这里用reflection来生成各种button组。创建button组以及JPanel的任务是由makeBPanel( )来完成的。它有两个参数,其中第二个是一个String的数组。makeBPanel( )会根据数组里的String创建由第一个参数决定的button,然后把它加进JPanel:

//: c14:ButtonGroups.java
// Uses reflection to create groups
// of different types of AbstractButton.
// <applet code=ButtonGroups width=500 height=300></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import java.lang.reflect.*;
import com.bruceeckel.swing.*;
public class ButtonGroups extends JApplet {
  private static String[] ids = {
    "June", "Ward", "Beaver",
    "Wally", "Eddie", "Lumpy",
  };
  static JPanel makeBPanel(Class klass, String[] ids) {
    ButtonGroup bg = new ButtonGroup();
    JPanel jp = new JPanel();
    String title = klass.getName();
    title = title.substring(title.lastIndexOf('.') + 1);
    jp.setBorder(new TitledBorder(title));
    for(int i = 0; i < ids.length; i++) {
      AbstractButton ab = new JButton("failed");
      try {
        // Get the dynamic constructor method
        // that takes a String argument:
        Constructor ctor =
          klass.getConstructor(new Class[]{String.class});
        // Create a new object:
        ab = (AbstractButton)
          ctor.newInstance(new Object[] { ids[i] });
      } catch(Exception ex) {
        System.err.println("can't create " + klass);
      }
      bg.add(ab);
      jp.add(ab);
    }
    return jp;
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(makeBPanel(JButton.class, ids));
    cp.add(makeBPanel(JToggleButton.class, ids));
    cp.add(makeBPanel(JCheckBox.class, ids));
    cp.add(makeBPanel(JRadioButton.class, ids));
  }
  public static void main(String[] args) {
    Console.run(new ButtonGroups(), 500, 300);
  }
} ///:~
边框的抬头取自类名,放上去之前已经把路径信息去掉了。我们先用标有"Failed"的JButton来初始化AbstractButton,这样即便你把异常信息忽略掉了,(一旦出了问题,)屏幕上还是会有反应。getConstructor( )方法会根据Class数组返回一个Constructor对象,这个Constructor所需的参数类型是由这个Class数组决定的。接下来就该调用newInstance( )方法了。这个方法的参数是一个Object数组,也就是构造函数真正要用的参数——这里就是ids数组里的字符串。

这几步给原本很简单任务加了点难度。要让button做到"几选一",你必须先创建一个button组,然后把要加进去的button加进去。运行程序的时候,你会发现除了JButton,所有的button都表现出了"几选一"的效果。

Icon
Icon能用于JLabel和AbstractButton(包括JButton,JCheckBox,JRadioButton以及JMenuItem)。把Icon用于JLabel的语法非常简单(马上就会演示了)。下面这段程序会把Icon依次用于button及其派生类。

你也可以使用你自己的gif文件,不过这里用的还是www.BruceEckel.com上发布的东西。如果想打开文件读取图像,只要创建一个ImageIcon,并且把文件的名字传给它就行了。接下来,你就可以在程序中使用这个Icon了。

//: c14:Faces.java
// Icon behavior in Jbuttons.
// <applet code=Faces width=400 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import com.bruceeckel.swing.*;
public class Faces extends JApplet {
  private static Icon[] faces;
  private JButton jb, jb2 = new JButton("Disable");
  private boolean mad = false;
  public void init() {
    faces = new Icon[] {
      new ImageIcon(getClass().getResource("Face0.gif")),
      new ImageIcon(getClass().getResource("Face1.gif")),
      new ImageIcon(getClass().getResource("Face2.gif")),
      new ImageIcon(getClass().getResource("Face3.gif")),
      new ImageIcon(getClass().getResource("Face4.gif")),
    };
    jb = new JButton("JButton", faces[3]);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    jb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if(mad) {
          jb.setIcon(faces[3]);
          mad = false;
        } else {
          jb.setIcon(faces[0]);
          mad = true;
        }
        jb.setVerticalAlignment(JButton.TOP);
        jb.setHorizontalAlignment(JButton.LEFT);
      }
    });
    jb.setRolloverEnabled(true);
    jb.setRolloverIcon(faces[1]);
    jb.setPressedIcon(faces[2]);
    jb.setDisabledIcon(faces[4]);
    jb.setToolTipText("Yow!");
    cp.add(jb);
    jb2.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if(jb.isEnabled()) {
          jb.setEnabled(false);
          jb2.setText("Enable");
        } else {
          jb.setEnabled(true);
          jb2.setText("Disable");
        }
      }
    });
    cp.add(jb2);
  }
  public static void main(String[] args) {
    Console.run(new Faces(), 400, 200);
  }
} ///:~
很多Swing组件构造函数都可以拿Icon作参数,不过你也可以用setIcon( )方法添加或修改Icon。此外这段程序还演示了,当按钮监听到事件的时候,也就是当你按下按钮,禁掉按钮,或(用鼠标)"滑过"按钮(但是并不按下)时,该怎样设置JButton(或其他AbstractButton),让它显示不同的图标。你会发现,你能用这种技巧做出一种带动画效果的按钮。

Tool tips
我们给上面那段程序里的按钮加了一个"tool tip"。几乎所有与GUI相关的类都继承自JComponent,而JComponent又包含了一个setToolText(String)方法。因此不管是哪种组件,只要能放到表单上,你几乎都可以用(假设这是个JComponent的派生类对象jc)

jc.setToolTipText("My tip");
来设置tool tip。这样只要鼠标停在JComponent上一段时间,旁边就会跳出一个提示框,里面就是你设置的文本。

Text fields
下面我们再来看看JTextField还能做些什么:

//: c14:TextFields.java
// Text fields and Java events.
// <applet code=TextFields width=375 height=125></applet>
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class TextFields extends JApplet {
  private JButton
    b1 = new JButton("Get Text"),
    b2 = new JButton("Set Text");
  private JTextField
    t1 = new JTextField(30),
    t2 = new JTextField(30),
    t3 = new JTextField(30);
  private String s = new String();
  private UpperCaseDocument ucd = new UpperCaseDocument();
  public void init() {
    t1.setDocument(ucd);
    ucd.addDocumentListener(new T1());
    b1.addActionListener(new B1());
    b2.addActionListener(new B2());
    DocumentListener dl = new T1();
    t1.addActionListener(new T1A());
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(b1);
    cp.add(b2);
    cp.add(t1);
    cp.add(t2);
    cp.add(t3);
  }
  class T1 implements DocumentListener {
    public void changedUpdate(DocumentEvent e) {}
    public void insertUpdate(DocumentEvent e) {
      t2.setText(t1.getText());
      t3.setText("Text: "+ t1.getText());
    }
    public void removeUpdate(DocumentEvent e) {
      t2.setText(t1.getText());
    }
  }
  class T1A implements ActionListener {
    private int count = 0;
    public void actionPerformed(ActionEvent e) {
      t3.setText("t1 Action Event " + count++);
    }
  }
  class B1 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(t1.getSelectedText() == null)
        s = t1.getText();
      else
        s = t1.getSelectedText();
      t1.setEditable(true);
    }
  }
  class B2 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      ucd.setUpperCase(false);
      t1.setText("Inserted by Button 2: " + s);
      ucd.setUpperCase(true);
      t1.setEditable(false);
    }
  }
  public static void main(String[] args) {
    Console.run(new TextFields(), 375, 125);
  }
}
class UpperCaseDocument extends PlainDocument {
  private boolean upperCase = true;
  public void setUpperCase(boolean flag) {
    upperCase = flag;
  }
  public void
  insertString(int offset, String str, AttributeSet attSet)
  throws BadLocationException {
    if(upperCase) str = str.toUpperCase();
    super.insertString(offset, str, attSet);
  }
} ///:~
之所以要有JTextField t3是为了要给JTextField t1的监听器准备一个能报告事件地方。你会发现只有在按了"enter"键之后,JTextField的监听器才会有反应。

JTextField t1上面连着好几个listener。T1属于DocumentListener,这种listener会对"document"(这里就是JTextField里面的内容)的变化做出反应。它(T1)会将t1里面的文字复制到t2里面。此外,由于t1的document是PlainDocument的派生类UpperCaseDocument型的,因此它会把文档里的所有字符全都强制转化成大写的。此外它还能检测出退格键,能自动进行删除操作,能调整光标(caret),其处理事件的方式与你的预期完全相同

Borders
JComponent里面有一个setBorder( )方法,它能让你为各种可视组件安上有趣的边框。下面我们写一个showBorder( )方法来看看这些边框。这个方法会创建一个JPanel,然后给它镶上边框。我们还是通过RTTI来获取边框的名称(去除了路径信息),然后将它放到在JPanel里的JLabel里面:

//: c14:Borders.java
// Different Swing borders.
// <applet code=Borders width=500 height=300></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;
public class Borders extends JApplet {
  static JPanel showBorder(Border b) {
    JPanel jp = new JPanel();
    jp.setLayout(new BorderLayout());
    String nm = b.getClass().toString();
    nm = nm.substring(nm.lastIndexOf('.') + 1);
    jp.add(new JLabel(nm, JLabel.CENTER),
      BorderLayout.CENTER);
    jp.setBorder(b);
    return jp;
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(2,4));
    cp.add(showBorder(new TitledBorder("Title")));
    cp.add(showBorder(new EtchedBorder()));
    cp.add(showBorder(new LineBorder(Color.BLUE)));
    cp.add(showBorder(
      new MatteBorder(5,5,30,30,Color.GREEN)));
    cp.add(showBorder(
      new BevelBorder(BevelBorder.RAISED)));
    cp.add(showBorder(
      new SoftBevelBorder(BevelBorder.LOWERED)));
    cp.add(showBorder(new CompoundBorder(
      new EtchedBorder(),
      new LineBorder(Color.RED))));
  }
  public static void main(String[] args) {
    Console.run(new Borders(), 500, 300);
  }
} ///:~
你还可以创建你自己的边框,然后把它放到按钮(button),标签(label)或者其它控件里面,——只要它是继承JComponent的就行。

JScrollPanes
大多数时候,你只需要JScrollPane能干好它的本职工作,但是你也可以去控制它,告诉它显示哪根滚动条——垂直的,水平的,还是两个都显示或者两个都不显示。

//: c14:JScrollPanes.java
// Controlling the scrollbars in a JScrollPane.
// <applet code=JScrollPanes width=300 height=725></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;
public class JScrollPanes extends JApplet {
  private JButton
    b1 = new JButton("Text Area 1"),
    b2 = new JButton("Text Area 2"),
    b3 = new JButton("Replace Text"),
    b4 = new JButton("Insert Text");
  private JTextArea
    t1 = new JTextArea("t1", 1, 20),
    t2 = new JTextArea("t2", 4, 20),
    t3 = new JTextArea("t3", 1, 20),
    t4 = new JTextArea("t4", 10, 10),
    t5 = new JTextArea("t5", 4, 20),
    t6 = new JTextArea("t6", 10, 10);
  private JScrollPane
    sp3 = new JScrollPane(t3,
      JScrollPane.VERTICAL_SCROLLBAR_NEVER,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER),
    sp4 = new JScrollPane(t4,
      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
      JScrollPane.HORIZONTAL_SCROLLBAR_NEVER),
    sp5 = new JScrollPane(t5,
      JScrollPane.VERTICAL_SCROLLBAR_NEVER,
      JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS),
    sp6 = new JScrollPane(t6,
      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
      JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
  class B1L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t5.append(t1.getText() + "/n");
    }
  }
  class B2L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t2.setText("Inserted by Button 2");
      t2.append(": " + t1.getText());
      t5.append(t2.getText() + "/n");
    }
  }
  class B3L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String s = " Replacement ";
      t2.replaceRange(s, 3, 3 + s.length());
    }
  }
  class B4L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t2.insert(" Inserted ", 10);
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    // Create Borders for components:
    Border brd = BorderFactory.createMatteBorder(
      1, 1, 1, 1, Color.BLACK);
    t1.setBorder(brd);
    t2.setBorder(brd);
    sp3.setBorder(brd);
    sp4.setBorder(brd);
    sp5.setBorder(brd);
    sp6.setBorder(brd);
    // Initialize listeners and add components:
    b1.addActionListener(new B1L());
    cp.add(b1);
    cp.add(t1);
    b2.addActionListener(new B2L());
    cp.add(b2);
    cp.add(t2);
    b3.addActionListener(new B3L());
    cp.add(b3);
    b4.addActionListener(new B4L());
    cp.add(b4);
    cp.add(sp3);
    cp.add(sp4);
    cp.add(sp5);
    cp.add(sp6);
  }
  public static void main(String[] args) {
    Console.run(new JScrollPanes(), 300, 725);
  }
} ///:~
通过给JScrollPane的构造函数传不同的参数,可以控制它的滚动条。这里我们还用边框做了一下装饰。

一个袖珍的编辑器
不用费多大的劲,JTextPane已经提供了很多编辑功能。下面我们就简单地演示一下这个组件,注意我们忽略了它的绝大多数的功能:

//: c14:TextPane.java
// The JTextPane control is a little editor.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
import com.bruceeckel.util.*;
public class TextPane extends JFrame {
  private JButton b = new JButton("Add Text");
  private JTextPane tp = new JTextPane();
  private static Generator sg =
    new Arrays2.RandStringGenerator(7);
  public TextPane() {
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        for(int i = 1; i < 10; i++)
          tp.setText(tp.getText() + sg.next() + "/n");
      }
    });
    Container cp = getContentPane();
    cp.add(new JScrollPane(tp));
    cp.add(BorderLayout.SOUTH, b);
  }
  public static void main(String[] args) {
    Console.run(new TextPane(), 475, 425);
  }
} ///:~
按钮的作用是随机生成一些文本,然后再把它加到JTextPane里。JTextPane的意思是要提供一个能让你编辑文本的地方,所以你会发现它没有append( )方法。这里(坦率地说,我们根本就没用足JTextPane的功能)我们只能用setText( )来获取文本,对其进行修改,然后再放回JTextPane里。

正如我们前面所讲的,applet的默认布局是BorderLayout。所以,如果你什么都不说,直接往面板里面加组件,那么它会填满整个面板。但是如果你像本例那样指明其位置(NORTH,SOUTH, EAST,或WEST),控件就会被钉在这个区域里。这里按钮会放在屏幕的下方。

注意JTextPane的内置功能,比如自动换行。此外它还有很多其他功能,具体详情请参阅JDK文档。

Check boxes
Check box能让你做逐项的开/关选择。它由一个小框和一个标签组成。一般来说,选中之后框里会有一个'x'(或者其它什么表示选中的标记),否则就是空的。

一般来说,你用构造函数创建JCheckBox的时候,会传给它一个用作标签的参数。JCheckBox创建完了之后,你还可以随时读取或设置它的状态,或者读取或重设它的标签。

不管是选中还是清除,JCheckBox都会引发一个事件。捕捉方法同按钮事件完全相同:用ActionListener。下面我们就来举一个例子。这里的JTextArea会记录下所有的check box的事件:

//: c14:CheckBoxes.java
// Using JCheckBoxes.
// <applet code=CheckBoxes width=200 height=200></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class CheckBoxes extends JApplet {
  private JTextArea t = new JTextArea(6, 15);
  private JCheckBox
    cb1 = new JCheckBox("Check Box 1"),
    cb2 = new JCheckBox("Check Box 2"),
    cb3 = new JCheckBox("Check Box 3");
  public void init() {
    cb1.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        trace("1", cb1);
      }
    });
    cb2.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        trace("2", cb2);
      }
    });
    cb3.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        trace("3", cb3);
      }
    });
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(new JScrollPane(t));
    cp.add(cb1);
    cp.add(cb2);
    cp.add(cb3);
  }
  private void trace(String b, JCheckBox cb) {
    if(cb.isSelected())
      t.append("Box " + b + " Set/n");
    else
      t.append("Box " + b + " Cleared/n");
  }
  public static void main(String[] args) {
    Console.run(new CheckBoxes(), 200, 200);
  }
} ///:~
trace( )会用append( )方法,把JCheckBox的名字以及是不是被选中的信息送到JTextField里面。所以你会看到的checkbox的选中记录及其当前状态的列表。

Radio buttons
GUI编程里的radio button的概念来自于老式的汽车收音机里的机械式按钮;当你按下一个按钮之后,另一个就会弹出来。因此你只能选一个。

要想创建一组相关联的JRadioButton,只要把它们加进ButtonGroup就可以了(一个表单里允许有任意数量的ButtonGroup)。如果你(用构造函数的第二个参数)把多个radio button设成true了,那么只有最后一个才是有效的。

下面我们举一个简单的例子。注意,捕捉radio button事件的方法同捕捉其它事件的完全相同:

//: c14:RadioButtons.java
// Using JRadioButtons.
// <applet code=RadioButtons width=200 height=100></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class RadioButtons extends JApplet {
  private JTextField t = new JTextField(15);
  private ButtonGroup g = new ButtonGroup();
  private JRadioButton
    rb1 = new JRadioButton("one", false),
    rb2 = new JRadioButton("two", false),
    rb3 = new JRadioButton("three", false);
  private ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      t.setText("Radio button " +
        ((JRadioButton)e.getSource()).getText());
    }
  };
  public void init() {
    rb1.addActionListener(al);
    rb2.addActionListener(al);
    rb3.addActionListener(al);
    g.add(rb1); g.add(rb2); g.add(rb3);
    t.setEditable(false);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    cp.add(rb1);
    cp.add(rb2);
    cp.add(rb3);
  }
  public static void main(String[] args) {
    Console.run(new RadioButtons(), 200, 100);
  }
} ///:~
为了观察状态,我们用了一个text field。我们把它设成不可编辑的,这是因为它不是用来收集数据的,而只是用来显示数据的。由此也能看出,它可以可以代替JLabel。

Combo boxes (下拉式列表)
同radio button组一样,下拉式列表只允许用户在一组选项里面选取一个元素。但是这种做法更简洁,而且能在不惊扰客户的情况下修改列表中的元素。(你也可以动态地修改radio button,但是这个视觉效果就太奇怪了)。

JComboBox的默认行为同Windows的combo box不太一样。在Windows里,你既可以从combo box的列表里选一个,也可以自己输入,但是在JComboBox里,这么做就必须先调用setEditable( )了。此外你只能从列表中选择一个。下面我们举一个例子。我们先给JComboBox加几个选项,然后每按一下按钮就加一个选项。

//: c14:ComboBoxes.java
// Using drop-down lists.
// <applet code=ComboBoxes width=200 height=125></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class ComboBoxes extends JApplet {
  private String[] description = {
    "Ebullient", "Obtuse", "Recalcitrant", "Brilliant",
    "Somnescent", "Timorous", "Florid", "Putrescent"
  };
  private JTextField t = new JTextField(15);
  private JComboBox c = new JComboBox();
  private JButton b = new JButton("Add items");
  private int count = 0;
  public void init() {
    for(int i = 0; i < 4; i++)
      c.addItem(description[count++]);
    t.setEditable(false);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if(count < description.length)
          c.addItem(description[count++]);
      }
    });
    c.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        t.setText("index: "+ c.getSelectedIndex() + "   " +
         ((JComboBox)e.getSource()).getSelectedItem());
      }
    });
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    cp.add(c);
    cp.add(b);
  }
  public static void main(String[] args) {
    Console.run(new ComboBoxes(), 200, 125);
  }
} ///:~
JTextField除了显示选中的文字之外还会显示"选中的索引号(selected index)",也就是当前被选中的那个选项的序列号。

List boxes
List box与JComboBox的不同不仅仅在外观上,它们之间有着非常重大的区别。JComboBox激活之后会弹出下拉框而JList则总是会在屏幕上占一块固定大小,永远也不会变。如果你想列表里的东西,只要调用getSelectedValues( )就行了,它会返回一个String的数组,其中包含着被选中的元素。

JList能做多项选择;如果你control-click了一个以上的选项(在用鼠标进行选择的时候,一直按着"control"键),那么已选中的选项就会一直保持"高亮(highlighted)",这样你能选任意多个了。如果你先选一项,然后再shift-click另一个,那么两个选项之间的所有选项就都被选中了。要取消一个选项只要control-click就可以了。

//: c14:List.java
// <applet code=List width=250 height=375></applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;
public class List extends JApplet {
  private String[] flavors = {
    "Chocolate", "Strawberry", "Vanilla Fudge Swirl",
    "Mint Chip", "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie"
  };
  private DefaultListModel lItems=new DefaultListModel();
  private JList lst = new JList(lItems);
  private JTextArea t =
    new JTextArea(flavors.length, 20);
  private JButton b = new JButton("Add Item");
  private ActionListener bl = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      if(count < flavors.length) {
        lItems.add(0, flavors[count++]);
      } else {
        // Disable, since there are no more
        // flavors left to be added to the List
        b.setEnabled(false);
      }
    }
  };
  private ListSelectionListener ll =
    new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        if(e.getValueIsAdjusting()) return;
        t.setText("");
        Object[] items=lst.getSelectedValues();
        for(int i = 0; i < items.length; i++)
          t.append(items[i] + "/n");
      }
    };
  private int count = 0;
  public void init() {
    Container cp = getContentPane();
    t.setEditable(false);
    cp.setLayout(new FlowLayout());
    // Create Borders for components:
    Border brd = BorderFactory.createMatteBorder(
      1, 1, 2, 2, Color.BLACK);
    lst.setBorder(brd);
    t.setBorder(brd);
    // Add the first four items to the List
    for(int i = 0; i < 4; i++)
      lItems.addElement(flavors[count++]);
    // Add items to the Content Pane for Display
    cp.add(t);
    cp.add(lst);
    cp.add(b);
    // Register event listeners
    lst.addListSelectionListener(ll);
    b.addActionListener(bl);
  }
  public static void main(String[] args) {
    Console.run(new List(), 250, 375);
  }
} ///:~
你会发现我们给列表安上了边框。

如果你只是想把String数组放到JList里面,那么还有一个更简单的办法;就是把数组当作参数传给JList的构造函数,这样它就会自动创建一个列表了。在上述例程中,使用"列表模型"的唯一的理由就是,要在程序运行的时候操控那个列表。

JList不会自动提供滚动轴。当然只要把它嵌到JScrollPane里它就会自动镶上滚动轴了,具体的细节它自会打理。

Tabbed panes
JTabbedPane能创建"带页签的对话框(tabbed dialog)",也就是对话框的边上有一个像文件夹的页签一样的东西,你只要点击这个页签,对话框就把这一页显示出来。

//: c14:TabbedPane1.java
// Demonstrates the Tabbed Pane.
// <applet code=TabbedPane1 width=350 height=200></applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class TabbedPane1 extends JApplet {
  private String[] flavors = {
    "Chocolate", "Strawberry", "Vanilla Fudge Swirl",
    "Mint Chip", "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie"
  };
  private JTabbedPane tabs = new JTabbedPane();
  private JTextField txt = new JTextField(20);
  public void init() {
    for(int i = 0; i < flavors.length; i++)
      tabs.addTab(flavors[i],
        new JButton("Tabbed pane " + i));
    tabs.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        txt.setText("Tab selected: " +
          tabs.getSelectedIndex());
      }
    });
    Container cp = getContentPane();
    cp.add(BorderLayout.SOUTH, txt);
    cp.add(tabs);
  }
  public static void main(String[] args) {
    Console.run(new TabbedPane1(), 350, 200);
  }
} ///:~
在Java编程当中熟练使用页签面板(tabbed panel)相当重要,因为每当applet要弹出一个对话框的时候,它都会自动加上一段警告,所以弹出式对话框在applet里并不受欢迎。

程序运行起来你就会发现,如果tab太多了,JTabbedPane还会自动把它们堆起来以适应一定的行宽。你可以在程序运行的时候调整窗口的大小以观察这个效果。

Message boxes
图形界面系统通常都包含一套标准的,能让你迅速地将消息传给用户,或者从用户那里得到信息的对话框。对于Swing来说,这些消息框就包含在JOptionPane里面了。你有很多选择(有些还相当复杂),但是最常用的可能还是"确认对话框(confirmation dialog)",它用static JOptionPane.showMessageDialog( )和JOptionPane.showConfirmDialog( )启动。下面我们来演示一些JOptionPane里的对话框。

//: c14:MessageBoxes.java
// Demonstrates JoptionPane.
// <applet code=MessageBoxes width=200 height=150></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class MessageBoxes extends JApplet {
  private JButton[] b = {
    new JButton("Alert"), new JButton("Yes/No"),
    new JButton("Color"), new JButton("Input"),
    new JButton("3 Vals")
  };
  private JTextField txt = new JTextField(15);
  private ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      String id = ((JButton)e.getSource()).getText();
      if(id.equals("Alert"))
        JOptionPane.showMessageDialog(null,
          "There's a bug on you!", "Hey!",
          JOptionPane.ERROR_MESSAGE);
      else if(id.equals("Yes/No"))
        JOptionPane.showConfirmDialog(null,
          "or no", "choose yes",
          JOptionPane.YES_NO_OPTION);
      else if(id.equals("Color")) {
        Object[] options = { "Red", "Green" };
        int sel = JOptionPane.showOptionDialog(
          null, "Choose a Color!", "Warning",
          JOptionPane.DEFAULT_OPTION,
          JOptionPane.WARNING_MESSAGE, null,
          options, options[0]);
        if(sel != JOptionPane.CLOSED_OPTION)
          txt.setText("Color Selected: " + options[sel]);
      } else if(id.equals("Input")) {
        String val = JOptionPane.showInputDialog(
            "How many fingers do you see?");
        txt.setText(val);
      } else if(id.equals("3 Vals")) {
        Object[] selections = {"First", "Second", "Third"};
        Object val = JOptionPane.showInputDialog(
          null, "Choose one", "Input",
          JOptionPane.INFORMATION_MESSAGE,
          null, selections, selections[0]);
        if(val != null)
          txt.setText(val.toString());
      }
    }
  };
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < b.length; i++) {
      b[i].addActionListener(al);
      cp.add(b[i]);
    }
    cp.add(txt);
  }
  public static void main(String[] args) {
    Console.run(new MessageBoxes(), 200, 200);
  }
} ///:~
为了能在一个ActionListener里面完成任务,我用了一个多少有点冒险的方法——检查按钮标签上的String。这么做的问题在于,你很可能会把标签给搞错了,特别是大小写,而且这种bug是很难找的。

注意showOptionDialog( )和showInputDialog( )会返回用户输入的信息。

菜单
所有能包含菜单的组件,包括JApplet,JFrame,JDialog以及它们所派生的组件,都有一个需要JMenuBar作参数的setJMenuBar( )方法(一个组件只能有一个JMenuBar)。你可以把JMenu加入JMenuBar,再把JMenuItem加入JMenu。每个JMenuItem都可以连一个ActionListener,当你选中菜单项(menu item)的时候,事件就发出了。

与使用资源的系统不同,Java和Swing要求你必须用源代码来组装菜单。下面是一个非常简单的菜单例程:

//: c14:SimpleMenus.java
// <applet code=SimpleMenus width=200 height=75></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class SimpleMenus extends JApplet {
  private JTextField t = new JTextField(15);
  private ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      t.setText(((JMenuItem)e.getSource()).getText());
    }
  };
  private JMenu[] menus = {
    new JMenu("Winken"), new JMenu("Blinken"),
    new JMenu("Nod")
  };
  private JMenuItem[] items = {
    new JMenuItem("Fee"), new JMenuItem("Fi"),
    new JMenuItem("Fo"),  new JMenuItem("Zip"),
    new JMenuItem("Zap"), new JMenuItem("Zot"),
    new JMenuItem("Olly"), new JMenuItem("Oxen"),
    new JMenuItem("Free")
  };
  public void init() {
    for(int i = 0; i < items.length; i++) {
      items[i].addActionListener(al);
      menus[i % 3].add(items[i]);
    }
    JMenuBar mb = new JMenuBar();
    for(int i = 0; i < menus.length; i++)
      mb.add(menus[i]);
    setJMenuBar(mb);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
  }
  public static void main(String[] args) {
    Console.run(new SimpleMenus(), 200, 75);
  }
} ///:~
模运算符"i%3"的作用是为三个JMenu分配菜单项。每个JMenuItem都得有一个ActionListener;我们这里用的是同一个ActionListener,但通常情况下是每个JMenuItem都得有一个它自己的ActionListener。

JMenuItem是AbstractButton的派生类,所以它有一些类似按钮的行为。JMenuItem本身就是一个可以置入下拉菜单的菜单选项。此外JMenuItem还有三个派生类:用来持有其它JMenuItem的JMenu(这样你就可以做层叠式菜单了);有一个能表示是否被选中的"候选标记(checkmark)"的JCheckBoxMenuItem;以及包含一个radio button的JRadioButtonMenuItem。

下面我们举一个更复杂一些的例子,这次我们又要用冰激凌的口味来举例了。这个例子还演示了层叠式菜单,快捷键,JCheckBoxMenuItem,以及其他能动态改变菜单的方法:

//: c14:Menus.java
// Submenus, checkbox menu items, swapping menus,
// mnemonics (shortcuts) and action commands.
// <applet code=Menus width=300 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class Menus extends JApplet {
  private String[] flavors = {
    "Chocolate", "Strawberry", "Vanilla Fudge Swirl",
    "Mint Chip", "Mocha Almond Fudge", "Rum Raisin",
    "Praline Cream", "Mud Pie"
  };
  private JTextField t = new JTextField("No flavor", 30);
  private JMenuBar mb1 = new JMenuBar();
  private JMenu
    f = new JMenu("File"),
    m = new JMenu("Flavors"),
    s = new JMenu("Safety");
  // Alternative approach:
  private JCheckBoxMenuItem[] safety = {
    new JCheckBoxMenuItem("Guard"),
    new JCheckBoxMenuItem("Hide")
  };
  private JMenuItem[] file = { new JMenuItem("Open") };
  // A second menu bar to swap to:
  private JMenuBar mb2 = new JMenuBar();
  private JMenu fooBar = new JMenu("fooBar");
  private JMenuItem[] other = {
    // Adding a menu shortcut (mnemonic) is very
    // simple, but only JMenuItems can have them
    // in their constructors:
    new JMenuItem("Foo", KeyEvent.VK_F),
    new JMenuItem("Bar", KeyEvent.VK_A),
    // No shortcut:
    new JMenuItem("Baz"),
  };
  private JButton b = new JButton("Swap Menus");
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JMenuBar m = getJMenuBar();
      setJMenuBar(m == mb1 ? mb2 : mb1);
      validate(); // Refresh the frame
    }
  }
  class ML implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JMenuItem target = (JMenuItem)e.getSource();
      String actionCommand = target.getActionCommand();
      if(actionCommand.equals("Open")) {
        String s = t.getText();
        boolean chosen = false;
        for(int i = 0; i < flavors.length; i++)
          if(s.equals(flavors[i])) chosen = true;
        if(!chosen)
          t.setText("Choose a flavor first!");
        else
          t.setText("Opening " + s + ". Mmm, mm!");
      }
    }
  }
  class FL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JMenuItem target = (JMenuItem)e.getSource();
      t.setText(target.getText());
    }
  }
  // Alternatively, you can create a different
  // class for each different MenuItem. Then you
  // Don't have to figure out which one it is:
  class FooL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Foo selected");
    }
  }
  class BarL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Bar selected");
    }
  }
  class BazL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText("Baz selected");
    }
  }
  class CMIL implements ItemListener {
    public void itemStateChanged(ItemEvent e) {
      JCheckBoxMenuItem target =
        (JCheckBoxMenuItem)e.getSource();
      String actionCommand = target.getActionCommand();
      if(actionCommand.equals("Guard"))
        t.setText("Guard the Ice Cream! " +
          "Guarding is " + target.getState());
      else if(actionCommand.equals("Hide"))
        t.setText("Hide the Ice Cream! " +
          "Is it hidden? " + target.getState());
    }
  }
  public void init() {
    ML ml = new ML();
    CMIL cmil = new CMIL();
    safety[0].setActionCommand("Guard");
    safety[0].setMnemonic(KeyEvent.VK_G);
    safety[0].addItemListener(cmil);
    safety[1].setActionCommand("Hide");
    safety[1].setMnemonic(KeyEvent.VK_H);
    safety[1].addItemListener(cmil);
    other[0].addActionListener(new FooL());
    other[1].addActionListener(new BarL());
    other[2].addActionListener(new BazL());
    FL fl = new FL();
    for(int i = 0; i < flavors.length; i++) {
      JMenuItem mi = new JMenuItem(flavors[i]);
      mi.addActionListener(fl);
      m.add(mi);
      // Add separators at intervals:
      if((i + 1) % 3 == 0)
        m.addSeparator();
    }
    for(int i = 0; i < safety.length; i++)
      s.add(safety[i]);
    s.setMnemonic(KeyEvent.VK_A);
    f.add(s);
    f.setMnemonic(KeyEvent.VK_F);
    for(int i = 0; i < file.length; i++) {
      file[i].addActionListener(fl);
      f.add(file[i]);
    }
    mb1.add(f);
    mb1.add(m);
    setJMenuBar(mb1);
    t.setEditable(false);
    Container cp = getContentPane();
    cp.add(t, BorderLayout.CENTER);
    // Set up the system for swapping menus:
    b.addActionListener(new BL());
    b.setMnemonic(KeyEvent.VK_S);
    cp.add(b, BorderLayout.NORTH);
    for(int i = 0; i < other.length; i++)
      fooBar.add(other[i]);
    fooBar.setMnemonic(KeyEvent.VK_B);
    mb2.add(fooBar);
  }
  public static void main(String[] args) {
    Console.run(new Menus(), 300, 100);
  }
} ///:~
这里我把菜单项放到数组里面,然后遍历这个数组并且逐个调用JMenuItem的add( )方法。这使得添加和删除菜单项的任务变得不那么讨厌了。

为了演示在程序运行期间动态地交换菜单条,我们创建了两个JMenuBar。你会发现JMenuBar是由JMenu组成的,而JMenu又是由JMenuItem,JCheckBoxMenuItem甚至JMenu(它会创建子菜单)组成的。等JMenuBar组装完毕,你就可以用setJMenuBar( )方法把它安装到当前程序里面了。注意当你按下钮按的时候,它会用getJMenuBar( )来判断当前的菜单,然后把另一菜单换上去。

测试"Open"的时候要注意拼写和大小写,但是如果和"Open"不匹配,Java也并不报错。这种字符串的比较是引发编程错误的一大诱因。

程序会自动的将菜单项勾掉或恢复。JCheckBoxMenuItem的代码演示了两种怎样决定该勾掉哪个菜单项的方法。一个是匹配字符串(虽然也管用,但就像上面我们所讲的,不是很安全),另一个则是匹配事件目标对象。正如你所看到的,getState( )方法可以返回JCheckBoxMenuItem的状态,而setState( )则可以设置它的状态。

菜单事件不是很一致,这一点可能会引起混乱。JMenuItem使用ActionListener事件,而JCheckBoxMenuItem则使用ItemListener事件。JMenu也支持ActionListener,但通常没什么用。总之,你得为每个JMenuItem,JCheckBoxMenuItem或JRadioButtonMenuItem都制备一个listener,不过这里我们偷了点懒,把一个ItemListener和ActionListener连到多个菜单组件上了。

Swing支持助记符,或者说"快捷键",这样你就可以扔掉鼠标用键盘来选取AbstractButton了(按钮,菜单项等)。要这么做很容易,就拿JMenuItem举例,你可以用它重载了的构造函数,把快捷键的标识符当作第二个参数传给它。不过绝大多数AbstractButton都没有提供类似的构造函数,所以比较通用的办法还是用setMnemonic( )方法。在上述例程中我们为按钮和多个菜单项加上了快捷键,这些快捷键的提示会自动显示在组件上。

此外我们还演示了setActionCommand( )的用法。它可能有些奇怪,因为"action command"就是菜单上的标签。为什么不直接使用标签,而要用字符串来代替呢?问题在国际化。如果你想不修改源代码就让程序能运行在另一种语言环境下(修改源代码,毫无疑问会带来新的错误),只要改变菜单上的标签就行了。所以为了简化核对菜单字符串的代码,你可以让"action command"保持不变,这样菜单上的标签再怎么改也不要紧了。注意,这里我们没有逐个检查菜单的action command,所以那些不检查的,我们也就不设action command了。

绝大多数的事情都在监听器里完成。BL负责JMenuBar的切换。ML先找"谁在按铃",思路是先找出ActionEvent的源,然后把它转换成JMenuItem,接着它再把action command传给一串级联的if语句。

虽然FL要处理flavor菜单里的所有口味,但还是很简单。如果你的思路足够清晰的话,这个方法还是很管用的,但是通常情况下,你得为每个菜单项准备一个listener,就像FooL,BarL和BazL那样,这样也就用不着再去写探测代码了,而且你还能准确的知道到底是谁在调用那个listener。虽然这会使类的数量大大增加,但是每个类的代码却减少了,而且程序的流程也够"防呆"。

你会发现菜单代码很快就变得又长又乱了。这里又是GUI builder能大显身手的地方了。好工具应该能帮你维护好菜单。

弹出式菜单
要想实现JPopupMenu,最直截了当的办法就是创建一个继承MouseAdapter的内部类,然后把这内部类的实例加到要提供弹出式菜单的组件里:

//: c14:Popup.java
// Creating popup menus with Swing.
// <applet code=Popup width=300 height=200></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class Popup extends JApplet {
  private JPopupMenu popup = new JPopupMenu();
  private JTextField t = new JTextField(10);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    ActionListener al = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        t.setText(((JMenuItem)e.getSource()).getText());
      }
    };
    JMenuItem m = new JMenuItem("Hither");
    m.addActionListener(al);
    popup.add(m);
    m = new JMenuItem("Yon");
    m.addActionListener(al);
    popup.add(m);
    m = new JMenuItem("Afar");
    m.addActionListener(al);
    popup.add(m);
    popup.addSeparator();
    m = new JMenuItem("Stay Here");
    m.addActionListener(al);
    popup.add(m);
    PopupListener pl = new PopupListener();
    addMouseListener(pl);
    t.addMouseListener(pl);
  }
  class PopupListener extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      maybeShowPopup(e);
    }
    public void mouseReleased(MouseEvent e) {
      maybeShowPopup(e);
    }
    private void maybeShowPopup(MouseEvent e) {
      if(e.isPopupTrigger())
        popup.show(((JApplet)e.getComponent())
          .getContentPane(), e.getX(), e.getY());
    }
  }
  public static void main(String[] args) {
    Console.run(new Popup(), 300, 200);
  }
} ///:~
JMenuItem用的是同一个ActionListener,它负责从菜单标签里面提文本并且把它插入JTextField。

画屏幕
一个好的GUI框架能让作图相对而言比较简单——确实如此,Swing就做到了。所有作图问题都面临同一个难点,那就是相比调用画图函数,计算该在哪里画东西通常会更棘手,但不幸的是这种计算又常常和作图函数的调用混在一起,所以作图函数的接口的实际的复杂程度很可能会比你认为的要简单。

长话短说,考虑一下怎样把数据画在屏幕上。假定我们用Java内置的Math.sin( )函数,也就是正弦函数来提供数据。为了提高大家的兴趣,同时也演示一下Swing组件的易用性,我们在表单的底边放一个slider,用它来动态地控制正弦波的周期数。此外你会发现,如果窗口的大小改变了,正弦波也会根据新的窗口的大小自动调整其形状。

虽然你可以在任何一个JComponent上作画,也就是说它们都能充当画布(canvas),但是如果你想要一块能直接画东西的白板,最好还是创建一个继承JPanel的类。这样你只需要覆写一个方法,也就是paintComponent( )就行了。当系统需要重画组件的时候,会自动调用这个方法(通常情况下,你不必为此操心,因为这是由Swing控制的)。调用的时候,Swing会传一个Graphics对象给这个方法,这样你就能用这个对象作画了。

在下面这个例子里,所有与作图相关的信息都包含在SineDraw类里;SineWave只负责配置程序和slider。SineDraw的setCycles( )方法是个钩子,它能让其它对象——这里就是slider——控制周期数。

//: c14:SineWave.java
// Drawing with Swing, using a JSlider.
// <applet code=SineWave width=700 height=400></applet>
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
class SineDraw extends JPanel {
  private static final int SCALEFACTOR = 200;
  private int cycles;
  private int points;
  private double[] sines;
  private int[] pts;
  public SineDraw() { setCycles(5); }
  public void setCycles(int newCycles) {
    cycles = newCycles;
    points = SCALEFACTOR * cycles * 2;
    sines = new double[points];
    for(int i = 0; i < points; i++) {
      double radians = (Math.PI/SCALEFACTOR) * i;
      sines[i] = Math.sin(radians);
    }
    repaint();
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    int maxWidth = getWidth();
    double hstep = (double)maxWidth/(double)points;
    int maxHeight = getHeight();
    pts = new int[points];
    for(int i = 0; i < points; i++)
      pts[i] =
        (int)(sines[i] * maxHeight/2 * .95 + maxHeight/2);
    g.setColor(Color.RED);
    for(int i = 1; i < points; i++) {
      int x1 = (int)((i - 1) * hstep);
      int x2 = (int)(i * hstep);
      int y1 = pts[i-1];
      int y2 = pts[i];
      g.drawLine(x1, y1, x2, y2);
    }
  }
}
public class SineWave extends JApplet {
  private SineDraw sines = new SineDraw();
  private JSlider adjustCycles = new JSlider(1, 30, 5);
  public void init() {
    Container cp = getContentPane();
    cp.add(sines);
    adjustCycles.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        sines.setCycles(
          ((JSlider)e.getSource()).getValue());
      }
    });
    cp.add(BorderLayout.SOUTH, adjustCycles);
  }
  public static void main(String[] args) {
    Console.run(new SineWave(), 700, 400);
  }
} ///:~
在计算正弦波的各个点时,所有的字段和数组都派上了用场;cycles表示要显示几个周期的正弦波,points表示要画几个点,sines把正弦函数的值存起来,而pts则把JPanel上各点的y坐标存起来。setCycles( )方法会根据需要几个点来创建数组,然后逐项计算sines数组的值。setCycles( )能通过repaint( )方法强制调用paintComponent( ),这样剩下的工作就水到渠成了。

覆写paintComponent( )的时候,必须首先调用其基类的同名方法。接下来再做什么就由你了决定了;通常是调用Graphics的方法在JPanel上画画或者是在其象素上着色。要想了解具体怎样使用这些方法,可以查阅java.awt.Graphics文档(可以到java.sun.com上面去找JDK文档)。你会发现,这里绝大多数代码都在做计算;真正控制屏幕输出的代码实际上只有两行,setColor( )和drawLine( )。将来你自己写这种程序的时候很有可能也会有类似的感觉;绝大多数时间都是画在确定该画些什么,而真正的作图过程却很简单。

我写这个程序的时候,绝大多数时间都花在该如何显示正弦波上。做完之后我发现如果能动态改变周期数的话,应该很不错。我知道,如果用其它编程语言的话,这个难度是相当大的,所以我有些犹豫。但想不到的是这竟是这个项目里最简单的一部分。我创建了一个JSlider(参数分别是JSlider移到最左边时表示什么值,最右边时表示什么值以及最初位置表示什么值,但除此之外它还有其它构造函数),然后把它放到JApplet的最下边。然后我翻了一下JDK文档,发现它只有一个监听器addChangeListener,当slider的变动大到足以产生一个新的值时,它就被触发了。它只有一个方法,名字也很好认,叫stateChanged( )。这个方法需要一个ChangeEvent对象,这样我就可以找到事件的源,并且发现新的值是多少了。接下来只要调用用sines的setCycles( )就能把这个值装进JPanel并且重画图形了。

总之,你会发现绝大多数Swing问题都可以通过类似的步骤加以解决,而且通常情况下这一步骤还相当简单,哪怕你从没用过这个组件。

如果问题非常复杂,那么还有一些更复杂的解决方案,比如第三方的JavaBean或者Java 2D API。这些内容已经超出了本书的范围,但是如果你的作图代码显得非常繁杂的话,应该去查查。

对话框
所谓对话框是指,能从其他窗口里面弹出来的窗口。其作用是,在不搞乱原先窗口前提下,具体处理其某一部分的细节问题。对话框在GUI编程中的用途很广,但是在applet中用的不多。

要想创建对话框,你得先继承JDialog。和JFrame一样,JDialog也是另一种Window,它也有布局管理器(默认情况下是BorderLayout),也可以用事件监听器来处理事件。它同JFrame有一个重要的区别,那就是在关对话框的时候别把程序也关了。相反你得用dispose( )方法将对话框窗口占用的资源全部释放出来。下面就举一个简单的例子:

//: c14:Dialogs.java
// Creating and using Dialog Boxes.
// <applet code=Dialogs width=125 height=75></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
class MyDialog extends JDialog {
  public MyDialog(JFrame parent) {
    super(parent, "My dialog", true);
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(new JLabel("Here is my dialog"));
    JButton ok = new JButton("OK");
    ok.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        dispose(); // Closes the dialog
      }
    });
    cp.add(ok);
    setSize(150,125);
  }
}
public class Dialogs extends JApplet {
  private JButton b1 = new JButton("Dialog Box");
  private MyDialog dlg = new MyDialog(null);
  public void init() {
    b1.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        dlg.show();
      }
    });
    getContentPane().add(b1);
  }
  public static void main(String[] args) {
    Console.run(new Dialogs(), 125, 75);
  }
} ///:~
一旦创建完JDialog,你就得用show( )来显示和激活它了。关闭对话框的时候,还得记住要dispose( )。

你会发现,对applet来说,包括对话框在内所有弹出的东西都是"不可信任的"。也就是说弹出来的窗口里面有一个警告。这是因为,从理论上讲恶意代码可以利用这个功能来愚弄用户,让他们觉得自己是在运行一个本地应用程序,然后误导它们输入自己的信用卡号码,再通过Web传出去。applet总是和网页联在一起,因此只能用浏览器运行,但是对话框却可以脱离网页,所以从理论上讲这种欺骗手段是成立的。所以这么一来,applet就不太会用到对话框了。

下面我们举一个复杂一点的例子;先定义一种名为ToeButton的特殊按钮,然后用它所组成的格子来创建对话框(用GridLayout)。这种按钮会在其边缘画一个框,并且根据其所处的状态显示空白,"x"或者"o"。刚开始的时候,格子是空白的,当你点击它的时候,它会根据现在是该"x"还是"o"下,来修改其状态。但是,当你按下按钮的时候,它还会在"x"和"o"状态下相互切换。(这比tie-tac-toe的概念更进了一步。)此外,这个对话框里的格子的行和列的数量还可以根据主窗口的参数调整。

//: c14:TicTacToe.java
// Dialog boxes and creating your own components.
// <applet code=TicTacToe width=200 height=100></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class TicTacToe extends JApplet {
  private JTextField
    rows = new JTextField("3"),
    cols = new JTextField("3");
  private static final int BLANK = 0, XX = 1, OO = 2;
  class ToeDialog extends JDialog {
    private int turn = XX; // Start with x's turn
    ToeDialog(int cellsWide, int cellsHigh) {
      setTitle("The game itself");
      Container cp = getContentPane();
      cp.setLayout(new GridLayout(cellsWide, cellsHigh));
      for(int i = 0; i < cellsWide * cellsHigh; i++)
        cp.add(new ToeButton());
      setSize(cellsWide * 50, cellsHigh * 50);
      setDefaultCloseOperation(DISPOSE_ON_CLOSE);
    }
    class ToeButton extends JPanel {
      private int state = BLANK;
      public ToeButton() { addMouseListener(new ML()); }
      public void paintComponent(Graphics g) {
        super.paintComponent(g);
        int
          x1 = 0, y1 = 0,
          x2 = getSize().width - 1,
          y2 = getSize().height - 1;
        g.drawRect(x1, y1, x2, y2);
        x1 = x2/4;
        y1 = y2/4;
        int wide = x2/2, high = y2/2;
        if(state == XX) {
          g.drawLine(x1, y1, x1 + wide, y1 + high);
          g.drawLine(x1, y1 + high, x1 + wide, y1);
        }
        if(state == OO)
          g.drawOval(x1, y1, x1 + wide/2, y1 + high/2);
      }
      class ML extends MouseAdapter {
        public void mousePressed(MouseEvent e) {
          if(state == BLANK) {
            state = turn;
            turn = (turn == XX ? OO : XX);
          }
          else
            state = (state == XX ? OO : XX);
          repaint();
        }
      }
    }
  }
  class BL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JDialog d = new ToeDialog(
        Integer.parseInt(rows.getText()),
        Integer.parseInt(cols.getText()));
      d.setVisible(true);
    }
  }
  public void init() {
    JPanel p = new JPanel();
    p.setLayout(new GridLayout(2,2));
    p.add(new JLabel("Rows", JLabel.CENTER));
    p.add(rows);
    p.add(new JLabel("Columns", JLabel.CENTER));
    p.add(cols);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.NORTH);
    JButton b = new JButton("go");
    b.addActionListener(new BL());
    cp.add(b, BorderLayout.SOUTH);
  }
  public static void main(String[] args) {
    Console.run(new TicTacToe(), 200, 100);
  }
} ///:~
由于static只能用于宿主类,因此内部类里不能再有static的数据或是嵌套类了。

paintComponent( )负责把panel的周围的方框以及"x"或"o"画出来。虽然充斥着单调的计算,但是还算简明。

MouseListener负责捕捉鼠标的点击,它首先看panel上是否有东西。如果没有,它就去查看ToeButton的状态,也就是去问父窗口该谁下。 基于内部类的机制,ToeButton可以访问并且修改宿主类的turn。如果按钮已经显示"x"或"o"了,那么切换状态。从这段代码里,你可以体会到我们第三章讲的if-else的三元表达式的好处了。状态改完之后,再刷新ToeButton。

ToeDialog的构造函数相当简单;根据你给出的参数把按钮加到GridLayout里面,然后将每个按钮设成50象素的见方。

最后TicTacToe创建了两个JTextField(用来输入按钮格的行和列)和一个"go"按钮,并且用这个按钮的ActionListener完成了整个程序。当你按下按钮的时候,监听器读取JTextField里面的数据。由于它是String类型的,因此你得先用static Integer.parseInt( )把它们转换成int。

文件对话框
有些操作系统还内置了一些特殊的对话框,比如让你选择字体,颜色,打印机之类的对话框。实际上所有的图形操作系统都提供了打开和存储文件的对话框,所以为了简化起鉴,Java把它们都封装到JFileChooser里面了。

下面这段程序演示了两种JFileChooser对话框,一个用来打开文件的,另一个用来存储文件。绝大多数的代码都已经是老熟人了,真正有意思的东西都集中在那两个按钮的事件监听器上:

//: c14:FileChooserTest.java
// Demonstration of File dialog boxes.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class FileChooserTest extends JFrame {
  private JTextField
    filename = new JTextField(),
    dir = new JTextField();
  private JButton
    open = new JButton("Open"),
    save = new JButton("Save");
  public FileChooserTest() {
    JPanel p = new JPanel();
    open.addActionListener(new OpenL());
    p.add(open);
    save.addActionListener(new SaveL());
    p.add(save);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.SOUTH);
    dir.setEditable(false);
    filename.setEditable(false);
    p = new JPanel();
    p.setLayout(new GridLayout(2,1));
    p.add(filename);
    p.add(dir);
    cp.add(p, BorderLayout.NORTH);
  }
  class OpenL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JFileChooser c = new JFileChooser();
      // Demonstrate "Open" dialog:
      int rVal = c.showOpenDialog(FileChooserTest.this);
      if(rVal == JFileChooser.APPROVE_OPTION) {
        filename.setText(c.getSelectedFile().getName());
        dir.setText(c.getCurrentDirectory().toString());
      }
      if(rVal == JFileChooser.CANCEL_OPTION) {
        filename.setText("You pressed cancel");
        dir.setText("");
      }
    }
  }
  class SaveL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JFileChooser c = new JFileChooser();
      // Demonstrate "Save" dialog:
      int rVal = c.showSaveDialog(FileChooserTest.this);
      if(rVal == JFileChooser.APPROVE_OPTION) {
        filename.setText(c.getSelectedFile().getName());
        dir.setText(c.getCurrentDirectory().toString());
      }
      if(rVal == JFileChooser.CANCEL_OPTION) {
        filename.setText("You pressed cancel");
        dir.setText("");
      }
    }
  }
  public static void main(String[] args) {
    Console.run(new FileChooserTest(), 250, 110);
  }
} ///:~
注意JFileChooser有很多变例可供选择,比方说加一个过滤器滤文件名之类的。

要用"open file"对话框就调用showOpenDialog( ),要用"save file"对话框,就调用showSaveDialog( )。在对话框关闭之前,这两个函数是不会返回的。即便对话框关了,JFileChooser对象仍然还在,所以你还去读它的数据。要想知道操作的结果,可以用getSelectedFile( )和getCurrentDirectory( )。如果返回null则说明用户按了cancel。

Swing组件上的HTML
所有能显示文件的组件都可以按照HTML的规则显示HTML的文本。也就是说你可以很方便地让Swing组件显示很炫的文本。比如:

//: c14:HTMLButton.java
// Putting HTML text on Swing components.
// <applet code=HTMLButton width=250 height=500></applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;
public class HTMLButton extends JApplet {
  private JButton b = new JButton(
    "<html><b><font size=+2>" +
    "<center>Hello!<br><i>Press me now!");
  public void init() {
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        getContentPane().add(new JLabel("<html>" +
          "<i><font size=+4>Kapow!"));
        // Force a re-layout to include the new label:
        validate();
      }
    });
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(b);
  }
  public static void main(String[] args) {
    Console.run(new HTMLButton(), 200, 500);
  }
} ///:~
文本必须以"<html>"开头,下面你就可以用普通的HTML标记了。注意,它没有强制你一定要关闭标记。

ActionListener给表单加了个新的,也显示HTML文本的JLabel。但是这个label不是在init( )里面加的,所以你必须调用Container的validate( )方法,强制它为组件重新排版(于是也就把这个新标签显示出来了)。

JTabbedPane,JMenuItem,JToolTip,JRadioButton以及JCheckBox都支持HTML文本。

Slider和进程条
Slider(我们已经在SineWave.java里面用过了)能让用户通过来回移动一个点来输入数据,有时这种做法还是很直观的(比方说调节音量)。进程条(progress bar)则以一种用类比的方式显示数据,它表示数据是"全部"还是"空的",这样用户就能有一个比较全面的了解了。要给这两样东西举例,我最喜欢做法是将Slider和进程条合起来,这样当你移动Slider的时候,进程条也相应的改变了:

//: c14:Progress.java
// Using progress bars and sliders.
// <applet code=Progress width=300 height=200></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.border.*;
import com.bruceeckel.swing.*;
public class Progress extends JApplet {
  private JProgressBar pb = new JProgressBar();
  private JSlider sb =
    new JSlider(JSlider.HORIZONTAL, 0, 100, 60);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(2,1));
    cp.add(pb);
    sb.setValue(0);
    sb.setPaintTicks(true);
    sb.setMajorTickSpacing(20);
    sb.setMinorTickSpacing(5);
    sb.setBorder(new TitledBorder("Slide Me"));
    pb.setModel(sb.getModel()); // Share model
    cp.add(sb);
  }
  public static void main(String[] args) {
    Console.run(new Progress(), 300, 200);
  }
} ///:~
要把这两样东西合起来,让它们表示同一个数据,关键是下面这行代码:

pb.setModel(sb.getModel());
当然你也可以用Listener来控制这两个组件,但是对于一些比较简单的问题,上述做法更直截了当。

JProgressBar还比较简单,而JSlider的选项就比较多了,比如摆放的方向,大小刻度等等。注意一下给Slider加带抬头的边框的那行代码,多简洁。


JTree的用法可以简单到只有下面这行代码:

add(new JTree(new Object[] {"this", "that", "other"}));
这样显示的是一棵最基本的树。JTree的API非常庞大,应该是Swing类库里最大的之一了。虽然你可以用它来做任何事情,但是要想完成比较复杂任务,就需要一定的研究和实验了。

好在这个类库还提供了变通手段,也就是一个"默认"的,能满足一般需求的树型组件。所以绝大多数情况下你都可以使用这个组件,只有在特殊情况下,你才需要去深入研究树。

下例我们用"默认"的树在applet里面显示一棵树。你按下按钮的时候,当前选定的节点下面就会生出一棵新的子树(如果没有选定任何节点,那么新的子树会加到根节点上):

//: c14:Trees.java
// Simple Swing tree. Trees can be vastly more complex.
// <applet code=Trees width=250 height=250></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.tree.*;
import com.bruceeckel.swing.*;
// Takes an array of Strings and makes the first
// element a node and the rest leaves:
class Branch {
  private DefaultMutableTreeNode r;
  public Branch(String[] data) {
    r = new DefaultMutableTreeNode(data[0]);
    for(int i = 1; i < data.length; i++)
      r.add(new DefaultMutableTreeNode(data[i]));
  }
  public DefaultMutableTreeNode node() { return r; }
}
public class Trees extends JApplet {
  private String[][] data = {
    { "Colors", "Red", "Blue", "Green" },
    { "Flavors", "Tart", "Sweet", "Bland" },
    { "Length", "Short", "Medium", "Long" },
    { "Volume", "High", "Medium", "Low" },
    { "Temperature", "High", "Medium", "Low" },
    { "Intensity", "High", "Medium", "Low" },
  };
  private static int i = 0;
  private DefaultMutableTreeNode root, child, chosen;
  private JTree tree;
  private DefaultTreeModel model;
  public void init() {
    Container cp = getContentPane();
    root = new DefaultMutableTreeNode("root");
    tree = new JTree(root);
    // Add it and make it take care of scrolling:
    cp.add(new JScrollPane(tree), BorderLayout.CENTER);
    // Capture the tree's model:
    model =(DefaultTreeModel)tree.getModel();
    JButton test = new JButton("Press me");
    test.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        if(i < data.length) {
          child = new Branch(data[i++]).node();
          // What's the last one you clicked?
          chosen = (DefaultMutableTreeNode)
            tree.getLastSelectedPathComponent();
          if(chosen == null)
            chosen = root;
          // The model will create the appropriate event.
          // In response, the tree will update itself:
          model.insertNodeInto(child, chosen, 0);
          // Puts the new node on the chosen node.
        }
      }
    });
    // Change the button's colors:
    test.setBackground(Color.BLUE);
    test.setForeground(Color.WHITE);
    JPanel p = new JPanel();
    p.add(test);
    cp.add(p, BorderLayout.SOUTH);
  }
  public static void main(String[] args) {
    Console.run(new Trees(), 250, 250);
  }
} ///:~
第一个类,Branch的作用是,用一个String数组来构建一个DefaultMutableTreeNode,它用数组的第一个String作根节点,其它String做叶节点。接下来就可以用node( )来返回这个"branch"的根节点了。

Trees类包含一个用来创建多个Branch的两维String数组,以及一个用来给数组定位的static int i。节点放在DefaultMutableTreeNode里面,但是实际在屏幕上显示则是由JTree及与之相的model——DefaultTreeModel控制的。注意JTree在加入applet之前,先套了一件JScrollPane,这样它就能提供自动的滚动轴了。

对JTree的操控是经由它的model来实现的。当model发生变化时,它会产生一个事件,让JTree对树的显示作出必要的更新。init( )用getModel( )提取这个model。当你按下按钮的时候,它会创建一个新的新的"branch" 。等它找到当前选中的那个节点(如果什么也没选,就用根节点)之后,model的insertNodeInto( )方法就会接管所有的任务了,包括修改树,刷新显示等等。

或许上述例程已能满足你的需求了。但是树的功能强大到只要你能想到它就能做到的地步——你可以把上述例程里的所有"default"全都替换成你自己的类,以实现新的功能。但是要知道:差不多每个类都有一个非常庞大的接口,所以你要花很多时间和精力去理解它的内部构造。但话说回来,它的设计还是很优秀的,其竞争者往往更糟。

表格
和树一样,Swing的表格控件也非常复杂强大。刚开始的时候,他们是想把它做成用JDBC(JDBC会在Thinking in Enterprise Java作讨论)连接数据库时常用的"grid"接口,因此它具有极高的灵活性,不过代价就是复杂度了。它能让你轻易创建一个全功能的电子表格程序,不过这要花整整一本书篇幅才能讲清楚。但是如果你弄懂了基本原理,也可以用它来创建一个相对简单的JTable。

JTable只负责怎样显示数据,而数据本身是由TableModel控制的。所以在创建JTable之前,你通常都得先创建一个TableModel。你可以从头开始去实现TableModel接口,但是Java提供了一个AbstractTableModel的帮助类,继承它会比较简单。

//: c14:JTableDemo.java
// Simple demonstration of JTable.
// <applet code=Table width=350 height=200></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.table.*;
import javax.swing.event.*;
import com.bruceeckel.swing.*;
public class JTableDemo extends JApplet {
  private JTextArea txt = new JTextArea(4, 20);
  // The TableModel controls all the data:
  class DataModel extends AbstractTableModel {
    Object[][] data = {
      {"one", "two", "three", "four"},
      {"five", "six", "seven", "eight"},
      {"nine", "ten", "eleven", "twelve"},
    };
    // Prints data when table changes:
    class TML implements TableModelListener {
      public void tableChanged(TableModelEvent e) {
        txt.setText(""); // Clear it
        for(int i = 0; i < data.length; i++) {
          for(int j = 0; j < data[0].length; j++)
            txt.append(data[i][j] + " ");
          txt.append("/n");
        }
      }
    }
    public DataModel() { addTableModelListener(new TML());}
    public int getColumnCount() { return data[0].length; }
    public int getRowCount() { return data.length; }
    public Object getValueAt(int row, int col) {
      return data[row][col];
    }
    public void setValueAt(Object val, int row, int col) {
      data[row][col] = val;
      // Indicate the change has happened:
      fireTableDataChanged();
    }
    public boolean isCellEditable(int row, int col) {
      return true;
    }
  }
  public void init() {
    Container cp = getContentPane();
    JTable table = new JTable(new DataModel());
    cp.add(new JScrollPane(table));
    cp.add(BorderLayout.SOUTH, txt);
  }
  public static void main(String[] args) {
    Console.run(new JTableDemo(), 350, 200);
  }
} ///:~
DataModel的数据存在数组里,不过你也可以从其它地方获取数据,比如数据库。 构造函数给DataModel加了一个TableModelListener,让它在表格发生变化的时候把数组打印出来。其他方法都遵循Bean的命名规范(用get和set,我们以后会讲)。当JTable要把DataModel里面的数据显示出来的时候就需要用到这些方法了。AbstractTableModel提供了默认的setValueAt( )和isCellEditable( )方法,因此你就不能再修改数据,如果你想要修改数据,就必须覆写这两个方法。

等做好TableModel,你就可以把它交给JTable的构造函数了。它会自动接管所有显示刷新之类的细节。我们还是照例把JTable嵌到JScrollPane里面。

选择Look & Feel
所谓"可插接式的外观风格(look & feel)"是指,你可以让程序模拟其他操作环境的外观。你甚至可以做一些更炫的事情,比如在程序运行期间动态改变其外观。但是通常情况下,你只会在下面两项中选一个:选择"跨平台"的外观(也就是Swing的"metal"),或者选当前操作系统的外观,让Java程序看上去就像是为这个操作系统定制的(绝大多数情况下,这几乎是勿庸置疑的选择,这样用户就不至于被搞糊涂了)。不管你选哪个,代码都很简单,但是必须先执行这些代码再创建组件,因为组件是按照当前的look and feel创建的,而且程序运行到一半的时候,你再去改look and feel,它就不会跟着你去改了。(这个过程非常复杂,而且并不实用,所以我们把它留给Swing专著了)。

实际上如果你认为跨平台的("metal")外观是Swing程序的特色,而你也想用它,那你就可以什么都不作了——它是默认的look and feel。但是如果你选择当前操作系统的外观风格,那么只要插入下面这段代码就可以了,一般来说是放在main( )开头的地方,但是最晚要在加第一个组件之前:

try {
  UIManager.setLookAndFeel(UIManager.
    getSystemLookAndFeelClassName());
} catch(Exception e) {
  throw new RuntimeException(e);
}
你根本不用在catch里面做任何事,因为如果选择其他look and feel失败的话,UIManager会回到默认的跨平台的look and feel。但是在调试的时候这个异常还是很起作用的,最起码你可以从catch里看到些什么。

下面是一个用命令行参数来选择look and feel的程序,顺便也看看这几个组件在不同的look and feel下都是什么样子:

//: c14:LookAndFeel.java
// Selecting different looks & feels.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
public class LookAndFeel extends JFrame {
  private String[] choices = {
    "eeny","meeny","Minnie","Mickey","Moe","Larry","Curly"
  };
  private Component[] samples = {
    new JButton("JButton"),
    new JTextField("JTextField"),
    new JLabel("JLabel"),
    new JCheckBox("JCheckBox"),
    new JRadioButton("Radio"),
    new JComboBox(choices),
    new JList(choices),
  };
  public LookAndFeel() {
    super("Look And Feel");
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < samples.length; i++)
      cp.add(samples[i]);
  }
  private static void usageError() {
    System.out.println(
      "Usage:LookAndFeel [cross|system|motif]");
    System.exit(1);
  }
  public static void main(String[] args) {
    if(args.length == 0) usageError();
    if(args[0].equals("cross")) {
      try {
        UIManager.setLookAndFeel(UIManager.
          getCrossPlatformLookAndFeelClassName());
      } catch(Exception e) {
        e.printStackTrace();
      }
    } else if(args[0].equals("system")) {
      try {
        UIManager.setLookAndFeel(UIManager.
          getSystemLookAndFeelClassName());
      } catch(Exception e) {
        e.printStackTrace();
      }
    } else if(args[0].equals("motif")) {
      try {
        UIManager.setLookAndFeel("com.sun.java."+
          "swing.plaf.motif.MotifLookAndFeel");
      } catch(Exception e) {
        e.printStackTrace();
      }
    } else usageError();
    // Note the look & feel must be set before
    // any components are created.
    Console.run(new LookAndFeel(), 300, 200);
  }
} ///:~
你可以明确地用字符串来指定look and feel,就像MotifLookAndFeel那样。但是只有它和"metal"才能真正用于所有平台上;虽然Java也提供了Windows和Macintosh的look and feel的字符串,但这两种外观只能用于他们自己的平台上(当你在这两个平台上调用getSystemLookAndFeelClassName时就能得到这个look and feel了)。

假如你为一个对程序外观有特殊要求的公司做一个framework的话,你甚至可以自创一套look and feel。不过这可是一个大工程,其难度远远超出了本书的范围(实际上它甚至超出了许多Swing专著的范围!)。

剪贴板
JFC与系统剪贴板的互动功能非常有限(在java.awt.datatransfer package里面)。你可以把String对象当作文本复制到剪贴板里,也可以把剪贴板里的文本粘贴到String对象里。当然剪贴板支持任何类型的数据,至于数据在剪贴板里该怎么表示,那是粘贴数据的程序的事。Java通过"flavor"这个概念加强了剪贴板API的扩展性。当数据进到剪贴板的时候还跟着一组与这个数据相关联的,可以转换这些数据的flavor(比方说一幅画可以表示成一个全部有数字组成的字符串或一个image),这样你就能知道剪贴板里的数据是否支持你感兴趣的flavor了。

下面我们来举一个在JTextArea里面剪切,复制和粘贴String的例子。你会发现我们平常用的剪切,复制和粘贴文本的快捷键这里也能用。不过如果你在其他程序里试过JTextField和JTextArea的话,你就会发现它们原本就已经支持剪贴板了。我们这里只是把代码写了出来,如果你要把剪贴板里的文字贴到JTextComponet以外的组件上,可以借用这里的技巧。

//: c14:CutAndPaste.java
// Using the clipboard.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import com.bruceeckel.swing.*;
public class CutAndPaste extends JFrame  {
  private JMenuBar mb = new JMenuBar();
  private JMenu edit = new JMenu("Edit");
  private JMenuItem
    cut = new JMenuItem("Cut"),
    copy = new JMenuItem("Copy"),
    paste = new JMenuItem("Paste");
  private JTextArea text = new JTextArea(20, 20);
  private Clipboard clipbd =
    getToolkit().getSystemClipboard();
  public CutAndPaste()  {
    cut.addActionListener(new CutL());
    copy.addActionListener(new CopyL());
    paste.addActionListener(new PasteL());
    edit.add(cut);
    edit.add(copy);
    edit.add(paste);
    mb.add(edit);
    setJMenuBar(mb);
    getContentPane().add(text);
  }
  class CopyL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String selection = text.getSelectedText();
      if(selection == null)
        return;
      StringSelection clipString =
        new StringSelection(selection);
      clipbd.setContents(clipString,clipString);
    }
  }
  class CutL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String selection = text.getSelectedText();
      if(selection == null)
        return;
      StringSelection clipString =
        new StringSelection(selection);
      clipbd.setContents(clipString, clipString);
      text.replaceRange("", text.getSelectionStart(),
        text.getSelectionEnd());
    }
  }
  class PasteL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      Transferable clipData =
        clipbd.getContents(CutAndPaste.this);
      try {
        String clipString = (String)clipData.
          getTransferData(DataFlavor.stringFlavor);
        text.replaceRange(clipString,
          text.getSelectionStart(),text.getSelectionEnd());
      } catch(Exception ex) {
        System.err.println("Not String flavor");
      }
    }
  }
  public static void main(String[] args) {
    Console.run(new CutAndPaste(), 300, 200);
  }
} ///:~
现在创建菜单和JTextArea应该很轻松了吧。所不同的是,这里我们用Toolkit创建了一个Clipboard类型的clipbd字段。

所有重要的东西都放在listener里面。除了最后一行,CutL和CopyL完全相同。这里有两行比较特别,就是用String创建StringSelection和用StringSelection调用setContents( )的那两行。就这些了,现在String已经被放到剪贴板里了。

PasteL用getContents( )把数据从剪贴板里拿出来。它返回的是一个比较陌生的Transferable对象,实际上你也不知道里面究竟是什么。有一个办法可以知道里面有什么,就是用getTransferDataFlavors( )方法。它会返回一个DataFlavor的数组,而这些flavor会告诉你这个对象究竟表示哪种数据。你也可以直接传一个你感兴趣的flavor给isDataFlavorSupported( )去问它。不过这里我们用了一个很大胆的方案:假定这个对象支持String flavor,直接调用getTransferData( ),如果错了,异常处理自会接管。

可以期待,未来Java会提供更多的flavor。你能得到更多的数据flavor的支持。

将applet打成JAR卷宗
JAR的一个主要用途就是优化applet的装载。在Java 1.0时代,程序员们都尽量把applet的代码塞进一个类里,这样当用户下载applet的时候只需向服务器发一次请求就可以了。但这么做不仅使代码变得非常难读(也难维护),而且.class文件也是未经压缩的,因此下载速度仍有提升的潜力。

JAR解决了这个问题,它把所有的.class文件全都压缩在一个供浏览器下载的文件里。现在你可以大胆运用正确的设计方案而不用再担心它会产生多少.class文件了,而用户的下载速度也更快了。

就讲TicTacToe.java吧。它看上去只有一个类,但实际上却包含了五个内部类,所以总共有六个类。一旦编译成功,你就可用下面这条命令把它压缩成一个JAR文件:

jar cf TicTacToe.jar *.class
这里假定当前目录下的所有.class文件都是由TicTacToe.java编译生成的(否则卷宗里就会多出点东西了)。

接下来你就得创建一个HTML页面了,这里要用archive标记来标注JAR文件。下面是最基本的applet标记:

<head><title>TicTacToe Example Applet
</title></head>
<body>
<applet code=TicTacToe.class
        archive=TicTacToe.jar
        width=200 height=100>
</applet>
</body>
你最好还是用JDK自带的HTMLconverter程序运行一遍这个文件。

签发applet
由于沙箱安全模型的限制,未获签名的applet是不能在客户端进行某些操作的,比如写文件,连接本地网络等。[83]一旦你签发了applet,用户就可以去核对那个自称创建了这个applet的人是不是真的就是创建者了,同时他们也可以确认JAR文件是不是在离开服务器之后被篡改了。没有这些最起码的保证,applet是根本不可能去做任何可能损坏计算机或泄漏个人隐私的事的。这层限制对于applet在Internet上的安全运用是至关重要的,但同时也削弱了applet的功能。

自从有了Java Plugin,签发applet的步骤也变得更简单也更标准化了,而applet也成为一种更简便的部署应用程序的方法了。签发applet已经变得非常简单了,而且也有了标准的Java工具了。

早先plugin还没出来的时候,你得用Netscape的工具为Netscape的用户签署.jar文件,用Microsoft的工具为Internet Explorer用户签署.cab文件,然后在HTML文件里面为两个平台各自准备一套标记。而用户也必须在浏览器里安装证书,这样applet才能获得信任。

Plugin不仅提供了标准化的签署和部署applet的方法,而且能自动安装证书,方便了用户。

设想有这么一个applet,它要能访问客户端的文件系统并且读写几个文件。这和FileChooserTest.java很相似,只是这是一个applet,所以要想能打开JFileChooser的对话框的话,它必须是一个经过签名的JAR文件。否则showOpenDialog( )的时候会抛出一个SecurityException。

//: c14:signedapplet:FileAccessApplet.java
// Demonstration of File dialog boxes.
package c14.signedapplet;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import com.bruceeckel.swing.*;
public class FileAccessApplet extends JApplet {
  private JTextField
    filename = new JTextField(),
    dir = new JTextField();
  private JButton
    open = new JButton("Open"),
    save = new JButton("Save");
  private JEditorPane ep = new JEditorPane();
  private JScrollPane jsp = new JScrollPane();
  private File file;
  public void init() {
    JPanel p = new JPanel();
    open.addActionListener(new OpenL());
    p.add(open);
    save.addActionListener(new SaveL());
    p.add(save);
    Container cp = getContentPane();
    jsp.getViewport().add(ep);
    cp.add(jsp, BorderLayout.CENTER);
    cp.add(p, BorderLayout.SOUTH);
    dir.setEditable(false);
    save.setEnabled(false);
    ep.setContentType("text/html");
    filename.setEditable(false);
    p = new JPanel();
    p.setLayout(new GridLayout(2, 1));
    p.add(filename);
    p.add(dir);
    cp.add(p, BorderLayout.NORTH);
  }
  class OpenL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JFileChooser c = new JFileChooser();
      c.setFileFilter(new TextFileFilter());
      // Demonstrate "Open" dialog:
      int rVal = c.showOpenDialog(FileAccessApplet.this);
      if(rVal == JFileChooser.APPROVE_OPTION) {
        file = c.getSelectedFile();
        filename.setText(file.getName());
        dir.setText(c.getCurrentDirectory().toString());
        try {
          System.out.println("Url is " + file.toURL());
          ep.setPage(file.toURL());
          // ep.repaint();
        } catch (IOException ioe) {
          throw new RuntimeException(ioe);
        }
      }
      if(rVal == JFileChooser.CANCEL_OPTION) {
        filename.setText("You pressed cancel");
        dir.setText("");
      } else {
        save.setEnabled(true);
      }
    }
  }
  class SaveL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      JFileChooser c = new JFileChooser(file);
      c.setSelectedFile(file);
      // Demonstrate "Save" dialog:
      int rVal = c.showSaveDialog(FileAccessApplet.this);
      if(rVal == JFileChooser.APPROVE_OPTION) {
        filename.setText(c.getSelectedFile().getName());
        dir.setText(c.getCurrentDirectory().toString());
        try {
          FileWriter fw = new FileWriter(file);
          ep.write(fw);
        } catch (IOException ioe) {
          throw new RuntimeException(ioe);
        }
      }
      if(rVal == JFileChooser.CANCEL_OPTION) {
        filename.setText("You pressed cancel");
        dir.setText("");
      }
    }
  }
  public class TextFileFilter extends
    javax.swing.filechooser.FileFilter {
    public boolean accept(File f) {
      return f.getName().endsWith(".txt")
        || f.isDirectory();
    }
    public String getDescription() {
      return "Text Files (*.txt)";
    }
  }
  public static void main(String[] args) {
    Console.run(new FileAccessApplet(), 500, 500);
  }
} ///:~
这似乎是一个很普通的applet。但是虽然它能在客户端上运行,但就是不能打开或是关闭文件。要想给他签名,你必须先把它做成一个JAR文件(见本章前面讲过的jar工具这一节),然后再签署这个文件。

有了JAR文件,你就得用证书或是密钥来签名了。如果是一个大公司,那么你可以跟Verisign或Thawte这样的"认证中心(signing authority)"提申请,它们会给你发给你证书的。证书是用来给代码签名的,这样用户就能确信你确实是他所下载的这段代码的提供者了,而且自你签发之后,这段代码未被篡改过。电子签名的本质是一串两进制的数,当有人要核对签名的时候,那个给你发证书的认证中心会为你作证。

认证中心发的证书是要付钱的,而且得定期更新。就这个问题而言,我们可以自己给自己签一张证书。这个证书会存在文件里(通常被称为keychain)。你可以用下面这条命令:

keytool –list
访问默认的文件。如果默认的文件不存在,那么你还得先建一个,或者告诉它去找哪个文件。或许你应该去试试"cacerts"文件。

keytool -list -file <path/filename>
其默认路径通常是

{java.home}/lib/security/cacerts
其中,java.home表示JRE所在的目录。

你也可以用keytool给自己发一份证书,供测试用。如果PATH环境变量里已经有Java的"bin"目录了,那么这个命令就是:

keytool –genkey –alias <keyname> -keystore <url>
其中keyname表示key的别名,比如“mykeyname”,url表示存放密钥的位置,通常就放在上面讲的cacerts文件里。

它会提示你输入(keystore的)密码。默认是"changeit"(提醒你该做些什么)。然后是姓名,部门,单位,城市,州,国家。这些信息会被放进证书里。最后它会要你给证书设一个密码。如果你对安全问题真的很在意,可以给它设一个单独的密码,默认情况下,证书的密码就是"存证书的文件(keystore)"的密码,一般来说这已经够了。上面这些信息还可以用命令行提供给像Ant这样的编译工具使用。

如果你不给参数,直接在命令行下用keytool命令,那么它会把所有的选项全部都打印出来。你或许想用-valid 选项,看看证书的有效期还有多长。

如果想确认证书确实保存在cacerts文件里,用

keytool –list –keystore <url>
然后输入前面设的密码。或许你的证书和别人的存放在一起(如果别人已经在这个keystore里存了证书的话)。

你刚获得的那张证书是你自己签发的,所以认证中心是不会认帐的。如果你用这张证书签发JAR文件,最终用户那里就会看到一个警告窗口,同时强烈建议他们不要使用这个程序。除非你去买一份有效力的证书,否则否则你和你的用户就得忍着。

签发JAR文件要用Java的jarsigner标准工具,命令如下:

jarsigner –keystore <url> <jarfile> <keyname>
url表示cacerts文件的位置,jarfile表示JAR文件的名字,而keyname则是证书的别名。你还得再输一遍密码。

现在这个JAR文件就带上你的这张证书的签名了,而用户也能知道它在签发之后是不是被篡改了(包括修改,添加或删除等)。

接下来你得操心一下HTML文件的applet标记的"archive"属性了,JAR的文件名就在这里。

如果浏览器用的是Java的plugin,applet的标记还要更复杂一些,不过你可以创建一个简单点的,就像这样:

<APPLET
  CODE=package.AppletSubclass.class
  ARCHIVE = myjar.jar
  WIDTH=300
  HEIGHT=200>
</APPLET>
然后用HTMLConverter过一遍(已经包括在JDK包里了,可以到网上免费下载),它会自动帮你生成正确的applet标记。

现在当用户下载applet时,浏览器就会提醒他们现在正在装载的是一个带签名的applet,并且问他是不是信任这个签发者。正如我们前面所讲的,测试用的证书并不具备很高的可信度,因此它会给一个警告。如果客户信任了,applet就能访问整个客户系统了,于是它就和普通的程序没什么两样了。

本书的源代码里已经包含了全套编译用的配置文件和Ant脚本,你可以到www.BruceEckel.com上下载。

JNLP和Java Web Start
虽然经过签名的applet功能强大,甚至能在有效地取代应用程序,但它还是得在Web浏览器上运行。这不仅使客户端增加了额外的运行浏览器的开销,而且常常使用户界面变得非常的单调和混乱。浏览器有它自己的菜单和工具条,而他们正好压在applet的上面。

Java的网络启动协议(Java Network Launch Protocol简称JNLP)能在不牺牲applet优点的前提下解决这个问题。你可以在客户端上下载并安装单独的JNLP应用程序。它可以用命令行,桌面图标,或随JNLP一同分发的应用程序管理器启动。程序甚至可以从最初下载的那个网站上启动。

JNLP程序运行的时候会动态地从Internet上下载资源,并且自动检查其版本(如果用户连着Internet的话)。也就是说它同时具备applet和application的优点。

和applet一样,客户机在对待JNLP应用程序的时候也必须注意安全问题。JNLP应用程序是一种易于下载的,基于Web的应用程序,因此有可能会被恶意利用。有鉴于此,JNLP应用程序应该和applet一样被放在沙箱里。同applet一样,它可以用带签名的JAR文件部署,这时用户可以选择是不是信任签发者。和applet的不同之处在于,即便没有签名,它仍然可以通过JNLP API去访问客户系统的某些资源(这就需要用户在程序运行时认可这些请求了)。

JNLP是一个协议而非产品,因而得先把它实现了才能用。Java Web Start有称JAWS就是Sun提供的,能免费下载的,JNLP的官方样板实现。你只要下载安装就行了,如果要做开发,不要忘了把JAR文件放到classpath里面。要想在网站上部署JNLP应用程序,只要确保服务器能认得application/x-java-jnlp-file的MIME类型就行了。如果是用最新版的Tomcat服务器(http://jakarta.apache.org/tomcat),那它应该已经帮你配置好了。否则就去查查服务器的用户手册。

创建JNLP应用程序并不难。先创建一个标准的应用程序,然后用JAR打包,最后再准备一个启动文件就行了。启动文件是一个很简单的XML文件,它负责向客户端传递下载和安装应用程序的信息。如果你决定用不带签名的JAR文件来部署软件,那还得用JNLP API来访问客户端系统上的资源。

下面是JFileChooser对话框程序的一个变型,不过这次我们用JNLP服务来打开它,这样你就可以把程序打成一个不带签名的JAR包,然后用JNLP来部署了。

//: c14:jnlp:JnlpFileChooser.java
// Opening files on a local machine with JNLP.
// {Depends: javaws.jar}
package c14.jnlp;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.jnlp.*;
public class JnlpFileChooser extends JFrame {
  private JTextField filename = new JTextField();
  private JButton
    open = new JButton("Open"),
    save = new JButton("Save");
  private JEditorPane ep = new JEditorPane();
  private JScrollPane jsp = new JScrollPane();
  private FileContents fileContents;
  public JnlpFileChooser() {
    JPanel p = new JPanel();
    open.addActionListener(new OpenL());
    p.add(open);
    save.addActionListener(new SaveL());
    p.add(save);
    Container cp = getContentPane();
    jsp.getViewport().add(ep);
    cp.add(jsp, BorderLayout.CENTER);
    cp.add(p, BorderLayout.SOUTH);
    filename.setEditable(false);
    p = new JPanel();
    p.setLayout(new GridLayout(2,1));
    p.add(filename);
    cp.add(p, BorderLayout.NORTH);
    ep.setContentType("text");
    save.setEnabled(false);
  }
  class OpenL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      FileOpenService fs = null;
      try {
        fs = (FileOpenService)ServiceManager.lookup(
          "javax.jnlp.FileOpenService");
      } catch(UnavailableServiceException use) {
        throw new RuntimeException(use);
      }
      if(fs != null) {
        try {
          fileContents = fs.openFileDialog(".",
            new String[]{"txt", "*"});
          if(fileContents == null)
            return;
          filename.setText(fileContents.getName());
          ep.read(fileContents.getInputStream(), null);
        } catch (Exception exc) {
          throw new RuntimeException (exc);
        }
        save.setEnabled(true);
      }
    }
  }
  class SaveL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      FileSaveService fs = null;
      try {
        fs = (FileSaveService)ServiceManager.lookup(
          "javax.jnlp.FileSaveService");
      } catch(UnavailableServiceException use) {
        throw new RuntimeException(use);
      }
      if(fs != null) {
        try {
          fileContents = fs.saveFileDialog(".",
            new String[]{"txt"},
            new ByteArrayInputStream(
              ep.getText().getBytes()),
            fileContents.getName());
          if(fileContents == null)
            return;
          filename.setText(fileContents.getName());
        } catch (Exception exc) {
          throw new RuntimeException (exc);
        }
      }
    }
  }
  public static void main(String[] args) {
    JnlpFileChooser fc = new JnlpFileChooser();
    fc.setSize(400, 300);
    fc.setVisible(true);
  }
} ///:~
注意,FileOpenService和FileCloseService是javax.jnlp里的类,这个程序从头至尾根本就没有提过JFileChooser对话框。要使用这两个服务,不但要用ServiceManager.lookup( )提出请求,而且要用这个方法所返回的对象来访问客户端资源。这里我们用JNLP的FileContent接口来读写客户端的文件系统。任何企图直接访问这些资源的举动,比方说创建一个File或FileReader对象,都会导致程序抛出一个SecurityException,其结果就如同在未签名的applet里一样。如果你不想受JNLP束缚,要直接使用这些类的话,那就必须使用签名的JAR文件(参阅上一节,签发JAR文件)。

现在我们已经做好JNLP程序了,接下来的任务就是把类放到JAR文件里,然后再写一个启动文件。下面就上述程序的启动文件:

<?xml version="1.0" encoding="UTF-8"?>
<jnlp spec = "1.0+"
  codebase="file://C:/TIJ3code/c14/jnlp"
  href="filechooser.jnlp">
    <information>
      <title>FileChooser demo application</title>
      <vendor>Mindview Inc.</vendor>
      <description>
        Jnlp File choose Application
      </description>
      <description kind="short">
        A demonstration of opening, reading and
        writing a text file
      </description>
      <icon href="images/tijicon.gif"/>
      <offline-allowed/>
    </information>
    <resources>
      <j2se version="1.3+"/>
      <jar href="jnlpfilechooser.jar" download="eager"/>
    </resources>
    <application-desc
      main-class="c14.jnlp.JnlpFileChooser"/>
</jnlp>
这个启动文件的后缀名必须是.jnlp,这里就是filechooser.jnlp,此外它还必须和JAR文件呆在一个目录里。

正如你所看到的,这是一个根节点为<jnlp>标记的XML文件。这个节点下面还包括了一些子元素,其中绝大部分是自解释的。

jnlp元素的spec属性告诉客户端系统,这个应用程序需要哪个版本的JNLP。codebase属性告诉客户端到哪个目录去找启动文件和资源。通常它应该是一个指向Web服务器的HTTP URL,但这里为了测试需要,我们把它指到本机的目录了。href属性表示文件的名字。

information标记里有多个提供与程序相关的信息的子元素。它们是供Java Web Start的管理控制台或其它类似程序使用的。这些程序会把JNLP应用安装到客户端上,让后让用户通过命令行,快捷方式或者其它什么方法启动。

resource标记的功能HTML文件里的applet标记相似。j2se子元素指明程序运行所需的j2se的版本,jar子元素告诉客户端class文件被打在哪个JAR文件里。此外jar元素还有一个download属性,其值可以是"eager"或"lazy",它的作用是告诉JNLP是不是应该下载完这个jar再开始运行程序。

application-desc属性告诉客户端系统,可执行的class,也就是JAR文件的入口是哪个类。

jnlp标记还有一个很有用的子元素,那就是这里没用到的security标记。下面我们来看看security标记长什么样子:

<security>
   <all-permissions/>
<security/>
只有在部署带签名的JAR文件时才能使用security标记。上面那段程序不需要这个标记,因为所有的本地资源都是通过JNLP服务来访问的。

此外还有一些其它标记,具体细节可以参考http://java.sun.com/products/javawebstart/download-spec.htm

现在.jnlp文件也写好了,接下来就是在网页里加超链接了。这个页面应该是个下载页面。页面上除了有复杂的格式和详细介之外,千万别忘了把这条加上:

<a href="filechooser.jnlp">click here</a>
这样你就可以点击链接启动JNLP应用程序的安装进程了。你只要下载一次,以后就可以通过管理控制台来进行配置了。如果你用的是Windows的Java Web Start的话,那么第二次启动程序的时候,它会提示你,是不是创建一个快捷方式。这种东西是可以配置的。

本书的源代码里包含了完整的配置文件和Ant的编译脚本,你可以到www.BruceEckel.com上去下载,然后用它们来编译和创建程序。

我们这里只介绍了两个JNLP服务,而当前版本里有七种。它们都是为特定的任务所设计的,比如像打印,剪贴板操作等。对这些服务的深入探讨超出了本章的范围。

编程技巧
由于Java的GUI编程是一个还在不断改进的技术,Java 1.0/1.1与Java 2的Swing类库之间就有着非常重大的区别,因此你或许也发现了,从Swing的观点来看,例程里面会不自觉的露出一些旧的编程习惯。此外与旧模式相比,Swing能让你用一种更好的方式编程。这里,我们会就其中一些问题做个介绍,同时检验一下这些编程技巧。

动态绑定事件
Swing的事件模型的优点就在于它的灵活性。你可以调用方法给组件添加或删除事件。下面我们来举一个例子:

//: c14:DynamicEvents.java
// You can change event behavior dynamically.
// Also shows multiple actions for an event.
// <applet code=DynamicEvents
// width=250 height=400></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
public class DynamicEvents extends JApplet {
  private java.util.List list = new ArrayList();
  private int i = 0;
  private JButton
    b1 = new JButton("Button1"),
    b2 = new JButton("Button2");
  private JTextArea txt = new JTextArea();
  class B implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      txt.append("A button was pressed/n");
    }
  }
  class CountListener implements ActionListener {
    private int index;
    public CountListener(int i) { index = i; }
    public void actionPerformed(ActionEvent e) {
      txt.append("Counted Listener " + index + "/n");
    }
  }
  class B1 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      txt.append("Button 1 pressed/n");
      ActionListener a = new CountListener(i++);
      list.add(a);
      b2.addActionListener(a);
    }
  }
  class B2 implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      txt.append("Button2 pressed/n");
      int end = list.size() - 1;
      if(end >= 0) {
        b2.removeActionListener(
          (ActionListener)list.get(end));
        list.remove(end);
      }
    }
  }
  public void init() {
    Container cp = getContentPane();
    b1.addActionListener(new B());
    b1.addActionListener(new B1());
    b2.addActionListener(new B());
    b2.addActionListener(new B2());
    JPanel p = new JPanel();
    p.add(b1);
    p.add(b2);
    cp.add(BorderLayout.NORTH, p);
    cp.add(new JScrollPane(txt));
  }
  public static void main(String[] args) {
    Console.run(new DynamicEvents(), 250, 400);
  }
} ///:~
这段程序里的新东西有:

Button连了不止一个listener。通常组件是以多播(multicast)方式处理事件的,也就是说你可以为一个事件注册多个listener。但是对于一些特殊的,以单播(unicast)方式处理事件的组件,这么做就会引发TooManyListenersException了。
程序运行的时候能动态地往Button b2上面添加或删除listener。你应该已经知道加listener的方法了,此外每个组件还有一个能用来删listener的removeXXXListener( )方法。
这种灵活性为你带来更大的便利

值得注意的是,listener的添加顺序并不一定就是它们的调用顺序(虽然绝大多数JVM确实是这么实现的)。

将业务逻辑(business logic)与用户界面分离开来
一般情况下,设计类的时候总是强调一个类"只作一件事情"。涉及用户界面的时候更是如此,因为你很可能会把"要作什么"同"要怎样显示"给混在一起了。这种耦合严重妨碍了代码的复用。比较好的做法是将"业务逻辑(business login)"同GUI分离开来。这样不仅方便了业务逻辑代码的复用,也简化了GUI的复用。

还有一种情况,就是多层系统(multitiered systems),也就是说”业务对象(business object)"完全贮存在另一台机器上。业务规则的集中管理能使规则的更新立即对新交易生效,因此这是这类系统所追求的目标。但是很多应用程序都会用到这些业务对象,所以它们绝不能同特定的显示模式连在一起。它们应该只做业务处理,别的什么都不管。[84]

下面我们来举一个例子,看看业务逻辑同GUI的分离有多容易:

//: c14:Separation.java
// Separating GUI logic and business objects.
// <applet code=Separation width=250 height=150></applet>
import javax.swing.*;
import java.awt.*;
import javax.swing.event.*;
import java.awt.event.*;
import java.applet.*;
import com.bruceeckel.swing.*;
class BusinessLogic {
  private int modifier;
  public BusinessLogic(int mod) { modifier = mod; }
  public void setModifier(int mod) { modifier = mod; }
  public int getModifier() { return modifier; }
  // Some business operations:
  public int calculation1(int arg){ return arg * modifier;}
  public int calculation2(int arg){ return arg + modifier;}
}
public class Separation extends JApplet {
  private JTextField
    t = new JTextField(15),
    mod = new JTextField(15);
  private JButton
    calc1 = new JButton("Calculation 1"),
    calc2 = new JButton("Calculation 2");
  private BusinessLogic bl = new BusinessLogic(2);
  public static int getValue(JTextField tf) {
    try {
      return Integer.parseInt(tf.getText());
    } catch(NumberFormatException e) {
      return 0;
    }
  }
  class Calc1L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText(Integer.toString(
        bl.calculation1(getValue(t))));
    }
  }
  class Calc2L implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      t.setText(Integer.toString(
        bl.calculation2(getValue(t))));
    }
  }
  // If you want something to happen whenever
  // a JTextField changes, add this listener:
  class ModL implements DocumentListener {
    public void changedUpdate(DocumentEvent e) {}
    public void insertUpdate(DocumentEvent e) {
      bl.setModifier(getValue(mod));
    }
    public void removeUpdate(DocumentEvent e) {
      bl.setModifier(getValue(mod));
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    calc1.addActionListener(new Calc1L());
    calc2.addActionListener(new Calc2L());
    JPanel p1 = new JPanel();
    p1.add(calc1);
    p1.add(calc2);
    cp.add(p1);
    mod.getDocument().addDocumentListener(new ModL());
    JPanel p2 = new JPanel();
    p2.add(new JLabel("Modifier:"));
    p2.add(mod);
    cp.add(p2);
  }
  public static void main(String[] args) {
    Console.run(new Separation(), 250, 100);
  }
} ///:~
你会发现BusinessLogic是一个相当简单的类,看它这样子你根本想不出它会被用到GUI里面。它只做了它自己的事。

Separation负责所有用户界面的细节,它通过BusinessLogic的public接口与之交流。所有操作都围绕着在用户界面和BusinessLogic对象之间收发消息。接下来轮到Separation做它自己的事了。由于Separation只知道它在和BusinessLogic对象交谈(也就是说两者并不高度相关),所以不用画很多功夫就能让它和其它对象交谈。

树立了将UI同业务逻辑相分离的观点之后,当你再碰到用Java去维护遗留下来的老代码时,也能稍微轻松一点。

范式
内部类,Swing事件模型,还能继续用下去的AWT事件模型,以及那些要我们用老办法用的新类库的功能,所有这些都使程序设计变得更混乱了。现在就连大家写乱七八糟的代码的方式也变得五花八门了。

这些情况都是事实,但是你应该总是使用最简单也最有条理的解决方案:用Listener(通常要写成内部类)来处理事件。这也是本章绝大多数的例程所采用方案。

用了这个模型,你可以少写很多"让我想想这个事件是谁发出的"这种代码。所有代码都在解决问题,而不是在做类型检查。这是最佳的编程风格,写出来的代码不仅便于总结,可读性和可维护性也高。

并发与Swing
写Swing程序的时候,你很可能会忘了它还正用着线程。虽然你并没有明确地创建过Thread对象,但它所引发的问题却会乘你不备吓你一跳。绝大多数情况下,你写的Swing或其他带窗口显示的GUI程序都是事件驱动的,而且除非用户用鼠标或键盘点击GUI组件,否则什么事都不会发生。

只要记住Swing有一个事件分派线程就行了,它会一直运行下去,并且按顺序处理Swing的事件。如果你想确保程序不会发生死锁或者竞争的情形,那么倒是要考虑一下这个问题。

本节我们会探讨几个多线程环境下Swing编程应注意的问题。

重访Runnable
在第13章,我曾建议大家在实现Runnable接口时一定要慎重。 当然如果你设计的类必须继承另一个类而这个类又得有线程的行为,那么选择Runnable还是对的。下面我们来举一个这方面的例子。这个程序创建了一个会自己决定该画什么颜色的Runnable JPanel类。它从命令行获取参数以决定格子的大小及sleep( )的长短。通过调整这些值,你会发现线程有一些有趣的,但又无法解释的特性。

//: c14:ColorBoxes.java
// Using the Runnable interface.
// <applet code=ColorBoxes width=500 height=400>
// <param name=grid value="12">
// <param name=pause value="50"></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
class CBox extends JPanel implements Runnable {
  private Thread t;
  private int pause;
  private static final Color[] colors = {
    Color.BLACK, Color.BLUE, Color.CYAN,
    Color.DARK_GRAY, Color.GRAY, Color.GREEN,
    Color.LIGHT_GRAY, Color.MAGENTA,
    Color.ORANGE, Color.PINK, Color.RED,
    Color.WHITE, Color.YELLOW
  };
  private static Random rand = new Random();
  private static final Color newColor() {
    return colors[rand.nextInt(colors.length)];
  }
  private Color cColor = newColor();
  public void paintComponent(Graphics  g) {
    super.paintComponent(g);
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
  public CBox(int pause) {
    this.pause = pause;
    t = new Thread(this);
    t.start();
  }
  public void run() {
    while(true) {
      cColor = newColor();
      repaint();
      try {
        t.sleep(pause);
      } catch(InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
  }
}
public class ColorBoxes extends JApplet {
  private boolean isApplet = true;
  private int grid = 12;
  private int pause = 50;
  public void init() {
    // Get parameters from Web page:
    if(isApplet) {
      String gsize = getParameter("grid");
      if(gsize != null)
        grid = Integer.parseInt(gsize);
      String pse = getParameter("pause");
      if(pse != null)
        pause = Integer.parseInt(pse);
    }
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(grid, grid));
    for(int i = 0; i < grid * grid; i++)
      cp.add(new CBox(pause));
  }
  public static void main(String[] args) {
    ColorBoxes applet = new ColorBoxes();
    applet.isApplet = false;
    if(args.length > 0)
      applet.grid = Integer.parseInt(args[0]);
    if(args.length > 1)
      applet.pause = Integer.parseInt(args[1]);
    Console.run(applet, 500, 400);
  }
} ///:~
ColorBoxes是一个很普通的,用init( )创建GUI的applet/application。它配了个GridLayout,所以它把整个平面都划分成小的grid。接着它把一些CBox对象放到格子里,再给每个对象传一个pause值。pause和grid的默认值是在main( )里确定的,此外它也告诉你该怎样用命令行或applet的参数修改这两个默认值。

CBox是整个程序的重点。它继承自JPanel,并且实现了Runnable接口,所以这些JPanel同时也是Thread。记住,当你实现Runnable的时候,并没有获得Thread对象,它们只是一些有着run( )方法的对象。因而你必须显式地创建Thread对象,也就是把这个Runnable传给Thread的构造函数,然后再(在构造函数里)调用start( )。对于CBox来说,这个线程是t。

再看看colors数组,这是一个包含了所有Color类所定义的颜色的枚举。newColor( )方法会从它里面随机选取一个颜色。cColor表示当前的颜色。

paintComponent( )很简单;它先把颜色设成cColor,然后再用这个颜色填满整个JPanel。

run( )里有一个无限循环,它先把cColor设成新的随机色,再用repaint( )把它显示出来,再根据命令行的参数sleep( )一段时间。

只是因为这个设计很灵活,而且线程是绑在各个JPanel上的,所以你可以创建任意数量的线程来做实验。(实际上JVM能有效管理的线程数还是有限的。)

这个程序同时还是一个很有意思的基准测试,因为它展示了不同的JVM,在如何实现线程方面,存在着巨大的性能和行为差异。

管理并发
当你用main方法或另一个线程修改Swing组件的属性时,一定要记住,有可能事件分派线程正在和你竞争同一个资源[85]

下面这个程序演示了忽略事件分派线程所引发的意外结果:

//: c14:EventThreadFrame.java
// Race Conditions using Swing Components.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.Console;
public class EventThreadFrame extends JFrame {
  private JTextField statusField =
    new JTextField("Initial Value");
  public EventThreadFrame() {
    Container cp = getContentPane();
    cp.add(statusField, BorderLayout.NORTH);
    addWindowListener(new WindowAdapter() {
      public void windowOpened(WindowEvent e) {
        try { // Simulate initialization overhead
          Thread.sleep(2000);
        } catch (InterruptedException ex) {
          throw new RuntimeException(ex);
        }
        statusField.setText("Initialization complete");
      }
    });
  }
  public static void main (String[] args) {
    EventThreadFrame etf = new EventThreadFrame();
    Console.run(etf, 150, 60);
    etf.statusField.setText("Application ready");
    System.out.println("Done");
  }
} ///:~
要判断程序本来想做什么很容易。main方法先创建一个EventThreadFrame对象,然后调用Console.run( )方法。在创建并运行这个frame之后,它把frame的text field的值设成了"Application ready",然后退出main( )并且在控制台上打印"Done"。

在创建frame的时候,构造函数会把text field的值设成"Initial Value",而且加一个专门用来监听窗口被打开事件的监听器。当(Console.run( )方法)调用setVisible(true)的时候,JFrame就收到了这个事件,所以这个事件也是做与显示窗口相关的初始化的好地方。这里,我们用sleep( )来模拟一些初始化代码,因为初始化可能会花上好几秒钟。干完这些之后,文本框的值应该被设成"Initialization complete"了。

你原本预想的顺序应该是,先在文本框上显示"Initial Value",接着"Initialization complete",再是"Application Ready",最后才是在控制台上打印"Done"。但实际过程却是,还没等EventThreadFrame处理完事件,main()就先调用了JTextField的setText( )了。也就是说实际上"Application ready"很可能在"Initialization complete"之前就已经显示过了。这还不一定就是实际运行的顺序,它还取决于你系统的速度。Swing的事件分派线程可能正在忙于处理windowOpened事件,所以在事件处理完毕之前你还看不到文本框的值,而等你看到了,它已经变成"Initializaiton Complete"了。因为这是文本框最后设定的值,所以"Application ready"丢了。更糟的是,在这一切还没做完之前,"Done”已经先被送上控制台了。

这种出人意料与不可预测性都源于这样一个简单的事实,这里有两个需要同步的线程。看来线程遇到Swing的时候,麻烦也跟着来了。要解决这个问题,你必须确保Swing组件的属性只能由事件分派线程来修改。

这要比听上去的容易一些。Swing提供了两个方法,SwingUtilities.invokeLater( )和SwingUtilities.invokeandWait( ),你可以从中选一个。它们负责绝大多数的工作,也就是说你不用去操心那些很复杂的线程同步的事了。

这两个方法都需要runnable对象作参数。当Swing的事件处理线程处理完队列里的所有待处理事件之后,就会启动它的run( )方法了。

//: c14:InvokeLaterFrame.java
// Eliminating race Conditions using Swing Components.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.Console;
public class InvokeLaterFrame extends JFrame {
  private JTextField statusField =
    new JTextField("Initial Value");
  public InvokeLaterFrame() {
    Container cp = getContentPane();
    cp.add(statusField, BorderLayout.NORTH);
    addWindowListener(new WindowAdapter() {
      public void windowOpened(WindowEvent e) {
        try { // Simulate initialization overhead
          Thread.sleep(2000);
        } catch (InterruptedException ex) {
           throw new RuntimeException(ex);
        }
        statusField.setText("Initialization complete");
      }
    });
  }
  public static void main(String[] args) {
    final InvokeLaterFrame ilf = new InvokeLaterFrame();
    Console.run(ilf, 150, 60);
    // Use invokeAndWait() to synchronize output to prompt:
    // SwingUtilities.invokeAndWait(new Runnable() {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        ilf.statusField.setText("Application ready");
      }
    });
    System.out.println("Done");
  }
} ///:~
main( )传了个Runnable的匿名内部类给SwingUtilities.invokeLater( ),而这个内部类又调用了text field的setText( )方法。这样runnable对象就以事件的身份排进了队列,而当事件分派线程处理完前面的事件之后,就能调用它的setText( )方法了。也就是说,它会先处理windowOpening事件,再在文本框显示"Application ready",这也正是我们所要的结果。


invokeLater( )

是异步的,所以能很快地返回。这个方法非常有用,因为它不会阻塞,所以程序能运行得很平稳。但是它还是不能解决"Done"会在什么都没做的情况下就被打印出来的问题。

要想解决这个问题,你可以把InvokeLater( )改成invokeAndWait( ),让它把文本框的值设成"Application Ready"。这个方法是同步的,也就是说在事件处理完毕并返回之前,它会一直处于阻塞状态。这样System.out.println("Done")语句就会在文本框的值被设置好之后再运行了。这样我们就获得了可完全预知的正确的行为了。

invokeAndWait( )提供了死锁的必要条件。所以如果你要用invokeAndWait( )的话,特别是用另一个线程调用的话,一定要仔细控制共享资源。

或许你用invokeLater的机会多些,但是请记住,只要是在初始化之后,你就只能用这两个方法来设置Swing组件的属性。

可视化编程与JavaBeans
看到现在你已经知道Java在代码复用方面的价值了。复用程度最高的代码是类,因为它是由一组紧密相关的特征(字段field)和行为(方法)组成的,它既能以合成(composition),也能以继承的方式被复用。

继承和多态是面向对象编程的基础,但是在构建应用程序的时候,绝大多数情况下,你真正需要的是能帮你完成特定任务的组件。你希望能把这些组件用到设计里面,就像电子工程师把芯片插到电路板上一样。同样,也应该有一些能加速这种"模块化安装"的编程方法。

Microsoft的Visual Basic为"可视化编程(Visual programming)"赢得了初次成功——非常巨大的成功,紧接着是第二代的Borland Delphi(直接启发了JavaBean的设计) 。有了这些工具,组件就变得看得见摸的着了,而组件通常都表示像按钮,文本框之类的可视组件,因此这样一来组件编程也变得有意义了。实际上组件的外观,通常是设计时是什么样子运行时也就这样,所以从控件框(palette)里往表单上拖放组件也就成了可视化编程的步骤了。而当你在这么做的时候,应用程序构造工具在帮你写代码,所以当程序运行时,它就会创建那些组件了。

通常简单地把组件拖到表单上还不足以创建程序。你还得修改一些特征,比如它的颜色,上面的文字,所连接的数据库等等。这些在设计时可以修改的特征被称为属性(properties)。你可以在应用程序的构建工具里控制组件的属性。当程序创建完毕,这些配置信息也被保存下来,这样程序运行时就能激活这些配置信息了。

看到现在你或许已经习惯这样来理解对象了,也就是对象不仅是一组特征,还是一组行为。设计的时候,可视组件的行为部分的表现为事件,也就是说"是些能发生在这个组件上的事情"。一般来说你会用把代码连到事件的方法来决定事件发生时该做些什么。

下面就是关键部分了:应用程序的构建工具用reflection动态地查询组件,找出这个组件支持哪些属性和事件。一旦知道它是谁,构建工具就能把这些属性显示出来,然后让你作修改了(创建程序的时候会把这些状态保存下来),当然还有事件。总之,只要你在事件上双击鼠标或者其他什么操作,编程工具就会帮你准备好代码的框架,然后连上事件。现在,你只要编写事件发生时该执行的代码就可以了。

编程工具帮你做了这么多事,这样你就能集中精力去解决程序的外观和功能问题了,至于把各部分衔接起来的细节问题,就交给构建工具吧。可视化编程工具之所以能获得如此巨大的成功,是因为它能极大的提高编程的效率,当然这一点首先体现在用户界面,但是其它方面往往也受益颇丰。

JavaBean是干什么用的?
言归正传,组件实际上是一段封装成类的代码。关键在于,它能让应用程序的构建工具把自己的属性和事件提取出来。创建VB组件的时候,程序员必须按照特定的约定,用相当复杂的代码把属性和事件发掘出来。Delphi是第二代的可视化编程工具,而且整个语言是围绕着可视化编程设计的,所以用它创建可视化组件要简单得多。但是Java凭借其JavaBean在可视化组件的创建技术领域领先群雄。Bean只是一个类,所以你不用为创建一个Bean而去编写任何额外的代码,也不用去使用特殊的语言扩展。事实上你所要做的只是稍稍改变一下方法命名的习惯。是方法的名字告诉应用程序构建工具,这是一个属性,事件还是一个普通的方法。

JDK文档把命名规范(naming convention)错误地表述成"设计模式(design pattern)”。这真是不幸,设计模式(请参阅www.BruceEckel.com上的Thinking in Patterns (with Java))本身已经够让人伤脑筋的了,居然还有人来混淆视听。重申一遍,这算不上是什么设计模式,只是命名规范而已,而且还相当简单。

对于名为xxx的属性,你通常都得创建两个方法:getXxx( )和setXxx( )。注意构建工具会自动地将"get"和"set"后面的第一个字母转换成小写,以获取属性的名字。"get"所返回的类型与”set"所使用的参数的类型相同。属性的名字同"get"和”set"方法返回的类型无关。
对于boolean型的属性,你既可以使用上述的"get"和"set"方法,也可以用"is"来代替"get"。
Bean的常规方法无需遵循上述命名规范,但它们都必须是public的。
用Swing的listener来处理事件。就是我们讲到现在一直在用的这个方案:用addBounceListener(BounceListener)和removeBounceListener(BounceListener)来处理BounceListener。绝大多数情况下,内置的事件和监听器已经可以满足你的需要了,但是你也可以创建你自己的事件和监听器接口。
第一点回答了你在比较新旧代码时或许会注意的一个问题:很多方法的名字都有了一些很小的,但明显没什么意义的变化。现在你应该知道了,为了把组件做成JavaBean,绝大多数修改是在同"get"和"set"的命名规范接轨。

我们可以根据这些原则来创建一个简单的Bean:

//: frogbean:Frog.java
// A trivial JavaBean.
package frogbean;
import java.awt.*;
import java.awt.event.*;
class Spots {}
public class Frog {
  private int jumps;
  private Color color;
  private Spots spots;
  private boolean jmpr;
  public int getJumps() { return jumps; }
  public void setJumps(int newJumps) {
    jumps = newJumps;
  }
  public Color getColor() { return color; }
  public void setColor(Color newColor) {
    color = newColor;
  }
  public Spots getSpots() { return spots; }
  public void setSpots(Spots newSpots) {
    spots = newSpots;
  }
  public boolean isJumper() { return jmpr; }
  public void setJumper(boolean j) { jmpr = j; }
  public void addActionListener(ActionListener l) {
    //...
  }
  public void removeActionListener(ActionListener l) {
    // ...
  }
  public void addKeyListener(KeyListener l) {
    // ...
  }
  public void removeKeyListener(KeyListener l) {
    // ...
  }
  // An "ordinary" public method:
  public void croak() {
    System.out.println("Ribbet!");
  }
} ///:~
首先,你会发现它只是一个类。通常所有的字段都是private的,而且只能用方法来访问。属性jumps,color,spots以及jumper的名字都严格遵循命名规范的约定(注意属性的第一个字母的大小写变化)。虽然与前三个属性所关联的内部变量的名字与属性的名字相同,但是你可以从jumper看出,属性的名字不一定要用内部变量的名字(实际上甚至都可以不要这个内部变量)。

根据"add"和"remove"方法所连的listener,我们知道这个Bean可以处理ActionEvent和KeyEvent事件。最后你会发现Bean还有一个常规方法croak( ),不是因为它遵循了命名规范,只是因为它是public的。

用Introspector提取BeanInfo
当你把Bean从控件框(palette)里拖到表单上的时候,JavaBean架构中最重要的一环就开始工作了。应用程序构建工具必须能创建这个Bean(有默认构造函数的话就可以了),然后在不看Bean源代码的情况下提取所有必须的信息,然后创建属性表和事件句柄。

从第十章看,我们已经能部分地解决这个问题了:Java的reflection机制可以帮我们找出类的所有方法。我们不希望像别的可视化编程语言那样用特殊的关键字来解决JavaBean的问题,因此这是个完美的解决方案。实际上给Java加上reflection的主要原因,就是为了支持JavaBean(虽然也是为了支持"对象的序列化(object serializaiton)"和"远程方法调用(remote method invocation)"。所以也许你会想设计应用程序构建工具的人会逐个地reflect Bean,找出所有的方法,再在里面挑出Bean的属性和事件。

这么做当然也可以,但是Java为我们提供了一个标准的工具。这不仅使Bean的使用变得更方便了,而且也为我们创建更复杂的Bean指出了一条标准通道。这个工具就是Introspector,其中最重要的方法就是static getBeanInfo( )。当你给这个方法传一个Class对象时,它会彻底盘查这个类,然后返回一个BeanInfo对象,这样你就可以通过这个对象找出Bean的属性,方法和事件了。

通常你根本不用为此操心;绝大多数Bean都是从供应商那里直接买过来的,更何况你也不必知道它在底层都玩了什么花样。你只要直接把Bean放到表单上,然后配置一下属性,再写个程序处理一下事件就可以了。但是用Introspector显示Bean的信息是一件很有意思,也很有意义的练习,所以下面我们就写了个工具:

//: c14:BeanDumper.java
// Introspecting a Bean.
import java.beans.*;
import java.lang.reflect.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;
public class BeanDumper extends JFrame {
  private JTextField query = new JTextField(20);
  private JTextArea results = new JTextArea();
  public void print(String s) { results.append(s + "/n"); }
  public void dump(Class bean) {
    results.setText("");
    BeanInfo bi = null;
    try {
      bi = Introspector.getBeanInfo(bean, Object.class);
    } catch(IntrospectionException e) {
      print("Couldn't introspect " +  bean.getName());
      return;
    }
    PropertyDescriptor[] properties =
      bi.getPropertyDescriptors();
    for(int i = 0; i < properties.length; i++) {
      Class p = properties[i].getPropertyType();
      if(p == null) continue;
      print("Property type:/n  " + p.getName() +
        "Property name:/n  " + properties[i].getName());
      Method readMethod = properties[i].getReadMethod();
      if(readMethod != null)
        print("Read method:/n  " + readMethod);
      Method writeMethod = properties[i].getWriteMethod();
      if(writeMethod != null)
        print("Write method:/n  " + writeMethod);
      print("====================");
    }
    print("Public methods:");
    MethodDescriptor[] methods = bi.getMethodDescriptors();
    for(int i = 0; i < methods.length; i++)
      print(methods[i].getMethod().toString());
    print("======================");
    print("Event support:");
    EventSetDescriptor[] events =
      bi.getEventSetDescriptors();
    for(int i = 0; i < events.length; i++) {
      print("Listener type:/n  " +
        events[i].getListenerType().getName());
      Method[] lm =  events[i].getListenerMethods();
      for(int j = 0; j < lm.length; j++)
        print("Listener method:/n  " + lm[j].getName());
      MethodDescriptor[] lmd =
        events[i].getListenerMethodDescriptors();
      for(int j = 0; j < lmd.length; j++)
        print("Method descriptor:/n  "
          + lmd[j].getMethod());
      Method addListener= events[i].getAddListenerMethod();
      print("Add Listener Method:/n  " + addListener);
      Method removeListener =
        events[i].getRemoveListenerMethod();
      print("Remove Listener Method:/n  "+ removeListener);
      print("====================");
    }
  }
  class Dumper implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      String name = query.getText();
      Class c = null;
      try {
        c = Class.forName(name);
      } catch(ClassNotFoundException ex) {
        results.setText("Couldn't find " + name);
        return;
      }
      dump(c);
    }
  }
  public BeanDumper() {
    Container cp = getContentPane();
    JPanel p = new JPanel();
    p.setLayout(new FlowLayout());
    p.add(new JLabel("Qualified bean name:"));
    p.add(query);
    cp.add(BorderLayout.NORTH, p);
    cp.add(new JScrollPane(results));
    Dumper dmpr = new Dumper();
    query.addActionListener(dmpr);
    query.setText("frogbean.Frog");
    // Force evaluation
    dmpr.actionPerformed(new ActionEvent(dmpr, 0, ""));
  }
  public static void main(String[] args) {
    Console.run(new BeanDumper(), 600, 500);
  }
} ///:~
BeanDumper.dump( )方法完成了所有的任务。它先试着创建一个BeanInfo对象,如果成功,就用BeanInfo的方法获取Bean的属性,方法和事件。你会发现Introspector.getBeanInfo( )需要两个参数。其中第二个是用来告诉Introspector,应该追溯到继承体系(inheritance hierarchy)的哪一级。这里,我们会让它在Object这里停下来,因为我们对此不感兴趣。

对于属性,getPropertyDescriptors( )会返回一个PropertyDescriptor数组。你可以对PropertyDescriptor调用getPropertyType( ),这样就能知道被当作属性传来传去的对象到底是什么类型的了。接下来你可以用getName( )获取属性的化名(pseudonym)了(通过方法的名字判断),读属性的方法用getReadMethod( ),写属性的方法用getWriteMethod( )。最后两个方法返回一个Method对象,这是真正能用来调用方法的对象(这部分属于reflection)。

对于public方法(包括属性方法),getMethodDescriptors( )会返回一个MethodDescriptor数组。你可以从中获取与之相关联的Method对象,然后把它的名字打印出来。

对于事件,getEventSetDescriptors( )会返回了一个EventSetDescriptor数组(除了它还能是谁?)。你可以通过它知道listener的类型,有什么方法,add和remove方法。BeanDumper会把这些信息全部打印出来。

程序启动的时候会先提取frogbean.Frog的信息。去除了所有无关紧要的东西,输出结果是这样的:

class name: Frog
Property type:
  Color
Property name:
  color
Read method:
  public Color getColor()
Write method:
  public void setColor(Color)
====================
Property type:
  Spots
Property name:
  spots
Read method:
  public Spots getSpots()
Write method:
  public void setSpots(Spots)
====================
Property type:
  boolean
Property name:
  jumper
Read method:
  public boolean isJumper()
Write method:
  public void setJumper(boolean)
====================
Property type:
  int
Property name:
  jumps
Read method:
  public int getJumps()
Write method:
  public void setJumps(int)
====================
Public methods:
public void setJumps(int)
public void croak()
public void removeActionListener(ActionListener)
public void addActionListener(ActionListener)
public int getJumps()
public void setColor(Color)
public void setSpots(Spots)
public void setJumper(boolean)
public boolean isJumper()
public void addKeyListener(KeyListener)
public Color getColor()
public void removeKeyListener(KeyListener)
public Spots getSpots()
======================
Event support:
Listener type:
  KeyListener
Listener method:
  keyTyped
Listener method:
  keyPressed
Listener method:
  keyReleased
Method descriptor:
  public void keyTyped(KeyEvent)
Method descriptor:
  public void keyPressed(KeyEvent)
Method descriptor:
  public void keyReleased(KeyEvent)
Add Listener Method:
  public void addKeyListener(KeyListener)
Remove Listener Method:
  public void removeKeyListener(KeyListener)
====================
Listener type:
  ActionListener
Listener method:
  actionPerformed
Method descriptor:
  public void actionPerformed(ActionEvent)
Add Listener Method:
  public void addActionListener(ActionListener)
Remove Listener Method:
  public void removeActionListener(ActionListener)
====================
这里包括了Introspect能从BeanInfo对象那里获取的关于Bean的绝大多数信息。你会发现属性的类型同他们的名字无关。注意,属性名都是小写的。(只有当"get/set"方法中的属性名是以两个或两个以上大写字母开头或其中有两个或两个以上大写字母的时候,这句话才不成立。)而且要记住,这里看到的方法名(比如读和写的方法)实际上是从Method对象那里提取出来的,而你可以用Method对象来调用与之相关联的那个方法。

public方法的清单不仅包括了像croak( )这样同属性或事件不相关的方法,也包括了与之相关的方法。它们是你编程时能调用的所有方法,当你需要调用方法时,应用程序构建工具会把它们全都列出来,这样就能减轻你的负担了。

最后你会发现所有的事件都被解析成了listener,listener的方法,以及listener的add和remove方法了。基本上,只要能获取BeanInfo,你就能找出Bean中的所有重要信息。你甚至可以在没有其它任何信息的情况下,只借助Bean本身,就去调用它的方法(这又是reflection的特性了)。

一个更复杂的Bean
下面我们举一个稍微复杂一点,但也有些无聊的例子。这是一个当鼠标经过上方,它会在上面画一个圆圈的JPanel。当你按下鼠标时,它会在屏幕的中央显示"Bang",同时触发一个事件监听器。

你能修改的属性包括圆圈的大小以及按下鼠标时显示的文本的颜色和字体大小。除此之外,BangBean还有他自己的addActionListener( )和removeActionListener( ),所以你可以连你自己的listener,这样当用户按下BangBean的时候,就会触发这个listener。你应该知道该怎样提供属性和事件:

//: bangbean:BangBean.java
// A graphical Bean.
package bangbean;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import com.bruceeckel.swing.*;
public class
BangBean extends JPanel implements Serializable {
  private int xm, ym;
  private int cSize = 20; // Circle size
  private String text = "Bang!";
  private int fontSize = 48;
  private Color tColor = Color.RED;
  private ActionListener actionListener;
  public BangBean() {
    addMouseListener(new ML());
    addMouseMotionListener(new MML());
  }
  public int getCircleSize() { return cSize; }
  public void setCircleSize(int newSize) {
    cSize = newSize;
  }
  public String getBangText() { return text; }
  public void setBangText(String newText) {
    text = newText;
  }
  public int getFontSize() { return fontSize; }
  public void setFontSize(int newSize) {
    fontSize = newSize;
  }
  public Color getTextColor() { return tColor; }
  public void setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.BLACK);
    g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize);
  }
  // This is a unicast listener, which is
  // the simplest form of listener management:
  public void addActionListener(ActionListener l)
  throws TooManyListenersException {
    if(actionListener != null)
      throw new TooManyListenersException();
    actionListener = l;
  }
  public void removeActionListener(ActionListener l) {
    actionListener = null;
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font("TimesRoman", Font.BOLD, fontSize));
      int width = g.getFontMetrics().stringWidth(text);
      g.drawString(text, (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      // Call the listener's method:
      if(actionListener != null)
        actionListener.actionPerformed(
          new ActionEvent(BangBean.this,
            ActionEvent.ACTION_PERFORMED, null));
    }
  }
  class MML extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public Dimension getPreferredSize() {
    return new Dimension(200, 200);
  }
} ///:~
首先,你会注意到BangBean实现了Serializable接口。也就是说当设计人员修改了BangBean的属性值之后,应用程序构建工具可以通过序列化把这些信息保存下来。当应用程序要创建Bean的时候,再把这些"保存下来"的属性恢复出来,这样就能得到设计的效果了。

你会发现所有的字段都是private的,其实这也是Bean的通常做法——也就是说做成"属性"之后,通常只能用方法来访问了。

看addActionListener( )的特征签名时(译者注:即参数表和返回值),你会发现它能抛出TooManyListenersException。这表示它是单播的(unicast),也就是说当事件发生时,它只会给一个监听器发通知。但通常你用的都是多播事件,也就是一个事件能传给多个监听器的那种。但是这么做牵涉到线程问题,而这部分内容要在"JavaBeans和同步(JavaBeans and synchronization) "一节里讲。所以现在我们就用单播把它对付过去。

当你按下鼠标时,文本会出现在BangBean的正中央,如果此时actionListener不为空,它就会创建一个ActionEvent,然后用去调用它的actionPerformed( )方法了。无论何时只要你移动了鼠标,BangBean都会捕捉到新的坐标,并且重画画布(canvas)(正如你所看到的,把画布上的文字全给擦了)。

下面是测试用的BangBeanTest类:

//: c14:BangBeanTest.java
import bangbean.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;
public class BangBeanTest extends JFrame {
  private JTextField txt = new JTextField(20);
  // During testing, report actions:
  class BBL implements ActionListener {
    private int count = 0;
    public void actionPerformed(ActionEvent e) {
      txt.setText("BangBean action "+ count++);
    }
  }
  public BangBeanTest() {
    BangBean bb = new BangBean();
    try {
      bb.addActionListener(new BBL());
    } catch(TooManyListenersException e) {
      txt.setText("Too many listeners");
    }
    Container cp = getContentPane();
    cp.add(bb);
    cp.add(BorderLayout.SOUTH, txt);
  }
  public static void main(String[] args) {
    Console.run(new BangBeanTest(), 400, 500);
  }
} ///:~
如果你用的是IDE提供的Bean,那么这个类是没多大用场的,但是它能帮你创建一个能快速测试你自己写的Bean的测试程序。BangBeanTest会把BangBean放到applet里,然后连一个简单的ActionListener,每发生一个事件,它就把累计的事件数打印到JTextField上。当然,通常应用程序构建工具会创建绝大多数代码。

如果你用BeanDumper来观察BangBean,或者把它放到一个支持Bean的IDE里,你会发现,它的属性和行为比代码里写的要多得多。这是因为BangBean是JPanel的派生类, 而JPanel也是Bean,所以你看到的也包括了它的属性和事件。

JavaBeans和同步
只要你创建了Bean,你就得保证它能在多线程环境下正常工作,这就是说:

只要允许,所有Bean的public方法都必须是synchronized。当然这会影响性能(不过在最新版本的JDK里,这种影响已经明显下降了)。如果性能下降确实是个问题,那么你可以把那些不致于引起问题的方法的synchronized给去掉,但是要记住,会不会引发问题不是一眼就能看出来的。这种方法首先是要小(就像上面那段程序里的getCircleSize( )),而且/或是"原子操作",就是说这个方法所调用的代码如此之少,以至于执行期间对象不会被修改了。所以把这种方法做成非synchronized的,也不会对性能产生什么重大影响。所以你应该把Bean的所有public方法都做成synchronized,只有在有绝对必要,而且确实对性能提高有效的时候,才能把synchronized移掉。
当你将多播事件发送给一队对此感兴趣的listener时,必须做好准备,listener会随时加入或从队列中删除。
第一个问题很好解决,但第二个问题就要好好想想了。上一版的BangBean.java用忽略synchronized关键词和单播事件回避了多线程。下面我们来写一个能在多线程环境下运行的,能使用多播事件的修改版:

//: c14:BangBean2.java
// You should write your Beans this way so they
// can run in a multithreaded environment.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import com.bruceeckel.swing.*;
public class BangBean2 extends JPanel
implements Serializable {
  private int xm, ym;
  private int cSize = 20; // Circle size
  private String text = "Bang!";
  private int fontSize = 48;
  private Color tColor = Color.RED;
  private ArrayList actionListeners = new ArrayList();
  public BangBean2() {
    addMouseListener(new ML());
    addMouseMotionListener(new MM());
  }
  public synchronized int getCircleSize() { return cSize; }
  public synchronized void setCircleSize(int newSize) {
    cSize = newSize;
  }
  public synchronized String getBangText() { return text; }
  public synchronized void setBangText(String newText) {
    text = newText;
  }
  public synchronized int getFontSize(){ return fontSize; }
  public synchronized void setFontSize(int newSize) {
    fontSize = newSize;
  }
  public synchronized Color getTextColor(){ return tColor;}
  public synchronized void setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.BLACK);
    g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize);
  }
  // This is a multicast listener, which is more typically
  // used than the unicast approach taken in BangBean.java:
  public synchronized void
  addActionListener(ActionListener l) {
    actionListeners.add(l);
  }
  public synchronized void
  removeActionListener(ActionListener l) {
    actionListeners.remove(l);
  }
  // Notice this isn't synchronized:
  public void notifyListeners() {
    ActionEvent a = new ActionEvent(BangBean2.this,
      ActionEvent.ACTION_PERFORMED, null);
    ArrayList lv = null;
    // Make a shallow copy of the List in case
    // someone adds a listener while we're
    // calling listeners:
    synchronized(this) {
      lv = (ArrayList)actionListeners.clone();
    }
    // Call all the listener methods:
    for(int i = 0; i < lv.size(); i++)
      ((ActionListener)lv.get(i)).actionPerformed(a);
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font("TimesRoman", Font.BOLD, fontSize));
      int width = g.getFontMetrics().stringWidth(text);
      g.drawString(text, (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      notifyListeners();
    }
  }
  class MM extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public static void main(String[] args) {
    BangBean2 bb = new BangBean2();
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println("ActionEvent" + e);
      }
    });
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println("BangBean2 action");
      }
    });
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        System.out.println("More action");
      }
    });
    Console.run(bb, 300, 300);
  }
} ///:~
给方法加synchronized是个很简单的修改,但是请注意addActionListener( )和removeActionListener( ),它们是在往ArrayList里增减ActionListener,所以现在ActionListener的数量已经没有限制了。

此外notifyListeners( )不是synchronized。它有可能被多个线程同时调用。同样addActionListener( )和removeActionListener( )也有可能会在notifyListener( )运行到一半的时候被其它线程所调用。由于notifyListener( )需要要遍历ArrayList actionListeners,因此这就会有问题了。为了解决这个问题,我们用关键段clone(克隆)了ArrayList,然后让代码去遍历这个clone(想了解clone的细节,请参阅附录A)。这样再修改原来的ArrayList就不会影响到notifyListeners( )了。

paintComponent( )也没有synchronized。决定覆写方法的时候是不是该加synchronized不像决定自己写的方法那样清楚。。这里,好像paintComponent( )加不加synchronized一样都能工作。但必须考虑的问题有:

这个方法是否会修改对象的"关键"变量?变量是否”关键"的判断标准是,它们是否会被其它线程所读写。(这里,读写实际上都是由synchronized方法来完成的,所以你只要看这一点就可以了)在这段程序里,paintComponent( )没有修改任何东西。
这个方法是否与这种"关键"变量的状态有关?如果有一个synchronized方法修改了这个方法要用到的变量,那么最好是把这个方法也作成synchronized的。基于这点,你或许会发现cSize是由synchronized方法修改的,因此paintComponent( )也应该是synchronized。但是这里你应该问问"如果在执行paintComponent( )的时候,cSize被修改了,最糟糕的情况是什么呢?"如果问题并不严重,而且转瞬即逝的,那么为了防止synchronized所造成的性能下降,你完全可以把paintComponent( )做成非synchronized的。
第三个思路是看基类的paintComponent( )是不是synchronized,答案是"否"。这不是一个万无一失的判断标准,只是一个思路。就拿上面那段程序说吧,paintComponent( )里面混了一个通过synchronized方法修改的cSize字段,所以情况也改变了。但是请注意,synchronized不会继承;也就是说派生类覆写的基类synchronized方法不会自动成为synchronized方法。
paint( )和paintComponent( )是那种执行得越快越好的方法。任何能够提升性能的做法都是值得大力推荐的,所以如果你发觉不得不对这些方法用synchronized,那么很有可能是一个设计失败的信号。
main( )的测试代码是根据BangBeanTest修改而得的。为了演示BangBean2的多播功能,它多加了几个监听器。

封装Bean
要想在可视化编程工具里面用JavaBean,必须先把它放入标准的Bean容器里。也就是把所有Bean的class文件以及一份申明"这是一个Bean"的"manifest"文件打成一个JAR的包。manifest文件是一种有一定格式要求的文本文件。对于BangBean,它的manifest文件是这样的:

Manifest-Version: 1.0
Name: bangbean/BangBean.class
Java-Bean: True
第一行表明manifest的版本,除非Sun今后发通知,否则就是1.0。第二行(空行忽略不计)特别提到了BangBean.class文件,而第三行的意思是"这是y一个Bean"。没有第三行,应用程序构建工具不会把它看成Bean。

唯一能玩点花样的地方是,你必须在"Name:"里指明正确的路径。如果你翻到前面去看BangBean.java,就会发觉它属于bangbean package(因此必须放到classpath的某个目录的"bangbean"的子目录里),而manifest的name也必须包含这个package的信息。此外还必须将manifest文件放到package路径的根目录的上一层目录里,这里就是将manifest放到"bangbean"子目录的上一层目录里。然后在存放manifest文件的目录里打入下面这条jar命令:

jar cfm BangBean.jar BangBean.mf bangbean
这里假定JAR文件的名字是BangBean.jar,而manifest文件的名字是BangBean.mf。

或许你会觉得有些奇怪,"我编译BangBean.java的时候还生成了一些别的class文件,它们都放到哪里去了?"是的,它们最后都放在bangbean子目录里,而上面那条jar命令的最后一个参数就是bangbean。当你给jar一个子目录做参数时,它会将整个子目录都打进JAR文件里(这里还包括BangBean.java的源代码——你自己写Bean的时候大概不会想把源代码打进包吧)。此外如果你把刚做好的JAR文件解开,就会发现你刚写的那个manifest已经不在里面了,取而代之的是jar自己生成的(大致根据你写的),名为MANIFEST.MF的manifest文件,而且它把它放在META-INF子目录里 (意思是“meta-information”)。如果你打开这个manifest文件,就会发现jar给每个文件加了条签名的信息,就像这样:

Digest-Algorithms: SHA MD5
SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0=
MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
总之,你不必为这些事担心。你作修改的时候可以只改你自己写的manifest文件,然后重新运行一遍jar,让它来创建新的JAR文件。你也可以往JAR文件里加新的Bean,只是要把它们的信息加到manifest里面就行了。

值得注意的是,你应该为每个Bean创建一个子目录。这是因为当你创建JAR文件的时候,你会把子目录的名字交给jar,而jar又会把子目录里的所有东西都放进JAR。所以Frog和BangBean都有它们自己的子目录。

等你把Bean封装成JAR文件之后,你就能把它们用到支持Bean的IDE里了。这个步骤会随开发工具的不同有一些差别,不过Sun在他们的"Bean Builder"里提供了一个免费的JavaBean的测试床(可以到java.sun.com/beans去下载)。要把Bean加入Bean Builer,只要把JAR文件拷贝到正确的目录里就行了。

Bean的高级功能
你已经知道做一个Bean有多简单了,但是它的功能并不仅限于此。JavaBean的架构能让你很快上手,但是经过扩展,它也可以适应更复杂的情况。这些用途已经超出了本书的范围,但是我会做一个简单的介绍。你可以在java.sun.com/beans上找到更多的细节。

属性是一个能加强的地方。在我们举的例子里,属性都是单个的,但是你也可以用一个数组来表示多个属性。这被称为索引化的属性(indexed property)。你只要给出正确的方法(还是要遵循方法的命名规范),Introspector就能找到索引化的属性,这样应用程序构建工具就能作出正确的反映了。

属性可以被绑定,也就是说它们能通过PropertyChangeEvent通知其它对象。而其它对象能根据Bean的变化,修改自己的状态。

属性是可以被限制的,也就是说如果其他对象认为属性的这个变化是不可接受的,那么它们可以否决这个变化。Bean用PropertyChangeEvent通知其他对象,而其他对象则用PropertyVetoException来表示反对,并且命令它将属性的值恢复到原来的状态。

你也可以修改Bean在设计时的表示方式:

你可以为Bean提供自定义的属性清单。当用户选择其它Bean的时候,构建工具会提供普通属性清单,但是当他们选用你的Bean时,它会提供你定义的清单。
你可以为属性创建一个自定义的编辑器,这样虽然构建工具用的是普通的属性清单,但当用户要编辑这个特殊属性时,编辑器就会自动启动了。
你可以为Bean提供一个自定义的BeanInfo类,它返回的信息,可以同Introspector默认提供的BeanInfo不同。
还可以把所有FeatureDescriptor的"专家(expert)"模式打开,看看基本功能和高级功能有什么区别。
Bean的其它资料
有很多关于JavaBean的书;比如Elliotte Rusty Harold的JavaBeans(IDG, 1998)

总结
在从Java1.0到Java 2的升级过程中,GUI类库是所有类库里变化最剧烈的。Java 1.0的 AWT曾饱受非议,甚至被称作是有史以来最糟糕的设计。尽管它达成了创建跨平台程序的目标,但其界面"在所有平台上都显得如此平庸"。在与它们各自平台上的应用程序开发工具相比,它也显得局促,古怪和不能令人满意。

当Java 1.1引入新的事件模型和JavaBean的时候,事情开始进入正轨了——现在你可以创建能够在可视化开发工具里面拖来拖去的GUI组件了。此外,事件模型和JavaBean充分展示了Java的设计人员对帮助程序员降低开发和维护的难度有多体贴了(在AWT 1.0里可看不出)。不过在JFC/Swing发布之前,这还不算功德圆满。有了Swing组件,跨平台的GUI编程终于变得优雅从容了。

实际上唯一欠缺的就是应用程序构建工具,而这正是革命的发源地。Microsoft的Visual BASIC和Visual C++要求你用Microsoft的编程工具,Borland的Delphi和C++ Builder也一样。如果你觉得这些都不能令你满意,你只能双手合十,希望能有开发商给你做出来。但Java是个开放的环境,所以它不仅允许甚至鼓励编程工具的竞争。而如果编程工具真的想参与竞争的话,它就必须支持JavaBean。这样就形成了一个平等的竞争环境;有了好的编程工具,你大可不必吊死在一棵树上。你可以挑出最满意的,然后把旧项目移过了,这样就能提高效率了。这种GUI开发工具市场的激烈竞争是前所未见的,而它的结果只会对程序员的生产效率产生积极的影响。

这一章只是想跟你介绍一下Swing的强大功能然后领你入门,这样当你知道相对而言Swing有多简单之后,你就能自己去探路了。你看到的这些已经能大致满足UI设计之需了。但是Swing不止于此;它的目标是要成为一种功能齐全的UI设计工具。只要你能想到,它都有办法能作到。

如果你在这里找不到你想要的,那么研究一下Sun的JDK文档吧,或者去搜Web,如果还不行,就去找一本Swing的专著。Walrath和Campione写的The JFC Swing Tutorial (Addison Wesley, 1999)是本不错的入门教材。


--------------------------------------------------------------------------------

[74] 它的另一种说法就是"最小惊讶原则"了,其本质是"别把用户给吓着了"。

[75] 注意一下IBM为他们的Eclipse编辑器(www.Eclipse.org)创建的新的开放源代码的GUI类库,你或许可以考虑用它来代替Swing。

[76] 应用框架是一种设计模式,被称作"模板方法(Template Method)"。

[77] 这里假定读者熟悉HTML的基础知识。其实这方面有很多书和资料,也不是太难懂。

[78] 依我看,你学过Swing之后就不用再浪费时间去学Swing以前的东西了。

[79] 正如前面所讲的,"Frame"已经被AWT给占了,所以Swing只能用JFrame了。

[80] 等你看到后面就会知道了。首先把JApplet做成类的static的成员(而不是main( )的本地变量)。然后在System.exit( )之前,用WindowAdapter.windowClosing( )方法调用applet.stop( )和applet.destroy( )。

[81] 虽然你会觉得应该有一个MouseMotionEvent,但实际却没有。点击和运动被合并成了MouseEvent,所以MouseEvent在表格里出现了两次,这不是笔误。

[82] 在Java 1.0/1.1里,你甚至不能有效地创建按钮的派生类。这只是其数不胜数的设计缺陷中的一例。

[83] 这部分以及下面一节由Jeremy Meyer撰稿。

[84] 这个概念会在Thinking in Enterprise Java里得到充分阐述,你可以到www.BruceEckel.com上面去下载这本书。

[85] 这一节由Jeremy Meyer撰稿。

--------------------------------------------------------------------------------

原创粉丝点击