JDK 7探秘三:用JLayer装饰Swing组件

JDK 7引入了一个新的Swing组件来装饰其它Swing组件,这个新的组件是通过javax.swing.JLayer类,基于Swing实验室项目Swing Helper的JXLayer实现的。JLayer结合javax.swing.plaf.LayerUI使用可以实现高级的绘制效果,并可以在它的边界范围内接收由java.awt.AWTEvents产生的所有通知。本文将为大家一一介绍这些类。

  JLayer和LayerUI概述

  根据JDK的文档描述,JLayer委托处理LayerUI对象的绘制和输入事件,LayerUI负责装饰,你可以使用这些类修改现有组件的外观和行为使它们的装饰效果更好。

  实际上,你可以自己动手扩展LayerUI,废除它们自带的方法,自己定制绘制和事件处理方法,然后将这个类的实例和装饰后的组件一道,传递给下面的JLayer构造器:

public JLayer(V view,LayerUI < V > ui)

  第一个参数可以是任何类的延伸java.awt.Component,表示你要装饰的Swing组件,这个组件可以是一个JPanel或其它容器,这个容器和它里面所有的组件都将被装饰,第二个参数代表装饰器。使用这些构造器创建JLayer时,可以延迟指定LayerUI实例和/或视图。

  如果初始化时不指定视图,之后你可以调用JLayer's public void setView(V view)方法来提供一个视图,这个类也提供了一个public V getView()方法返回组件是否被装饰,没有装饰就返回null。

  如果初始化时不指定LayerUI实例,之后你可以调用JLayer's public void setUI(LayerUI ui方法提供一个实例,这个类也提供了一个public LayerUI getUI()方法返回当前的装饰器,没有装饰器就返回null。

  JLayer自定义绘制

  为了演示JLayer的自定义绘制特性,我创建了一个ReverseText程序,其代码显示在清单1中,当按下按钮时,输入到textfield中的文本将全部颠倒,这个程序使用JLayer在用户界面后绘制了一个墙纸图案。

  清单1. ReverseText.java

// ReverseText.java
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.plaf.LayerUI;
public class ReverseText
{
  
private static Color PALE_YELLOW = new Color ( 1.0f 1.0f 0.0f 0.2f );
  
private static Color PALE_GREEN = new Color ( 0.0f 1.0f 0.0f 0.2f );
  
private static JLayer < JPanel > createLayer ()
   {
      LayerUI
< JPanel > layerUI;
      layerUI
= new LayerUI < JPanel > ()
      {
        
public void paint (Graphics g,JComponent c)
         {
            
// Paint the wallpaper.
            Graphics2D g2 = (Graphics2D) g;
            g2.setPaint (
new GradientPaint ( 0 0 ,PALE_YELLOW,
                                            
5 0 ,PALE_GREEN, true ));
            g2.fillRect (
0 0 ,c.getWidth (),c.getHeight ());
            
// Make sure that layer's panel view is not opaque.
            JLayer l = (JLayer) c;
            
if (l.getView ().isOpaque ())
                ((JPanel) l.getView ()).setOpaque (
false );
            
// Paint the view minus its background.
             super .paint (g,c);
         }
      };
      
// Create a user interface to be decorated.
      JPanel pnl = new JPanel ();
      JLabel lblName
= new JLabel ( " Name: " );
      pnl.add (lblName);
      
final JTextField txtName = new JTextField ( 20 );
      pnl.add (txtName);
      JButton btnReverse
= new JButton ( " Reverse " );
      pnl.add (btnReverse);
      ActionListener al;
      al
= new ActionListener ()
           {
              
public void actionPerformed (ActionEvent ae)
               {
                  String txt
= txtName.getText ();
                  txt
= new StringBuffer (txt).reverse ().toString ();
                  txtName.setText (txt);
               }
           };
      btnReverse.addActionListener (al);
      
// Create the layer for the panel using our custom layerUI.
       return new JLayer < JPanel > (pnl,layerUI);
   }
  
private static void createAndShowUI ()
   {
      JFrame frame
= new JFrame ( " Reverse Text " );
      frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
      frame.add (createLayer ());
      frame.pack ();
      frame.setLocationRelativeTo (
null );
      frame.setVisible (
true );
   }
  
public static void main (String [] args)
   {
      Runnable r
= new Runnable ()
                   {
                      
public void run ()
                       {
                          createAndShowUI ();
                       }
                   };
      EventQueue.invokeLater (r);
   }
}

  其中createLayer()方法是最重要的代码,它创建了一个匿名LayerUI子类的实例,绘制了墙纸和JPanel视图,创建了UI,在实例中包含了UI的面板容器。

  绘制操作是由LayerUI's public void paint(Graphics g,JComponent c)方法实现的,第二个参数引用了视图(被装饰的组件)中的JLayer实例,不是引用的视图。

  在视图后创建了渐变渲染墙纸后,调用paint()方法确保视图(没有嵌套面板的单一面板)是透明的,它将会隐藏墙纸,然后绘制视图。

  paint()对比paintLayer()

  JDK文档中除了提到paint()方法外,还提到了paintLayer()方法,我这里之所以选择paint()方法,是因为LayerUI中不存在paintLayer(),此外,文档还错误地引用了paintLayer() doesn't exist in LayerUI. Furthermore,the documentation incorrectly refers to addPropertyChange(),configureGraphics(),processComponentEvent(),processFocusEvent(),processHierarchyBoundsEvent(),processHierarchyEvent(),processKeyEvent(),processMouseEvent(),processMouseMotionEvent(),processMouseWheelEvent(),and repaintLayer()这些在LayerUI中根本不存在的方法,当然这些方法也可能在JDK 7最终发布时会包含进来。

  图1显示了有墙纸背景的UI。

JLayer自定义绘制

图 1 在用户界面后加了一层墙纸背景

  ReverseText程序演示了自定义绘制,避开了事件触发,不需要检测事件,因为程序只关心墙纸的绘制效果。相反,清单2显示了一个需要响应鼠标移动事件的程序代码。

  清单2. BrandedUI.java

// BrandedUI.java
import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayer;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.plaf.LayerUI;
public class BrandedUI
{
  
private static Color PALE_BLUE = new Color ( 0.0f , 0.0f , 1.0f , 0.3f );
  
private static Color PALE_RED = new Color ( 1.0f , 0.0f , 0.0f , 0.3f );
  
private static Font BRAND_FONT = new Font ( " Arial " , Font.BOLD, 18 );
  
private static String MSG = " My brand " ;
  
private static JLayer < JPanel > createLayer ()
   {
      LayerUI
< JPanel > layerUI;
      layerUI
= new LayerUI < JPanel > ()
      {
        
private Color color = PALE_BLUE;
        
public void installUI (JComponent c)
         {
            
super .installUI (c);
            ((JLayer) c).setLayerEventMask (AWTEvent.MOUSE_MOTION_EVENT_MASK);
         }
        
public void eventDispatched (AWTEvent e,
                                      JLayer
<? extends JPanel > l)
         {
            MouseEvent me
= (MouseEvent) e;
            Point pt
= SwingUtilities.convertPoint ((Component) me.getSource (),
                                                    me.getX (), me.getY (), l);
            
int cx = l.getWidth () / 2 ;
            
int cy = l.getHeight () / 2 ;
            
if (pt.x > cx - 45 && pt.x < cx + 45 && pt.y > cy - 10 && pt.y < cy + 10 )
                color
= PALE_RED;
            
else
                color
= PALE_BLUE;
            l.repaint ();
         }
        
public void paint (Graphics g, JComponent c)
         {
            
// Paint the view.
             super .paint (g, c);
            
// Paint the brand.
            g.setColor (color);
            g.setFont (BRAND_FONT);
            
int width = g.getFontMetrics ().stringWidth (MSG);
            
int height = g.getFontMetrics ().getHeight ();
            g.drawString (MSG, (c.getWidth ()
- width) / 2 ,
                          c.getHeight ()
/ 2 + height / 4 );
         }
        
public void uninstallUI (JComponent c)
         {
            
super .uninstallUI (c);
            ((JLayer) c).setLayerEventMask (
0 );
         }
      };
      
// Create a user interface to be decorated.
      JPanel pnlMain = new JPanel ();
      pnlMain.setLayout (
new GridLayout ( 2 , 1 ));
      JPanel pnlTemp
= new JPanel ();
      JLabel lblName
= new JLabel ( " Name: " );
      pnlTemp.add (lblName);
      JTextField txtName
= new JTextField ( 20 );
      pnlTemp.add (txtName);
      pnlMain.add (pnlTemp);
      pnlTemp
= new JPanel ();
      JLabel lblAddr
= new JLabel ( " Address: " );
      pnlTemp.add (lblAddr);
      JTextField txtAddr
= new JTextField ( 20 );
      pnlTemp.add (txtAddr);
      pnlMain.add (pnlTemp);
      
// Create the layer for the main panel using our custom layerUI.
       return new JLayer < JPanel > (pnlMain, layerUI);
   }
  
private static void createAndShowUI ()
   {
      JFrame frame
= new JFrame ( " Branded UI " );
      frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
      frame.add (createLayer ());
      frame.pack ();
      frame.setLocationRelativeTo (
null );
      frame.setVisible (
true );
   }
  
public static void main (String [] args)
   {
      Runnable r
= new Runnable ()
                   {
                      
public void run ()
                       {
                          createAndShowUI ();
                       }
                   };
      EventQueue.invokeLater (r);
   }
}

  上面的代码渲染UI上的文本印记,我们通常使用印记提醒用户使用的是试用软件,印记文本是半透明的,以便背景可以全部显示,我们不希望这个印记给用户造成太大的干扰。

  另一方面,我们希望用户能注意到这个印记,让他们下定决心购买这款软件,清单2中的代码通过改变印记的颜色(改成淡红色)来达到这个目的,当鼠标移到初始值是绿色的印记面板上时,颜色就变成淡红色。 


  事件检测

  JLayer和LayerUI结合起来可以检测视图任意区域上发生的事件(包括嵌套的子组件),这些类共同提供了4个方法来检测事件。

  · public void setLayerEventMask(long layerEventMask)

  调用这个JLayer方法时必须使用位掩码AWTEvent常量选择它检测到的事件类型,如:setLayerEventMask (AWTEvent.KEY_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK);可以检测到按键和焦点改变事件。

  · public void installUI(JComponent c)

  这个LayerUI方法通常放在setLayerEventMask()方法之前,这个方法类的代码首先调用超类方法(super.installUI (c);),然后是引用JLayer的JComponent参数,最后使用setLayerEventMask(): ((JLayer) c).setLayerEventMask(AWTEvent.KEY_EVENT_MASK);返回的结果。

  · public void uninstallUI(JComponent c)

  这个LayerUI方法放在没有参数的setLayerEventMask()方法后,这个方法内的代码首先调用超类方法(super.uninstallUI (c);),然后是引用JLayer的JComponent参数,最后使用setLayerEventMask(): ((JLayer) c).setLayerEventMask(0);返回的结果。

  · public void eventDispatched(AWTEvent e, Jlayer l)

  只要前面注册的事件发生了,就会调用这个LayerUI方法,在这个方法中插入的代码负责响应事件,并恰当地更新层,更新了不同的绘制属性(如颜色)后,通过传递给这个方法的JLayer参数调用repaint()方法重新绘制视图。

  在清单2中,LayerUI的installUI()方法调用setLayerEventMask(AWTEvent.MOUSE_MOTION_EVENT_MASK)检测鼠标移动事件,它又调用eventDispatched()方法返回结果。

  这个方法首先调用javax.swing.SwingUtilities类的convertPoint()方法确定鼠标移动事件相对于层的坐标位置。

  接下来这个方法通过检查它的坐标是否落在围绕UI中心的一个矩形区域内,检测鼠标指针是否移到印记文本上方,如果坐标刚好落在这个矩形区域内,印记文本的颜色就变为淡红色,除此以外,印记文本的颜色就恢复为蓝色。

  图2显示了鼠标移到印记文本上方前后的颜色变化。

事件检测

图 2 鼠标指针移到文本上方时,重新绘制文本颜色给用户一个不刺眼的提示

  小结

  JLayer对自定义绘制和事件检测的支持让你可以改进UI的各个组件,你可以将这个Swing组件和半透明及任意形状窗口特性结合起来使用,让你可以设计出更有趣的用户界面。下一篇文章将带来NIO.2的介绍,敬请期待!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值