talk is cheap , show me the code.

Swing中的事件

事件驱动

所有的GUI程序都是事件驱动的。Swing当然也是。

GUI程序不同于Command Line程序,一个很大的区别是程序执行的驱动条件:命令行程序是接受用户输入的文本参数,对命令解析,然后通过类似switch的选择来执行不同的功能模块。而GUI程 序就不一样了。GUI程序由界面元素组成,如Button,CheckBox,TextArea,等等。用户操作不同的组件,就会引发不同的事件,然后, 程序编写时注册到UI组件上的事件处理程序得到调用,以此来和用户交互。

        

事件Event

事件有点类似于异常:事件是事件类的对象,它携带了事件相关的信息,异常是异常类的对象,他携带了异常信息。无论是异常,还是事件

发生时,我们的程序都要事先写好相应的代码应对并处理。只不过,对于程序员来说,事件是正派的,而异常则是反派,谁也不希望自己的程序出现异常。

java中,所有的事件类都是EventObject类的子类,所有的事件都有一个成员字段:source用来保存事件源,即引发事件的对象。

public class EventObject implements java.io.Serializable {

    private static final long serialVersionUID = 5516075349620653480L;

    /* source保存 引发事件的对象的引用*/

    protected transient Object  source;

    public EventObject(Object source) {
    if (source == null)
        throw new IllegalArgumentException("null source");

        this.source = source;
    }

    public Object getSource() {
        return source;
    }
    public String toString() {
        return getClass().getName() + "[source=" + source + "]";
    }
}

Swing的事件机制由AWT提供,下面是Swing中常用的高级事件 ActionEvnet类的部分代码。还有其他事件。

public class ActionEvent extends AWTEvent {

    public ActionEvent(Object source, int id, String command, long when,
                       int modifiers) {
        super(source, id);
        this.actionCommand = command;
        this.when = when;
        this.modifiers = modifiers;
    }
    //.......
}

事件队列Event Queue

Swing应用程序中的所有事件都会由 java.awt.EventQueue对象来管理。

1、EventQueue中的事件会按序一 一 分发,一个时间点只能处理1个事件。

2、如果时间A先入队,B后入队,则A一定会在B的前面被处理。

3、队列中的事件会在一个单独的线程中被处理,这个线程就是EDT线程。

由于Swing是单线程的,线程不安全的,所以Swing中的UI操作就必须以事件的形式放置到EventQueue中按序分发处理。这就保证了Swing组件状态的可预知性和正确性。

Swing的正确使用姿势是:所有与GUI相关的操作都必 以事件的形式发布到Event Queue中,并在EDT中处置。

事件源EventSource

异常,有引发异常的原因,事件,也有引发事件的对象,这就是事件源。谁引发了事件,谁就是事件源。

比如,Button被点击时引发事件,Button就是事件源,JFrame 状态变化时,JFrame也是事件源。Swing中所有的组件,都有感知自己被操作的能力。

Swing中,事件源一般是一些用户组件,他们能感知用户的操作,并引发相应的事件,最后通知对自己注册的监听器。

事件源都会提供事件的注册接口,所有对某个组件的某个事件感兴趣的其他代码,都可以提前注册到这个组件上,事件发生时,此组件就会调用相应的注册的

事件处理程序。

下面是JButton的父类 AbstractButton的一个方法。

protected void fireActionPerformed(ActionEvent event)
{
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        ActionEvent e = null;
        // Process the listeners last to first, notifying
        // those that are interested in this event
        for (int i = listeners.length-2; i>=0; i-=2)
        {
            if (listeners[i]==ActionListener.class)
            {
                // Lazily create the event:
                if (e == null) {
                      String actionCommand = event.getActionCommand();
                      if(actionCommand == null)
                      {
                         actionCommand = getActionCommand();
                      }
                      e = new ActionEvent(AbstractButton.this,
                                          ActionEvent.ACTION_PERFORMED,
                                          actionCommand,
                                          event.getWhen(),
                                          event.getModifiers());
                }
                ((ActionListener)listeners[i+1]).actionPerformed(e);
            }
        }
}

监听者Listener

监听者(有的也叫侦听器):实现了某个监听接口的类对象。某个类实现了一个监听器接口,它就是一个监听者。

当事件发生时,并不是事件源处理事件,而是注册在事件源的上的监听器去处理。事件源只是通知监听器,通知实质是调用所有监听器对象按接口约定实现的的接口方法。

我们知道,对象实现了某个接口,就代表这个对象能做什么。同理,一个对象想成为监听器,它就必须实现相应的监听器接口,表明他有处理某个事件的能力。

监听器实现了监听接口,就必然要实现接口中定义的方法,用来应对事件。

所有的监听器接口都必须扩展自EventListener,它是一个空接口。一个事件往往对应一个监听者接口。

JComponnet类是所有Swing组件的父类。JComponnet 类中有一个 EventListenerList成员,它是一个表,用来存储所有注册的监听者。那也就是说,所有的Swing组件内部都包含一个存储监听者的列表,这也是为什么能向Swing组件中注册监听器的本质。

public abstract class JComponent extends Container implements Serializable,TransferHandler.HasGetTransferHandler{
    /** A list of event listeners for this component. */
    protected EventListenerList listenerList = new EventListenerList();
   //.......
}    
/**   EventListenerList类
   这是一个用于保存监听器的一个表类型。这个表可以存储任何类型的EventListener,因为内部是用的一个Object数组存储的。
 */
public class EventListenerList implements Serializable
{
    protected transient Object[] listenerList = NULL_ARRAY;
    //获取所有监听者的数组
    public Object[] getListenerList()
    {
        return listenerList;
    }
     /**
     * 返回监听者的数量*/
    public int getListenerCount()
    {
        return listenerList.length/2;
    }

    /**
       向监听者列表中添加 “一对” 新的监听者。其实是添加一个监听者,
       只不过对于一个监听者需要保存2项:监听者的类  t,和监听者本身 l
     */
    public synchronized <T extends EventListener> void add(Class<T> t, T l)
    {
        if (l==null) {return;}

        if (!t.isInstance(l))
        {   throw new IllegalArgumentException("Listener " + l +" is not of type " + t);  }

        if (listenerList == NULL_ARRAY)
        {
            //如果是第一次添加监听者,则 new 一个Object 数组。
            listenerList = new Object[] { t, l };
        }
        else
        {

            int i = listenerList.length;
            Object[] tmp = new Object[i+2];
            System.arraycopy(listenerList, 0, tmp, 0, i);

            tmp[i] = t;
            tmp[i+1] = l;

            listenerList = tmp;
        }
    }

}

这个时候你再回去看事件源分块中的那段代码,是不是思路清晰许多了呢?

所以,事件源通知监听者,实质是遍历内部的监听者表,将自己作为EventSorece,构造一个事件对象,并调用所有监听者的事件处理程序时,将构造的事件对象传递过去。

如果你还是有点迷糊,下面通过一例子说明下。

下面是一个简单的Swing程序。

监听者:ButtonClickListener 类对象,它实现了监听器接口。一般我们会使用匿名内部类完成监听者的实例化,这里写出成员内部类是为了更清晰。当使用addActionListener方法注册后,ButtonClickListener对象就被存储在Button对象内部的一个EventListenerList列表中了。

事件   :点击Button时生成。

事件源:被点击的Button对象。

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class SwingDrive {

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new TestFrame("测试");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setVisible(true);

            }
        });
    }

}

class TestFrame extends JFrame
{

    private static final int FRAME_WIDTH = 530;
    private static final int FRAME_HEIGHT = 360;

    /*************View******************/
    private JPanel mainPanel = null;
    private JButton msgButton = null;
    private JLabel msgLabel = null;

    public TestFrame(String title)
    {
        super(title);
        initUI();
    }

    private void initUI()
    {
        //内容面板
        mainPanel = new JPanel();
        mainPanel.setPreferredSize(new Dimension(FRAME_WIDTH,FRAME_HEIGHT));
        this.setContentPane(mainPanel);

        //按钮
        msgButton = new JButton("我是按钮");
        //监听者表示对按钮的点击事件感兴趣,于是注册到按钮上。
        msgButton.addActionListener(new ButtonClickListener());

        //msg显示文本
        msgLabel = new JLabel();

        //将组建添加到窗体的内容面板中
        this.add(msgButton);
        this.add(msgLabel);
        this.pack();

    }

    /*监听者,实现了监听接口*/
    private class ButtonClickListener implements ActionListener
    {

        @Override
        public void actionPerformed(ActionEvent e) {

            msgLabel.setText("你点击了按钮");
        }
    }
}

还有一点疑问

who invoke the fireActionPerformed(ActionEvent event)   method?

谁调用了JButton的fireActionPerformed方法呢?

如果你能想到这个问题,说明你已经开始深入了。这是Swing本身的机制,确切说是AWT提供的机制。一个Swing程序中会有一个toolkit线程不断运行着,它监视用户对组件的操作,当组件被点击,获取焦点,被最大化,状态改变等,都会被toolkit线程发现,并将fireXXX发送带EDT中执行,fireXXX的执行,又会导致所有监听器的执行。

先不急,这涉及到Swing线程的知识,请往下看。

Swing中的线程

1、主线程,main方法,程序执行的入口。任何程序都必须有的。

2、初始化线程。创建和初始化图形界面。

3、tookit线程:负责捕捉系统事件,如鼠标,键盘等。负责感知组件的操作,并将事件发通知EDT。

4、EDT线程:处理Swing中的各种事件。UI绘制,UI的修改操作,UI的绘制渲染.,监听者的事件处理函数,等。所有的UI操作都必须在EDT线程中执行,不允许在其他线程中。

5、N个后台工作线程:处理耗时任务,如网络资源下载,可能阻塞的IO操作。

初始化线程

public static void main(String [] args)
{

    SwingUtilities.invokeLater(new Runnable{

        public void run()
        {

            //初始化线程逻辑代码在这里执行
        }

    });

}

 Swing多线程的执行

图画完后,我才发现图画的有一问题:其中EDT线程和toolkit线程是循环线程,并没有确切的执行终点,也就是不知道这2个线程什么时候执行任务到100%。只要Swing程序没有结束,他们就一直工作,因为用户可能在任何时候执行UI操作。

后台工作线程当执行完任务后就结束了。

一、不要在EDT线程中执行耗时的任务。

一旦EDT线程被阻塞,UI组件就不能及时渲染,更新,使得整个程序失去对用户的响应。用户体验十分糟糕。

Swing本身是设计为单线程操作的,并非线程安全的.这就意味着:所有的UI操作都会必须在EDT线程中进行。内置的组件都是遵守这个约定的,比如一个JButton被按下时,它需要显示为按下的状态,那么,这个渲染为按下的状态,就会以事件的形式发布到EDT线程中去执行。同样,按钮弹起时,需要渲染为普通状态,也会引发事件,并在EDT中处理。

不要让EDT干 "体力活"。很明显,Swing中组件UI的更新,都会形成事件置于事件队列,并等待EDT派发,也就是UI更新依赖EDT线程完成。如果你的事件处理程序太耗时了,那么,UI就很久得不到及时更新,造成界面假死现象。

下面这个程序中,用户点击下载按钮后,真个界面都失去了响应,按钮久久不能弹起,窗口也失去了响应,体验很糟糕。

class BadFrame extends JFrame
{

    public BadFrame()
    {
        super();
        initUI();
    }

    private JButton downloadButton ;
    private JPanel  mainPane ;

    private void initUI()
    {
        mainPane =  new JPanel();
        mainPane.setPreferredSize(new Dimension(430,250));
        this.setContentPane(mainPane);

        downloadButton = new JButton("下载");
        this.getContentPane().add(downloadButton);
        downloadButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                downloadMovie();

            }
        });

        this.pack();

    }

    //模拟下载任务
    private void downloadMovie()
    {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

}

二、不要在非EDT线程中访问UI,操作UI组件。

Swing组件都不是线程安全的,只有把他们的操作限制在一个线程中,才能保证所有的UI的操作都符合预期。这个线程就是EDT线程。那么,怎样将UI操作发送到EDT中执行呢?

通过以下之一。

EDT线程维护一个事件队列EventQueue , 所有的UI渲染,UI事件都打包为事件放到EventQueue中排队处理。这样就让Swing在并发编程安全运行,因为单一线是顺序执行的,不会有线程安全性的问题。

 SwingUtilities.invokeLater(new Runnable() {

             @Override
             public void run() {

             }
         });9          
 SwingUtilities.invokeAndWait(new Runnable() {

             @Override
             public void run() {
                 // TODO Auto-generated method stub

             }
         });9

他们有什么区别?

SwingUtilities.invokeLater调用后立即返回。然后执行第9行后的代码。其他线程和 invokeLater中的参数线程异步执行。互不阻塞。

SwingUtilities.invokeAndWait调用后,必须等到 线程对象 run方法在EDT中执行完了,才返回,然后继续执行第9行后的代码。

下面是一个简单的例子:用户输入2个整数 start ,end,程序计算从start 累加到end 的结果。我依然使用了线程睡眠来模拟耗时任务。因为如果我使用更加贴近现实的例子的话,又会引出更多的知识点。

虽然简单,但说明了如何让Swing更好的工作。

import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class Demo {

    public static void main(String[] args)
    {

        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                MFrame frame = new MFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setVisible(true);

            }
        });

    }

}

class MFrame extends JFrame
{

    public MFrame()
    {
        initUI();
        onButtonClick();
    }

    /***************Model*************/
    private int start  = 0;
    private int end    = 0;
    private int result = 0;

    /************View***************/
    private JButton     calcButton  = null;
    private JTextField  startField  = null;
    private JTextField  endField    = null;
    private JTextField  resultField = null;
    private JPanel      mainpane    = null;

    private void initUI()
    {
        calcButton =new JButton("计算");

        startField = new JTextField();
        startField.setColumns(5);

        endField = new JTextField();
        endField.setColumns(5);

        resultField = new JTextField();
        resultField.setColumns(5);
        resultField.setEditable(false);

        mainpane = new JPanel(new GridLayout(1, 4,5,0));
        mainpane.setPreferredSize(new Dimension(300,50));

        mainpane.add(startField);
        mainpane.add(endField);
        mainpane.add(resultField);
        mainpane.add(calcButton);

        this.setContentPane(mainpane);
        this.setLocationRelativeTo(null);
        this.pack();

    }

    //为button注册监听者
    private void onButtonClick()
    {

        calcButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent event) {

                Thread calcThread = new Thread(new Runnable() {

                    @Override
                    public void run()
                    {

                        try{
                            start = Integer.parseInt(startField.getText());
                            end   = Integer.parseInt(endField.getText());

                            for (int i = start; i <=end; i++)
                            {
                                result += i;    //假设计算过程十分耗时,就像挖矿一样。

                                Thread.sleep(500);
                            }

                            //耗时任务完成后了,通过SwingUtilities.invokeLater将设置任务到UI的事件发送到EDT线程中。
                            SwingUtilities.invokeLater(new Runnable()
                            {
                                @Override
                                public void run() {
                                    resultField.setText(result+"");
                                }
                            });

                        }

                        catch(NumberFormatException e)
                        {
                            JOptionPane.showMessageDialog(MFrame.this, "请输入一个合法的整数", "错误", JOptionPane.ERROR_MESSAGE);
                        }
                        catch (InterruptedException e) {
                            System.out.println("计算时错误");
                        }

                    }
                });  //wrok Thread new End

                calcThread.start();  //启用任务线程

            }
        });

    }

}

更优雅的解决办法:SwingWorker线程类

当Swing程序复杂后,自定义线程会让代码越来越庞大,不好理解。于是jdk1.6中引入了SwingWorker线程类,简化了程序员的工作。今天就写到这里,我会在以后的文章中介绍。:)

【Swing】理解Swing中的事件与线程的更多相关文章

  1. 怎么理解js中的事件委托

    怎么理解js中的事件委托 时间 2015-01-15 00:59:59  SegmentFault 原文  http://segmentfault.com/blog/sunchengli/119000 ...

  2. 理解JavaScript中的事件轮询

    原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 为什么JavaScript是单线程 JavaScript语言的一大特点就是单线程,也 ...

  3. 学习和理解C#中的事件

    注:本文系学习笔记. 上一篇文章记录了我对C#中委托的理解.委托实际上是一种类型.可以将一个或多个方法绑定到委托上面,调用委托时,一次执行委托上面绑定的方法.本文要讲述的事件实际上和委托有很深的“感情 ...

  4. 深入理解javascript中的事件循环event-loop

    前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...

  5. 理解Javascript中的事件绑定与事件委托

    最近在深入实践js中,遇到了一些问题,比如我需要为动态创建的DOM元素绑定事件,那么普通的事件绑定就不行了,于是通过上网查资料了解到事件委托,因此想总结一下js中的事件绑定与事件委托. 事件绑定   ...

  6. 一个demo让你彻底理解Android中触摸事件的分发

    注:本文涉及的demo的地址:https://github.com/absfree/TouchDispatch 1. 触摸动作及事件序列 (1)触摸事件的动作 触摸动作一共有三种:ACTION_DOW ...

  7. 再次理解javascript中的事件

    一.事件流的概念 + 事件流描述的是从页面中接收事件的顺序. 二.事件捕获和事件冒泡 +    事件冒泡接收事件的顺序:

  8. 理解DOM中的事件流

    浏览器发展到第四代时(IE4和Netscape Communicator 4),浏览器团队遇到一个很有意思的问题:页面的哪一部分会拥有特定的事件?想象下在一张纸上有一组同心圆,如果你把手指放在圆心上, ...

  9. 理解JavaScript中的事件流

    原文地址:http://my.oschina.net/sevenhdu/blog/332014 目录[-] 事件冒泡 事件捕获 DOM事件流 当浏览器发展到第四代时(IE4和Netscape Comm ...

随机推荐

  1. jQuery入门第二天

    3种选择器:元素选择器:$("button").class选择器:$(".btn").id选择器:$("#target1"). <sc ...

  2. UVa12298 Super Poker II(母函数 + FFT)

    题目 Source http://acm.hust.edu.cn/vjudge/problem/23590 Description I have a set of super poker cards, ...

  3. How to retrieve instance parameters from an uninstantiated (uninserted) family

    The trick to be able to read the default values for instance parameters is to get to the FamilyManag ...

  4. Handling events in an MVVM WPF application

      Posted: June 30, 2013 | Filed under: MVVM, WPF, XAML |1 Comment In a WPF application that uses the ...

  5. hive :MetaException(message:Version information not found in metastore. )

    MetaException(message:Version information not found in metastore. ) Hive now records the schema vers ...

  6. js/jQuery判断浏览器名称、内核版本、浏览器壳

    1.js方法 /* 判断浏览器名称和版本 目前只能判断:ie/firefox/chrome/opera/safari 2012年5月16日23:47:08 浏览器内核UA:UA; 浏览器内核名称:NV ...

  7. Codeforce - Runtime Error

    Bahosain was trying to solve this simple problem, but he got a Runtime Error on one of the test case ...

  8. HDU-3549 最大流模板题

    1.HDU-3549   Flow Problem 2.链接:http://acm.hdu.edu.cn/showproblem.php?pid=3549 3.总结:模板题,参考了 http://ww ...

  9. Cortex-M0(NXP LPC11C14)启动代码分析

    作者:刘老师,华清远见嵌入式学院讲师. 启动代码的一般作用 1.堆和栈的初始化: 2.向量表定义: 3.地址重映射及中断向量表的转移: 4.初始化有特殊要求的断口: 5.处理器模式: 6.进入C应用程 ...

  10. GO语言练习:第一个Go语言工程--排序

    1.代码 2.编译 3.运行 1.代码框架 /home/fengbo/sorter $ tree . ├── bin ├── pkg ├── readme.txt └── src ├── algori ...