转载:http://blog.csdn.net/sunyiz/article/details/8004573
关于 Java 的 GUI ,关于 Swing,其实有一件事情是非常重要的
那就是线程! 如何正确的使用 GUI 线程, 什么样的代码应该在 GUI 线程上执行? 什么样的代码应该用其他线程执行? 其实这些都很重要, 但是,很多关于 Java 的教材都没有强调过这些,甚至有的书完全就忽略了这一点 所以,我在这里要给所以开始接触 Swing 的人,讲述一个重要概念: Swing 中的线程 --------------------------------------------------------------------------- 要说明这个问题,我们要先来看一个贴子: 这是我昨天发的贴子,里面已经简单叙述了 GUI 中的各个线程 现在,我们先来回顾一下 EDT----这个至关重要的线程 这个 EDT 是干什么的呢?它负责指派所有的 GUI 事件 比如键盘按钮按下后,派发给对应控件的监听器 比如鼠标点击后,派发给对应控件的监听器 比如:绘制控件 所以,我们一般又喜欢把 EDT 叫做 GUI 线程。 EDT 是一种排队的模式,就是各种事件会在其中排队等待,依次执行 其实所有的绘制,在 Swing 内部处理时,全都包装成了 Paint 事件然后进入 EDT 排队 而且 EDT 还会智能的合并多个连续的 Paint 事件,把它们包装成一个 Paint 事件 下面阐述一个重要的规范:Swing 单线程规范 “所有的界面相关更新,都应当在 EDT 中执行” 这个规范非常重要,在你编写 Swing 程序的过程中,请一直牢记他 --------------------------------------------------------------------------- 现在,让我们先来设定一个小小的目标,然后我们去实现它,从中探讨和发现问题 就做一个按时间变化的进度条吧,很多新手在刚开始时,都对这个功能的实现表示纠结 下面,我们先来写一段代码:- 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 程序,直到永远