我们知道大多数图形界面程序是事件驱动的(游戏除外),因此事件处理的实现是非常重要的。Java中,Swing底层依赖于AWT的事件处理机制。
Swing中,事件源就是发出事件的对象,例如按钮,点击按钮就会发出按钮点击事件。事件监听器则需要注册到事件源,事件监听器通常是一个回调函数,告知当此事件发生时,程序该怎么做。
我们简单看一下ActionListener
这个接口:
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent e);
}
我想经验丰富的同学一看这个就知道监听器怎么写了,其实actionPerformed
就是一个回调函数,事件发生时就会被回调,传入一个事件对象,我们根据这个事件对象作出相应判断即可。我们可以实现这个接口实现自己的监听器类,或者直接使用匿名类。
除了ActionListener
,Swing中还有许多其它的监听器实现GUI界面的各种功能,具体可以参考文档。
事件被封装在事件对象中,事件对象有很多,如ActionEvent
,WindowEvent
等,事件对象又可以划分成不同的继承层次,所有事件派生自java.util.EventObject
。
JButton jButton = new JButton("hello");
jButton.addActionListener((e)->{
System.out.println("button clicked!");
});
我们可以使用lambda
表达式简化了监听器的注册。实际上使用匿名类的方式绑定事件的写法非常简单,且十分常用。
事件对象还有一些其他的实用方法:
Object getSource()
返回发生事件的对象的引用。String getActionCommand()
返回这个事件的动作字符串。如果事件来自按钮,默认动作字符串是按钮标签,除非使用setActionCommand()
对按钮对象的动作字符串进行修改。有时候多个事件源(控件)需要绑定同一种命令(操作),多个控件绑定同一个监听器可以实现,但是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线程,界面就会卡死。然而下载过程中我们还要实时更新进度条,大家都知道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();
}
}