Swing


天行健 君子以自强不息

posts - 69, comments - 215, trackbacks - 0, articles - 16
   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

Swing框架之Component

Posted on 2007-10-12 11:39 zht 阅读(3154) 评论(3)  编辑  收藏 所属分类: Swing
 昨天晚上写完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 Delegate的操作的地方,是低层次事件处理的地方,是高层事件发生的地方,是同父组件和子组件交互的地方。掌握的这些角度,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方法,实现按钮状态的重画
   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模型的全部图像。

=====================================================================================
昨晚回去后还是觉得Component对象本身说的太简单,想来想去,觉得内容实在是太多,有必要补充两个续文说明Component的其它概念。今天介绍Swing组件paint方法的处理流程,这个流程能使我们理解许多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);
}

         可以看出,背景透明机制在这儿实现。首先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){     
        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-order遮挡顺序,可以覆盖这个方法,从相反顺序调用组件的paint方法。

         到这儿我们对Swing的结构有了更深化的理解,UI Delegate机制也已经初露倪端。还有几个重要Swing Component概念或者机制没有讲,明天的续文再对它们做出说明。


=================================================================================

         Swing的事件处理过程为:事件调度线程(Event Dispatch Thread)从事件队列(EventQueue)中获取底层系统捕获的原生事件,如鼠标、键盘、焦点、PAINT事件等。接着调用该事件源组件的dispachEvent。该方法过滤出特殊事件后,调用processEvent进行处理。processEvent方法根据事件类型调用注册在这个组件上的相应事件处理器函数。事件处理器函数根据这些事件的特征,判断出用户的期望行为,然后根据期望行为改变组件的状态,然后根据需要刷新组件外观,触发带有特定语义的高级事件。此事件继续传播下去,直至调用应用程序注册在该组件上的处理器函数。下图是这个过程的示意图:

 

         上图所示意的过程简要说就是:

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的过失。

         说点题外话,所有采用这种事件模型的用户界面工具都会产生这种问题,包括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对象被执行返回才继续。在前面塞车的例子中,司机在路边修车解决了塞车问题,但是他突然想起来要家里办些事情,这时他就可以打个电话让家里开车来。假如修车不受这件事情的影响,比如叫家人送他朋友一本书,他可以继续修车,这时就相当于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的主要方法和它们的作用在下面的示意图:

          从上面示意图可以看出,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(){
        //查询数据库
        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模型中是事件触发源。前一篇文章在描述Swing的事件处理模型时就已经提到了这个事件处理过程。简单来说,Swing组件在侦听到原生事件并处理后,往往产生新的逻辑事件。逻辑事件是某些组件所特有的、具有特定语义的事件,比如JButton按下时产生ActionEvent、JComboBox一项被选中时产生ItemEvent,等等。和原生事件不同,它们并不被派发到系统事件队列中,而是由组件直接触发。事件处理器作为组件的观察者添加到组件上并侦听触发的事件。假设事件名叫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=new ArrayList<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){
       
...
       
if(应该触发action1)
           
fireAction1(new XXXEvent(this));
       
...
       
if(应该触发action2)
           
fireAction2(new XXXEvent(this));
       
...
    
}
}

         XXXEvent、XXXListener、addXXXListener、removeXXXListener以及各种fireAction函数多是重复性代码,有些Java IDE如JBuilder中能够根据开发者的指定参数的自动生成这些代码。

         实际上这个观察者模式的编程范式可以推广到任何JavaBeans,不一定是可视化的Swing组件。以前曾经见过JBuilder做的一个所谓数据库操作的JavaBeans,它没有界面,但它和Swing组件完全一样添加删除处理器。它的功能是异步操作数据库,在数据操作完了之后触发注册在上面的事件处理器,该事件处理器就可以将查询结果展现在表格中,或者输出成报表等等。

         在这个模型中,JavaBeans本身既可以是事件源(被观察对象),也可以是事件处理器(观察者),JavaBeans也可以侦听自身的事件并且处理。比如前面文章所提的MyButton在处理鼠标事件时就是自己侦听自己发出的鼠标事件,自己既是事件源,又是事件处理器,形成自反系统。各种各样的JavaBeans通过这种机制联系成一张事件网,各种JavaBeans就是这个网上的节点,而它们之间的事件触发与事件处理关系就是这张网络上的线。当某个节点被外界或自身发出的事件所触发时,行成了事件的传播。这个过程很像网络上节点的振动引起周围周围节点振动的模型。下图示意了这种JavaBeans之间的事件网:

         例如new JscrollPane(new JtextArea())这个系统,它里面包括两个JScrollBar和一个JTextArea,当鼠标拖动事件触发JScrollBar时,JScrollBar处理了这个鼠标拖动事件,并发出滚动条拖动事件,这个事件传播给JTextArea,JTextArea处理这个拖动事件,相应的更新自己显示的内容,如果JTextArea之后又根据更新发出了一个新的事件,这个事件便会继续传播下去。

Swing布局管理器

         现在高级图形用户界面工具一般都包括布局管理器机制。什么叫做布局管理器?如果所有窗口的大小是不变的,那么我们在往窗口中添加组件时,只要将组件的拖放到固定位置、调整好尺寸就可以了,就像VB的界面工具一样。可大多数情况并非如此,用户经常需要调整窗口的大小,以便和其他程序协同工作。这种情况下,在传统界面工具中,比如VB,就需要显式的侦听窗口尺寸调整事件,根据当前窗口的大小重新计算并调整各个组件的大小和位置。AWT/SWT/Swing将这个过程自动化、模块化了,抽象出一个布局管理器来负责管理界面组件的布局。

         它们实现原理是相似的:容器类组件侦听初始化、invalide/validate以及容器尺寸调整等事件,一旦发生这些事件,容器类组件检查自己是否配置了布局管理器,如果没有,则不做任何事情;如果有,则将容器内组件的布局代理给布局管理器,让它来完成容器内组件的重新布局。

         容器管理器对象对实现两类接口:LayoutManager和LayoutManager2,LayoutManager2是LayoutManager的一个扩展,允许组件在添加时指定位置参数。它们的定义和含义如下:

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用作指定如何以及位置的参数,这个函数主要是弥补LayoutManager版的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中都分别提供大量的布局管理器,这些布局管理器有简单的如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。