Swing的UIDelegate机制使得Swing组件可以动态地切换LAF。注:所谓LAF就是Look andfeel,外观和感觉,为了更准确表达这个意思,后文用LAF,表示Look AndFeel。这套LAF机制是封装在Swing组件内部的,开发者编写Swing程序,不需要指定特定LAF。Swing工具提供了一套缺省LAF;当然Swing的LAF的API是开放的,允许开发者通过扩展已有的LAF或者从头创建一个LAF。虽然这种可插拔LAF的API是可扩展的,但是它被有意设计成基本组件下一层的接口,这样开发者不必要理解LAF机制的复杂细节就能创建Swing图形用户界面。
虽然不提倡开发者自己创建新的LAF,但Swing开发小组意识到了PLAF(PlatformLook And Feel,平台相关的外观和感觉)对于想创建独特外观的应用程序来说是一个非常强大的功能。事实证明,PLAF对于创建残障用户友好的界面是很理想的(残障用户:有视力缺陷的用户或者不能操作鼠标的用户)。
本质上说,可插拔LAF的设计意味着实现组件展现(Look)和事件处理(Feel)的部分是被代理到独立的UI对象中的,这些UI对象是由当前的LAF提供的,它们可以被动态地修改。
这些可插拔LAF的API包括:
- Swing组件(component)类中的钩子
- LAF管理的顶层API
- 独立的包中实际实现LAF的接口
组件钩子
拥有有LAF特征的Swing组件在javax.swing.plaf包都有一个抽象类来代表它的UIDelegate,这些UI类的命名规则是该组件类去掉前缀J,加上后缀UI。比如JButton的UIDelegate类的名字是ButtonUI。
UIDelegate在组件的构造函数中创建,并以组件的限定JavaBean属性的形式访问,比如JScrollBar提供了下面的方法访问它的UIDelegate:
public ScrollBarUIgetUI()
public void setUI(ScrollBarUIui)
组件创建并设置UIDelegate的过程实际上是“安装”组件LAF的过程。每个组件同时提供方法创建和设置“缺省”LAF的UIDelegate,组件的构造函数在安装UI时使用该方法。
public voidupdateUI()
LAF实现为每个抽象UI类提供了具体子类,比如,WindowsLAF定义了WindowsButtonUI, WindowsScrollBar等。当组件安装它的UIDelegate时,须有办法动态查找到当前缺省LAF的具体实现类名。该操作使用了一个hash表,它的主键是由组件的getUIClassID()方法获得,习惯是使用平台抽象类名,比如JScrollBar的getUIClassID()是这样定义的:
public StringgetUIClassID() {
return"ScrollBarUI";
}
相应在此hash表中,WindowsLAF将他ScrollBarUI映射成JScrollBar在Windows平台的UI实现类:
com.sun.java.swing.plaf.windows.WindowsScrollBarUI
LAF管理
Swing定义了一个抽象类LookAndFeel来表达所有LAF实现的核心信息,比如LAF的名字、描述、是否是本地化LAF,以及一个Hash表(也称作“DefaultsTable”)来存储各种各样LAF属性的缺省值,比如颜色和字体。每种LAF实现都定义一个LookAndFeel的子类,如swing.plaf.motif.MotifLookAndFeel,来为Swing提供管理LAF的必须信息。
UIManager是组件和程序访问LAF信息的API(尽量不要直接访问LookAndFeel实例)。UIManager负责跟踪当前有哪些LookAndFeel类可用,哪些安装了,谁是缺省的。UIManager还管理对于当前LAF的DefaultsTable的访问。
“缺省”LAF
UIManager还提供了设置和获取当前缺省LookAndFeel的方法:
public static LookAndFeelgetLookAndFeel()
public static voidsetLookAndFeel(LookAndFeelnewLookAndFeel)
public static voidsetLookAndFeel(String className)
Swing初始化了一个跨平台Java外观(以前称作“Metal”)作为缺省的LAF。然而,当Swing程序想明确设置缺省LAF时,可使用UIManager.setLookAndFeel()方法,比如下面的代码将缺省的LAF设置成CDE/Motif:
UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
有时应用程序并不想指定特定的LAF,而想动态匹配当前平台类型的LAF,比如运行在WindowsNT上就使用WindowsLAF,运行在Solaris上就使用CDE/Motif,或者程序想设置成为跨平台的JavaLAF,UIManager为这些情况提供了下面的静态方法,动态地获取合适LookAndFeel的类名:
public static StringgetSystemLookAndFeelClassName()
public static StringgetCrossPlatformLookAndFeelClassName()
为确保程序总是使用系统LAF,可使用下面的代码:
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
动态调整缺省的LAF
Swing程序使用上面方法动态地设置LAF时,理想位置是在任何Swing组件初始化以前,因为UIManager.setLookAndFeel()方法通过装载并初始化LookAndFeel实例来使它成为当前缺省的LAF,但是它不能自动使当前所有的组件改变它们的外观。记住组件在构造函数里初始化它们的UIDelegate对象,如果当前缺省LAF在它们构造函数完毕之后发生变化,它们不会相应地自动更新,需程序遍历组件树更新组件来实现外观切换,Swing在SwingUtilities中提供了updateComponentTreeUI()方法来帮程序员完成此过程。
组件外观可在任何时候通过调用它的updateUI()方法来切换到当前缺省的LAF,该方法调用如下UIManager的静态方法来获得合适的UIDelegate 对象:
public static ComponentUIgetUI(JComponent c)
比如JScrollBar的updateUI()实现代码如下:
public voidupdateUI() {
setUI((ScrollBarUI)UIManager.getUI(this));
}
如果程序在初始化后需要改变它的GUI树的外观,可使用下面的代码:
//GUI已经初始化了,假设myframe变量是最顶层的frame
try {
UIManager.setLookAndFeel("com.sun.java.swing.plaf.motif.MotifLookAndFeel");
myframe.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
SwingUtilities.updateComponentTreeUI(myframe);
myframe.validate();
} catch(UnsupportedLookAndFeelException e) {
} finally {
myframe.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
管理LAF数据
UIManager定义了一个静态类UIManager.LookAndFeelInfo来存储LAF的名称(如“Metal”)及LAF类名如com.sun.java.swing.plaf.MetalLookAndFeel,它在内部使用这些名字来管理LookAndFeel对象,这些信息能通过UIManager下面的静态方法访问获得:
public staticLookAndFeelInfo[] getInstalledLookAndFeels()
public static voidsetInstalledLookAndFeels(LookAndFeelInfo[] infos)
throws SecurityException
public static voidinstallLookAndFeel(LookAndFeelInfo info)
public static voidinstallLookAndFeel(String name, String className)
这些方法可在编程中决定哪个LAF实现可用,当创建允许用户动态切换皮肤的界面时很有用。
LAF包
javax.swing.plaf包中的UIDelegate类(ButtonUI,ScrollBarUI等)定义了组件可以用来访问UIDelegate对象的接口,这些类在早期Swing实现中使用的是接口,后来替换成了抽象类。这些平台API接口是所有LAF实现的基类。
每个LAF实现提供这些抽象类的具体类,LAF实现定义的这些类是分别放在javax.swing.plaf包的子包中,如javax.swing.plaf.motif、javax.swing.plaf.metal等。LAF包中包含下面的文件:
- LookAndFeel的子类,比如MetalLookAndFeel
- 所有LAF的UIdelegate类,比如MetalButtonUI、MetalTreeUI等。
- 所有LAF的工具类,比如MetalGraphicsUtils、MetalIconFactory等。
- 其他和LAF相关的资源,比如图像文件等。
在实现各种SwingLAF中,有许多共性的东西,这些共同的代码被重构成一个基本LAF实现,称作basicLAF,各平台包括motif、windows等的LAF又继承basic LAF,basicLAF包支持桌面级别的LAF,比如Windows和CDE/Motif。BasicLAF包只是实现可插拔LAF的一个例子,Swing的LAF框架足够灵活,足够容纳其他的实现。许多第三方的LAF是基于basicLAF,当然更多的是基于metal、motif或者windows这些更为具体的LAF实现的。
LookAndFeel子类
LookAndFeel类声明了如下抽象方法,继承它的子类必须实现这些方法:
public StringgetName();
public String getID();
public StringgetDescription();
public booleanisNativeLookAndFeel();
public booleanisSupportedLookAndFeel();
getName()、getID()以及getDescription()方法返回描述LAF的普通属性。如果LAF相对当前平台是本地外观,那么isNativeLookAndFeel()方法返回true。比如,运行在Solaris操作系统上时MotifLookAndFeel返回true,其他平台则返回false。isSupportedLookAndFeel()方法返回该LAF是否允许运行于在当前平台上,比如WindowsLookAndFeel在所有Windows操作系统上都返回true。LookAndFeel类还提供了初始化和卸载的方法:
public voidinitialize()
public voiduninitialize()
当使用UIManager.setLookAndFeel()方法将LookAndFeel设置成缺省的LAF时,UIManager调用该LAF类的initialize()方法,当LookAndFeel被其它LookAndFeel所替换时,UIManager调用它的unintialize方法。
DefaultsTable
此外LookAndFeel类还提供返回LAF的Defaults Table的方法:
public UIDefaultsgetDefaults()
DefaultsTable是一个UIDefaults对象,UIDefaults继承java.util.Hashtable,还添加了访问LAF某些类型信息的专门方法。该表须包括UIClassID对classname的映射信息,还要包括许多展现相关属性的缺省值,比如颜色、字体、边框和图标等。下面例子假设有一个LAF实现,其LAF包名为mine,那么getDefaults()方法的代码应该如下:
publicUIDefaults getDefaults() {
UIDefaults table = newUIDefaults();
Object[] uiDefaults ={
"ButtonUI", "mine.MyButtonUI",
"CheckBoxUI", "mine.MyCheckBoxUI",
"MenuBarUI", "mine.MyMenuBarUI",
...
"Button.background",
new ColorUIResource(Color.gray),
"Button.foreground",
new ColorUIResource(Color.black),
"Button.font",
new FontUIResource("Dialog", Font.PLAIN, 12),
"CheckBox.background",
new ColorUIResource(Color.lightGray),
"CheckBox.font",
new FontUIResource("Dialog", Font.BOLD, 12),
...
}
table.putDefaults(uiDefaults);
return table;
}
当通过UIManager.setLookAndFeel()设置缺省LAF时,UIManager调用该LookAndFeel实例的getDefaults()方法,并存储返回的hash表。之后对于UIManager查找方法的调用将作用到该表上,比如将“mine”设置成缺省LAF后,下面代码返回ButtonUI对应的实现类mine.MyButtonUI。
UIManager.get("ButtonUI") =>"mine.MyButtonUI"
UI类使用相同方法访问缺省信息,如ButtonUI类使用如下代码初始化JButton的background属性:
button.setBackground(
UIManager.getColor("Button.background");
区分UI和应用程序设置的属性
Swing允许应用程序单独对组件设置属性值(颜色、字体等),保证这些值不和LAF设置的缺省值相冲突很重要。在组件UIdelegate第一次初始化时(在组件的构造函数中)这还不是个问题,因为所有属性都可以卸载,且可通过LAF来设置。问题出现在当应用程序在组件构造完毕后,单独设置这些属性,然后设置新的LAF时(也就是动态切换LAF)。这意味着LAF须能区分应用程序和LAF设置的值。
解决此问题的方法是通过javax.swing.plaf.UIResource接口标识所有通过LAF设置的值。javax.swing.plaf包提供了一些“标识”类来表达这些值,比如ColorUIResource、FontUIResource以及BorderUIResource,前面所述代码演示了如何使用这些类来标识MyButtonUI类的缺省属性值。
UIdelegate对象
所有UIDelegate类的基类是javax.swing.plaf.ComponentUI,此类包含了可插拔LAF工作原理的基本“机制”,其方法包括UI安装、卸载以及代理组件几何布局和重画等。许多UIDelegate子类还提供同特定组件进行交互的方法。本文只讲述ComponentUI实现的通用机制。
UI安装和卸载
ComponentUI类定义了如下方法来安装和卸载UI Delegate对象:
public void installUI(JComponent c)
public void uninstallUI(JComponentc)
看一下JComponent.setUI()的代码实现,就能清晰地看出UIdelegate安装与卸载是如何工作的。注意ComponentUI的子类总是激活JComponent的setUI方法实现各自的setUI。
protected void setUI(ComponentUI newUI) {
if (ui != null) {
ui.uninstallUI(this);
}
ComponentUI oldUI = ui;
ui = newUI;
if (ui != null) {
ui.installUI(this);
}
invalidate();
firePropertyChange("UI",oldUI, newUI);
}
下图是一幅描述安装和卸载UIdelegate对象过程的示意图:
UI delegate的installUI()方法负责以下过程:
- 设置组件的缺省字体、颜色、边框、透明属性。
- 为组件安装适当的布局管理器。
- 向组件添加适当的子组件。
- 为组件注册必须的事件处理器。
- 为组件注册特定LAF的键盘动作(快捷键等)。
- 注册repaint时需要通知的模型listener。
- 初始化某些适当的实例数据。
比如,ButtonUI扩展类的installUI()可用下面代码实现:
protectedMyMouseListener mouseListener;
protected MyChangeListenerchangeListener;
public voidinstallUI(JComponent c){
AbstractButton b =(AbstractButton)c;
// Install default colors& opacity
Color bg =c.getBackground();
if (bg == null || bginstanceof UIResource) {
c.setBackground(
UIManager.getColor("Button.background"));
}
Color fg =c.getForeground();
if (fg == null || fginstanceof UIResource) {
c.setForeground(
UIManager.getColor("Button.foreground"));
}
c.setOpaque(false);
// Installlisteners
mouseListener = newMyMouseListener();
c.addMouseListener(mouseListener);
c.addMouseMotionListener(mouseListener);
changeListener = newMyChangeListener();
b.addChangeListener(changeListener);
}
初始化组件属性约定俗成的做法
安装LAF时,Swing初始化组件属性的约定俗成的做法包括以下两种:
- 从DefaultsTable获取所有用来设置颜色、字体、边框等属性的值。
- 颜色、字体和边框属性应该只有在应用程序没有设置它们时设置。
对于做法1,UIManager定义了几个静态方法来获取指定类型的属性值,如getColor()和getFont()等。对于做法2,Swing通过设置属性前先检查是否为空,或者是否是UIResource实例来设置。
ComponentUI的uninstall()方法必须仔细清除所有installUI()方法的操作,让组件Component在安装另外一个UIdelegate对象前处于干净的原始状态。uninstall()方法负责:
- 删除installUI()设置的边框。
- 删除installUI()设置的布局管理器。
- 删除installUI()添加的组件。
- 删除installUI()添加的事件/模型处理器。
- 删除installUI()安装的特定于LAF的键盘动作。
- 将任何实例化的数据设置为空(允许GC进行垃圾收集)。
比如,为了取消前面例子中的操作,uninstall()方法代码应该如下:
publicvoid uninstallUI(JComponent c) {
AbstractButton b = (AbstractButton)c;
//Uninstall listeners
c.removeMouseListener(mouseListener);
c.removeMouseMotionListener(mouseListener);
mouseListener = null;
b.removeChangeListener(changeListener);
changeListener = null;
}
定义几何布局
在AWT/Swing中,容器的LayoutManager会根据不同算法来布局容器的组件,即所谓容器层次结构的“有效化(validation)”。典型的LayoutManager会查询其内部组件的preferredSize属性(根据具体算法有时还会查询minimumSize和/或maximumSize),目的是精确确定组件位置、设置组件大小。LAF通常需要指定某些组件的几何属性,ComponentUI为此提供了以下方法:
publicDimension getPreferredSize(JComponent c)
publicDimension getMinimumSize(JComponent c)
publicDimension getMaximumSize(JComponent c)
public booleancontains(JComponent c, int x, int y)
如果应用程序对这些几何属性没有明确设置,那么JComponent中的对应方法(LayoutManager在validation界面时调用这些方法)会简单将方法代理给相应UI对象,下面是JComponent.getPreferredSize()的方法实现:
publicDimension getPreferredSize() {
if(preferredSize != null) {
return preferredSize;
}
Dimension size = null;
if (ui!= null) {
size = ui.getPreferredSize(this);
}
return(size != null) ? size :
super.getPreferredSize();
}
尽管所有组件的边界是一个Rectangle对象,但可以通过覆盖java.awt.Component继承的contains()方法来模拟非矩形组件(contains()方法用来确定鼠标事件的点击位置)。不像其他Swing的几何属性,UIdelegate定义自己的contains()方法,Jcomponent的contains()方法代理给它完成:
public booleancontains(JComponent c, int x, int y) {
return(ui != null) ? ui.contains(this, x, y) : super.contains(x,y);
}
因此UIdelegate对象可以通过特定的contains()实现提供非矩形的“感觉”(比如希望用MyButtonUI类实现一个圆角按钮)。
重画
最后,UIdelegate对象须实现当前LAF下的组件重画,为此ComponentUI定义有如下方法:
public voidpaint(Graphics g, JComponent c)
public voidupdate(Graphics g, JComponent c)
JComponent.paintComponent()方法负责组件重画过程的代理工作:
protected voidpaintComponent(Graphics g) {
if (ui!= null) {
Graphics scratchGraphics =SwingGraphics.createSwingGraphics(g.create());
try {
ui.update(scratchGraphics, this);
}
finally {
scratchGraphics.dispose();
}
}
}
同AWT中使用的方法类似,UIdelegate对象的update()方法清除背景(如果不透明),接着激活自己的paint()方法,最后由paint()方法渲染出组件的内容。我们在前面的文章中已经提到过这段代码:
public voidupdate(Graphics g, JComponent c) {
if(c.isOpaque()) {
g.setColor(c.getBackground());
g.fillRect(0, 0, c.getWidth(),c.getHeight());
}
paint(g,c);
}
无状态代理及有状态代理
所有ComponentUI的方法的参数都有一个Jcomponent对象。这种约定俗成的做法可用来实现无状态的UIdelegate对象。代理对象可通过查询组件来获得信息。无状态UIdelegate的实现,允许单个UIdelegate实例被所有此组件实例共享,极大减少了实例化的代理对象。
ComponengUI定义了一个静态方法,返回代理实例:
public staticComponentUI createUI(JComponent c)
此方法的具体实现决定了代理是否是无状态的,组件通过激活UIManager.getUI()方法来创建UIdelegate,而getUI()激活该UIdelegate的静态方法createUI()来产生实例。这两种类型的代理对象在Swing的LAF实现中都有,比如Swing的BasicButtonUI实现了无状态的代理:
// Shared UIobject
protectedstatic ButtonUI buttonUI;
public staticComponentUI createUI(JComponent c)
if(buttonUI == null) {
buttonUI = new BasicButtonUI();
}
returnbuttonUI;
}
而Swing的BasicTabbedPaneUI实现了有状态的代理:
public staticComponentUI createUI(JComponent c){
returnnew BasicTabbedPaneUI();
}
LAF总结
Swing可插拔外观的功能很强大,但也是很复杂。其设计目的是,供少数需要实现新外观的开发者使用。应用程序开发者只需要理解这种机制的能力,以便决定支持外观的策略,是使用单一外观还是支持用户配置的多外观。Swing的UIManager为应用程序在这一层的管理提供了编程接口。你如果需要定义一套自己的外观,在编写程序前理解这些基础知识很重要的。
来源:http://blog.sina.com.cn/swingjava