|
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。
|