Swing如何正确的处理界面中的线程(EDT)

当你运行一个 Swing 的程序时,会自动创建三个线程。
  第一个就是主线程,负责执行你的 main 方法.


  第二个叫 toolkit 线程,负责捕捉系统事件,比如键盘、鼠标移动等,虽然这个 toolkit 非常重要,但是你不会有任何代码在这个线程上执行,这个线程的作用是,把这些事件传递给第三个线程:EDT


那 EDT 是什么呢?


  EDT 叫做事件指派线程(Event Dispatcher Thread)
这个线程十分重要,他负责接收来自 toolkit 线程的事件, 然后派发给对应的控件的监听器,然后对应控件的监听器中对应方法的代码就会执行了, 注意,Swing 有一个单线程规范,即:
所有的界面相关的更新,都应该在 EDT 上执行, 而耗时的后台运行,不应该在 EDT 上执行。




 
   这个 EDT 是干什么的呢?它负责指派所有的 GUI 事件, 比如键盘按钮按下后,派发给对应控件的监听器, 比如鼠标点击后,派发给对应控件的监听器, 比如:绘制控件。 所以,我们一般又喜欢把 EDT 叫做 GUI 线程。


EDT 是一种排队的模式,就是各种事件会在其中排队等待,依次执行其实所有的绘制,在 Swing 内部处理时,全都包装成了 Paint 事件然后进入 EDT 排队, 而且 EDT 还会智能的合并多个连续的 Paint 事件,把它们包装成一个 Paint 事件。


下面阐述一个重要的规范:Swing 单线程规范: “所有的界面相关更新,都应当在 EDT 中执行”
这个规范非常重要,在你编写 Swing 程序的过程中,请一直牢记他。


  现在,让我们先来设定一个小小的目标,然后我们去实现它,从中探讨和发现问题
就做一个按时间变化的进度条吧,很多新手在刚开始时,都对这个功能的实现表示纠结。
下面,我们先来写一段代码:


import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class ProgressFrame extends JFrame implements ActionListener {  
  
    private JButton btn = new JButton("Start");  
    private JProgressBar bar = new JProgressBar(){  
        public void paint(Graphics g) {  
            super.paint(g);  
            System.out.println("paint");  
        }  
    };  
  
    public ProgressFrame() {  
        init();  
    }  
  
    private void init() {  
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  
        setSize(300,200);  
        setLocationRelativeTo(null);  
        setLayout(new FlowLayout());  
        add(btn);  
        add(bar);  
        btn.addActionListener(this);  
        setVisible(true);  
    }  
  
    @Override  
    public void actionPerformed(ActionEvent e) {  
        for (int i = 0; i <= 10; i++) {  
            bar.setValue(i * 10);  
        }  
    }  
      
    public static void main(String[] args) {  
        new ProgressFrame();  
    }  
  
}  
我们的想法很简单,就是在点下按钮后,让进度条设值 11 次,达到动画效果, 但是你运行后,点下按钮时,发现几乎是瞬间,进度条就满了, “paint”也只打印了一次.


是不是没有延时太快了? 那让我们对 actionPerformed 方法做一点小小的修改:


@Override  
public void actionPerformed(ActionEvent e) {  
    for (int i = 0; i <= 10; i++) {  
        try {  
            Thread.sleep(200);  
        } catch (InterruptedException e1) {  
            e1.printStackTrace();  
        }  
        bar.setValue(i * 10);  
    }  
}  
你会发现,按下按钮后,界面如死机般僵硬 2 秒之后,依然是进度条直接满, “paint”也只打印了一次.


这是什么原因呢?
下面我们要再说一个在编写 Swing 程序中,应该遵守的规则:


“不要在 EDT 中执行耗时代码,耗时工作应当有一个单独的线程去处理”
  因为如果你让耗时代码占用了 EDT,那 EDT 中的那些绘制啊什么的任务都将没空执行, 这些事件被压到 EDT 的最后去排队,然后又被 EDT 合并成了一个 Paint 事件, 从而结果就是:Paint 只在最后不再 sleep 之后,执行了一次


   也许有人会很奇怪,这个 actionPerformed 怎么就在 EDT 中执行了呢? 还记得我们说过的 EDT 的作用么?
它会接受 toolkit 线程传递来的系统事件, 然后传递给对应控件的对应监听器,执行对应的方法。
   所以其实,这个 actionPerformed ,是由 EDT 调用执行的, 其实 Swing 控件的大部分监听器的各种方法,都是在 EDT 上执行的, 所以,我们要避免在这些监听器的方法中执行耗时操作,否则界面就会卡死!
   那我们要如何修改才能实现这样的效果呢? 我们需要让这个耗时的工作,在 EDT 之外的线程执行才行:
再次修改 actionPerformed 方法:
@Override  
 public void actionPerformed(ActionEvent e) {  
     Runnable runnable = new Runnable() {  
           
         @Override  
         public void run() {  
             for (int i = 0; i <= 10; i++) {  
                 try {  
                     Thread.sleep(200);  
                 } catch (InterruptedException e1) {  
                     e1.printStackTrace();  
                 }  
                 bar.setValue(i * 10);  
             }  
         }  
     };  
     new Thread(runnable).start();  
 }  
   我们用了一个单独的线程来执行这段代码。现在让我们再次执行,终于,我们看到了我们期望中的效果,进度条慢慢增加
“paint”也打印了十一次,那我们的程序是否就 OK 了呢?


   答案是否定的,因为我们在这样实现的同时,又破坏了 Swing 的单线程规范, 没有在 EDT 中执行界面更新操作,
bar.setValue(i * 10); 这句话,应该在 EDT 中执行。 那这里不就有个矛盾了么?在 EDT 中执行也不行,
不在 EDT 中执行也不行……


   其实我们要的只是 bar.setValue(i * 10); 这一句话在 EDT 中执行而已, 而 Swing 提供了一个强大的工具类:SwingUtilities
它提供了好几个功能强大的方法,这里,我们需要的是:invokeLater 这个方法,
这个方法的作用是:把一个任务,排队到 EDT 的最后,等待执行, 我们现在再次修改 actionPerformed 方法:


@Override  
public void actionPerformed(ActionEvent e) {  
    Runnable runnable = new Runnable() {  
          
        int i = 0;  
          
        @Override  
        public void run() {  
            for (i = 0; i <= 10; i++) {  
                try {  
                    Thread.sleep(200);  
                } catch (InterruptedException e1) {  
                    e1.printStackTrace();  
                }  
                SwingUtilities.invokeLater(new Runnable() {  
                      
                    @Override  
                    public void run() {  
                        bar.setValue(i * 10);  
                    }  
                });  
                  
            }  
        }  
    };  
    new Thread(runnable).start();  
}  
再次执行,效果和上一次一样,也满足了规范,皆大欢喜。 其实 Swing 为了处理这种类似的问题,
专门提供了一个功能强大的类:SwingWorker 
关于这个 SwingWorker 我会在以后找时间进行详细的解释,今天先说这么多吧。 当然,其实我们的程序还有一个小小的瑕疵:
public static void main(String[] args) {  
    new ProgressFrame();  
}  
这里,new ProgressFrame(); 的过程,其实也包含了大量的界面刷新等等 我们不应该让这样的代码在主线程中执行,应该把它放到 EDT 中去: 这样修改一下:
public static void main(String[] args) {  
    SwingUtilities.invokeLater(new Runnable() {  
          
        @Override  
        public void run() {  
            new ProgressFrame();  
        }  
    });      
}  
  这就是关于 Swing 的最重要的第一课:两个规范 “所有的界面相关更新,都应当在 EDT 中执行” “不要在 EDT 中执行耗时代码,耗时工作应当有一个单独的线程去处理” 这两条规范,将会伴随你的 Swing 程序,直到永远 
posted on 2013-04-16 20:27  道法自然smile  阅读(448)  评论(0编辑  收藏  举报