事件处理详解

我们知道大多数图形界面程序是事件驱动的(游戏除外),因此事件处理的实现是非常重要的。Java中,Swing底层依赖于AWT的事件处理机制。

EventListener 事件监听器

Swing中,事件源就是发出事件的对象,例如按钮,点击按钮就会发出按钮点击事件。事件监听器则需要注册到事件源,事件监听器通常是一个回调函数,告知当此事件发生时,程序该怎么做。

我们简单看一下ActionListener这个接口:

public interface ActionListener extends EventListener {
    public void actionPerformed(ActionEvent e);
}

我想经验丰富的同学一看这个就知道监听器怎么写了,其实actionPerformed就是一个回调函数,事件发生时就会被回调,传入一个事件对象,我们根据这个事件对象作出相应判断即可。我们可以实现这个接口实现自己的监听器类,或者直接使用匿名类。

除了ActionListener,Swing中还有许多其它的监听器实现GUI界面的各种功能,具体可以参考文档。

EventObject 事件对象

事件被封装在事件对象中,事件对象有很多,如ActionEventWindowEvent等,事件对象又可以划分成不同的继承层次,所有事件派生自java.util.EventObject

JButton jButton = new JButton("hello");
jButton.addActionListener((e)->{
  System.out.println("button clicked!");
});

我们可以使用lambda表达式简化了监听器的注册。实际上使用匿名类的方式绑定事件的写法非常简单,且十分常用。

事件对象还有一些其他的实用方法:

  • Object getSource() 返回发生事件的对象的引用。
  • String getActionCommand() 返回这个事件的动作字符串。如果事件来自按钮,默认动作字符串是按钮标签,除非使用setActionCommand()对按钮对象的动作字符串进行修改。

Action 动作

有时候多个事件源(控件)需要绑定同一种命令(操作),多个控件绑定同一个监听器可以实现,但是Swing再次封装了一层,提供了Action接口用来封装命令,并把它连接到多个事件源。具体使用Action的好处,请往下看。

我们大致看下Action接口有什么内容。

public interface Action extends ActionListener {
    public static final String DEFAULT = "Default";
    public static final String NAME = "Name";
    public static final String SHORT_DESCRIPTION = "ShortDescription";
    public static final String LONG_DESCRIPTION = "LongDescription";
    public static final String SMALL_ICON = "SmallIcon";
    public static final String ACTION_COMMAND_KEY = "ActionCommandKey";
    public static final String ACCELERATOR_KEY="AcceleratorKey";
    public static final String MNEMONIC_KEY="MnemonicKey";
    public static final String SELECTED_KEY = "SwingSelectedKey";
    public static final String DISPLAYED_MNEMONIC_INDEX_KEY = "SwingDisplayedMnemonicIndexKey";
    public static final String LARGE_ICON_KEY = "SwingLargeIconKey";
    public Object getValue(String key);
    public void putValue(String key, Object value);
    public void setEnabled(boolean b);
    public boolean isEnabled();
    public void addPropertyChangeListener(PropertyChangeListener listener);
    public void removePropertyChangeListener(PropertyChangeListener listener);
}

这个接口实际上并不复杂,我们一组一组看。

首先,Action接口继承了ActionListener,那它就由ActionPerformed()回调函数。除此之外,还定义了上述的一些属性的方法。

  • setEnabled()/isEnabled() 启用禁用这个动作/检查动作是否可用,不可用按钮会显示为灰色
  • getValue()/putValue() 存储/读出动作键属性值对,有一些swing预定义的键值对可以使用:
  • NAME 动作名称,显示在按钮和菜单上
  • SMALL_ICON 存储小图标,显示在按钮,菜单或工具栏中
  • SHORT_DESCRIPTION 动作的简短说明
  • LONG_DESCRIPTION 详细说明,swing组件不使用这个值
  • MNEMONIC_KEY 快捷键缩写,显示在菜单栏中
  • ACCELERATOR_KEY 存储加速击键的地方,swing组件不使用这个值
  • DEFAULT 常用的综合属性,swing组件不使用这个值
  • addPropertyChangeListener()/removePropertyChangeListener() 属性变更监听器

当动作被赋予一个控件时,动作属性就会自动被显示出来。这比多个控件编写相同的动作名、图标、说明等等简洁多了。

实际上,我们不需要自己实现Action接口,Swing提供了一个AbstractAction实现了这个接口,我们继承这个类,并重写ActionPerformed()就好了。

一个自定义Action的例子:

class MyAction extends AbstractAction {

    public MyAction(String name, Icon icon) {
        putValue(Action.NAME, name);
        putValue(Action.SMALL_ICON, icon);
        putValue(Action.SHORT_DESCRIPTION, "test description");
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("hello");
    }
}

调用这个Action创建按钮:

JButton jButton = new JButton(new MyAction("btn", icon));

使用适配器模式

上面介绍的Listener接口如果有不止一个回调方法,那么显然我们创建类时就需要实现其全部方法,比如WindowListener用于监听窗体的生命周期,非常麻烦,我们实现如何优雅的实现此类Listner呢?其实Swing还提供了一系列适配器,比如对应WindowListener就有WindowAdaptor

package demoswing;

import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class MyFrame extends JFrame {
    public MyFrame() {
        // 设置监听器
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowDeactivated(WindowEvent e) {
                System.out.println("窗口失去了焦点");
            }
        });
        // 属性
        this.setSize(300, 200);
    }
}

此时虽然不能使用lambda表达式,但是我们创建的匿名类只用实现一个方法即可。

分派操作到UI线程

我们实际开发中经常涉及多线程环境,比如下载工具,显然下载逻辑需要实现在一个单独的线程中,否则下载过程中阻塞UI线程,界面就会卡死。然而下载过程中我们还要实时更新进度条,大家都知道GUI程序不能在非UI线程中更新界面,Swing中提供了SwingUtilities.invokeLater(Runnable doRun)工具方法实现将操作分派到UI线程执行。

package demoswing;

import javax.swing.*;

public class MyFrame extends JFrame {
    private JButton btnDownload;
    private JProgressBar pbDownloadPercent;

    public MyFrame() {
        btnDownload = new JButton("开始下载");
        btnDownload.addActionListener((e) -> handleBtnClick());
        pbDownloadPercent = new JProgressBar(0, 100);

        // 这里用一个JPanel包装组件后,添加到窗体中
        JPanel pMain = new JPanel();
        pMain.add(btnDownload);
        pMain.add(pbDownloadPercent);
        this.add(pMain);
        this.pack();
    }

    private void handleBtnClick() {
        new Thread(() -> {
            for (int i = 0; i <= 100; i++) {
                // 使用Thread.sleep()模拟下载过程中线程阻塞
                try {
                    Thread.sleep(10);
                } catch (InterruptedException ex) {
                    throw new RuntimeException(ex);
                }
                // 更新UI
                final int v = i;
                SwingUtilities.invokeLater(() -> pbDownloadPercent.setValue(v));
            }
        }).start();
    }
}

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap