14: 创建窗口与Applet
设计的宗旨是"能轻松完成简单的任务,有办法完成复杂的任务"。
本章只介绍Java 2的Swing类库,并且合理假定Swing是Java GUI类库的发展方向。
本章的开头部分会讲,用Swing创建applet与创建应用程序有什么不同,以及怎样创建一个既能当applet在浏览器里运行,又能当普通的应用程序,在命令行下运行程序。
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不能访问本地磁盘。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不需要main()。它已经包括在应用框架里了;你只要把启动代码放到init( )里面就行了。
JLabel的构造函数需要一个String作参数。
init( )方法负责将组件add( )到表单上。或许你会觉得,应该能直接调用它自己(JApplet)的add( )方法。实际上AWT就是这么做的。Swing要求你将所有的组件都加到表单的"内容面板(content pane)"上,所以add( )的时候,必须先调用getContentPane( )。
在Web浏览器里运行applet
要想运行程序,先得把applet放到Web页面里,然后用一个能运行Java的Web浏览器打开页面。你得用一种特殊的标记把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"-->
code的值是applet所处的.class文件的名字,width和height则表示applet的初始尺寸(和前面一样,以象素为单位)。此外applet标记里面还可以放一些东西:到哪里去找.class文件(codebase),怎样对齐(align),applet相互通讯的时候要用的标识符(name),以及提供给applet的参数。参数的格式是这样的:
<param name="identifier"
value = "information">
你可以根据需要,加任意多个参数。
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的测试文件了。
Applet的测试
如果只是想简单的测试一下,那么根本不用上网。直接启动Web浏览器,打开含applet标记的HTML文件就可以了。浏览器装载HTML的时候会发现applet标记,并且按照code的值去找相应的.class文件。当然,是在CLASSPATH里面找。如果CLASSPATH里没有,它就在浏览器的状态栏里给一个错误信息,告诉你找不到.class文件。
如果客户端的浏览器不能在服务器上找到.class文件,它会到客户机的CLASSPATH里面去找。这样一来,就有可能发生,浏览器在服务器上找不到.class文件,因此用你机器上的文件启动了applet的情形了。于是在你看来一切都很正常,而其他人则什么都看不见了。所以测试之前,一定要把本地的.class文件(或.jar文件)全部删了,只有这样才能知道程序是不是呆在正确的服务器目录里。
我就曾经在这里栽过一个很冤枉的跟头。有一次,我上载HTML和applet的时候搞错了package的名字,因此把applet放错了目录。但是我的浏览器还能在本地的CLASSPATH里找到程序,所以我成了唯一一个能正常运行这个applet的人了。所以给applet标记的CODE参数赋值的时候,一定要给全名,这一点非常重要。很多公开发表的applet都没有打包,但是实际工作中,最好还是打个包。
用命令行启动applet
有时你还会希望让GUI程序能做一些别的事情。比方说在保留Java引以为豪的跨平台性的前提下,让它做一些"普通"程序能作的事。
你可以用Swing类库写出与当前操作系统的外观风格完全一致的应用程序。如果你要编写GUI程序,先看看是不是能最新版的Java及与之相关的工具,只有是,那才值得去写。因为只有这样,才能写出不会让用户觉得讨厌的程序。。
你肯定会希望能编写出既能在命令行下启动,又能当applet运行的程序。这样测试applet的时候,会非常方便。因为通常从命令行启动要比从Web浏览器和Appletviewer启动更快,也更方便。
要想创建能用命令行启动的applet,只要在类里加一个main( ),让它把这个applet的实例嵌到JFrame里面就行了。
我们只加了一个main( ),其它什么都没动。main( )创建了applet的实例,并把它加到JFrame里面,这样我们就能看到它了。
你会看到main( )明确地调用了applet的init(
)和start( )。这是因为,这两步原先是交给浏览器做的。当然这并不能完全替代浏览器,因为前者还会调用stop( )和destroy( ),不过大致说来,这个办法还是可行的。如果还有问题,你可以亲自去调用。[80]
注意,要是没有这行:
frame.setVisible(true);
那么你什么都看不见。
一个专门用来显示Applet的框架
虽然将applet转换成应用程序的代码是非常有用的,但写多了就既浪费纸张又让人生厌了。
setDefaultCloseOperation( )的作用是,告诉程序,一旦JFrame被关掉了,程序也应该退出了。默认情况下关掉JFrame并不意味着退出程序,所以除非你调用setDefaultCloseOperation( )或者写一些类似的代码,否则程序是不会中止的。
制作按钮
做按钮很简单:想让按钮显示什么,就拿它作参数去调用JButton的构造函数。
通常按钮是类的一个字段,这样就能用它来表示按钮了。
JButton是一个组件——它有自己的小窗口——更新的时候会自动刷新。也就是说,你不用明确地指示该如何显示按钮或是其他什么控件;只要把它们放到表单上,它们就能自己把自己给画出来了。所以你得在init( )把按钮放到表单上。
在往content pane里面加东西之前,我们先把它的"布局管理器(layout manager)"设成FlowLayout。布局管理器的作用是告诉面板将控件放在表单上的哪个位置。这里我们不能用applet默认的BorderLayout,这是因为如果用它,一旦你往表单上加了新的控件,旧控件就会被全部遮住了。而FlowLayout则会让控件均匀地散布在表单上,从左到右,从上到下。
捕捉事件
编写事件驱动的程序是GUI编程的一个重要重要组成部分,而驱动程序的基本思路就是,将事件与代码联系起来。
Swing的思路是,将接口(图形组件)和实现(当组件发生了某个事件之后,你要运行的代码)明明白白地分开来。Swing组件能通报在它身上可以发生什么事件,以及发生了什么事件。所以,如果你对某个事件不感兴趣,比如鼠标从按钮的上方经过,你完全可以不去理会这个事件。用这种方法能非常简洁优雅地解决事件驱动的问题,一旦你理解了其基本概念,你甚至可以去直接使用过去从未看到过的Swing组件。实际上这个模型也适用于JavaBean。
我们还是先来关心一下我们要用的这个控件的主要事件。就这个例子而言,这个控件JButton,而"我们感兴趣的事件"就是按下按钮。要想表示你对按钮被按下感兴趣,可以调用addActionListener(
)。这个方法需要一个ActionListener参数。ActionListener是一个接口,它只有一个方法,actionPerformed( )。所以要想让JButton执行任务,你就得实现先定制一个ActionListener,然后用addActionListener方法把这个类的实例注册给JButton。当按钮被按下的时候,这个方法就会启动了。(这通常被称作回调(callback))
JTextField这个组件可以显示文本。它的文本既可以是用户输入的,也可以是程序插的。虽然创建JTextField的方法有很多,但是最简单的还是把宽度直接传给JTextField的构造函数。等到JTextField被放上了表单,你就可以用setText( )来修改内容了(JTextField还有很多方法,请参阅JDK文档)。
JTextArea
与JTextField相比,JTextArea能容纳多行文字,提供更多的功能,除此之外它们很相似。append( )方法很有用;你可以用它把程序的输出直接导到JTextArea里,这样你就能用Swing程序来改进我们先前写的,往标准输出上打印的命令行程序了(因为你能用滚动条看到前面的输出了)。
在把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。如果没有其它指令,BorderLayout会把所有控件全都放到表单的正中,并且拉伸到最大。
好在BorderLayout的功能还不止这些。它还有四个边界以及中央的概念。当你往BorderLayout的面板上加控件加时,你还可以选择重载过的add( )方法。这时要给它一个常量。这个常量可以是下边五个中的一个:
BorderLayout. NORTH
|
顶边
|
BorderLayout. SOUTH
|
底边
|
BorderLayout. EAST
|
右边
|
BorderLayout. WEST
|
左边
|
BorderLayout.CENTER
|
填满当中,除非碰到其它组件或表单的边缘
|
如果你不指明摆放对象的区域,默认它就使用CENTER。
除了CENTER,其它控件都会在一个方向上压缩到最小,在另一个方向上拉伸到最大。而CENTER则会往两个方向上拉伸,直至占满整个中间区域。
FlowLayout
它会让组件直接"流"到表单上,从左到右,从上到下,一行满了再换一行。
用了FlowLayout之后,组件就会冻结在它的"固有"尺寸上。比如JButton就会根据字符串的长度来安排大小。
由于FlowLayout面板上的控件会被自动地压缩到最小,因此时常会出现一些意外。比方说,JLabel是根据字符串来决定控件的大小的,所以当你对FlowLayout面板上的JLable控件的文本作右对齐时,显示效果不会有任何变化。
GridLayout
GridLayout的意思是,把表单视作组件的表格,当你往里加东西的时候,它会按从左到右,从上到下的顺序把组件放到格子里。创建布局管理器的时候,你得在构造函数里指明行和列的数目,这样它就能帮你把表单划分成相同大小的格子了。
GridBagLayout
GridBagLayout的作用是,当窗口的大小发生变化时,你能用它来准确地控制窗口各部分的反应。但与此同时,它也是最难和最复杂的布局管理器。它主要是供GUI builder生成代码用的(或许GUI
builder用的不是绝对定位而是GridBagLayout)。如果你的设计非常复杂,以至于不得不用GridBagLayout,那么建议你使用GUI builder。
绝对定位
还可以这样对图形组件进行绝对定位:
- 将Container的布局管理器设成null:setLayout(null)。
- 往面板上加组件的时候,先调用setBounds( )或reshape( )方法(根据各个版本)为组件设一个以象素为单位的矩形。这个步骤可以放在构造函数里,也可以放在paint( )里面,看你的需要。
有些GUI builder主要用的就是这个方法,但是通常情况下,这并不是最好的方法。
BoxLayout
由于GridBagLayout实在是太难学难用了,所以Swing引入了一个新的BoxLayout。它保留了GridBagLayout的很多优点,但是却没那么复杂。所以当你想手工控制控件的布局时,可以优先考虑用它(再重申一遍,如果设计非常复杂,最好还是用GUI builder)。BoxLayout能让你在垂直和水平两个方向上控制组件的摆放,它用一些被称为支柱(struts)和胶水(glue)的东西来控制组件间的距离。
BoxLayout的构造函数同其它布局管理器稍有些不同——它的第一个参数是这个BoxLayout将要控制的Container,第二个参数是布局的方向。
为了简化起鉴,Swing还提供了一种内置BoxLayout的特殊容器,Box(译者注:这里的容器是指java.awt.Container类)。Box有两个能创建水平和垂直对齐的box的static的方法,下面我们就用它来排列组件。
有了Box之后,再要往content pane里加组件的时候,你就可以把它成第二的参数了。
支柱(struts)会把组件隔开,它是大小是按象素算的。用的时候,只要把它当作空格加在两个组件的中间就行了。
与固定间距的struts不同,glue(胶水)会尽可能地将组件隔开。所以更准确的说,它应该是"弹簧(spring)"而不是"胶水"(再加上这种设计是基于被称为"springs and struts"的思想的,因此这个术语就显得有些奇怪了)。
strut只控制一个方向上的距离,而rigid area(刚性区域)则在两个方向上固定组件间的距离。
应该告诉你,对rigid area的评价不是那么的统一。实际上它就是绝对定位,因此有些人认为,它根本就是多余的。
最佳方案?
Swing的功能非常强大,短短几行代码就能做很多事。实际上你完全可以把这几种布局结合起来,完成一些比较复杂的任务。但是,有些时候用手写代码创建GUI表单就不那么明智了;首先是太复杂,其次也不值得。Java和Swing的设计者们是想用语言和类库去支持GUI builder工具,然后让你用工具来简化编程。实际上只要知道布局是怎么一回事,以及事件该怎么处理(下面讲)就行了,懂不懂手写代码控制控件的摆放,其实并不重要。你完全可以选一个趁手的工具(毕竟Java的初衷就是想让你提高编程的效率)。
Swing的事件模型
在Swing的事件模型中,组件可以发起(或"射出")事件。各种事件都是类。当有事件发生时,一个或多个"监听器(listener)"会得到通知,并做出反应。这样事件的来源就同它的处理程序分隔开来了。一般说来,程序员是不会去修改Swing组件的,他们写的都是些事件处理程序,当组件收到事件时,会自动调用这些代码,因此Swing的事件模型可称得上是将接口与实现分隔开来的绝好范例了。
实际上事件监听器(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
|
一旦你知道了组件所支持的事件,你就用不着再去查文档了。你只要:
- 把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的话,事情就不那么轻松愉快了。
为了解决这个问题,有些(但不是全部)多方法的listener接口提供了适配器(adapter)。从上面那张表已经列出了它们的名字。适配器会为接口提供默认的空方法。这样,你只要继承适配器,根据需要覆写方法就可以了。
adapter就是用来简化listener的创建的。
但是适配器这种东西也有缺点。
只是大小写方面的一个疏漏,它就成为一个新方法了。还好,这还不是关闭窗口时会调用的方法,所以最坏的结果也只是不能实现预期的效果。虽然有种种不方便,但接口却能保证让你实现所有应该实现的方法。
跟踪多个事件
Swing组件的一览表
现在你已经知道布局管理器和事件模型了,接下来就要学习怎样使用Swing组件了。这部分只是一个大致的介绍,我们讲的都是常用的Swing组件及其特性。
记住:
- JDK文档里有所有Swing组件的资料。
- Swing事件的命名规范比较合理,所以要猜该怎样编写和安装事件处理程序也比较简单。用我们前面讲的ShowAddListeners.java来检查组件。
- 如果事情开始变得复杂了,那么恭喜你毕业了,该用GUI Builder了。
Button
Swing收录了很多Button,包括各种按钮,check box, radio button,甚至连菜单项(menu item)都是继承AbstractButton的(鉴于菜单项也牵涉进来了,(AbstractButton)可能还是叫"AbstractSelector"或其它什么名字更好一些)。
Button组
如果想让radio button以"几选一"的方式运行(译者注:原文为exclusive or,字面的意思是"排他性的逻辑与"),你就必须把他们加到一个"button组(button group)"里。但只要是AbstractButton,都可以加进ButtonGroup。
这几步给原本很简单任务加了点难度。要让button做到"几选一",你必须先创建一个button组,然后把要加进去的button加进去。
Icon
Icon能用于JLabel和AbstractButton(包括JButton,JCheckBox,JRadioButton以及JMenuItem)。把Icon用于JLabel的语法非常简单
你也可以使用你自己的gif文件。如果想打开文件读取图像,只要创建一个ImageIcon,并且把文件的名字传给它就行了。接下来,你就可以在程序中使用这个Icon了。
很多Swing组件构造函数都可以拿Icon作参数,不过你也可以用setIcon( )方法添加或修改Icon。
Tool tips
几乎所有与GUI相关的类都继承自JComponent,而JComponent又包含了一个setToolText(String)方法。因此不管是哪种组件,只要能放到表单上,你几乎都可以用(假设这是个JComponent的派生类对象jc)
jc.setToolTipText("My
tip");
来设置tool tip。这样只要鼠标停在JComponent上一段时间,旁边就会跳出一个提示框,里面就是你设置的文本。
Text fields
Borders
JComponent里面有一个setBorder( )方法,它能让你为各种可视组件安上有趣的边框。
你还可以创建你自己的边框,然后把它放到按钮(button),标签(label)或者其它控件里面,——只要它是继承JComponent的就行。
JScrollPanes
大多数时候,你只需要JScrollPane能干好它的本职工作,但是你也可以去控制它,告诉它显示哪根滚动条——垂直的,水平的,还是两个都显示或者两个都不显示。
通过给JScrollPane的构造函数传不同的参数,可以控制它的滚动条。
一个袖珍的编辑器
不用费多大的劲,JTextPane已经提供了很多编辑功能。
applet的默认布局是BorderLayout。所以,如果你什么都不说,直接往面板里面加组件,那么它会填满整个面板。但是如果你指明其位置(NORTH,SOUTH, EAST,或WEST),控件就会被钉在这个区域里。
注意JTextPane的内置功能,比如自动换行。此外它还有很多其他功能,具体详情请参阅JDK文档。
Check boxes
Check box能让你做逐项的开/关选择。它由一个小框和一个标签组成。一般来说,选中之后框里会有一个'x'(或者其它什么表示选中的标记),否则就是空的。
一般来说,你用构造函数创建JCheckBox的时候,会传给它一个用作标签的参数。JCheckBox创建完了之后,你还可以随时读取或设置它的状态,或者读取或重设它的标签。
不管是选中还是清除,JCheckBox都会引发一个事件。捕捉方法同按钮事件完全相同:用ActionListener。
Radio buttons
GUI编程里的radio button的概念来自于老式的汽车收音机里的机械式按钮;当你按下一个按钮之后,另一个就会弹出来。因此你只能选一个。
要想创建一组相关联的JRadioButton,只要把它们加进ButtonGroup就可以了(一个表单里允许有任意数量的ButtonGroup)。如果你(用构造函数的第二个参数)把多个radio
button设成true了,那么只有最后一个才是有效的。
注意,捕捉radio button事件的方法同捕捉其它事件的完全相同。
text field它可以可以代替JLabel。
Combo boxes (下拉式列表)
同radio
button组一样,下拉式列表只允许用户在一组选项里面选取一个元素。但是这种做法更简洁,而且能在不惊扰客户的情况下修改列表中的元素。(你也可以动态地修改radio button,但是这个视觉效果就太奇怪了)。
JComboBox的默认行为同Windows的combo box不太一样。在Windows里,你既可以从combo box的列表里选一个,也可以自己输入,但是在JComboBox里,这么做就必须先调用setEditable( )了。此外你只能从列表中选择一个。下面我们举一个例子。我们先给JComboBox加几个选项,然后每按一下按钮就加一个选项。
List boxes
List box与JComboBox的不同不仅仅在外观上,它们之间有着非常重大的区别。JComboBox激活之后会弹出下拉框而JList则总是会在屏幕上占一块固定大小,永远也不会变。如果你想列表里的东西,只要调用getSelectedValues( )就行了,它会返回一个String的数组,其中包含着被选中的元素。
JList能做多项选择;如果你control-click了一个以上的选项(在用鼠标进行选择的时候,一直按着"control"键),那么已选中的选项就会一直保持"高亮(highlighted)",这样你能选任意多个了。如果你先选一项,然后再shift-click另一个,那么两个选项之间的所有选项就都被选中了。要取消一个选项只要control-click就可以了。
如果你只是想把String数组放到JList里面,那么还有一个更简单的办法;就是把数组当作参数传给JList的构造函数,这样它就会自动创建一个列表了。
JList不会自动提供滚动轴。当然只要把它嵌到JScrollPane里它就会自动镶上滚动轴了,具体的细节它自会打理。
Tabbed panes
JTabbedPane能创建"带页签的对话框(tabbed dialog)",也就是对话框的边上有一个像文件夹的页签一样的东西,你只要点击这个页签,对话框就把这一页显示出来。
在Java编程当中熟练使用页签面板(tabbed panel)相当重要,因为每当applet要弹出一个对话框的时候,它都会自动加上一段警告,所以弹出式对话框在applet里并不受欢迎。
程序运行起来你就会发现,如果tab太多了,JTabbedPane还会自动把它们堆起来以适应一定的行宽。
Message boxes
图形界面系统通常都包含一套标准的,能让你迅速地将消息传给用户,或者从用户那里得到信息的对话框。对于Swing来说,这些消息框就包含在JOptionPane里面了。你有很多选择(有些还相当复杂),但是最常用的可能还是"确认对话框(confirmation dialog)",它用static
JOptionPane.showMessageDialog( )和JOptionPane.showConfirmDialog(
)启动。
注意showOptionDialog( )和showInputDialog( )会返回用户输入的信息。
菜单
所有能包含菜单的组件,包括JApplet,JFrame,JDialog以及它们所派生的组件,都有一个需要JMenuBar作参数的setJMenuBar( )方法(一个组件只能有一个JMenuBar)。你可以把JMenu加入JMenuBar,再把JMenuItem加入JMenu。每个JMenuItem都可以连一个ActionListener,当你选中菜单项(menu item)的时候,事件就发出了。
与使用资源的系统不同,Java和Swing要求你必须用源代码来组装菜单。
通常情况下每个JMenuItem都得有一个它自己的ActionListener。
JMenuItem是AbstractButton的派生类,所以它有一些类似按钮的行为。JMenuItem本身就是一个可以置入下拉菜单的菜单选项。此外JMenuItem还有三个派生类:用来持有其它JMenuItem的JMenu(这样你就可以做层叠式菜单了);有一个能表示是否被选中的"候选标记(checkmark)"的JCheckBoxMenuItem;以及包含一个radio button的JRadioButtonMenuItem。
为了演示在程序运行期间动态地交换菜单条,我们创建了两个JMenuBar。你会发现JMenuBar是由JMenu组成的,而JMenu又是由JMenuItem,JCheckBoxMenuItem甚至JMenu(它会创建子菜单)组成的。等JMenuBar组装完毕,你就可以用setJMenuBar( )方法把它安装到当前程序里面了。注意当你按下钮按的时候,它会用getJMenuBar(
)来判断当前的菜单,然后把另一菜单换上去。
字符串的比较是引发编程错误的一大诱因。
程序会自动的将菜单项勾掉或恢复。JCheckBoxMenuItem的代码演示了两种怎样决定该勾掉哪个菜单项的方法。一个是匹配字符串(虽然也管用,但就像上面我们所讲的,不是很安全),另一个则是匹配事件目标对象。正如你所看到的,getState( )方法可以返回JCheckBoxMenuItem的状态,而setState( )则可以设置它的状态。
菜单事件不是很一致,这一点可能会引起混乱。JMenuItem使用ActionListener事件,而JCheckBoxMenuItem则使用ItemListener事件。JMenu也支持ActionListener,但通常没什么用。总之,你得为每个JMenuItem,JCheckBoxMenuItem或JRadioButtonMenuItem都制备一个listener,不过这里我们偷了点懒,把一个ItemListener和ActionListener连到多个菜单组件上了。
Swing支持助记符,或者说"快捷键",这样你就可以扔掉鼠标用键盘来选取AbstractButton了(按钮,菜单项等)。要这么做很容易,就拿JMenuItem举例,你可以用它重载了的构造函数,把快捷键的标识符当作第二个参数传给它。不过绝大多数AbstractButton都没有提供类似的构造函数,所以比较通用的办法还是用setMnemonic(
)方法。在上述例程中我们为按钮和多个菜单项加上了快捷键,这些快捷键的提示会自动显示在组件上。
好工具应该能帮你维护好菜单。
弹出式菜单
要想实现JPopupMenu,最直截了当的办法就是创建一个继承MouseAdapter的内部类,然后把这内部类的实例加到要提供弹出式菜单的组件里:
JMenuItem用的是同一个ActionListener,它负责从菜单标签里面提文本并且把它插入JTextField。
画屏幕
一个好的GUI框架能让作图相对而言比较简单——确实如此,Swing就做到了。所有作图问题都面临同一个难点,那就是相比调用画图函数,计算该在哪里画东西通常会更棘手,但不幸的是这种计算又常常和作图函数的调用混在一起,所以作图函数的接口的实际的复杂程度很可能会比你认为的要简单。
虽然你可以在任何一个JComponent上作画,也就是说它们都能充当画布(canvas),但是如果你想要一块能直接画东西的白板,最好还是创建一个继承JPanel的类。这样你只需要覆写一个方法,也就是paintComponent( )就行了。当系统需要重画组件的时候,会自动调用这个方法(通常情况下,你不必为此操心,因为这是由Swing控制的)。调用的时候,Swing会传一个Graphics对象给这个方法,这样你就能用这个对象作画了。
覆写paintComponent( )的时候,必须首先调用其基类的同名方法。接下来再做什么就由你了决定了;通常是调用Graphics的方法在JPanel上画画或者是在其象素上着色。要想了解具体怎样使用这些方法,可以查阅java.awt.Graphics文档(可以到java.sun.com上面去找JDK文档)。
如果问题非常复杂,那么还有一些更复杂的解决方案,比如第三方的JavaBean或者Java 2D API。
对话框
所谓对话框是指,能从其他窗口里面弹出来的窗口。其作用是,在不搞乱原先窗口前提下,具体处理其某一部分的细节问题。对话框在GUI编程中的用途很广,但是在applet中用的不多。
要想创建对话框,你得先继承JDialog。和JFrame一样,JDialog也是另一种Window,它也有布局管理器(默认情况下是BorderLayout),也可以用事件监听器来处理事件。它同JFrame有一个重要的区别,那就是在关对话框的时候别把程序也关了。相反你得用dispose( )方法将对话框窗口占用的资源全部释放出来。
一旦创建完JDialog,你就得用show( )来显示和激活它了。关闭对话框的时候,还得记住要dispose( )。
你会发现,对applet来说,包括对话框在内所有弹出的东西都是"不可信任的"。也就是说弹出来的窗口里面有一个警告。这是因为,从理论上讲恶意代码可以利用这个功能来愚弄用户,让他们觉得自己是在运行一个本地应用程序,然后误导它们输入自己的信用卡号码,再通过Web传出去。applet总是和网页联在一起,因此只能用浏览器运行,但是对话框却可以脱离网页,所以从理论上讲这种欺骗手段是成立的。所以这么一来,applet就不太会用到对话框了。
由于static只能用于宿主类,因此内部类里不能再有static的数据或是嵌套类了。
paintComponent( )负责把panel的周围的方框以及"x"或"o"画出来。虽然充斥着单调的计算,但是还算简明。
文件对话框
有些操作系统还内置了一些特殊的对话框,比如让你选择字体,颜色,打印机之类的对话框。实际上所有的图形操作系统都提供了打开和存储文件的对话框,所以为了简化起鉴,Java把它们都封装到JFileChooser里面了。
注意JFileChooser有很多变例可供选择,比方说加一个过滤器滤文件名之类的。
要用"open file"对话框就调用showOpenDialog( ),要用"save file"对话框,就调用showSaveDialog(
)。在对话框关闭之前,这两个函数是不会返回的。即便对话框关了,JFileChooser对象仍然还在,所以你还去读它的数据。要想知道操作的结果,可以用getSelectedFile( )和getCurrentDirectory( )。如果返回null则说明用户按了cancel。
Swing组件上的HTML
所有能显示文件的组件都可以按照HTML的规则显示HTML的文本。也就是说你可以很方便地让Swing组件显示很炫的文本。比如:
文本必须以"<html>"开头,下面你就可以用普通的HTML标记了。注意,它没有强制你一定要关闭标记。
JTabbedPane,JMenuItem,JToolTip,JRadioButton以及JCheckBox都支持HTML文本。
Slider和进程条
Slider能让用户通过来回移动一个点来输入数据,有时这种做法还是很直观的(比方说调节音量)。进程条(progress bar)则以一种用类比的方式显示数据,它表示数据是"全部"还是"空的",这样用户就能有一个比较全面的了解了。
JProgressBar还比较简单,而JSlider的选项就比较多了,比如摆放的方向,大小刻度等等。注意一下给Slider加带抬头的边框的那行代码,多简洁。
树
JTree的用法可以简单到只有下面这行代码:
add(new JTree(new Object[] {"this",
"that", "other"}));
这样显示的是一棵最基本的树。JTree的API非常庞大,应该是Swing类库里最大的之一了。虽然你可以用它来做任何事情,但是要想完成比较复杂任务,就需要一定的研究和实验了。
好在这个类库还提供了变通手段,也就是一个"默认"的,能满足一般需求的树型组件。所以绝大多数情况下你都可以使用这个组件,只有在特殊情况下,你才需要去深入研究树。
Trees类包含一个用来创建多个Branch的两维String数组,以及一个用来给数组定位的static int i。节点放在DefaultMutableTreeNode里面,但是实际在屏幕上显示则是由JTree及与之相的model——DefaultTreeModel控制的。注意JTree在加入applet之前,先套了一件JScrollPane,这样它就能提供自动的滚动轴了。
对JTree的操控是经由它的model来实现的。当model发生变化时,它会产生一个事件,让JTree对树的显示作出必要的更新。init( )用getModel( )提取这个model。当你按下按钮的时候,它会创建一个新的新的"branch" 。等它找到当前选中的那个节点(如果什么也没选,就用根节点)之后,model的insertNodeInto(
)方法就会接管所有的任务了,包括修改树,刷新显示等等。
或许上述例程已能满足你的需求了。但是树的功能强大到只要你能想到它就能做到的地步。但是要知道:差不多每个类都有一个非常庞大的接口,所以你要花很多时间和精力去理解它的内部构造。但话说回来,它的设计还是很优秀的,其竞争者往往更糟。
表格
和树一样,Swing的表格控件也非常复杂强大。刚开始的时候,他们是想把它做成用JDBC连接数据库时常用的"grid"接口,因此它具有极高的灵活性,不过代价就是复杂度了。它能让你轻易创建一个全功能的电子表格程序,不过这要花整整一本书篇幅才能讲清楚。但是如果你弄懂了基本原理,也可以用它来创建一个相对简单的JTable。
JTable只负责怎样显示数据,而数据本身是由TableModel控制的。所以在创建JTable之前,你通常都得先创建一个TableModel。你可以从头开始去实现TableModel接口,但是Java提供了一个AbstractTableModel的帮助类,继承它会比较简单。
选择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下都是什么样子:
假如你为一个对程序外观有特殊要求的公司做一个framework的话,你甚至可以自创一套look and feel。不过这可是一个大工程,其难度远远超出了本书的范围。
剪贴板
JFC与系统剪贴板的互动功能非常有限(在java.awt.datatransfer package里面)。你可以把String对象当作文本复制到剪贴板里,也可以把剪贴板里的文本粘贴到String对象里。当然剪贴板支持任何类型的数据,至于数据在剪贴板里该怎么表示,那是粘贴数据的程序的事。Java通过"flavor"这个概念加强了剪贴板API的扩展性。当数据进到剪贴板的时候还跟着一组与这个数据相关联的,可以转换这些数据的flavor(比方说一幅画可以表示成一个全部有数字组成的字符串或一个image),这样你就能知道剪贴板里的数据是否支持你感兴趣的flavor了。
JTextField和JTextArea它们原本就已经支持剪贴板了。
可以期待,未来Java会提供更多的flavor。你能得到更多的数据flavor的支持。
将applet打成JAR卷宗
JAR的一个主要用途就是优化applet的装载。在Java 1.0时代,程序员们都尽量把applet的代码塞进一个类里,这样当用户下载applet的时候只需向服务器发一次请求就可以了。但这么做不仅使代码变得非常难读(也难维护),而且.class文件也是未经压缩的,因此下载速度仍有提升的潜力。
JAR解决了这个问题,它把所有的.class文件全都压缩在一个供浏览器下载的文件里。现在你可以大胆运用正确的设计方案而不用再担心它会产生多少.class文件了,而用户的下载速度也更快了。
签发applet
由于沙箱安全模型的限制,未获签名的applet是不能在客户端进行某些操作的,比如写文件,连接本地网络等。一旦你签发了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的方法,而且能自动安装证书,方便了用户。
} ///:~
要想签名,你必须先把它做成一个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过一遍,它会自动帮你生成正确的applet标记。
现在当用户下载applet时,浏览器就会提醒他们现在正在装载的是一个带签名的applet,并且问他是不是信任这个签发者。正如我们前面所讲的,测试用的证书并不具备很高的可信度,因此它会给一个警告。如果客户信任了,applet就能访问整个客户系统了,于是它就和普通的程序没什么两样了。
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来访问客户端系统上的资源。
注意,FileOpenService和FileCloseService是javax.jnlp里的类,要使用这两个服务,不但要用ServiceManager.lookup(
)提出请求,而且要用这个方法所返回的对象来访问客户端资源。如果你不想受JNLP束缚,要直接使用这些类的话,那就必须使用签名的JAR文件。
这个启动文件的后缀名必须是.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="classname.jnlp">click
here</a>
这样你就可以点击链接启动JNLP应用程序的安装进程了。你只要下载一次,以后就可以通过管理控制台来进行配置了。如果你用的是Windows的Java Web Start的话,那么第二次启动程序的时候,它会提示你,是不是创建一个快捷方式。这种东西是可以配置的。
我们这里只介绍了两个JNLP服务,而当前版本里有七种。它们都是为特定的任务所设计的,比如像打印,剪贴板操作等。
编程技巧
由于Java的GUI编程是一个还在不断改进的技术,Java 1.0/1.1与Java 2的Swing类库之间就有着非常重大的区别,与旧模式相比,Swing能让你用一种更好的方式编程。这里,我们会就其中一些问题做个介绍,同时检验一下这些编程技巧。
动态绑定事件
Swing的事件模型的优点就在于它的灵活性。你可以调用方法给组件添加或删除事件。
- Button可以连不止一个listener。通常组件是以多播(multicast)方式处理事件的,也就是说你可以为一个事件注册多个listener。但是对于一些特殊的,以单播(unicast)方式处理事件的组件,这么做就会引发TooManyListenersException了。
- 程序运行的时候能动态地往Button b2上面添加或删除listener。你应该已经知道加listener的方法了,此外每个组件还有一个能用来删listener的removeXXXListener( )方法。
这种灵活性为你带来更大的便利
值得注意的是,listener的添加顺序并不一定就是它们的调用顺序(虽然绝大多数JVM确实是这么实现的)。
将业务逻辑(business logic)与用户界面分离开来
一般情况下,设计类的时候总是强调一个类"只作一件事情"。涉及用户界面的时候更是如此,因为你很可能会把"要作什么"同"要怎样显示"给混在一起了。这种耦合严重妨碍了代码的复用。比较好的做法是将"业务逻辑(business login)"同GUI分离开来。这样不仅方便了业务逻辑代码的复用,也简化了GUI的复用。
还有一种情况,就是多层系统(multitiered systems),也就是说”业务对象(business object)"完全贮存在另一台机器上。业务规则的集中管理能使规则的更新立即对新交易生效,因此这是这类系统所追求的目标。但是很多应用程序都会用到这些业务对象,所以它们绝不能同特定的显示模式连在一起。它们应该只做业务处理,别的什么都不管。
树立了将UI同业务逻辑相分离的观点之后,当你再碰到用Java去维护遗留下来的老代码时,也能稍微轻松一点。
范式
内部类,Swing事件模型,还能继续用下去的AWT事件模型,以及那些要我们用老办法用的新类库的功能,所有这些都使程序设计变得更混乱了。现在就连大家写乱七八糟的代码的方式也变得五花八门了。
这些情况都是事实,但是你应该总是使用最简单也最有条理的解决方案:用Listener(通常要写成内部类)来处理事件。
用了这个模型,你可以少写很多"让我想想这个事件是谁发出的"这种代码。所有代码都在解决问题,而不是在做类型检查。这是最佳的编程风格,写出来的代码不仅便于总结,可读性和可维护性也高。
并发与Swing
写Swing程序的时候,你很可能会忘了它还正用着线程。虽然你并没有明确地创建过Thread对象,但它所引发的问题却会乘你不备吓你一跳。绝大多数情况下,你写的Swing或其他带窗口显示的GUI程序都是事件驱动的,而且除非用户用鼠标或键盘点击GUI组件,否则什么事都不会发生。
只要记住Swing有一个事件分派线程就行了,它会一直运行下去,并且按顺序处理Swing的事件。如果你想确保程序不会发生死锁或者竞争的情形,那么倒是要考虑一下这个问题。
重访Runnable
在第13章,我曾建议大家在实现Runnable接口时一定要慎重。 当然如果你设计的类必须继承另一个类而这个类又得有线程的行为,那么选择Runnable还是对的。
不同的JVM,在如何实现线程方面,存在着巨大的性能和行为差异。
管理并发
当你用main方法或另一个线程修改Swing组件的属性时,一定要记住,有可能事件分派线程正在和你竞争同一个资源。
看来线程遇到Swing的时候,麻烦也跟着来了。要解决这个问题,你必须确保Swing组件的属性只能由事件分派线程来修改。
这要比听上去的容易一些。Swing提供了两个方法,SwingUtilities.invokeLater( )和SwingUtilities.invokeandWait( ),你可以从中选一个。它们负责绝大多数的工作,也就是说你不用去操心那些很复杂的线程同步的事了。
这两个方法都需要runnable对象作参数。当Swing的事件处理线程处理完队列里的所有待处理事件之后,就会启动它的run( )方法了。
能用这两个方法来设置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"的命名规范接轨。
用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放到表单上,然后配置一下属性,再写个程序处理一下事件就可以了。
一个更复杂的Bean
所有的字段都是private的这是Bean的通常做法——也就是说做成"属性"之后,通常只能用方法来访问了。
JavaBeans和同步
只要你创建了Bean,你就得保证它能在多线程环境下正常工作,这就是说:
- 只要允许,所有Bean的public方法都必须是synchronized。当然这会影响性能(不过在最新版本的JDK里,这种影响已经明显下降了)。如果性能下降确实是个问题,那么你可以把那些不致于引起问题的方法的synchronized给去掉,但是要记住,会不会引发问题不是一眼就能看出来的。这种方法首先是要小(就像上面那段程序里的getCircleSize( )),而且/或是"原子操作",就是说这个方法所调用的代码如此之少,以至于执行期间对象不会被修改了。所以把这种方法做成非synchronized的,也不会对性能产生什么重大影响。所以你应该把Bean的所有public方法都做成synchronized,只有在有绝对必要,而且确实对性能提高有效的时候,才能把synchronized移掉。
- 当你将多播事件发送给一队对此感兴趣的listener时,必须做好准备,listener会随时加入或从队列中删除。
第一个问题很好解决,但第二个问题就要好好想想了。
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)"模式打开,看看基本功能和高级功能有什么区别。
总结
这一章只是想跟你介绍一下Swing的强大功能然后领你入门,这样当你知道相对而言Swing有多简单之后,你就能自己去探路了。你看到的这些已经能大致满足UI设计之需了。但是Swing不止于此;它的目标是要成为一种功能齐全的UI设计工具。只要你能想到,它都有办法能作到。
如果你在这里找不到你想要的,那么研究一下Sun的JDK文档吧,或者去搜Web,如果还不行,就去找一本Swing的专著。
PS:
TIJ
的学习暂时就告与段落了,因为3th目前网上翻译只到第14章,第三次看这本书,算是比较明白了。但是从第8章开始,很多地方还是有一些不太懂的地方。特
别是I/O部分,需要稍后再尽快加强一下。总之,再第三次看了才发现这本书的好,真的实在太经典了,我觉得自己最少应该再看个2~3遍才差不多。
最后,感谢本书的作者Bruce
Eckel的无私奉献,写了这么好的一本书而且还免费放到网上。当然还要感谢shhgs,这位热心的网友的翻译,对于我这种英盲来说,这种帮助实在是太大
了。作者和译者的无私奉献的精神值得我们大家学习,和那些惟利是图,见钱眼开的人来说。这两位的风格,人品何止高出一两倍,实在是小辈的榜样和偶像,真的
是万分的敬仰之情难于言表。
2005年04月06日 5:08 AM