Swing框架之UI Delegate

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初始化组件属性的约定俗成的做法包括以下两种:

  1. 从DefaultsTable获取所有用来设置颜色、字体、边框等属性的值。
  2. 颜色、字体和边框属性应该只有在应用程序没有设置它们时设置。

       对于做法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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值