☕ 第十四章 图形用户界面

14.4 事件处理机制

📄 一句话概括

界面长到今天,还是"死的"——按钮点了没反应。本节给它装上灵魂:事件驱动(程序不再从头跑到尾,而是"等事发生")。三个角色就够:事件源(谁出的事)、事件对象(出了什么事,ActionEvent)、监听器(事后干活的代码)。绑定口诀一句:事件源.addActionListener(监听器)——给按钮装个门铃,有人按才响。


一、思维切换:从"流水线"到"等门铃"

前十三章的程序都是流水线:main 从上往下跑完就结束。GUI 程序反过来——setVisible(true) 之后 main 其实"跑完了",程序却不退出,而是站岗等事:等你点按钮、敲键盘、关窗口。每出一次事,就触发一段代码,跑完继续站岗。

这就是事件驱动:你的代码不再是"主旋律",而是被事件"召唤"出来的。

二、事件三要素:谁出事、出了什么事、谁管

角色 是什么 本节对应
事件源 出事的组件 按钮 btnOK
事件对象 装着"出了什么事"的信封 ActionEvent(谁点的、什么时候点的)
监听器 事出后干活的代码 实现了 ActionListener 接口的对象

三者的接头方式就一行:

btnOK.addActionListener(监听器);   // 事件源.addActionListener(监听器)

读法:按钮(事件源)上装一个监听器,以后按钮被点,Swing 就把打包好的 ActionEvent 信封递给监听器,监听器拆信干活。

💡 ActionListener 本质 = 4.3 的一张接口合同

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,其他组件各有各的门铃合同(都在 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)在这一刻全部咬合。

🧪 实验 1:勾掉协议按钮会怎样

注册后再把勾去掉:按钮当场又灰了——isSelected() 实时反映勾选状态,门铃每次响都重新执行一遍。

🧪 实验 2:一个按钮装两个门铃

给 btnOK 再 addActionListener(e -> System.out.println("第二个门铃也响了")):点一下,两个都执行(按添加顺序)。监听器是"追加"不是"覆盖"。

🧪 实验 3:Lambda 里改局部变量

在 main 里 String name = "波仔";,然后 Lambda 里写 name = "改一下";——编译报错:must be final or effectively final。Lambda 抓的是局部变量的"快照",抓完不许改。

六、五个常见误区

⚠️ 误区 1:以为按钮天生点了就有反应

不装监听器,按钮被点时 Swing 只是画了个按下弹起的效果,你的代码一行都不会跑——Demo27"点了没反应"不是 bug,是没装门铃。

⚠️ 误区 2:写好了监听器却忘了 addActionListener

new ActionListener(){...} 造了一堆代码,但没挂到按钮上——门铃造好了没接线,照样哑。检查:绑定的那一行 btn.addActionListener(...) 在不在。

⚠️ 误区 3:什么接口都想上 Lambda

Lambda 只伺候函数式接口(单抽象方法)。ActionListener 可以;多方法的监听器(如 MouseListener 有五个方法)只能匿名内部类或适配器类。

⚠️ 误区 4:Lambda/匿名内部类里改局部变量

被它俩抓走的局部变量必须 final 或 effectively final(抓走后没改过)。想改:把变量提到 main 外面当成员变量,或用数组/集合包一层。

⚠️ 误区 5:以为第二次 addActionListener 会覆盖第一次

不会,两个都执行(区别于 setLayout 的"换师傅"思维)。想换逻辑就先 removeActionListener 或干脆重写一个。

七、自测题

❓ Q1:事件三要素是什么?Demo29 里各对应谁?

事件源=出事的组件(btnOK)、事件对象=装着事情的信封(ActionEvent)、监听器=事后干活的代码(实现 ActionListener 的 Lambda)。

❓ Q2:把监听器挂到按钮上的那行代码怎么写?

btnOK.addActionListener(监听器);——事件源.addActionListener(监听器),像给按钮装门铃。

❓ Q3:为什么 ActionListener 能用 Lambda 写?

它是函数式接口——只有一个抽象方法 actionPerformed。多方法接口(MouseListener)就不行,只能匿名内部类。

❓ Q4:匿名内部类和 Lambda 里的局部变量有什么限制?

必须是 final 或 effectively final(从声明到被抓走没重新赋值过)。想改就提为成员变量。

❓ Q5:(毕业题)把 Demo29 改成登录窗:用户名 admin、密码 123456,对了弹"登录成功",错了弹"用户名或密码错误",协议勾选换成"记住密码"。

参考骨架:

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 的老规矩)?

八、下一步学什么


相关笔记