14.4 事件处理机制
界面长到今天,还是"死的"——按钮点了没反应。本节给它装上灵魂:事件驱动(程序不再从头跑到尾,而是"等事发生")。三个角色就够:事件源(谁出的事)、事件对象(出了什么事,ActionEvent)、监听器(事后干活的代码)。绑定口诀一句:事件源.addActionListener(监听器)——给按钮装个门铃,有人按才响。
一、思维切换:从"流水线"到"等门铃"
前十三章的程序都是流水线:main 从上往下跑完就结束。GUI 程序反过来——setVisible(true) 之后 main 其实"跑完了",程序却不退出,而是站岗等事:等你点按钮、敲键盘、关窗口。每出一次事,就触发一段代码,跑完继续站岗。
这就是事件驱动:你的代码不再是"主旋律",而是被事件"召唤"出来的。
二、事件三要素:谁出事、出了什么事、谁管
| 角色 | 是什么 | 本节对应 |
|---|---|---|
| 事件源 | 出事的组件 | 按钮 btnOK |
| 事件对象 | 装着"出了什么事"的信封 | ActionEvent(谁点的、什么时候点的) |
| 监听器 | 事出后干活的代码 | 实现了 ActionListener 接口的对象 |
三者的接头方式就一行:
btnOK.addActionListener(监听器); // 事件源.addActionListener(监听器)
读法:按钮(事件源)上装一个监听器,以后按钮被点,Swing 就把打包好的 ActionEvent 信封递给监听器,监听器拆信干活。
ActionListener 是 JDK 写好的接口,里面只有一个方法 actionPerformed(ActionEvent e)。你提供实现类=签合同,Swing 负责在出事时调用它。方法上的 @Override(9.4)就是"我真的按合同重写了"的标签。
三、三种写法演进:从杀鸡牛刀到一行流
同一个"点按钮打印一句话",三种姿势一步步瘦身:
// 写法一:单独写一个类(能懂,但为一个按钮造个文件太重)
class MyListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("注册!");
}
}
btn.addActionListener(new MyListener());
// 写法二:匿名内部类(4.6 的"一次性实现"主场)
btn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("注册!");
}
});
// 写法三:Lambda(4.7 的老主场)
btn.addActionListener(e -> System.out.println("注册!"));
ActionListener只有一个抽象方法 → 正好是 4.7 说的函数式接口,Lambda 随便用。- Lambda 里的
e就是递过来的ActionEvent信封,想查"谁点的"就e.getSource()。 - 本节起默认用写法三,但前两种必须看得懂——老项目和教材里全是它们。
四、多面手监听器家族:会认就行
按钮用 ActionListener,其他组件各有各的门铃合同(都在 java.awt.event 包):
| 监听器 | 装在谁身上 | 什么时候响 |
|---|---|---|
ActionListener |
按钮、复选框、密码框回车 | 被"激活"(点击/回车) |
ItemListener |
复选框、单选、下拉 | 选中状态变化 |
KeyListener |
文本框 | 敲键盘 |
MouseListener |
任何组件 | 按下/松开/进入/离开 |
套路完全统一:组件.addXxxListener(监听器)。先混脸熟,用到哪个查哪个。
五、动手:Demo29 注册表单活了
把 14.3 排好版的表单接上事件——勾协议按钮复活,点注册校验弹窗:
import javax.swing.*;
import java.awt.*;
public class Demo29 {
public static void main(String[] args) {
JFrame frame = new JFrame("注册新账号 · 活了");
frame.setSize(360, 220);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTextField txtName = new JTextField(15);
JPasswordField pwd = new JPasswordField(15);
JCheckBox cbAgree = new JCheckBox("我同意注册协议");
JButton btnOK = new JButton("注册");
btnOK.setEnabled(false); // 14.2 的灰按钮回来了
JPanel north = new JPanel();
north.add(new JLabel("用户名"));
north.add(txtName);
JPanel center = new JPanel();
center.add(new JLabel("密码"));
center.add(pwd);
JPanel south = new JPanel();
south.add(cbAgree);
south.add(btnOK);
// 门铃一:勾协议 → 按钮复活(ItemListener 岗位,ActionListener 也常用于勾选)
cbAgree.addActionListener(e -> btnOK.setEnabled(cbAgree.isSelected()));
// 门铃二:点注册 → 校验并弹窗(14.1 的 JOptionPane 回岗)
btnOK.addActionListener(e -> {
String name = txtName.getText();
String password = new String(pwd.getPassword()); // 14.2 红牌坑的规矩
if (name.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(frame, "用户名和密码不能为空!");
} else {
JOptionPane.showMessageDialog(frame, "欢迎你," + name + "!");
}
});
frame.add(north, BorderLayout.NORTH);
frame.add(center, BorderLayout.CENTER);
frame.add(south, BorderLayout.SOUTH);
frame.setVisible(true);
}
}
跑起来:默认按钮是灰的 → 勾协议,按钮当场复活 → 点注册,空了弹警告,填了弹欢迎。三节累计的零件(14.1 弹窗、14.2 组件、14.3 布局、4.7 Lambda)在这一刻全部咬合。
注册后再把勾去掉:按钮当场又灰了——isSelected() 实时反映勾选状态,门铃每次响都重新执行一遍。
给 btnOK 再 addActionListener(e -> System.out.println("第二个门铃也响了")):点一下,两个都执行(按添加顺序)。监听器是"追加"不是"覆盖"。
在 main 里 String name = "波仔";,然后 Lambda 里写 name = "改一下";——编译报错:must be final or effectively final。Lambda 抓的是局部变量的"快照",抓完不许改。
六、五个常见误区
不装监听器,按钮被点时 Swing 只是画了个按下弹起的效果,你的代码一行都不会跑——Demo27"点了没反应"不是 bug,是没装门铃。
new ActionListener(){...} 造了一堆代码,但没挂到按钮上——门铃造好了没接线,照样哑。检查:绑定的那一行 btn.addActionListener(...) 在不在。
Lambda 只伺候函数式接口(单抽象方法)。ActionListener 可以;多方法的监听器(如 MouseListener 有五个方法)只能匿名内部类或适配器类。
被它俩抓走的局部变量必须 final 或 effectively final(抓走后没改过)。想改:把变量提到 main 外面当成员变量,或用数组/集合包一层。
不会,两个都执行(区别于 setLayout 的"换师傅"思维)。想换逻辑就先 removeActionListener 或干脆重写一个。
七、自测题
事件源=出事的组件(btnOK)、事件对象=装着事情的信封(ActionEvent)、监听器=事后干活的代码(实现 ActionListener 的 Lambda)。
btnOK.addActionListener(监听器);——事件源.addActionListener(监听器),像给按钮装门铃。
它是函数式接口——只有一个抽象方法 actionPerformed。多方法接口(MouseListener)就不行,只能匿名内部类。
必须是 final 或 effectively final(从声明到被抓走没重新赋值过)。想改就提为成员变量。
参考骨架:
btnLogin.addActionListener(e -> {
String name = txtName.getText();
String pwd = new String(pwdField.getPassword());
if ("admin".equals(name) && "123456".equals(pwd)) {
JOptionPane.showMessageDialog(frame, "登录成功!");
} else {
JOptionPane.showMessageDialog(frame, "用户名或密码错误!");
}
});
自检:门铃挂对按钮了吗?密码走的是 getPassword() 吗?字符串比较用的是 equals 而不是 == 吗(6.2 的老规矩)?
八、下一步学什么
- ☑ 14.5 JavaFX ✅ —— 点此复习:Swing 的新一代兄弟、Stage/Scene/Node 三层、Application 套路与 launch 代调、Demo30 第一扇窗
- ☐ 14.6(按教材顺序) —— 报个标题接着走。等你报标题。
相关笔记
- 第十四章 图形用户界面/14.3 布局管理器:本节的界面骨架——Demo29 的三层板子就是 Demo28 原班人马
- 第十四章 图形用户界面/14.2 Swing的常见组件:灰按钮钩子正式收网——setEnabled + isSelected 联动
- 第十四章 图形用户界面/14.1 Swing概述:JOptionPane 弹窗回岗当"干活的嘴"
- 第四章 面向对象(下)/4.7 函数式编程:Lambda 从练习场开进 GUI 正式岗位
- 第四章 面向对象(下)/4.6 内部类:匿名内部类写法二的出处
- 第四章 面向对象(下)/4.3 抽象类和接口:ActionListener 就是一张 JDK 写好的接口合同