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. 由addOneMember引发的思考

    addOneMember是一个方法,这个方法在两处地方重复了. 所以在修改页面的时候,发现修改了一处,如果是新手,肯定不会注意到另外一处有问题,他如果没有看清楚这个类到底整体怎样,那么他会犯的错误是就 ...

  2. Oracle 中的游标(用Javase中Iterator 类比之)

    当使用 pl/sql 查询 Oracle 数据库时,有时我们想输出多条记录的数据.:select * from scott.emp; 这时,我们一定会想利用循环来输出.但是,在pl/sql 中是没有数 ...

  3. jsp页面中创建方法

    在JSP页面中是用 <%! void function(){ } %> 这种方式可以定义函数. 如果只使用 <% //todo %> 代码块中的代码在编译时将会都被加到 sev ...

  4. 再探CSS 中 class 命名规范

    一直以来我的CSS 的 class命名都是比较随意,有时采用驼峰式.有时采用下划线,好像没有什么统一的标准,想到什么英文单词就拿过来用,这对于自己瞎写的小项目无伤大雅,遇到冲突的问题可稍加调整改变即可 ...

  5. Codeforces Round #235 (Div. 2) A. Vanya and Cards

    #include <iostream> using namespace std; int main(){ int n,x; cin >> n >> x; ; ; i ...

  6. lsof用法简介

    lsof:一个功能强大的命令 lsof命令的原始功能是列出打开的文件的进程,但LINUX下,所有的设备都是以文件的行式存在的,所以,lsof的功能很强大!  [root@limt01 ~]# lsof ...

  7. 僵尸进程的产生和避免,如何kill杀掉linux系统中的僵尸defunct进程

    在 Unix系统管理中,当用ps命令观察进程的执行状态时,经常看到某些进程的状态栏为defunct,这就是所谓的"僵尸"进程."僵尸"进程是一个早已 死亡的进程 ...

  8. linux文本操作界面 vi面板如何复制一行

    linux文本操作界面 vi面板如何复制一行 1)把光标移动到要复制的行上2)按yy3)把光标移动到要复制的位置4)按p 在vi里如何复制一行中间的几个字符?如果你要从光标处开始复制 4 个字符,则先 ...

  9. jquery插件之文字无缝向上滚动

    该插件乃本博客作者所写,目的在于提升作者的js能力,也给一些js菜鸟在使用插件时提供一些便利,老鸟就悠然地飞过吧. 此插件旨在实现目前较为流行的无缝向上滚动特效,当鼠标移动到文字上时,向上滚动会停止, ...

  10. Week1 学长的经验教训

    我手头拿到的是上一届学长的软件工程大作业,作业的名称是——汽车4S店信息管理系统. 这个大作业我认为还是非常典型的传统模式的大作业,由手机端(客户端)和服务端组成,非常的传统.             ...