总的来说Swing/AWT 和SWT 在事件处理机制上是类似的,窗口组件的树状结构也是
类似的。图形用户界面系统在事件处理设计上有两大类,一类是单线程模型,一类是多线程
模型。在事件处理机制上,三者都是遵循单线程规则。
单线程模型对于事件处理不保证线程安全性(Thread Safety),所有的事件处理都在Event
Dispatch Thread(EDT)上进行,此一类事件模型通常叫做单线程模型。这种模型规定所有对组件的访问操作必须在EDT 上完成。为什么对于组件的访问需要在EDT 上完成?这主要是
为了保证对于组件状态的改变是同步的,保证了界面组件的可确定性。这中模型是大部分图
形用户界面工具采用的模型,包括Swing/AWT、SWT、GTK、WinForm 等等。
这种模型的好处是,结构设计和代码实现都比较简单,避免了为了实现线程同步的复杂
处理。但是也带来了一些问题,最常见的问题是,程序员容易将长时间复杂任务的处理放在
事件处理函数完成,造成EDT 线程被阻塞,给用户造成界面失去响应的错觉。
其实人们对于Swing 速度慢和反映迟钝的感觉大部分来源于此,简单的说,是程序员
的问题,而不是Swing 自身的问题,是因为程序员没有理解这种事件处理机制造成的。其
实在SWT、GTK、WinForm 等任何以这种事件模型为基础的工具都会出现。重现的方法就
是你简单的将长时间处理的任务放在事件处理函数中,你的用户界面就会失去响应。
如何解决这种问题?通用的办法就是采用异步线程处理长时间任务。但是还要记住的
是,在这种任务中对于界面的更新要采用SwingUtilities.invokeLater 或者在SWT 采用
Synchronize 方法,将访问操作放到EDT 上进行。关于如何写一个有效处理长时间任务的
Swing 程序,将会在其他文章中描述。
多线程模型中,所有的事件处理都是在异步线程中进行,界面组件同步访问的操作需要
程序员来保证。这种模型设计本身很复杂,而且对于程序员来说要求比较高,必须对线程同
步编程很熟悉,而且花在同步上的操作影响了平台的性能。一般现在的图形界面工具都不再
采用这种方式。
下面比较一下Swing/AWT/SWT 在API、GUI 特征以及实现方法的不同。
在API 上,Swing 和AWT 是兼容的,SWT 是单独的一套接口。
1.Swing/AWT 的组件在生成时可以脱离父组件独立存在,SWT 必须有父组件存在。这
主要是由于SWT 的资源是自己管理,SWT 程序必须负责释放不用的资源,为了避免这种释
放资源的重复性,SWT 父组件被设计成在析构时自动递归调用子组件的析构函数。
2.Swing/AWT 的资源回收由垃圾收集器负责,SWT 必须由SWT 程序显式释放。
3.Swing/AWT 的事件线程循环不需要程序员显式启动,SWT 必须要程序员来显式启动。
Swing/AWT 和SWT 在布局管理器上是类似的,没有太大区别。
在GUI 特征上,有两个比较层面,一个是组件种类,一个是组件本身特征。在理解这
些特征时,一定要牢牢记住这样一个准则:Java 是平台无关的语言,因此它对应用程序所提
供的API 一定要各个平台都相同,GUI 特征其实也是API,所以GUI 的特征必须在各个平
台都相同。
组件类型上,AWT 采用的是最小公约数方法,而Swing/SWT 采用的是最大公倍数方法。
简单的说AWT 是各个平台所有组件集合的交集,而Swing 和SWT 则是各个平台组件的并
集。下图所示,假设操作系统平台OS1 上提供组件{C1, C2, C3, C4, C7},而OS2 提供{C1,
C2,C3, C4, C6},OS3 提供{C1, C2, C3, C4, C5},那么其中的阴影部分就是AWT 所实现的组
件,由于C5、C6 和C7 是各个平台所特有的,因此AWT 中就不包含这些组件。比如Table
和Tree 在Java 支持某些操作系统平台中不包含,所以在AWT 中就没有Table 和Tree。由于
AWT 的组件太贫乏,所以AWT 在现在复杂应用程序几乎没有什么用。Swing 和SWT 提供
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
的组件是各平台所有组件的并集,这样就解决AWT 的组件贫乏的缺陷。也就是说,Swing
和SWT 提供的组件包括C1 到C7 的所有组件,而AWT 只提供C1 到C4 的所有组件。
从组件本身的特征来看,SWT 和AWT 采用了相同的策略,即最小公约数,而Swing
采用的是最大公倍数。如下图所示,假设对于同一个组件C,如果它在OS1 上提供的特征
包括{a,b,c,d,e},而OS2 上提供的特征包括{a,b,c},而OS3 包括的特征有{a,b,c,d},那么SWT
和AWT 提供的组件特征只包括{a,b,c},而Swing 的包含的平台特征包括{a, b, c, d, e}。比如
由于Solaris 平台上的按钮不提供对于图标的支持,所以SWT 和AWT 的独立按钮就不提供
对于按钮图标的支持,而Swing 提供按钮图标的支持。
在实现方法上,AWT 采用Java+Native C Peer(一种JNI 调用)方法,SWT 根据各平台
的不同情况,一部分组件使用Java+Java Peer+JNI Wrapper,一部分采用Java 模拟的方法,
而Swing 则采用所有组件都纯粹Java 模拟的方法。
AWT 的接口和各操作系统组件之间的差别采用Native C Peer 实现的方法来填平,下面
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
是它的结构示意图:
其中AWT Native Peer Impl 部分都是C 语言写的,在各种操作系统上是不同的,但是它
们和AWT Component 组件之间的AWT-Peer JNI 调用接口是不变的,AWT Component 的Java
代码实现在各个平台上都是相同的,最后AWT Component 向Application 提供同一的API
接口。
SWT 同AWT 不同,它在Native Component 上进行了一层薄薄的JNI 封装,所有操作
系统的API 调用都被映射到一个JNI 调用上,然后SWT 通过Java 代码组合这些JNI 调用实
现同一的API,下面是其结构示意图:
因此SWT 的Java 代码实现部分在各个平台是不同的,它的C 代码部分即JNI Wrapper
部分只是一个各平台GUI API 的JNI 简单映射,SWT 通过Java Peer 在各平台的不同实现填
平了各平台差异,从而给Application 提供同一的API 接口。当然SWT 对于某种平台上缺少
的组件采用的方法和Swing 基本类似。
Swing 和上两中方式完全不同,它直接调用Java2D,抛弃了本地操作系统平台组件的
实现,完全自己画出来了整个组件,当然Java2D 底层也是调用平台的图形系统。下面是它
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
的示意图:
当然Swing 是建立在AWT 基础上的,对于一些顶层容器类如Frame / Dialog / Window
以及Applet 是直接采用AWT 的,这儿为了方便并没有画出。由于Java2D API 是个平台无
关的,因此Swing 的Java 实现代码在个平台都是一样的,都是一套,当然除了与Swing 的Look
And Feel 相关的东西以外。
由于篇幅原因,今天就先谈到这儿,至于这三种不同架构、实现会对它们的性能和外观
以及编程难度产生什么影响,我们以后的文章逐渐讨论。
Swing 模型与渲染器
本文承接Swing/AWT/SWT 比较一文,概要叙述Swing 的体系结构,解释了Swing 架构
关键概念:模型与渲染器,解释如何使用渲染对象扩展该体系架构来支持大数据量的组
件。后面的文章还会简要概述SWT 的体系结构,为Swing/AWT 和SWT 的比较做一铺垫。
Java 基础类(JFC)Swing 工具提供了使用Java 平台创建高度可交互性图形用户界面的类。
Swing 是高度灵活的,但是也因此相当复杂,虽然新手能够使用Swing 创建基本的图形用户
界面(GUI),但是真要创建一个复杂、专业的GUI 界面,你必须理解Swing 的体系架构的基
础,尤其是使用Swing 创建复杂、像JTable 、JTree、JComboBox 以及JList 这样基于渲染器
的组件,Swing 提供的基于模型和渲染器的组件是构建高性能、可扩展GUI 的关键。
Swing体系结构
最初Smalltalk 系统的UI 工具使用所谓的模型-视图-控制(MVC)模式,MVC 引入这
样一个概念:数据源应该同屏幕展现分开。这是一个优秀的体系设计结构,能促进代码重用
和程序框架。Swing 使用的是一个变体的MVC 架构,如图所示:
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
典型的Swing GUI 组件包括至少三个对象:一个Component,一个Model 和一个UI
Delegate,在这个框架中,Model 负责存储数据,UI Delegate 负责从Model 获取数据并渲染
到屏幕上去,Component 通常协调Model 和Delegate 之间的操作,并同时负责将Swing 嵌
入到AWT 窗口系统中。
注意,UI Delegate 对象可以在运行的时候动态替换,这就使Swing 具备了可插拔的外
观(Pluggable Look-And-Feel, PLAF)。
虽然Swing 的MVC 结构显然具备灵活性的好处,但是这个结构通常被指责为一些程序
慢的根源。虽然基于MVC 结构需要更多的方法调用来支持额外的重定向,其实花费在这儿
的消耗很小。对基于Swing 的应用程序profile 的结果显示,model-vie w 分隔的开销可以忽
略不计,不到CPU 总开销的1%,复杂的Swing 用户界面的多数处理事件其实都花费在了
底层的图形操作上了。Swing 的model-vie w 结构并不是低性能的根源,它是构建可扩展程序的关键。
矢量组件
Swing 提供了一些处理大数据量数据集的组件,包括JTable、JTree、JList 以及
JComboBox。这些矢量组件被设计成能够处理成千上万甚至数百万的数据,为了避免占用大
量内存,这些组件在Swing 的体系架构增加了渲染器(renderer)概念。下图是增加了渲染器结构的Swing 体系架构。
渲染器(Renderer)
在这些更为复杂的Swing 组件中,渲染器是提供可扩展性的关键。我们以JTable 作为
渲染器的示例。缺省表格中的每一格可能都有一个JLabel,这对于比较小的数据集来说可行,
但是对于大数据集就行不通。比如,如果使用这种表格显示1000x1000 的数据集,需要的内
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
存可能要1G,即使每个格子都是空的。
如果解决这种扩展性问题?Swing 的JTable 使用一个组件来画出所有相同类型的格子。
比如所有的String 对象的格子都使用相同的组件画。这种类型的组件被称作渲染器
(renderer),使用渲染器显示多个表格极大的减小了大型数据表存储空间。
当渲染器用来显示表格时,JTable 从model 中获取格子中的数据,然后使用这些数据对
渲染器进行配置,然后使用该渲染器画出该格子。接下来,渲染器继续移动到下一个格子,
然后重复这个过程。
注意你可以通过操作渲染器和模型来控制这个过程,所有的矢量组件,包括JTree、JList
以及JComboBox 都使用渲染器方法,并不仅限于JTable。
模型(Model)
直接操作Swing 的模型(Model)对于编写可扩展的用户界面至关重要,下面代码是往
JComboBox 添加数据项的通常做法:
JComboBox box = new JComboBox();
for (int i = 0; i < numItems; i++) {
box.addItem(new Integer(i));
}
这些代码只是简单的往JComboBox 中添加数据项,代码同往AWT 的Choice 中添加选
项类似,这种方法对于小数据量来说可以,但是当要添加大量数据时就会明显变得非常慢。
尽管上面的代码没有明确引用任何模型,JComboBox 的模型对象实际上参与这个过程,
每次调用addItem 时,JComboBox 内部发生了许多操作:组件将请求传递给JComboBox 的
模型,模型发送一个事件表明一个新项被添加。很明显,如果你直接操作模型的将会更高效,
如下例所示:
Vector v = new Vector(numItems);
for (int i = 0; i < numItems; i++) {
v.add(new Integer(i));
}
ComboBoxModel model = new DefaultComboBoxModel(v);
JComboBox box = new JComboBox(model);
这样为什么会更快呢?原因有两个。第一,因为所有项是一次添加到模型去,而不是一
个一个的,只有一个事件发出,这意味着更少的事件触发,更少的方法调用。第二是因为需
要通知变化的对象更少,总的工作量等于触发次数乘以侦听器数目。因为模型是新创建的,
侦听在上面的侦听器为零,这意味着没有触发事件发生。
从上面的例子可以学到两点:
尽可能使用批操作,尽量减少触发事件的数量。
当初始化或者需要完全替换模型的内容时,考虑重新生成模型,不要使用已经存在的模
型,已存在模型上已经保持了很多的侦听器,新生成的模型没有侦听器,这样避免了不必要
的处理函数的调用。
触发事件数量严重影响你的程序启动时间,也会影响打开对话框和相似操作的时间。
本想详细举几个例子进一步说明模型和渲染器的用法和好处,但网络速度还是太慢,写
一篇文章太痛苦了...加上篇幅原因,准备以后再写一文,弥补这方面的知识。
以后几篇文章包括(非文章标题):
使用Swing 模型和渲染器优化Swing 程序。
Swing/AWT 事件处理模型以及线程安全。
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
如何编写响应快速的Swing 程序。
SWT 设计和实现介绍。
JNI 与Java 性能的关系。
垃圾收集与虚拟机与Java 性能。
Swing 与SWT 性能比较(前面所有的文章都和这篇有关,是基础)。
再往以后,准备专门写一些Swing 和Java2D 等方面的文章。
Swing 框架之Component
昨天晚上写完Swing 的模型和渲染器之后,觉得对Swing 的体系结构还是没有说清楚。
Swing 的基础体系结构中的四大基本对象Component、Model、UI Delegate 以及Renderer
都值得详细解释。Swing 的树状组件结构(虽然这是用户界面工具通有的特征)也值得详细
解释,因为这是完成某些复杂Swing 组件,尤其像JTable、JTree、JList 和JComboBox 这种
复杂组件中编辑功能得关键。此外,Swing / AWT 的事件模型如Event Dispatching 和
Propagation 和事件处理线程模型也需要详细解释,理解这部份是编写高效率Swing 界面的
关键。
今天从Swing 的四大基本对象Component 说起。
============================
Component 在Swing 的MVC 模型中担任Controller 的角色,同时它也是Swing API 中
代表具体组件的对象。
Component 在Swing 中对外负责提供API 接口,对内负责协调控制Model 和UI Delegate
(有时可能还包括Renderer)的操作,可以说是整个Swing 结构的中心角色。为了方便你回
忆Swing 的MVC 模型,特地将上一篇文章中的Swing 模型示意图引了过来:
Component 代表Swing 对应用程序提供了如下几类编程接口:
1.用户界面的组件树的创建和修改的方法。这包括组件的添加和删除等操作。
2.组件属性访问的方法,比如组件位置、组件前后背景色、组件字体等等。
3.组件状态及生命周期的管理的方法,比如隐藏和显示、创建和销毁等等。
4.组件位置、大小的管理,包括通过布局管理器的方法。
5.组件事件处理接口的管理,包括添加、删除等操作。
从开发者的角度来看,Component 是组件树上的节点、是控制外观和行为的入口、是组
件事件的发源地。从Swing 组件实现者的角度来看,Component 是协调Model 和UI Deleg ate
的操作的地方,是低层次事件处理的地方,是高层事件发生的地方,是同父组件和子组件交
互的地方。掌握的这些角度,Swing 程序员完全可以实现自己的自定义简单组件,当然如需
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
要实现类似于JTable 和JTree 等复杂的矢量组件,还需要进一步了解Swing 的Model 和UI
Delegate 以及Renderer 模型。
对于复合型(Composite)组件很简单,这儿要讲述的是如何实现自定义的组件,比如表盘,
比如温度计这些没有标准组件可以使用的组件,那么如何自己实现这种自定义组件呢?
不考虑Model 分隔和UI Delegate 皮肤分离问题,能够简化自定义Component 的模型。
总的来说自定义组件需要完成两样基本任务:第一侦听并处理低层事件,根据具体情况改变
组件状态,如需要还要发出高级事件;第二,根据当前组件的状态画出当前组件的外观。
侦听底层的事件是指侦听类似于mouse、keyboard、focus 等事件,然后处理此事件,如
果发现此事件带有特定语义,表达某种组件行为,则改变当前的组件状态以记录,并触发某
种事件通知应用程序进行处理。举例说明,想象你准备实现一个简单的按钮,你可以通过继
承JComponent 来完成。你可以在按钮初始化时,注册此按钮的鼠标事件侦听器,以侦听发
生自己组件上的鼠标事件。当按钮捕获到鼠标按下时,检查鼠标按下的点是否在按钮有效区
域内。如果是,则认为当前是一个按钮按下动作,那么改变按钮的状态为按下去,调用repaint
方法通知按钮重画成按下去的状态,然后发出ActionPerformed 的事件,通知注册在此按钮
上的应用程序的ActionListener 处理这个动作。下面是一个简单示意代码:
public class MyButton extends Jcomponent implements MouseListener{
private String text;
private boolean pressed=false;
private ArrayList<ActionListener> listeners=new ArrayList<ActionListener>();
public MyButton(){
addMouseListener(this);//将自己注册为自己的鼠标事件侦听器,监听鼠标事件
}
....
public void mousePressed(MouseEvent evt){
Point p=evt.getPoint();
if(getBounds().contains(p)){//判断鼠标落点是否在有效区域内。
pressed=true; //鼠标点击的含义是按钮被按下!改表按钮状态。
repaint(); //通知按钮重画,由原来的抬起状态改变成按下状态。
fireActionPerformed(new ActionEvent(this)); //这是一个按钮动作事件,触发它。
}
}
public void addActionListener(ActionListener listener){
listeners.add(listener);
}
public void removeActionListener(ActionListener listener){
listeners.remove(listener);
}
protected fireActionPerformed(ActionEvent evt){
for(ActionListener listener:listeners){
listener.actionPerformed(evt);
}
}
...
//这儿你要覆盖paint 方法,实现按钮状态的重画
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
public void paint(Graphics g){
if(pressed){
//画出按下的样子
}else{
//画出抬起的样子
}
}
}
上面要注意的是你要自己管理自定义组件的事件监听器,包括addListener 和
removeListener 方法,以及如何触发。这个过程很简单,基本上就是上面的模板来实现添加
删除和触发。
除了要负责事件的处理和新事件的触发,自定义组件第二个要完成的任务就是要根据
组件当前的状态改变重画组件的外观。重画组件的外观只需要覆盖public void paint(Graphics
g)方法即可以,在这个方法里,你只需要根据当前的组件状态分别画出当前的组件即可。
当然除了上面两个基本准则外,不要忘了添加访问你的组件属性的方法,比如,如果
上面的按钮是个二元按钮(相当于JCheckbox/JToggleButton 的那种按钮),你可能需要提供
isPressed 或者setPressed 来获取和设置当前按钮的状态。注意,在设置状态按钮变化的访问
方法中,比如setPressed,你需要使用repaint 方法通知按钮重新渲染(复杂的实现可能包括触
发propertyChange 事件,这儿从简):
public void setPressed(boolean p){
pressed=p;
repaint();
}
到此为止,你已经能根据上面的两条准则简单的实现你想要的组件了。但是你发现没有,
你的按钮状态和外观行为都被堆到了Component (MyButton)中实现了,而且,对于各个平
台都是一个样子,不能换皮肤。这对于比较简单、不想要皮肤的组件,可能没有什么,但是
对于复杂的组件,比如JTable 或者甚至Excel 类似的电子表格的那种组件,你把数据(组件
状态)和外观堆在这儿实现就严重违反了MVC 原则。
如何简化这种组件的实现呢?使你实现的此种组件容易维护、扩展以及重用,皮肤容易
换呢?这就需要Swing 结构中的另外三个元素:Model、UI Delegate 和Renderer,后面的几
个文章将讲述Model、UI Delegate 和Renderer 帮你逐步实现一个复杂、灵活、可扩展、高
效的矢量组件。
=====================================================
今天这样讲,不知道讲明白没有。当初刚开始学习Swing 的时候,还不了解Swing 的
这种MVC 结构,因此当时自己做的自定义组件都是这样写的,没有单独的Model 和可以定
制的UI Delegate,更不用说Renderer 了。但是我觉得自己的这个学习过程,恰恰是人们学
习Swing 的最佳途径,先简化模型,然后逐步扩展,直到了解Swing 模型的全部图像。
Swing 框架之Component:续文一
昨晚回去后还是觉得Component 对象本身说的太简单,想来想去,觉得内容实在是太
多,有必要补充两个续文说明Component 的其它概念。今天介绍Swing 组件paint 方法的处
理流程,这个流程能使我们理解许多Swing 机制。明天续文讲述Swing 事件处理器、双缓
冲和布局管理器等原理。
=======================
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
Swing 组件的paint 方法是内部接口方法,一般用户不要直接调用这个方法,它总是在
事件调度线程中调用。一般说来除了系统刷新事件触发这个方法,Component 的repaint 也
触发这个方法的调用。repaint 方法常用于当组件状态发生变化时刷新界面使用。repaint 方法
是Swing 中少数几个线程安全的方法,可以在任何线程中调用它。它的原理是往事件队列
中post 一个PAINT 事件。由于事件队列的事件是被事件调度线程同步执行的,所以这个方
法总是线程安全的。
事件调度线程从PAINT 事件中获取事件源组件,从系统申请到图形设备资源后,调用
该组件的update 方法。update 是AWT 时代遗留下来的产物,本意是AWT 组件画好组件背
景后,再调用paint 方法画出组件的前景。Swing 出现后这个方法就被弃用了,所有逻辑都
转到paint 方法里。Update 只是简单地调用paint 方法来完成组件的渲染。老的Java 教材上
经常可以看到,所谓repaint 调度update 方法,update 接着调用paint 方法,自定义组件需要
重载paint 方法等话语,就是因为这个历史造成的。
上篇文章中的MyButton 的paint 方法实现是一个非常老式的做法。现在JComponent 的
实现已经把paint 方法改造成可以嵌套多重机制地方,这些机制包括层次渲染、边框、透明
背景、双缓冲以及皮肤等。这些机制分别实现不同目的的组件提供了方便。
图形用户界面的组件按照其在组件树上的角色可以分为容器组件和叶组件。Swing 模型
把叶组件当作是特殊、没有子组件的容器组件,只是JComponent 继承Container 类,所有
Swing 组件继承JComponent 的原因。
JComponent 在paint 方法中首先根据组件是否需要使用双缓冲,封装好图形设备对象,
然后经过一番处理后调用paintComponent 方法画出自身,然后调用paintBorder 画出边框,
最后调用paintChildren 来完成子组件的渲染。
paintComponent 意思是画出组件自身,不包括子组件。因此前一文章中的MyButton 可
以通过覆盖paintComponent 方法来完成MyButton 的重画。在JComponent 实现中,JDK 6
的paintComponent 的代码为:
protected void paintComponent(Graphics g) {
if (ui != null) {
Graphics scratchGraphics = (g == null) ? null : g.create();
try {
ui.update(scratchGraphics, this);
}
finally {
scratchGraphics.dispose();
}
}
}
这个方法首先检测组件是否安装了UI Delegate,如果安装了就将渲染过程代理给UI
Delegate。这儿是嵌入皮肤的地方。JDK 6 中JComponent 对应的UI Delegate 的update 方法
缺省的实现是:
public void update(Graphics g, JComponent c) {
if (c.isOpaque()) {
g.setColor(c.getBackground());
g.fillRect(0, 0, c.getWidth(),c.getHeight());
}
paint(g, c);
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
}
可以看出,背景透明机制在这儿实现。首先UI Delegate 对象判断Component 是否背景
透明的,如果不是透明的,则使用背景色填充整个Component 区域,然后调用paint(g, c)来
完成组件在这种LookAndFeel 种的渲染。了解了这些后,我们几乎就明白了Swing 如何实
现背景透明和如何切换皮肤。由于后面的文章还会对UI Delegate 和皮肤机制详细描述,这
儿就到此为止。
目前还不要求实现皮肤,在这种情况下只需要重载paintComponent 方法就行了,如果
需要背景透明机制,可以模仿上面代码,MyButton 的paintComponent 可以这样写:
public void paintComponent(Graphics g) {
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
if(pressed){//按钮按下去了
//画出按下的样子
}else{
//画出抬起的样子
}
}
paintBorder 意思是画出组件的边框。Swing 所有组件都有边框的概念,就是说可以为任
何组件添加各种边框,包括自定义的边框。JDK 6 中JComponent 的paintBorder 的实现是这
样的:
protected void paintBorder(Graphics g) {
Border border = getBorder();
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
非常直接,如果自己有border,就将画自己边框的任务代理给了这个border,并传给它
图形设备和边界参数。Swing 缺省提供了大量的各种各样的边框。同样可以定义自己的边框,
实现方法就是继承Border 类,Border 类中有三个方法要实现,它们的含义如下:
public interface Border
{
//这儿是画出组件边框的地方。
void paintBorder(Component c, Graphics g, int x, int y, int width, int height);
//这儿是定义边框边界的地方,组件可以根据这信息,安排它的内容。
Insets getBorderInsets(Component c);
//边框的背景是不是透明的?不是透明的要负责画出边框的背景。是透明的使用组件的
背景。
boolean isBorderOpaque();
}
这儿实现一个简单的红线边框作为演示:
public class RedLineBorder implements Border{
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height){
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
g.setColor(Color.red);//设置为红色
g.drawRect(x,y, width, height);//画出边框
}
public Insets getBorderInsets(Component c){
return new Insets(1,1,1,1); //四周都是1
}
public boolean isBorderOpaque(){
return false; //背景透明
}
}
paintChildren 完成容器类组件的子组件的渲染。JDK 缺省的实现是调用各个自组件的
paint 方法。一般来说不需要重载这个方法。如果想改变诸如组件Z-orde r 遮挡顺序,可以覆
盖这个方法,从相反顺序调用组件的paint 方法。
到这儿我们对Swing 的结构有了更深化的理解,UI Delegate 机制也已经初露倪端。还
有几个重要Swing Component 概念或者机制没有讲,明天的续文再对它们做出说明。
Swing 框架之Component:续文二
Swing 的事件处理过程为:事件调度线程(Event Dispatch Thread)从事件队列
(EventQueue)中获取底层系统捕获的原生事件,如鼠标、键盘、焦点、PAINT 事件等。接着
调用该事件源组件的dispachEvent。该方法过滤出特殊事件后,调用processEvent 进行处理。
processEvent 方法根据事件类型调用注册在这个组件上的相应事件处理器函数。事件处理器
函数根据这些事件的特征,判断出用户的期望行为,然后根据期望行为改变组件的状态,然
后根据需要刷新组件外观,触发带有特定语义的高级事件。此事件继续传播下去,直至调用
应用程序注册在该组件上的处理器函数。下图是这个过程的示意图:
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
上图所示意的过程简要说就是:
Pump an Event->Dispatch & Process Event->MouseListener.mousePressed-
>fireActionPerformed->ActionListener.actionPeformed->Do database query and display result to
a table->Return from actionPerformed->Return from fireActionPerformed->Return from
MouseListener.mousePressed->Pump another Event.
事件调度线程在应用程序事件处理函数actionPerformed 没有完成之前是不能处理下一
个事件的,如果应用程序处理函数是一个时间复杂的任务(比如查询数据库并将结果显示到
表格中),后面包括PAINT 事件将在长时间内得不到执行。由于PAINT 事件负责将界面更新,
所以这就使用户界面失去响应。
打一个比方,事件处理线程就像进入某城唯一的单行道一样,事件相当于汽车。有种
PAINT 汽车负责为城市运输非常重要的生活物资。但是有一天,PAINT 前面有一辆汽车突
然坏掉了,司机下来修车。但是这车太难修,一修就是几天,结果后面的PAINT 汽车无法
前进,物资无法按时运到城里。市民急了,市长虽然不停的打电话催PAINT 公司,但即使
PAINT 公司多添加几辆车也没用。由于进城的唯一条路被那辆车给占着,所以再多的PAINT
车也只能堵在路上。
不了解Swing 的这种事件处理模型的人往往将时间复杂的任务放在处理函数中完成,
这是造成Swing 应用程序速度很慢的原因。用户触发这个动作,用户界面就失去了响应,
于是给用户的感觉就是Swing 太慢了。其实这个错误是程序员造成的,并不是Swing 的过
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
失。
说点题外话,所有采用这种事件模型的用户界面工具都会产生这种问题,包括SWT、GTK、MFC
等流行的用户界面工具。之所以只有Swing 被误解,主要是和Swing 的历史、市场时机、商业宣传策略和
心理学相关的。
首先Swing 的历史和市场时机极差。Swing 出现早期性能也差、错误也多,而Java 程序员脱身于传
统图形界面工具,对于Swing 这种新的事件处理模型并不太了解,而此时正处于Java 第一轮狂热的时期,
大家都满怀希望做了大量的Swing 应用程序,而这些程序中大量存在这种错误方法。于是市场上涌现了大
批的这种程序。自从那个时代,因为这些程序,Swing 被贴上了慢的标签。又由于当时的Swing 界面也丑,
和一般的Windows 程序风格炯异,更加深人们的这种印象。这种印象一直持续到现在,像烙印一样深深的
刻在人们的脑海里。
其次,Swing 还有一个致命的问题,就是没有涌现出一个具有标识性的好程序,这是造成它比SWT
印象惨的原因。为什么SWT 采用相同的事件处理模型,而获得了速度快的声誉呢?这是因为人们当时对于
Java 做桌面应用的期望心理达到了低谷,而SWT 的出现恰恰是伴随Eclipse 出现的,早期的Eclipse 的确是
在速度快、界面漂亮,这一扫当时人们认为Java 慢,Java 界面丑陋,Java 无法做桌面应用的印象,继而这
个印象被加在SWT 身上,人们认为Eclipse 速度快、漂亮是因为SWT,其实如果你知道Swing/SWT 事件
处理模型的话,你就明白功劳是Eclipse 开发者的,Eclipse 界面漂亮其实要归功于Eclipse 界面设计专家,
他们的高水平造就了这个好的IDE,从而也抬起了SWT 的声誉。而Swing 的名誉恰恰就被早期Swing 低水
平开发者给毁了。
再次, 这和商业宣传策略有关。IBM 和Eclipse 很懂得市场宣传,人们不是认为Java 慢吗,就宣传SWT
使用原生组件,人们不是认为Swing 丑陋、风格炯异吧,就宣传SWT 风格一致性,人们不是认为Java 不
能做桌面应用吗,就宣传基于SWT 的Eclipse。其实这一切的背后原因只是“人”的不同,Eclipse 的开发者
和Swing 应用程序的开发者,Swing 和SWT 技术差异并没有造成那么大的差别,如果是相近能力的人使用
他们开发的话,应该能做出相近的产品。这可以从现在Eclipse 和NetBeans、Intellij IDEA、JDeveloper 和
JBuilder 看的出来。
最后,人类有一个心理学现象,就是一旦形成对某种事物的印象,很难摆脱旧的认识,有时甚至人们
不愿意承认摆在眼前的事实。总而言之,Swing 和SWT 不同遭遇是因为历史、市场时机、商业宣传策略、
心理学的种种原因造成的。
那么如何避免这个问题,编写响应速度快的Swing 应用程序呢?在SwingWorker 的
javadoc 中有这样两条原则:
Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the
application becomes unresponsive. 耗时任务不要放到事件调度线程上执行,否则程序就会失
去响应。
Swing components should be accessed on the Event Dispatch Thread only. Swing 组件只能在
事件调度线程上访问。
因此处理耗时任务时,首先要启动一个专门线程,将当前任务交给这个线程处理,而当
前处理函数立即返回,继续处理后面未决的事件。这就像前面塞车的例子似的,那个司机只
要简单的把车开到路边或者人行道上修理,整个公路系统就会恢复运转。
其次,在为耗时任务启动的线程访问Swing 组件时,要使用SwingUtilties. invokeLater
或者SwingUtilities.invokeAndWait 来访问,invokeLater 和invokeAndWait 的参数都是一个
Runnable 对象,这个Runnable 对象将被像普通事件处理函数一样在事件调度线程上执行。
这两个函数的区别是,invokeLater 不阻塞当前任务线程,invokeAndWait 阻塞当前线程,直
到Runnable 对象被执行返回才继续。在前面塞车的例子中,司机在路边修车解决了塞车问
题,但是他突然想起来要家里办些事情,这时他就可以打个电话让家里开车来。假如修车不
受这件事情的影响,比如叫家人送他朋友一本书,他可以继续修车,这时就相当于
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
invokeLater;假如修车受影响,比如缺少某个汽车零件,叫家人给他送过来,那么在家人来
之前,他就没法继续修车,这时就相当于invokeAndWait。
下面举一个例子说明这两点,比如按下查询按钮,查询数据量很大的数据库,并显示在
一个表中,这个过程需要给用户一个进度提示,并且能动态显示表格数据动态增加的过程。
假设按钮的处理函数是myButton_actionPerformed,则:
void myButton_actionPerformed(ActionEvent evt){
new MyQueryTask().start();
}
public class MyQueryTask extends Thread{
public void run(){
//查询数据库
final ResultSet result=...;
/ /显示记录
for(;result.next();){
//往表的Model 中添加一行数据,并更新进度条,注意这都是访问组件
SwingUtilities.invokeLater(new Runnable(){
public void run(){
addRecord(result);
}
});
}
....
}
void addRecord(ResultSet result){
//往表格中添加数据
jTable.add....
//更新进度条
jProgress.setValue(....);
}
}
JDK1.6 以后,Swing 提供了一个专门的类SwingWorker 能帮你解决这个编程范式,你
所需要做的就是继承这个类,重载doInBackground,然后在actionPeformed 中调用它的
execute 方法,并通过publish/process 方法来更新界面。SwingWorker 的主要方法和它们的作
用在下面的示意图:
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
从上面示意图可以看出,SwingWorker 实际上不过是封装了前面我所说的例子中的
MyQueryTask,并做了更详尽的考虑。execute 方法相当于MyQueryTask 线程start,它启动
这个后台线程并立刻返回。SwingWorker 可以注册PropertyChangeListener,这些listener 都
被在事件调度线程上执行,相当于MyQueryTask 中的那些访问组件的Runnable 对象。另外,
publish、setProgress 只不过是特殊的property 事件吧,process 和done 不过是响应publish 和
PropertyChangeEvent.DONE 这个事件的方法罢了。因此我们很容易将上面的例子改成
SwingWorker 的版本:
void myButton_actionPerformed(ActionEvent evt){
new MyQueryTask().execute();
}
public class MyQueryTask extends SwingWorker{
public void doInBackground(){
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
//查询数据库
final ResultSet result=...;
//显示记录
for(;result.next();){
//往表的Model 中添加一行数据,并更新进度条,注意这都是访问组件
publish(result);
}
....
}
public void process(Object ... result){
//往表格中添加数据
jTable.add....
//更新进度条
jProgress.setValue(....);
}
}
对于一般的耗时任务这样做是比较普遍的,但是有一些任务是一旦触发之后,会周期性
的触发,如何做处理这种任务呢?JDK 中提供了两个Timer 类帮你完成定时任务,一个是
javax.swing.Timer,一个java.util.Timer。使用它们的方法很简单,对于Swing 的timer,使用
方法如下:
public void myActionPerformed(){
//假设点击了某个按钮开始记时
Action myAction=new AbstractAction(){
public void actionPerformed(ActionEvent e){
//做周期性的活动,比如显示当前时间
Date date=new Date();
jMyDate.setDate(date);//jMyDate 是个假想的组件,能显示日期时间
}
};
new Timer(1000, myAction).start();
}
java.util.Timer 类似,只不过使用TimerTask 完成动作封装。注意这两个Timer 有一个关
键的区别:Swing 的Timer 的事件处理都是在事件调度线程上进行的,因而它里面的操作可
以直接访问Swing 组件。而java.util.Timer 则可能在其他线程上,因而访问组件时要使用
SwingUtilities.invokeLater 和invokeAndWait 来进行。这一点要记住。
如果要了解更详细的信息,可以查阅SwingWorker、Swing Timer 和util Timer 这些类
javadoc 文档和其他网上资料。最重要的是要记住了那两条原则。
Swing 框架之Component:续文三
Swing事件与事件处理器模型
Component 在Swing 模型中是事件触发源。前一篇文章在描述Swing 的事件处理模型时
就已经提到了这个事件处理过程。简单来说,Swing 组件在侦听到原生事件并处理后,往往
产生新的逻辑事件。逻辑事件是某些组件所特有的、具有特定语义的事件,比如JButton 按
下时产生ActionEvent、JComboBox 一项被选中时产生ItemEvent,等等。和原生事件不同,
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
它们并不被派发到系统事件队列中,而是由组件直接触发。事件处理器作为组件的观察者添
加到组件上并侦听触发的事件。假设事件名叫XXX,Swing 中实现这个模式的一般模式是:
1.定义一个XXXEvent
public class XXXEvent extends Event{
...
public void XXXEvent(Object src){
super(src);
...
} ...
}
2.定义一个事件处理器接口XXXListener,声明所有和该事件相关的处理方法:
public interface XXXListener extends EventListener{
void action1(XXXEvent evt);
void action2(XXXEvent evt);
...
}
3.在触发它的组件中定义一下方法:
public class MyComponent extends Jcomponent{
...
//存放事件处理器的队列
private ArrayList<XXXListener>xxxListeners=newArrayList<XXXListener>();
//定义以下各种方法,访问符号用public,以方便添加删除处理器
public void addXXXListener(XXXListener listener){
xxxListeners.add(listener);
}
public void removeXXXListener(XXXListener listener){
xxxListeners.remove(listener);
}
//定义各种触发(fire)action1、action2...的方法,注意一般使用protected,以便
继承和扩展
//每一个action 都要定义一个相应触发(fire)的方法
protected void fireAction1(XXXEvent evt){
for(XXXListener listener:xxxListeners){
listener.action1(evt);
}
}
protected void fireAction2(XXXEvent evt){
for(XXXListener listener:xxxListeners){
listener.action2(evt);
}
}
...
//在某些地方,比如鼠标处理函数中触发相应的动作
void myMouseReleased(MouseEvent evt){
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
...
if(应该触发action1)
fireAction1(new XXXEvent(this));
...
if(应该触发action2)
fireAction2(new XXXEvent(this));
...
}
}
XXXEvent、XXXListener、addXXXListener、removeXXXListener 以及各种fireAction
函数多是重复性代码,有些Java IDE 如JBuilde r 中能够根据开发者的指定参数的自动生成
这些代码。
实际上这个观察者模式的编程范式可以推广到任何JavaBeans,不一定是可视化的Swing
组件。以前曾经见过JBuilder 做的一个所谓数据库操作的JavaBeans,它没有界面,但它和
Swing 组件完全一样添加删除处理器。它的功能是异步操作数据库,在数据操作完了之后触
发注册在上面的事件处理器,该事件处理器就可以将查询结果展现在表格中,或者输出成报
表等等。
在这个模型中,JavaBeans 本身既可以是事件源(被观察对象),也可以是事件处理器(观
察者),JavaBeans 也可以侦听自身的事件并且处理。比如前面文章所提的MyButton 在处理
鼠标事件时就是自己侦听自己发出的鼠标事件,自己既是事件源,又是事件处理器,形成自
反系统。各种各样的JavaBeans 通过这种机制联系成一张事件网,各种JavaBeans 就是这个
网上的节点,而它们之间的事件触发与事件处理关系就是这张网络上的线。当某个节点被外
界或自身发出的事件所触发时,行成了事件的传播。这个过程很像网络上节点的振动引起周
围周围节点振动的模型。下图示意了这种JavaBeans 之间的事件网:
例如new JscrollPane(new JtextArea())这个系统,它里面包括两个JScrollBar 和一个
JTextArea,当鼠标拖动事件触发JScrollBar 时,JScrollBar 处理了这个鼠标拖动事件,并发
出滚动条拖动事件,这个事件传播给JTextArea,JTextArea 处理这个拖动事件,相应的更新
自己显示的内容,如果JTextArea 之后又根据更新发出了一个新的事件,这个事件便会继续
传播下去。
Swing布局管理器
现在高级图形用户界面工具一般都包括布局管理器机制。什么叫做布局管理器?如果所
有窗口的大小是不变的,那么我们在往窗口中添加组件时,只要将组件的拖放到固定位置、
调整好尺寸就可以了,就像VB 的界面工具一样。可大多数情况并非如此,用户经常需要调
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
整窗口的大小,以便和其他程序协同工作。这种情况下,在传统界面工具中,比如VB,就
需要显式的侦听窗口尺寸调整事件,根据当前窗口的大小重新计算并调整各个组件的大小和
位置。AWT/SWT/Swing 将这个过程自动化、模块化了,抽象出一个布局管理器来负责管理
界面组件的布局。
它们实现原理是相似的:容器类组件侦听初始化、invalide/validate 以及容器尺寸调整等
事件,一旦发生这些事件,容器类组件检查自己是否配置了布局管理器,如果没有,则不做
任何事情;如果有,则将容器内组件的布局代理给布局管理器,让它来完成容器内组件的重
新布局。
容器管理器对象对实现两类接口:LayoutManage r 和LayoutManager2,LayoutManager2
是LayoutManage r 的一个扩展,允许组件在添加时指定位置参数。它们的定义和含义如下:
public interface LayoutManager {
//添加组件comp,并和name 关联起来,name 可以作为位置等特殊含义参数来使
用
void addLayoutComponent(String name, Component comp);
//删除组件comp
void removeLayoutComponent(Component comp);
//根据容器内的当前组件,计算容器parent 的最优尺寸。
Dimension preferredLayoutSize(Container parent);
//根据容器内的当前组件,计算容器parent 的最小尺寸。
Dimension minimumLayoutSize(Container parent);
//重新布局容器parent,这儿是主要布局逻辑所在。
void layoutContainer(Container parent);
}
public interface LayoutManager2 extends LayoutManager {
//添加组件comp,constraints 用作指定如何以及位置的参数,这个函数主要是弥补
LayoutManage r 版的addLayoutComponent 表达能力欠缺而添加。
void addLayoutComponent(Component comp, Object constraints);
//根据容器内的当前组件,计算容器parent 的最大尺寸。看来除了最优、最小,某
些情况下还是需要知道最大。
public Dimension maximumLayoutSize(Container target);
//指定水平方向上组件之间的相对对齐方式,0 表示和源组件对齐,1 表示远离源
组件。
public float getLayoutAlignmentX(Container target);
//指定垂直方向上组件之间的相对对齐方式,0 表示和源组件对齐,1 表示远离源
组件。
public float getLayoutAlignmentY(Container target);
//invalidate 这个布局管理器,有时布局管理器为了计算迅速,可能第一次计算之后
就将一些数据给缓冲,但是后容器内的组件数目发生变化,这儿的缓冲值就需要调用这个方
法通知更新
public void invalidateLayout(Container target);
}
Swing 在java.awt 和javax.swing 中都分别提供大量的布局管理器,这些布局管理器有简
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
单的如FlowLayout,有复杂的如GridBadLayout。用户还可以自己定义自己的布局管理器,
由于篇幅原因,这儿略去例子。
Java 6 中在布局管理中引入了BaseLine / Anchor 的概念,能协助Java IDE 的用户界面设
计工具,方便用户来设计布局组件。NetBeans 的Matisse 组件首先引入了一个GroupLayout
布局管理器,结合Matisse 使用,提供了非常方便的布局管理和界面设计。GroupLayout 和
BaseLine/Anchor 概念以及Matisse 可以说是Java 界面设计工具的一大进步,可以说足以成
为Java 桌面应用史上的一个里程碑。在这之前,缺乏有力的界面设计工具是Java 在桌面应
用失败的一个重要原因。虽然Anchor 概念早就在Delphi 界面设计工具出现过,但是这个工
具的出现还是Java 界面设计史上的一大事件。随着Java 6 桌面应用支持的增强,以及
NetBeans Matisse 之类界面设计工具的出现,使得Java 桌面应用时代已经到来。Seeing is
believing,你不妨试一下就知道了。
=====================================
本想再加一节讲述Swing 双缓冲机制,但是想到双缓冲并不是Swing 模型的核心概念,
没有它并不影响理解Swing 的总体模型,因此打算把它作为以后的一篇专门技术文章来写。
这样Swing 模型中的Component 部分就算是描述完了,从明天开始,讲述Swing 模型
中的另外三个重要概念:Model、UI Delegate 和Renderer。
Swing 框架之Model之一
构建应用程序应该以数据为中心,而不是以用户界面为中心,这是一个良好的编程习惯。
为支持这种编程范式,Swing 为每种带有逻辑数据或值的组件定义了独立的模型接口,这种
分割使程序可以选择向Swing 组件中嵌入自己的模型实现。
下面表格列出Swing 中组件及其模型的映射关系:
组件Model 接口Model 类型
JButton ButtonModel GUI 状态
JToggleButton ButtonModel GUI 状态/应用数据
JCheckBox ButtonModel GUI 状态/应用数据
JRadioButton ButtonModel GUI 状态/应用数据
JMenu ButtonModel GUI 状态
JMenuItem ButtonModel GUI 状态
JCheckBoxMenuItem ButtonModel GUI 状态/应用数据
JRadioButtonMenuItem ButtonModel GUI 状态/应用数据
JComboBox ComboBoxModel 应用数据
JProgressBar BoundedRangeModel GUI 状态/应用数据
JScrollBar BoundedRangeModel GUI 状态/应用数据
JSlider BoundedRangeModel GUI 状态/应用数据
JTabbedPane SingleSelectionModel GUI 状态
JList ListModel 应用数据
JList ListSelectionModel GUI 状态
JTable TableModel 应用数据
JTable TableColumnModel GUI 状态
JTree TreeModel 应用数据
JTree TreeSelectionModel GUI 状态
JEditorPane Document 应用数据
JTextPane Document 应用数据
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
Swing模型分类
Swing 提供的模型分为两大类:GUI 状态模型和应用数据模型。
GUI 状态模型是描述GUI 控件可视化状态的接口,如按钮是否按下,或列表中那一项
被选中。GUI 状态模型通常仅在图形用户界面(GUI)环境中用到。通常来说,虽然编写使用
GUI 状态模型分离程序,尤其是当多个GUI 控件共享状态,或当操作一个控件自动更新另
一个的值时比较有用,但GUI 状态模型在Swing 中并不是必需的,完全可以通过组件顶层
方法操作GUI 控件的状态,而不必和模型直接交互。
应用数据模型是描述具有应用程序含义数据的接口,比如表格中的数据,或列表显示的
选项。这些数据模型为Swing 提供了一个清晰分割应用程序界面和数据逻辑的强大编程模
式。对于以数据为核心的Swing 组件,比如JTree 和JTable,强烈推荐使用数据模型进行交
互。
当然一些组件的模型根据应用场景的不同其分类介于GUI 状态模型和应用数据模型之
间,比如JSlider 和JProgressBar 的BoundedRangeModel。
Swing 的可分离模型接口并没有明确界定GUI 状态模型和应用数据模型。这儿所以做
此说明,目的是让你更好的理解何时以及为何要需要使用分离的模型。
共享模型定义
值得注意的是,上文中表格中,许多组件的数据抽象相似,只需一个接口而不用过分泛
化时,组件可以共享同一模型定义。共享模型定义允许在不同组件之间自动连接。比如,
JSlider 和JScrollBar 都使用BoundedRangeModel 接口,因此可以在一个JScrollBar 和一个
JSlider 之间共享同一个BoundedRangeModel 实例,这样它们之间的状态就总是同步的。
分离模型编程接口
使用模型的Swing 组件必须提供访问修改模型的set/get 方法,即模型必须是该组件的
限定性属性。比如,JSlider 使用BoundedRangeModel 接口作为它的模型定义,因此它必须
提供下面方法:
public BoundedRangeModel getModel()
publicvoidsetModel(BoundedRangeModelmodel)
所有Swing 组件有一个共同点:如果你不设置它的模型,组件会在内部创建/安装一个
缺省模型。这些缺省模型类的命名习惯是在接口名称之前加上“Default”,比如JSlider 的构
造函数中初始化一个DefaultBoundedModel 对象。
public JSlider(int orientation, int min, int max, intvalue){
checkOrientation(orientation);
this.orientation = orientation;
JTextArea Document 应用数据
JTextField Document 应用数据
JPasswordField Document 应用数据
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
this.model = newDefaultBoundedRangeModel(value, 0, min, max);
this.model.addChangeListener(changeListener);
updateUI();
}
如果程序接着调用setModel(),缺省的模型就被替换了,比如下面例子:
JSlider slider = new JSlider();
BoundedRangeModel myModel = new DefaultBoundedRangeModel() {
public void setValue(int n){
System.out.println("SetValue: "+ n);
super.setValue(n);
}
});
slider.setModel(myModel);
对于更复杂的模型(如JTable 和JList), Swing 还提供一个抽象模型实现,让开发者不
需要从头开始创建自己的模型。
如JList 的模型接口是ListModel,Swing 同时提供了DefaultListModel 和
AbstractListModel 两个类来协助开发者创建自定义的列表模型。
模型改变通知
当数据或者发生变动时,模型必须通知所有相关方(比如视图)。Swing 模型使用前面
文章所讲述的事件模型来实现这种触发。Swing 中有两种方法发送这种通知:
发送轻量级通知,表明状态已经改变,需要Listener 通过查询模型,发现什么改变了并
做出响应。此方法的优点是单独事件实例能用作该模型的所有通知,同时对于需要频繁通知
的事件非常有用(比如JScrollBar 被拖动时)。
发送状态化通知,详细描述模型如何改变。这种方法需要为每个通知创建一个新的事件
实例。当通知通过查询模型不能有效地给Listener 提供足够的信息时,此方法非常有用。比
如当JTable 的一列表格数据发生改变时。
(待续)
Swing 框架之Model之二
轻量级通知
下面Swing 中的模型使用轻量级通知,它们是基于ChangeListener、ChangeEvent 接口
的:
ChangeListener 接口只有一个通用方法:
public void stateChanged(ChangeEvent e)
ChangeEvent 中仅有的状态是事件源,因为所有通知中的事件源都是相同的,单独一个
事件实例可以用作所有来自该模型的通知。使用此机制的模型支持下面的方法来添加和删除
ChangeListeners:
public void addChangeListener(ChangeListenerl)
public voidremoveChangeListener(ChangeListenerl)
Model Listener Event
BoundedRangeModel ChangeListener ChangeEvent
ButtonModel ChangeListener ChangeEvent
SingleSelectionModel ChangeListener ChangeEvent
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
获知JSlider 数据发生变化的代码可以使用如下代码实现:
JSlider slider = new JSlider();
BoundedRangeModelmodel =slider.getModel();
model.addChangeListener(new ChangeListener() {
public voidstateChanged(ChangeEvent e) {
// need to query themodel
// to getupdatedvalue...
BoundedRangeModel m=(BoundedRangeModel)e.getSource();
System.out.println("mode l changed: "+
m.getValue());
}
});
为给不想和分离式模型交互的程序提供方便,一些Swing 组件类提供了直接在组件上
注册ChangeListener 的方法(组件可在组件内部侦听模型的数据变化,并将事件传播给任何
注册在组件上的Listener),这些通知的唯一区别是,使用模型注册方式的事件源是该模型实
例,而使用组件注册方式的事件源是该组件。
因此我们可以将前面的例子简化成:
JSlider slider = newJSlider();
slider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
//the source will be
// the slider this time..
JSlider s=(JSlider)e.getSource();
System.out.println("valuechanged: "+s.getValue());
}
});
状态化通知
支持状态化通知的模型根据它们的目的提供不同的Listener 接口和事件对象。下表是这
些模型接口和事件对象的类:
Listener 除了可以直接查询事件对象来跟踪内容改变外,这些API 的作用与轻量级通知相似。
比如下面的代码动态的跟踪JList 被选中的项:
Stringitems[] = {"One", "Two", "Three");
Model Listener Event
ListModel ListDataListener ListDataEvent
ListSelectionModel ListSelectionListener ListSelectionEvent
ComboBoxModel ListDataListener ListDataEvent
TreeModel TreeModelListener TreeModelEvent
TreeSelectionModel TreeSelectionListener TreeSelectionEvent
TableModel TableModelListener TableModelEvent
TableColumnModel TableColumnModelListener TableColumnModelEvent
Document DocumentListener DocumentEvent
Document UndoableEditListener UndoableEditEvent
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
JList list = new JList(items);
ListSelectionModel sModel =list.getSelectionModel();
sModel.addListSelectionListener(new ListSelectionListener() {
publicvoidvalueChanged(ListSelectionEvent e) {
// get change information directly
// fromtheevent instance...
if (!e.getValueIsAdjusting()) {
System.out.println("selection changed: " +
e.getFirstIndex());
}
}
});
自动视图更新
模型没有任何表现它视图的固有知识,相反模型只有关心其状态改变的Listener 列表,
这种需求对于同个模型多个视图的框架来说是至关重要的。Swing 组件负责将合适的模型
Listener 连接起来,以便于模型改变时能正确地重画出自己。如果你发现模型改变时,组件
不能自动更新,说明组件的实现就存在错误。
忽略模型
正如前面提到的,大多数组件直接在Component 类中提供模型定义的API,以方便组件
能不用和模型交互就直接操作,这是相当可行的编程方法,尤其是对于GUI 状态模型来说。
比如下面的JSlider 内部getValue 的实现,它将调用代理给模型:
public int getValue(){
return getModel().getValue();
}
因此程序完全可以这样写:
JSlider slider = new JSlider();
int value=slider.getValue();
Swing模型总结
虽然理解了Swing 模型设计是如何工作的,但没有必要在所有Swing 编程中都使用模
型API。你需要注意考虑应用程序各自的需求,决定哪儿使用模型API 能帮你提升代码,且
不带来不必要的复杂性。
我特别推荐在Swing 中使用应用数据模型(如JTable 和JTree 等的模型),因为从长期
来看,它们能极大地提高你的应用程序可扩展性和模块化度。
下一篇文章将就Swing 框架的另一个概念UIDelegate 进行阐述。
Swing 框架之UI Delegate 之一
Swing 的UI Delegate 机制使得Swing 组件可以动态地切换LAF。注:所谓LAF 就是Look
and feel,外观和感觉,为了更准确表达这个意思,后文用LAF,表示Look And Feel。这套LAF
机制是封装在Swing 组件内部的,开发者编写Swing 程序,不需要指定特定LAF。Swing
工具提供了一套缺省LAF;当然Swing 的LAF 的API 是开放的,允许开发者通过扩展已有
的LAF 或者从头创建一个LAF。虽然这种可插拔LAF 的API 是可扩展的,但是它被有意设
计成基本组件下一层的接口,这样开发者不必要理解LAF 机制的复杂细节就能创建Swing
图形用户界面。
虽然不提倡开发者自己创建新的LAF,但Swing 开发小组意识到了PLAF(Platform Look
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
And Feel, 平台相关的外观和感觉)对于想创建独特外观的应用程序来说是一个非常强大的
功能。事实证明,PLAF 对于创建残障用户友好的界面是很理想的(残障用户:有视力缺陷
的用户或者不能操作鼠标的用户)。
本质上说,可插拔LAF 的设计意味着实现组件展现(Look)和事件处理(Feel)的部分是被
代理到独立的UI 对象中的,这些UI 对象是由当前的LAF 提供的,它们可以被动态地修改。
这些可插拔LAF 的API 包括:
1.Swing 组件(component)类中的钩子
2.LAF 管理的顶层API
3.独立的包中实际实现LAF 的接口
组件钩子
拥有有LAF 特征的Swing 组件在javax.swing.plaf 包都有一个抽象类来代表它的UI
Delegate,这些UI 类的命名规则是该组件类去掉前缀J,加上后缀UI。比如JButton 的UI
Delegate 类的名字是ButtonUI。
UI Delegate 在组件的构造函数中创建,并以组件的限定JavaBean 属性的形式访问,比
如JScrollBar 提供了下面的方法访问它的UI Delegate:
public ScrollBarUI getUI()
public void setUI(ScrollBarUI ui)
组件创建并设置UI Delegate 的过程实际上是“安装”组件LAF 的过程。每个组件同时提
供方法创建和设置“缺省”LAF 的UI Delegate,组件的构造函数在安装UI 时使用该方法。
public void updateUI()
LAF 实现为每个抽象UI 类提供了具体子类,比如,Windows LAF 定义了
WindowsButtonUI, WindowsScrollBar 等。当组件安装它的UI Delegate 时,须
类似的。图形用户界面系统在事件处理设计上有两大类,一类是单线程模型,一类是多线程
模型。在事件处理机制上,三者都是遵循单线程规则。
单线程模型对于事件处理不保证线程安全性(Thread Safety),所有的事件处理都在Event
Dispatch Thread(EDT)上进行,此一类事件模型通常叫做单线程模型。这种模型规定所有对组件的访问操作必须在EDT 上完成。为什么对于组件的访问需要在EDT 上完成?这主要是
为了保证对于组件状态的改变是同步的,保证了界面组件的可确定性。这中模型是大部分图
形用户界面工具采用的模型,包括Swing/AWT、SWT、GTK、WinForm 等等。
这种模型的好处是,结构设计和代码实现都比较简单,避免了为了实现线程同步的复杂
处理。但是也带来了一些问题,最常见的问题是,程序员容易将长时间复杂任务的处理放在
事件处理函数完成,造成EDT 线程被阻塞,给用户造成界面失去响应的错觉。
其实人们对于Swing 速度慢和反映迟钝的感觉大部分来源于此,简单的说,是程序员
的问题,而不是Swing 自身的问题,是因为程序员没有理解这种事件处理机制造成的。其
实在SWT、GTK、WinForm 等任何以这种事件模型为基础的工具都会出现。重现的方法就
是你简单的将长时间处理的任务放在事件处理函数中,你的用户界面就会失去响应。
如何解决这种问题?通用的办法就是采用异步线程处理长时间任务。但是还要记住的
是,在这种任务中对于界面的更新要采用SwingUtilities.invokeLater 或者在SWT 采用
Synchronize 方法,将访问操作放到EDT 上进行。关于如何写一个有效处理长时间任务的
Swing 程序,将会在其他文章中描述。
多线程模型中,所有的事件处理都是在异步线程中进行,界面组件同步访问的操作需要
程序员来保证。这种模型设计本身很复杂,而且对于程序员来说要求比较高,必须对线程同
步编程很熟悉,而且花在同步上的操作影响了平台的性能。一般现在的图形界面工具都不再
采用这种方式。
下面比较一下Swing/AWT/SWT 在API、GUI 特征以及实现方法的不同。
在API 上,Swing 和AWT 是兼容的,SWT 是单独的一套接口。
1.Swing/AWT 的组件在生成时可以脱离父组件独立存在,SWT 必须有父组件存在。这
主要是由于SWT 的资源是自己管理,SWT 程序必须负责释放不用的资源,为了避免这种释
放资源的重复性,SWT 父组件被设计成在析构时自动递归调用子组件的析构函数。
2.Swing/AWT 的资源回收由垃圾收集器负责,SWT 必须由SWT 程序显式释放。
3.Swing/AWT 的事件线程循环不需要程序员显式启动,SWT 必须要程序员来显式启动。
Swing/AWT 和SWT 在布局管理器上是类似的,没有太大区别。
在GUI 特征上,有两个比较层面,一个是组件种类,一个是组件本身特征。在理解这
些特征时,一定要牢牢记住这样一个准则:Java 是平台无关的语言,因此它对应用程序所提
供的API 一定要各个平台都相同,GUI 特征其实也是API,所以GUI 的特征必须在各个平
台都相同。
组件类型上,AWT 采用的是最小公约数方法,而Swing/SWT 采用的是最大公倍数方法。
简单的说AWT 是各个平台所有组件集合的交集,而Swing 和SWT 则是各个平台组件的并
集。下图所示,假设操作系统平台OS1 上提供组件{C1, C2, C3, C4, C7},而OS2 提供{C1,
C2,C3, C4, C6},OS3 提供{C1, C2, C3, C4, C5},那么其中的阴影部分就是AWT 所实现的组
件,由于C5、C6 和C7 是各个平台所特有的,因此AWT 中就不包含这些组件。比如Table
和Tree 在Java 支持某些操作系统平台中不包含,所以在AWT 中就没有Table 和Tree。由于
AWT 的组件太贫乏,所以AWT 在现在复杂应用程序几乎没有什么用。Swing 和SWT 提供
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
的组件是各平台所有组件的并集,这样就解决AWT 的组件贫乏的缺陷。也就是说,Swing
和SWT 提供的组件包括C1 到C7 的所有组件,而AWT 只提供C1 到C4 的所有组件。
从组件本身的特征来看,SWT 和AWT 采用了相同的策略,即最小公约数,而Swing
采用的是最大公倍数。如下图所示,假设对于同一个组件C,如果它在OS1 上提供的特征
包括{a,b,c,d,e},而OS2 上提供的特征包括{a,b,c},而OS3 包括的特征有{a,b,c,d},那么SWT
和AWT 提供的组件特征只包括{a,b,c},而Swing 的包含的平台特征包括{a, b, c, d, e}。比如
由于Solaris 平台上的按钮不提供对于图标的支持,所以SWT 和AWT 的独立按钮就不提供
对于按钮图标的支持,而Swing 提供按钮图标的支持。
在实现方法上,AWT 采用Java+Native C Peer(一种JNI 调用)方法,SWT 根据各平台
的不同情况,一部分组件使用Java+Java Peer+JNI Wrapper,一部分采用Java 模拟的方法,
而Swing 则采用所有组件都纯粹Java 模拟的方法。
AWT 的接口和各操作系统组件之间的差别采用Native C Peer 实现的方法来填平,下面
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
是它的结构示意图:
其中AWT Native Peer Impl 部分都是C 语言写的,在各种操作系统上是不同的,但是它
们和AWT Component 组件之间的AWT-Peer JNI 调用接口是不变的,AWT Component 的Java
代码实现在各个平台上都是相同的,最后AWT Component 向Application 提供同一的API
接口。
SWT 同AWT 不同,它在Native Component 上进行了一层薄薄的JNI 封装,所有操作
系统的API 调用都被映射到一个JNI 调用上,然后SWT 通过Java 代码组合这些JNI 调用实
现同一的API,下面是其结构示意图:
因此SWT 的Java 代码实现部分在各个平台是不同的,它的C 代码部分即JNI Wrapper
部分只是一个各平台GUI API 的JNI 简单映射,SWT 通过Java Peer 在各平台的不同实现填
平了各平台差异,从而给Application 提供同一的API 接口。当然SWT 对于某种平台上缺少
的组件采用的方法和Swing 基本类似。
Swing 和上两中方式完全不同,它直接调用Java2D,抛弃了本地操作系统平台组件的
实现,完全自己画出来了整个组件,当然Java2D 底层也是调用平台的图形系统。下面是它
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
的示意图:
当然Swing 是建立在AWT 基础上的,对于一些顶层容器类如Frame / Dialog / Window
以及Applet 是直接采用AWT 的,这儿为了方便并没有画出。由于Java2D API 是个平台无
关的,因此Swing 的Java 实现代码在个平台都是一样的,都是一套,当然除了与Swing 的Look
And Feel 相关的东西以外。
由于篇幅原因,今天就先谈到这儿,至于这三种不同架构、实现会对它们的性能和外观
以及编程难度产生什么影响,我们以后的文章逐渐讨论。
Swing 模型与渲染器
本文承接Swing/AWT/SWT 比较一文,概要叙述Swing 的体系结构,解释了Swing 架构
关键概念:模型与渲染器,解释如何使用渲染对象扩展该体系架构来支持大数据量的组
件。后面的文章还会简要概述SWT 的体系结构,为Swing/AWT 和SWT 的比较做一铺垫。
Java 基础类(JFC)Swing 工具提供了使用Java 平台创建高度可交互性图形用户界面的类。
Swing 是高度灵活的,但是也因此相当复杂,虽然新手能够使用Swing 创建基本的图形用户
界面(GUI),但是真要创建一个复杂、专业的GUI 界面,你必须理解Swing 的体系架构的基
础,尤其是使用Swing 创建复杂、像JTable 、JTree、JComboBox 以及JList 这样基于渲染器
的组件,Swing 提供的基于模型和渲染器的组件是构建高性能、可扩展GUI 的关键。
Swing体系结构
最初Smalltalk 系统的UI 工具使用所谓的模型-视图-控制(MVC)模式,MVC 引入这
样一个概念:数据源应该同屏幕展现分开。这是一个优秀的体系设计结构,能促进代码重用
和程序框架。Swing 使用的是一个变体的MVC 架构,如图所示:
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
典型的Swing GUI 组件包括至少三个对象:一个Component,一个Model 和一个UI
Delegate,在这个框架中,Model 负责存储数据,UI Delegate 负责从Model 获取数据并渲染
到屏幕上去,Component 通常协调Model 和Delegate 之间的操作,并同时负责将Swing 嵌
入到AWT 窗口系统中。
注意,UI Delegate 对象可以在运行的时候动态替换,这就使Swing 具备了可插拔的外
观(Pluggable Look-And-Feel, PLAF)。
虽然Swing 的MVC 结构显然具备灵活性的好处,但是这个结构通常被指责为一些程序
慢的根源。虽然基于MVC 结构需要更多的方法调用来支持额外的重定向,其实花费在这儿
的消耗很小。对基于Swing 的应用程序profile 的结果显示,model-vie w 分隔的开销可以忽
略不计,不到CPU 总开销的1%,复杂的Swing 用户界面的多数处理事件其实都花费在了
底层的图形操作上了。Swing 的model-vie w 结构并不是低性能的根源,它是构建可扩展程序的关键。
矢量组件
Swing 提供了一些处理大数据量数据集的组件,包括JTable、JTree、JList 以及
JComboBox。这些矢量组件被设计成能够处理成千上万甚至数百万的数据,为了避免占用大
量内存,这些组件在Swing 的体系架构增加了渲染器(renderer)概念。下图是增加了渲染器结构的Swing 体系架构。
渲染器(Renderer)
在这些更为复杂的Swing 组件中,渲染器是提供可扩展性的关键。我们以JTable 作为
渲染器的示例。缺省表格中的每一格可能都有一个JLabel,这对于比较小的数据集来说可行,
但是对于大数据集就行不通。比如,如果使用这种表格显示1000x1000 的数据集,需要的内
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
存可能要1G,即使每个格子都是空的。
如果解决这种扩展性问题?Swing 的JTable 使用一个组件来画出所有相同类型的格子。
比如所有的String 对象的格子都使用相同的组件画。这种类型的组件被称作渲染器
(renderer),使用渲染器显示多个表格极大的减小了大型数据表存储空间。
当渲染器用来显示表格时,JTable 从model 中获取格子中的数据,然后使用这些数据对
渲染器进行配置,然后使用该渲染器画出该格子。接下来,渲染器继续移动到下一个格子,
然后重复这个过程。
注意你可以通过操作渲染器和模型来控制这个过程,所有的矢量组件,包括JTree、JList
以及JComboBox 都使用渲染器方法,并不仅限于JTable。
模型(Model)
直接操作Swing 的模型(Model)对于编写可扩展的用户界面至关重要,下面代码是往
JComboBox 添加数据项的通常做法:
JComboBox box = new JComboBox();
for (int i = 0; i < numItems; i++) {
box.addItem(new Integer(i));
}
这些代码只是简单的往JComboBox 中添加数据项,代码同往AWT 的Choice 中添加选
项类似,这种方法对于小数据量来说可以,但是当要添加大量数据时就会明显变得非常慢。
尽管上面的代码没有明确引用任何模型,JComboBox 的模型对象实际上参与这个过程,
每次调用addItem 时,JComboBox 内部发生了许多操作:组件将请求传递给JComboBox 的
模型,模型发送一个事件表明一个新项被添加。很明显,如果你直接操作模型的将会更高效,
如下例所示:
Vector v = new Vector(numItems);
for (int i = 0; i < numItems; i++) {
v.add(new Integer(i));
}
ComboBoxModel model = new DefaultComboBoxModel(v);
JComboBox box = new JComboBox(model);
这样为什么会更快呢?原因有两个。第一,因为所有项是一次添加到模型去,而不是一
个一个的,只有一个事件发出,这意味着更少的事件触发,更少的方法调用。第二是因为需
要通知变化的对象更少,总的工作量等于触发次数乘以侦听器数目。因为模型是新创建的,
侦听在上面的侦听器为零,这意味着没有触发事件发生。
从上面的例子可以学到两点:
尽可能使用批操作,尽量减少触发事件的数量。
当初始化或者需要完全替换模型的内容时,考虑重新生成模型,不要使用已经存在的模
型,已存在模型上已经保持了很多的侦听器,新生成的模型没有侦听器,这样避免了不必要
的处理函数的调用。
触发事件数量严重影响你的程序启动时间,也会影响打开对话框和相似操作的时间。
本想详细举几个例子进一步说明模型和渲染器的用法和好处,但网络速度还是太慢,写
一篇文章太痛苦了...加上篇幅原因,准备以后再写一文,弥补这方面的知识。
以后几篇文章包括(非文章标题):
使用Swing 模型和渲染器优化Swing 程序。
Swing/AWT 事件处理模型以及线程安全。
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
如何编写响应快速的Swing 程序。
SWT 设计和实现介绍。
JNI 与Java 性能的关系。
垃圾收集与虚拟机与Java 性能。
Swing 与SWT 性能比较(前面所有的文章都和这篇有关,是基础)。
再往以后,准备专门写一些Swing 和Java2D 等方面的文章。
Swing 框架之Component
昨天晚上写完Swing 的模型和渲染器之后,觉得对Swing 的体系结构还是没有说清楚。
Swing 的基础体系结构中的四大基本对象Component、Model、UI Delegate 以及Renderer
都值得详细解释。Swing 的树状组件结构(虽然这是用户界面工具通有的特征)也值得详细
解释,因为这是完成某些复杂Swing 组件,尤其像JTable、JTree、JList 和JComboBox 这种
复杂组件中编辑功能得关键。此外,Swing / AWT 的事件模型如Event Dispatching 和
Propagation 和事件处理线程模型也需要详细解释,理解这部份是编写高效率Swing 界面的
关键。
今天从Swing 的四大基本对象Component 说起。
============================
Component 在Swing 的MVC 模型中担任Controller 的角色,同时它也是Swing API 中
代表具体组件的对象。
Component 在Swing 中对外负责提供API 接口,对内负责协调控制Model 和UI Delegate
(有时可能还包括Renderer)的操作,可以说是整个Swing 结构的中心角色。为了方便你回
忆Swing 的MVC 模型,特地将上一篇文章中的Swing 模型示意图引了过来:
Component 代表Swing 对应用程序提供了如下几类编程接口:
1.用户界面的组件树的创建和修改的方法。这包括组件的添加和删除等操作。
2.组件属性访问的方法,比如组件位置、组件前后背景色、组件字体等等。
3.组件状态及生命周期的管理的方法,比如隐藏和显示、创建和销毁等等。
4.组件位置、大小的管理,包括通过布局管理器的方法。
5.组件事件处理接口的管理,包括添加、删除等操作。
从开发者的角度来看,Component 是组件树上的节点、是控制外观和行为的入口、是组
件事件的发源地。从Swing 组件实现者的角度来看,Component 是协调Model 和UI Deleg ate
的操作的地方,是低层次事件处理的地方,是高层事件发生的地方,是同父组件和子组件交
互的地方。掌握的这些角度,Swing 程序员完全可以实现自己的自定义简单组件,当然如需
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
要实现类似于JTable 和JTree 等复杂的矢量组件,还需要进一步了解Swing 的Model 和UI
Delegate 以及Renderer 模型。
对于复合型(Composite)组件很简单,这儿要讲述的是如何实现自定义的组件,比如表盘,
比如温度计这些没有标准组件可以使用的组件,那么如何自己实现这种自定义组件呢?
不考虑Model 分隔和UI Delegate 皮肤分离问题,能够简化自定义Component 的模型。
总的来说自定义组件需要完成两样基本任务:第一侦听并处理低层事件,根据具体情况改变
组件状态,如需要还要发出高级事件;第二,根据当前组件的状态画出当前组件的外观。
侦听底层的事件是指侦听类似于mouse、keyboard、focus 等事件,然后处理此事件,如
果发现此事件带有特定语义,表达某种组件行为,则改变当前的组件状态以记录,并触发某
种事件通知应用程序进行处理。举例说明,想象你准备实现一个简单的按钮,你可以通过继
承JComponent 来完成。你可以在按钮初始化时,注册此按钮的鼠标事件侦听器,以侦听发
生自己组件上的鼠标事件。当按钮捕获到鼠标按下时,检查鼠标按下的点是否在按钮有效区
域内。如果是,则认为当前是一个按钮按下动作,那么改变按钮的状态为按下去,调用repaint
方法通知按钮重画成按下去的状态,然后发出ActionPerformed 的事件,通知注册在此按钮
上的应用程序的ActionListener 处理这个动作。下面是一个简单示意代码:
public class MyButton extends Jcomponent implements MouseListener{
private String text;
private boolean pressed=false;
private ArrayList<ActionListener> listeners=new ArrayList<ActionListener>();
public MyButton(){
addMouseListener(this);//将自己注册为自己的鼠标事件侦听器,监听鼠标事件
}
....
public void mousePressed(MouseEvent evt){
Point p=evt.getPoint();
if(getBounds().contains(p)){//判断鼠标落点是否在有效区域内。
pressed=true; //鼠标点击的含义是按钮被按下!改表按钮状态。
repaint(); //通知按钮重画,由原来的抬起状态改变成按下状态。
fireActionPerformed(new ActionEvent(this)); //这是一个按钮动作事件,触发它。
}
}
public void addActionListener(ActionListener listener){
listeners.add(listener);
}
public void removeActionListener(ActionListener listener){
listeners.remove(listener);
}
protected fireActionPerformed(ActionEvent evt){
for(ActionListener listener:listeners){
listener.actionPerformed(evt);
}
}
...
//这儿你要覆盖paint 方法,实现按钮状态的重画
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
public void paint(Graphics g){
if(pressed){
//画出按下的样子
}else{
//画出抬起的样子
}
}
}
上面要注意的是你要自己管理自定义组件的事件监听器,包括addListener 和
removeListener 方法,以及如何触发。这个过程很简单,基本上就是上面的模板来实现添加
删除和触发。
除了要负责事件的处理和新事件的触发,自定义组件第二个要完成的任务就是要根据
组件当前的状态改变重画组件的外观。重画组件的外观只需要覆盖public void paint(Graphics
g)方法即可以,在这个方法里,你只需要根据当前的组件状态分别画出当前的组件即可。
当然除了上面两个基本准则外,不要忘了添加访问你的组件属性的方法,比如,如果
上面的按钮是个二元按钮(相当于JCheckbox/JToggleButton 的那种按钮),你可能需要提供
isPressed 或者setPressed 来获取和设置当前按钮的状态。注意,在设置状态按钮变化的访问
方法中,比如setPressed,你需要使用repaint 方法通知按钮重新渲染(复杂的实现可能包括触
发propertyChange 事件,这儿从简):
public void setPressed(boolean p){
pressed=p;
repaint();
}
到此为止,你已经能根据上面的两条准则简单的实现你想要的组件了。但是你发现没有,
你的按钮状态和外观行为都被堆到了Component (MyButton)中实现了,而且,对于各个平
台都是一个样子,不能换皮肤。这对于比较简单、不想要皮肤的组件,可能没有什么,但是
对于复杂的组件,比如JTable 或者甚至Excel 类似的电子表格的那种组件,你把数据(组件
状态)和外观堆在这儿实现就严重违反了MVC 原则。
如何简化这种组件的实现呢?使你实现的此种组件容易维护、扩展以及重用,皮肤容易
换呢?这就需要Swing 结构中的另外三个元素:Model、UI Delegate 和Renderer,后面的几
个文章将讲述Model、UI Delegate 和Renderer 帮你逐步实现一个复杂、灵活、可扩展、高
效的矢量组件。
=====================================================
今天这样讲,不知道讲明白没有。当初刚开始学习Swing 的时候,还不了解Swing 的
这种MVC 结构,因此当时自己做的自定义组件都是这样写的,没有单独的Model 和可以定
制的UI Delegate,更不用说Renderer 了。但是我觉得自己的这个学习过程,恰恰是人们学
习Swing 的最佳途径,先简化模型,然后逐步扩展,直到了解Swing 模型的全部图像。
Swing 框架之Component:续文一
昨晚回去后还是觉得Component 对象本身说的太简单,想来想去,觉得内容实在是太
多,有必要补充两个续文说明Component 的其它概念。今天介绍Swing 组件paint 方法的处
理流程,这个流程能使我们理解许多Swing 机制。明天续文讲述Swing 事件处理器、双缓
冲和布局管理器等原理。
=======================
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
Swing 组件的paint 方法是内部接口方法,一般用户不要直接调用这个方法,它总是在
事件调度线程中调用。一般说来除了系统刷新事件触发这个方法,Component 的repaint 也
触发这个方法的调用。repaint 方法常用于当组件状态发生变化时刷新界面使用。repaint 方法
是Swing 中少数几个线程安全的方法,可以在任何线程中调用它。它的原理是往事件队列
中post 一个PAINT 事件。由于事件队列的事件是被事件调度线程同步执行的,所以这个方
法总是线程安全的。
事件调度线程从PAINT 事件中获取事件源组件,从系统申请到图形设备资源后,调用
该组件的update 方法。update 是AWT 时代遗留下来的产物,本意是AWT 组件画好组件背
景后,再调用paint 方法画出组件的前景。Swing 出现后这个方法就被弃用了,所有逻辑都
转到paint 方法里。Update 只是简单地调用paint 方法来完成组件的渲染。老的Java 教材上
经常可以看到,所谓repaint 调度update 方法,update 接着调用paint 方法,自定义组件需要
重载paint 方法等话语,就是因为这个历史造成的。
上篇文章中的MyButton 的paint 方法实现是一个非常老式的做法。现在JComponent 的
实现已经把paint 方法改造成可以嵌套多重机制地方,这些机制包括层次渲染、边框、透明
背景、双缓冲以及皮肤等。这些机制分别实现不同目的的组件提供了方便。
图形用户界面的组件按照其在组件树上的角色可以分为容器组件和叶组件。Swing 模型
把叶组件当作是特殊、没有子组件的容器组件,只是JComponent 继承Container 类,所有
Swing 组件继承JComponent 的原因。
JComponent 在paint 方法中首先根据组件是否需要使用双缓冲,封装好图形设备对象,
然后经过一番处理后调用paintComponent 方法画出自身,然后调用paintBorder 画出边框,
最后调用paintChildren 来完成子组件的渲染。
paintComponent 意思是画出组件自身,不包括子组件。因此前一文章中的MyButton 可
以通过覆盖paintComponent 方法来完成MyButton 的重画。在JComponent 实现中,JDK 6
的paintComponent 的代码为:
protected void paintComponent(Graphics g) {
if (ui != null) {
Graphics scratchGraphics = (g == null) ? null : g.create();
try {
ui.update(scratchGraphics, this);
}
finally {
scratchGraphics.dispose();
}
}
}
这个方法首先检测组件是否安装了UI Delegate,如果安装了就将渲染过程代理给UI
Delegate。这儿是嵌入皮肤的地方。JDK 6 中JComponent 对应的UI Delegate 的update 方法
缺省的实现是:
public void update(Graphics g, JComponent c) {
if (c.isOpaque()) {
g.setColor(c.getBackground());
g.fillRect(0, 0, c.getWidth(),c.getHeight());
}
paint(g, c);
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
}
可以看出,背景透明机制在这儿实现。首先UI Delegate 对象判断Component 是否背景
透明的,如果不是透明的,则使用背景色填充整个Component 区域,然后调用paint(g, c)来
完成组件在这种LookAndFeel 种的渲染。了解了这些后,我们几乎就明白了Swing 如何实
现背景透明和如何切换皮肤。由于后面的文章还会对UI Delegate 和皮肤机制详细描述,这
儿就到此为止。
目前还不要求实现皮肤,在这种情况下只需要重载paintComponent 方法就行了,如果
需要背景透明机制,可以模仿上面代码,MyButton 的paintComponent 可以这样写:
public void paintComponent(Graphics g) {
if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
if(pressed){//按钮按下去了
//画出按下的样子
}else{
//画出抬起的样子
}
}
paintBorder 意思是画出组件的边框。Swing 所有组件都有边框的概念,就是说可以为任
何组件添加各种边框,包括自定义的边框。JDK 6 中JComponent 的paintBorder 的实现是这
样的:
protected void paintBorder(Graphics g) {
Border border = getBorder();
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
非常直接,如果自己有border,就将画自己边框的任务代理给了这个border,并传给它
图形设备和边界参数。Swing 缺省提供了大量的各种各样的边框。同样可以定义自己的边框,
实现方法就是继承Border 类,Border 类中有三个方法要实现,它们的含义如下:
public interface Border
{
//这儿是画出组件边框的地方。
void paintBorder(Component c, Graphics g, int x, int y, int width, int height);
//这儿是定义边框边界的地方,组件可以根据这信息,安排它的内容。
Insets getBorderInsets(Component c);
//边框的背景是不是透明的?不是透明的要负责画出边框的背景。是透明的使用组件的
背景。
boolean isBorderOpaque();
}
这儿实现一个简单的红线边框作为演示:
public class RedLineBorder implements Border{
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height){
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
g.setColor(Color.red);//设置为红色
g.drawRect(x,y, width, height);//画出边框
}
public Insets getBorderInsets(Component c){
return new Insets(1,1,1,1); //四周都是1
}
public boolean isBorderOpaque(){
return false; //背景透明
}
}
paintChildren 完成容器类组件的子组件的渲染。JDK 缺省的实现是调用各个自组件的
paint 方法。一般来说不需要重载这个方法。如果想改变诸如组件Z-orde r 遮挡顺序,可以覆
盖这个方法,从相反顺序调用组件的paint 方法。
到这儿我们对Swing 的结构有了更深化的理解,UI Delegate 机制也已经初露倪端。还
有几个重要Swing Component 概念或者机制没有讲,明天的续文再对它们做出说明。
Swing 框架之Component:续文二
Swing 的事件处理过程为:事件调度线程(Event Dispatch Thread)从事件队列
(EventQueue)中获取底层系统捕获的原生事件,如鼠标、键盘、焦点、PAINT 事件等。接着
调用该事件源组件的dispachEvent。该方法过滤出特殊事件后,调用processEvent 进行处理。
processEvent 方法根据事件类型调用注册在这个组件上的相应事件处理器函数。事件处理器
函数根据这些事件的特征,判断出用户的期望行为,然后根据期望行为改变组件的状态,然
后根据需要刷新组件外观,触发带有特定语义的高级事件。此事件继续传播下去,直至调用
应用程序注册在该组件上的处理器函数。下图是这个过程的示意图:
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
上图所示意的过程简要说就是:
Pump an Event->Dispatch & Process Event->MouseListener.mousePressed-
>fireActionPerformed->ActionListener.actionPeformed->Do database query and display result to
a table->Return from actionPerformed->Return from fireActionPerformed->Return from
MouseListener.mousePressed->Pump another Event.
事件调度线程在应用程序事件处理函数actionPerformed 没有完成之前是不能处理下一
个事件的,如果应用程序处理函数是一个时间复杂的任务(比如查询数据库并将结果显示到
表格中),后面包括PAINT 事件将在长时间内得不到执行。由于PAINT 事件负责将界面更新,
所以这就使用户界面失去响应。
打一个比方,事件处理线程就像进入某城唯一的单行道一样,事件相当于汽车。有种
PAINT 汽车负责为城市运输非常重要的生活物资。但是有一天,PAINT 前面有一辆汽车突
然坏掉了,司机下来修车。但是这车太难修,一修就是几天,结果后面的PAINT 汽车无法
前进,物资无法按时运到城里。市民急了,市长虽然不停的打电话催PAINT 公司,但即使
PAINT 公司多添加几辆车也没用。由于进城的唯一条路被那辆车给占着,所以再多的PAINT
车也只能堵在路上。
不了解Swing 的这种事件处理模型的人往往将时间复杂的任务放在处理函数中完成,
这是造成Swing 应用程序速度很慢的原因。用户触发这个动作,用户界面就失去了响应,
于是给用户的感觉就是Swing 太慢了。其实这个错误是程序员造成的,并不是Swing 的过
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
失。
说点题外话,所有采用这种事件模型的用户界面工具都会产生这种问题,包括SWT、GTK、MFC
等流行的用户界面工具。之所以只有Swing 被误解,主要是和Swing 的历史、市场时机、商业宣传策略和
心理学相关的。
首先Swing 的历史和市场时机极差。Swing 出现早期性能也差、错误也多,而Java 程序员脱身于传
统图形界面工具,对于Swing 这种新的事件处理模型并不太了解,而此时正处于Java 第一轮狂热的时期,
大家都满怀希望做了大量的Swing 应用程序,而这些程序中大量存在这种错误方法。于是市场上涌现了大
批的这种程序。自从那个时代,因为这些程序,Swing 被贴上了慢的标签。又由于当时的Swing 界面也丑,
和一般的Windows 程序风格炯异,更加深人们的这种印象。这种印象一直持续到现在,像烙印一样深深的
刻在人们的脑海里。
其次,Swing 还有一个致命的问题,就是没有涌现出一个具有标识性的好程序,这是造成它比SWT
印象惨的原因。为什么SWT 采用相同的事件处理模型,而获得了速度快的声誉呢?这是因为人们当时对于
Java 做桌面应用的期望心理达到了低谷,而SWT 的出现恰恰是伴随Eclipse 出现的,早期的Eclipse 的确是
在速度快、界面漂亮,这一扫当时人们认为Java 慢,Java 界面丑陋,Java 无法做桌面应用的印象,继而这
个印象被加在SWT 身上,人们认为Eclipse 速度快、漂亮是因为SWT,其实如果你知道Swing/SWT 事件
处理模型的话,你就明白功劳是Eclipse 开发者的,Eclipse 界面漂亮其实要归功于Eclipse 界面设计专家,
他们的高水平造就了这个好的IDE,从而也抬起了SWT 的声誉。而Swing 的名誉恰恰就被早期Swing 低水
平开发者给毁了。
再次, 这和商业宣传策略有关。IBM 和Eclipse 很懂得市场宣传,人们不是认为Java 慢吗,就宣传SWT
使用原生组件,人们不是认为Swing 丑陋、风格炯异吧,就宣传SWT 风格一致性,人们不是认为Java 不
能做桌面应用吗,就宣传基于SWT 的Eclipse。其实这一切的背后原因只是“人”的不同,Eclipse 的开发者
和Swing 应用程序的开发者,Swing 和SWT 技术差异并没有造成那么大的差别,如果是相近能力的人使用
他们开发的话,应该能做出相近的产品。这可以从现在Eclipse 和NetBeans、Intellij IDEA、JDeveloper 和
JBuilder 看的出来。
最后,人类有一个心理学现象,就是一旦形成对某种事物的印象,很难摆脱旧的认识,有时甚至人们
不愿意承认摆在眼前的事实。总而言之,Swing 和SWT 不同遭遇是因为历史、市场时机、商业宣传策略、
心理学的种种原因造成的。
那么如何避免这个问题,编写响应速度快的Swing 应用程序呢?在SwingWorker 的
javadoc 中有这样两条原则:
Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the
application becomes unresponsive. 耗时任务不要放到事件调度线程上执行,否则程序就会失
去响应。
Swing components should be accessed on the Event Dispatch Thread only. Swing 组件只能在
事件调度线程上访问。
因此处理耗时任务时,首先要启动一个专门线程,将当前任务交给这个线程处理,而当
前处理函数立即返回,继续处理后面未决的事件。这就像前面塞车的例子似的,那个司机只
要简单的把车开到路边或者人行道上修理,整个公路系统就会恢复运转。
其次,在为耗时任务启动的线程访问Swing 组件时,要使用SwingUtilties. invokeLater
或者SwingUtilities.invokeAndWait 来访问,invokeLater 和invokeAndWait 的参数都是一个
Runnable 对象,这个Runnable 对象将被像普通事件处理函数一样在事件调度线程上执行。
这两个函数的区别是,invokeLater 不阻塞当前任务线程,invokeAndWait 阻塞当前线程,直
到Runnable 对象被执行返回才继续。在前面塞车的例子中,司机在路边修车解决了塞车问
题,但是他突然想起来要家里办些事情,这时他就可以打个电话让家里开车来。假如修车不
受这件事情的影响,比如叫家人送他朋友一本书,他可以继续修车,这时就相当于
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
invokeLater;假如修车受影响,比如缺少某个汽车零件,叫家人给他送过来,那么在家人来
之前,他就没法继续修车,这时就相当于invokeAndWait。
下面举一个例子说明这两点,比如按下查询按钮,查询数据量很大的数据库,并显示在
一个表中,这个过程需要给用户一个进度提示,并且能动态显示表格数据动态增加的过程。
假设按钮的处理函数是myButton_actionPerformed,则:
void myButton_actionPerformed(ActionEvent evt){
new MyQueryTask().start();
}
public class MyQueryTask extends Thread{
public void run(){
//查询数据库
final ResultSet result=...;
/ /显示记录
for(;result.next();){
//往表的Model 中添加一行数据,并更新进度条,注意这都是访问组件
SwingUtilities.invokeLater(new Runnable(){
public void run(){
addRecord(result);
}
});
}
....
}
void addRecord(ResultSet result){
//往表格中添加数据
jTable.add....
//更新进度条
jProgress.setValue(....);
}
}
JDK1.6 以后,Swing 提供了一个专门的类SwingWorker 能帮你解决这个编程范式,你
所需要做的就是继承这个类,重载doInBackground,然后在actionPeformed 中调用它的
execute 方法,并通过publish/process 方法来更新界面。SwingWorker 的主要方法和它们的作
用在下面的示意图:
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
从上面示意图可以看出,SwingWorker 实际上不过是封装了前面我所说的例子中的
MyQueryTask,并做了更详尽的考虑。execute 方法相当于MyQueryTask 线程start,它启动
这个后台线程并立刻返回。SwingWorker 可以注册PropertyChangeListener,这些listener 都
被在事件调度线程上执行,相当于MyQueryTask 中的那些访问组件的Runnable 对象。另外,
publish、setProgress 只不过是特殊的property 事件吧,process 和done 不过是响应publish 和
PropertyChangeEvent.DONE 这个事件的方法罢了。因此我们很容易将上面的例子改成
SwingWorker 的版本:
void myButton_actionPerformed(ActionEvent evt){
new MyQueryTask().execute();
}
public class MyQueryTask extends SwingWorker{
public void doInBackground(){
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
//查询数据库
final ResultSet result=...;
//显示记录
for(;result.next();){
//往表的Model 中添加一行数据,并更新进度条,注意这都是访问组件
publish(result);
}
....
}
public void process(Object ... result){
//往表格中添加数据
jTable.add....
//更新进度条
jProgress.setValue(....);
}
}
对于一般的耗时任务这样做是比较普遍的,但是有一些任务是一旦触发之后,会周期性
的触发,如何做处理这种任务呢?JDK 中提供了两个Timer 类帮你完成定时任务,一个是
javax.swing.Timer,一个java.util.Timer。使用它们的方法很简单,对于Swing 的timer,使用
方法如下:
public void myActionPerformed(){
//假设点击了某个按钮开始记时
Action myAction=new AbstractAction(){
public void actionPerformed(ActionEvent e){
//做周期性的活动,比如显示当前时间
Date date=new Date();
jMyDate.setDate(date);//jMyDate 是个假想的组件,能显示日期时间
}
};
new Timer(1000, myAction).start();
}
java.util.Timer 类似,只不过使用TimerTask 完成动作封装。注意这两个Timer 有一个关
键的区别:Swing 的Timer 的事件处理都是在事件调度线程上进行的,因而它里面的操作可
以直接访问Swing 组件。而java.util.Timer 则可能在其他线程上,因而访问组件时要使用
SwingUtilities.invokeLater 和invokeAndWait 来进行。这一点要记住。
如果要了解更详细的信息,可以查阅SwingWorker、Swing Timer 和util Timer 这些类
javadoc 文档和其他网上资料。最重要的是要记住了那两条原则。
Swing 框架之Component:续文三
Swing事件与事件处理器模型
Component 在Swing 模型中是事件触发源。前一篇文章在描述Swing 的事件处理模型时
就已经提到了这个事件处理过程。简单来说,Swing 组件在侦听到原生事件并处理后,往往
产生新的逻辑事件。逻辑事件是某些组件所特有的、具有特定语义的事件,比如JButton 按
下时产生ActionEvent、JComboBox 一项被选中时产生ItemEvent,等等。和原生事件不同,
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
它们并不被派发到系统事件队列中,而是由组件直接触发。事件处理器作为组件的观察者添
加到组件上并侦听触发的事件。假设事件名叫XXX,Swing 中实现这个模式的一般模式是:
1.定义一个XXXEvent
public class XXXEvent extends Event{
...
public void XXXEvent(Object src){
super(src);
...
} ...
}
2.定义一个事件处理器接口XXXListener,声明所有和该事件相关的处理方法:
public interface XXXListener extends EventListener{
void action1(XXXEvent evt);
void action2(XXXEvent evt);
...
}
3.在触发它的组件中定义一下方法:
public class MyComponent extends Jcomponent{
...
//存放事件处理器的队列
private ArrayList<XXXListener>xxxListeners=newArrayList<XXXListener>();
//定义以下各种方法,访问符号用public,以方便添加删除处理器
public void addXXXListener(XXXListener listener){
xxxListeners.add(listener);
}
public void removeXXXListener(XXXListener listener){
xxxListeners.remove(listener);
}
//定义各种触发(fire)action1、action2...的方法,注意一般使用protected,以便
继承和扩展
//每一个action 都要定义一个相应触发(fire)的方法
protected void fireAction1(XXXEvent evt){
for(XXXListener listener:xxxListeners){
listener.action1(evt);
}
}
protected void fireAction2(XXXEvent evt){
for(XXXListener listener:xxxListeners){
listener.action2(evt);
}
}
...
//在某些地方,比如鼠标处理函数中触发相应的动作
void myMouseReleased(MouseEvent evt){
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
...
if(应该触发action1)
fireAction1(new XXXEvent(this));
...
if(应该触发action2)
fireAction2(new XXXEvent(this));
...
}
}
XXXEvent、XXXListener、addXXXListener、removeXXXListener 以及各种fireAction
函数多是重复性代码,有些Java IDE 如JBuilde r 中能够根据开发者的指定参数的自动生成
这些代码。
实际上这个观察者模式的编程范式可以推广到任何JavaBeans,不一定是可视化的Swing
组件。以前曾经见过JBuilder 做的一个所谓数据库操作的JavaBeans,它没有界面,但它和
Swing 组件完全一样添加删除处理器。它的功能是异步操作数据库,在数据操作完了之后触
发注册在上面的事件处理器,该事件处理器就可以将查询结果展现在表格中,或者输出成报
表等等。
在这个模型中,JavaBeans 本身既可以是事件源(被观察对象),也可以是事件处理器(观
察者),JavaBeans 也可以侦听自身的事件并且处理。比如前面文章所提的MyButton 在处理
鼠标事件时就是自己侦听自己发出的鼠标事件,自己既是事件源,又是事件处理器,形成自
反系统。各种各样的JavaBeans 通过这种机制联系成一张事件网,各种JavaBeans 就是这个
网上的节点,而它们之间的事件触发与事件处理关系就是这张网络上的线。当某个节点被外
界或自身发出的事件所触发时,行成了事件的传播。这个过程很像网络上节点的振动引起周
围周围节点振动的模型。下图示意了这种JavaBeans 之间的事件网:
例如new JscrollPane(new JtextArea())这个系统,它里面包括两个JScrollBar 和一个
JTextArea,当鼠标拖动事件触发JScrollBar 时,JScrollBar 处理了这个鼠标拖动事件,并发
出滚动条拖动事件,这个事件传播给JTextArea,JTextArea 处理这个拖动事件,相应的更新
自己显示的内容,如果JTextArea 之后又根据更新发出了一个新的事件,这个事件便会继续
传播下去。
Swing布局管理器
现在高级图形用户界面工具一般都包括布局管理器机制。什么叫做布局管理器?如果所
有窗口的大小是不变的,那么我们在往窗口中添加组件时,只要将组件的拖放到固定位置、
调整好尺寸就可以了,就像VB 的界面工具一样。可大多数情况并非如此,用户经常需要调
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
整窗口的大小,以便和其他程序协同工作。这种情况下,在传统界面工具中,比如VB,就
需要显式的侦听窗口尺寸调整事件,根据当前窗口的大小重新计算并调整各个组件的大小和
位置。AWT/SWT/Swing 将这个过程自动化、模块化了,抽象出一个布局管理器来负责管理
界面组件的布局。
它们实现原理是相似的:容器类组件侦听初始化、invalide/validate 以及容器尺寸调整等
事件,一旦发生这些事件,容器类组件检查自己是否配置了布局管理器,如果没有,则不做
任何事情;如果有,则将容器内组件的布局代理给布局管理器,让它来完成容器内组件的重
新布局。
容器管理器对象对实现两类接口:LayoutManage r 和LayoutManager2,LayoutManager2
是LayoutManage r 的一个扩展,允许组件在添加时指定位置参数。它们的定义和含义如下:
public interface LayoutManager {
//添加组件comp,并和name 关联起来,name 可以作为位置等特殊含义参数来使
用
void addLayoutComponent(String name, Component comp);
//删除组件comp
void removeLayoutComponent(Component comp);
//根据容器内的当前组件,计算容器parent 的最优尺寸。
Dimension preferredLayoutSize(Container parent);
//根据容器内的当前组件,计算容器parent 的最小尺寸。
Dimension minimumLayoutSize(Container parent);
//重新布局容器parent,这儿是主要布局逻辑所在。
void layoutContainer(Container parent);
}
public interface LayoutManager2 extends LayoutManager {
//添加组件comp,constraints 用作指定如何以及位置的参数,这个函数主要是弥补
LayoutManage r 版的addLayoutComponent 表达能力欠缺而添加。
void addLayoutComponent(Component comp, Object constraints);
//根据容器内的当前组件,计算容器parent 的最大尺寸。看来除了最优、最小,某
些情况下还是需要知道最大。
public Dimension maximumLayoutSize(Container target);
//指定水平方向上组件之间的相对对齐方式,0 表示和源组件对齐,1 表示远离源
组件。
public float getLayoutAlignmentX(Container target);
//指定垂直方向上组件之间的相对对齐方式,0 表示和源组件对齐,1 表示远离源
组件。
public float getLayoutAlignmentY(Container target);
//invalidate 这个布局管理器,有时布局管理器为了计算迅速,可能第一次计算之后
就将一些数据给缓冲,但是后容器内的组件数目发生变化,这儿的缓冲值就需要调用这个方
法通知更新
public void invalidateLayout(Container target);
}
Swing 在java.awt 和javax.swing 中都分别提供大量的布局管理器,这些布局管理器有简
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
单的如FlowLayout,有复杂的如GridBadLayout。用户还可以自己定义自己的布局管理器,
由于篇幅原因,这儿略去例子。
Java 6 中在布局管理中引入了BaseLine / Anchor 的概念,能协助Java IDE 的用户界面设
计工具,方便用户来设计布局组件。NetBeans 的Matisse 组件首先引入了一个GroupLayout
布局管理器,结合Matisse 使用,提供了非常方便的布局管理和界面设计。GroupLayout 和
BaseLine/Anchor 概念以及Matisse 可以说是Java 界面设计工具的一大进步,可以说足以成
为Java 桌面应用史上的一个里程碑。在这之前,缺乏有力的界面设计工具是Java 在桌面应
用失败的一个重要原因。虽然Anchor 概念早就在Delphi 界面设计工具出现过,但是这个工
具的出现还是Java 界面设计史上的一大事件。随着Java 6 桌面应用支持的增强,以及
NetBeans Matisse 之类界面设计工具的出现,使得Java 桌面应用时代已经到来。Seeing is
believing,你不妨试一下就知道了。
=====================================
本想再加一节讲述Swing 双缓冲机制,但是想到双缓冲并不是Swing 模型的核心概念,
没有它并不影响理解Swing 的总体模型,因此打算把它作为以后的一篇专门技术文章来写。
这样Swing 模型中的Component 部分就算是描述完了,从明天开始,讲述Swing 模型
中的另外三个重要概念:Model、UI Delegate 和Renderer。
Swing 框架之Model之一
构建应用程序应该以数据为中心,而不是以用户界面为中心,这是一个良好的编程习惯。
为支持这种编程范式,Swing 为每种带有逻辑数据或值的组件定义了独立的模型接口,这种
分割使程序可以选择向Swing 组件中嵌入自己的模型实现。
下面表格列出Swing 中组件及其模型的映射关系:
组件Model 接口Model 类型
JButton ButtonModel GUI 状态
JToggleButton ButtonModel GUI 状态/应用数据
JCheckBox ButtonModel GUI 状态/应用数据
JRadioButton ButtonModel GUI 状态/应用数据
JMenu ButtonModel GUI 状态
JMenuItem ButtonModel GUI 状态
JCheckBoxMenuItem ButtonModel GUI 状态/应用数据
JRadioButtonMenuItem ButtonModel GUI 状态/应用数据
JComboBox ComboBoxModel 应用数据
JProgressBar BoundedRangeModel GUI 状态/应用数据
JScrollBar BoundedRangeModel GUI 状态/应用数据
JSlider BoundedRangeModel GUI 状态/应用数据
JTabbedPane SingleSelectionModel GUI 状态
JList ListModel 应用数据
JList ListSelectionModel GUI 状态
JTable TableModel 应用数据
JTable TableColumnModel GUI 状态
JTree TreeModel 应用数据
JTree TreeSelectionModel GUI 状态
JEditorPane Document 应用数据
JTextPane Document 应用数据
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
Swing模型分类
Swing 提供的模型分为两大类:GUI 状态模型和应用数据模型。
GUI 状态模型是描述GUI 控件可视化状态的接口,如按钮是否按下,或列表中那一项
被选中。GUI 状态模型通常仅在图形用户界面(GUI)环境中用到。通常来说,虽然编写使用
GUI 状态模型分离程序,尤其是当多个GUI 控件共享状态,或当操作一个控件自动更新另
一个的值时比较有用,但GUI 状态模型在Swing 中并不是必需的,完全可以通过组件顶层
方法操作GUI 控件的状态,而不必和模型直接交互。
应用数据模型是描述具有应用程序含义数据的接口,比如表格中的数据,或列表显示的
选项。这些数据模型为Swing 提供了一个清晰分割应用程序界面和数据逻辑的强大编程模
式。对于以数据为核心的Swing 组件,比如JTree 和JTable,强烈推荐使用数据模型进行交
互。
当然一些组件的模型根据应用场景的不同其分类介于GUI 状态模型和应用数据模型之
间,比如JSlider 和JProgressBar 的BoundedRangeModel。
Swing 的可分离模型接口并没有明确界定GUI 状态模型和应用数据模型。这儿所以做
此说明,目的是让你更好的理解何时以及为何要需要使用分离的模型。
共享模型定义
值得注意的是,上文中表格中,许多组件的数据抽象相似,只需一个接口而不用过分泛
化时,组件可以共享同一模型定义。共享模型定义允许在不同组件之间自动连接。比如,
JSlider 和JScrollBar 都使用BoundedRangeModel 接口,因此可以在一个JScrollBar 和一个
JSlider 之间共享同一个BoundedRangeModel 实例,这样它们之间的状态就总是同步的。
分离模型编程接口
使用模型的Swing 组件必须提供访问修改模型的set/get 方法,即模型必须是该组件的
限定性属性。比如,JSlider 使用BoundedRangeModel 接口作为它的模型定义,因此它必须
提供下面方法:
public BoundedRangeModel getModel()
publicvoidsetModel(BoundedRangeModelmodel)
所有Swing 组件有一个共同点:如果你不设置它的模型,组件会在内部创建/安装一个
缺省模型。这些缺省模型类的命名习惯是在接口名称之前加上“Default”,比如JSlider 的构
造函数中初始化一个DefaultBoundedModel 对象。
public JSlider(int orientation, int min, int max, intvalue){
checkOrientation(orientation);
this.orientation = orientation;
JTextArea Document 应用数据
JTextField Document 应用数据
JPasswordField Document 应用数据
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
this.model = newDefaultBoundedRangeModel(value, 0, min, max);
this.model.addChangeListener(changeListener);
updateUI();
}
如果程序接着调用setModel(),缺省的模型就被替换了,比如下面例子:
JSlider slider = new JSlider();
BoundedRangeModel myModel = new DefaultBoundedRangeModel() {
public void setValue(int n){
System.out.println("SetValue: "+ n);
super.setValue(n);
}
});
slider.setModel(myModel);
对于更复杂的模型(如JTable 和JList), Swing 还提供一个抽象模型实现,让开发者不
需要从头开始创建自己的模型。
如JList 的模型接口是ListModel,Swing 同时提供了DefaultListModel 和
AbstractListModel 两个类来协助开发者创建自定义的列表模型。
模型改变通知
当数据或者发生变动时,模型必须通知所有相关方(比如视图)。Swing 模型使用前面
文章所讲述的事件模型来实现这种触发。Swing 中有两种方法发送这种通知:
发送轻量级通知,表明状态已经改变,需要Listener 通过查询模型,发现什么改变了并
做出响应。此方法的优点是单独事件实例能用作该模型的所有通知,同时对于需要频繁通知
的事件非常有用(比如JScrollBar 被拖动时)。
发送状态化通知,详细描述模型如何改变。这种方法需要为每个通知创建一个新的事件
实例。当通知通过查询模型不能有效地给Listener 提供足够的信息时,此方法非常有用。比
如当JTable 的一列表格数据发生改变时。
(待续)
Swing 框架之Model之二
轻量级通知
下面Swing 中的模型使用轻量级通知,它们是基于ChangeListener、ChangeEvent 接口
的:
ChangeListener 接口只有一个通用方法:
public void stateChanged(ChangeEvent e)
ChangeEvent 中仅有的状态是事件源,因为所有通知中的事件源都是相同的,单独一个
事件实例可以用作所有来自该模型的通知。使用此机制的模型支持下面的方法来添加和删除
ChangeListeners:
public void addChangeListener(ChangeListenerl)
public voidremoveChangeListener(ChangeListenerl)
Model Listener Event
BoundedRangeModel ChangeListener ChangeEvent
ButtonModel ChangeListener ChangeEvent
SingleSelectionModel ChangeListener ChangeEvent
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
获知JSlider 数据发生变化的代码可以使用如下代码实现:
JSlider slider = new JSlider();
BoundedRangeModelmodel =slider.getModel();
model.addChangeListener(new ChangeListener() {
public voidstateChanged(ChangeEvent e) {
// need to query themodel
// to getupdatedvalue...
BoundedRangeModel m=(BoundedRangeModel)e.getSource();
System.out.println("mode l changed: "+
m.getValue());
}
});
为给不想和分离式模型交互的程序提供方便,一些Swing 组件类提供了直接在组件上
注册ChangeListener 的方法(组件可在组件内部侦听模型的数据变化,并将事件传播给任何
注册在组件上的Listener),这些通知的唯一区别是,使用模型注册方式的事件源是该模型实
例,而使用组件注册方式的事件源是该组件。
因此我们可以将前面的例子简化成:
JSlider slider = newJSlider();
slider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
//the source will be
// the slider this time..
JSlider s=(JSlider)e.getSource();
System.out.println("valuechanged: "+s.getValue());
}
});
状态化通知
支持状态化通知的模型根据它们的目的提供不同的Listener 接口和事件对象。下表是这
些模型接口和事件对象的类:
Listener 除了可以直接查询事件对象来跟踪内容改变外,这些API 的作用与轻量级通知相似。
比如下面的代码动态的跟踪JList 被选中的项:
Stringitems[] = {"One", "Two", "Three");
Model Listener Event
ListModel ListDataListener ListDataEvent
ListSelectionModel ListSelectionListener ListSelectionEvent
ComboBoxModel ListDataListener ListDataEvent
TreeModel TreeModelListener TreeModelEvent
TreeSelectionModel TreeSelectionListener TreeSelectionEvent
TableModel TableModelListener TableModelEvent
TableColumnModel TableColumnModelListener TableColumnModelEvent
Document DocumentListener DocumentEvent
Document UndoableEditListener UndoableEditEvent
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
JList list = new JList(items);
ListSelectionModel sModel =list.getSelectionModel();
sModel.addListSelectionListener(new ListSelectionListener() {
publicvoidvalueChanged(ListSelectionEvent e) {
// get change information directly
// fromtheevent instance...
if (!e.getValueIsAdjusting()) {
System.out.println("selection changed: " +
e.getFirstIndex());
}
}
});
自动视图更新
模型没有任何表现它视图的固有知识,相反模型只有关心其状态改变的Listener 列表,
这种需求对于同个模型多个视图的框架来说是至关重要的。Swing 组件负责将合适的模型
Listener 连接起来,以便于模型改变时能正确地重画出自己。如果你发现模型改变时,组件
不能自动更新,说明组件的实现就存在错误。
忽略模型
正如前面提到的,大多数组件直接在Component 类中提供模型定义的API,以方便组件
能不用和模型交互就直接操作,这是相当可行的编程方法,尤其是对于GUI 状态模型来说。
比如下面的JSlider 内部getValue 的实现,它将调用代理给模型:
public int getValue(){
return getModel().getValue();
}
因此程序完全可以这样写:
JSlider slider = new JSlider();
int value=slider.getValue();
Swing模型总结
虽然理解了Swing 模型设计是如何工作的,但没有必要在所有Swing 编程中都使用模
型API。你需要注意考虑应用程序各自的需求,决定哪儿使用模型API 能帮你提升代码,且
不带来不必要的复杂性。
我特别推荐在Swing 中使用应用数据模型(如JTable 和JTree 等的模型),因为从长期
来看,它们能极大地提高你的应用程序可扩展性和模块化度。
下一篇文章将就Swing 框架的另一个概念UIDelegate 进行阐述。
Swing 框架之UI Delegate 之一
Swing 的UI Delegate 机制使得Swing 组件可以动态地切换LAF。注:所谓LAF 就是Look
and feel,外观和感觉,为了更准确表达这个意思,后文用LAF,表示Look And Feel。这套LAF
机制是封装在Swing 组件内部的,开发者编写Swing 程序,不需要指定特定LAF。Swing
工具提供了一套缺省LAF;当然Swing 的LAF 的API 是开放的,允许开发者通过扩展已有
的LAF 或者从头创建一个LAF。虽然这种可插拔LAF 的API 是可扩展的,但是它被有意设
计成基本组件下一层的接口,这样开发者不必要理解LAF 机制的复杂细节就能创建Swing
图形用户界面。
虽然不提倡开发者自己创建新的LAF,但Swing 开发小组意识到了PLAF(Platform Look
文章出自http://blog.sina.com.cn/swingjava,作者WilliamChen,仅细微地方改动,xjlnju730 整理
WilliamChen 乃Swing 大师级的人物,这篇文章对深入理解Swing 相当有帮助!
And Feel, 平台相关的外观和感觉)对于想创建独特外观的应用程序来说是一个非常强大的
功能。事实证明,PLAF 对于创建残障用户友好的界面是很理想的(残障用户:有视力缺陷
的用户或者不能操作鼠标的用户)。
本质上说,可插拔LAF 的设计意味着实现组件展现(Look)和事件处理(Feel)的部分是被
代理到独立的UI 对象中的,这些UI 对象是由当前的LAF 提供的,它们可以被动态地修改。
这些可插拔LAF 的API 包括:
1.Swing 组件(component)类中的钩子
2.LAF 管理的顶层API
3.独立的包中实际实现LAF 的接口
组件钩子
拥有有LAF 特征的Swing 组件在javax.swing.plaf 包都有一个抽象类来代表它的UI
Delegate,这些UI 类的命名规则是该组件类去掉前缀J,加上后缀UI。比如JButton 的UI
Delegate 类的名字是ButtonUI。
UI Delegate 在组件的构造函数中创建,并以组件的限定JavaBean 属性的形式访问,比
如JScrollBar 提供了下面的方法访问它的UI Delegate:
public ScrollBarUI getUI()
public void setUI(ScrollBarUI ui)
组件创建并设置UI Delegate 的过程实际上是“安装”组件LAF 的过程。每个组件同时提
供方法创建和设置“缺省”LAF 的UI Delegate,组件的构造函数在安装UI 时使用该方法。
public void updateUI()
LAF 实现为每个抽象UI 类提供了具体子类,比如,Windows LAF 定义了
WindowsButtonUI, WindowsScrollBar 等。当组件安装它的UI Delegate 时,须