GUI 编程与事件驱动基础 | JavaSE

GUI 编程与事件驱动基础

一、学习目标

完成本章后,应能够:

  1. 能够解释 GUI 与 CLI 的区别,理解 GUI 程序为什么主要采用事件驱动模型。
  2. 能够说明 AWT 与 Swing 的关系,认识常用 Swing 容器与组件。
  3. 能够独立创建 JFrame 窗口,并使用 JPanelJLabelJButtonJTextFieldJPasswordField 等组件搭建界面。
  4. 能够使用常见布局管理器组织组件,而不是完全依赖绝对坐标。
  5. 能够为按钮、键盘和鼠标操作注册事件监听器,理解事件源、事件对象和监听器之间的关系。
  6. 能够使用匿名内部类和 Lambda 编写事件处理代码
  7. 理解 Swing 的事件派发线程(Event Dispatch Thread, EDT),避免在 GUI 线程中执行长时间阻塞任务。
  8. 能够独立完成一个简单的登录界面,为下一章石头迷阵 GUI 项目做好准备。

二、核心知识

2.1 什么是 GUI

GUI,全称:

Graphical User Interface

即:

图形用户界面。

GUI 程序通过:

  • 窗口
  • 按钮
  • 文本框
  • 标签
  • 图片
  • 菜单
  • 表格

等图形元素与用户交互。

例如:

┌──────────────────────────────┐
│           用户登录            │
│                              │
│ 用户名:[____________]        │
│ 密  码:[____________]        │
│                              │
│        [ 登录 ] [ 取消 ]      │
└──────────────────────────────┘

这就是典型 GUI。


2.2 GUI 与 CLI

CLI:

Command Line Interface,命令行界面。

前面开发的学生管理系统就是典型 CLI:

==========学生管理系统==========
1. 添加学生
2. 查询学生
3. 删除学生
4. 修改学生
0. 退出

请选择:

用户主要通过:

键盘输入
    ↓
Scanner
    ↓
程序执行

与系统交互。

GUI 则变成:

鼠标点击 / 键盘输入
        ↓
窗口组件
        ↓
产生事件
        ↓
Java事件处理代码

二者核心区别:

| 对比 | CLI | GUI | | ---------- | ---------- | ------------ | | 交互形式 | 命令、文本 | 窗口、按钮等 | | 用户门槛 | 相对高 | 相对低 | | 开发复杂度 | 较低 | 较高 | | 界面表现 | 简单 | 丰富 | | 典型应用 | 命令行工具 | 桌面软件 |


2.3 GUI 最重要的思想:事件驱动

控制台程序经常是:

System.out.println("请输入年龄:");
int age = scanner.nextInt();

System.out.println("请输入姓名:");
String name = scanner.next();

程序决定:

接下来发生什么。

执行流程非常明显:

第一句
 ↓
第二句
 ↓
第三句
 ↓
第四句

但是 GUI 不一样。

窗口显示之后:

用户可能点击登录
用户可能点击取消
用户可能输入用户名
用户可能按回车
用户可能移动鼠标
用户可能关闭窗口

程序根本无法事先知道:

用户下一秒会干什么。

因此 GUI 使用:

事件驱动编程(Event-Driven Programming)

基本思想:

程序先创建界面
       ↓
等待用户操作
       ↓
某个事件发生
       ↓
调用对应事件处理代码

例如:

用户点击登录按钮
        ↓
产生点击事件
        ↓
执行登录按钮监听器
        ↓
读取用户名和密码
        ↓
验证登录

所以 GUI 程序不是:

“我接下来一定执行什么?”

而是:

“如果某件事情发生,我应该做什么?”

这是非常重要的编程思想转变。


2.4 AWT 与 Swing

JavaSE 中经典 GUI 技术主要涉及:

AWT
Swing

AWT

AWT 全称:

Abstract Window Toolkit

中文通常称:

抽象窗口工具包。

主要包:

java.awt
java.awt.event

AWT 提供:

  • 图形
  • 颜色
  • 字体
  • 布局
  • 事件模型
  • 基础 GUI 能力

例如:

Color
Font
FlowLayout
BorderLayout
GridLayout
ActionEvent

Swing

Swing 主要位于:

javax.swing

例如:

JFrame
JPanel
JLabel
JButton
JTextField
JPasswordField
JTable
JOptionPane

可以把二者简单理解为:

           Java GUI
              │
       ┌──────┴──────┐
       │             │
      AWT          Swing
       │             │
  基础设施       更丰富的组件
       └──────┬──────┘
              │
        一起构建GUI程序

Swing 建立在 AWT 的窗口、事件、绘图等基础设施之上。

所以实际代码经常同时看到:

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

这完全正常。


2.5 JFrame:窗口

JFrame 可以理解为:

一个顶级窗口。

例如:

JFrame frame = new JFrame("我的第一个窗口");

你可以把它想象成:

┌────────────────────────────┐
│ 我的第一个窗口        ×     │
├────────────────────────────┤
│                            │
│         内容区域            │
│                            │
└────────────────────────────┘

常用方法:

frame.setSize(500, 400);

设置大小。

frame.setLocationRelativeTo(null);

让窗口居中。

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

关闭窗口时结束程序。

frame.setResizable(false);

禁止调整大小。

frame.setVisible(true);

显示窗口。

注意:

setVisible(true) 通常放在界面组件准备完成以后。


2.6 JPanel:面板

JPanel 可以理解为:

窗口内部用于组织组件的小容器。

例如:

JFrame
│
├── JPanel 顶部
│     ├── JLabel
│     └── JTextField
│
├── JPanel 中部
│     └── JTable
│
└── JPanel 底部
      ├── JButton
      └── JButton

代码:

JPanel panel = new JPanel();

frame.add(panel);

实际开发中很少把所有组件乱七八糟直接塞进 JFrame

通常:

JFrame
 ↓
多个 JPanel
 ↓
具体组件

这样结构更加清晰。


2.7 JLabel:文本或图片标签

创建:

JLabel label = new JLabel("用户名:");

可以用于:

显示文字
显示图片
展示状态信息

例如:

JLabel statusLabel = new JLabel("请登录");

点击按钮后:

statusLabel.setText("登录成功");

于是 GUI 内容会发生变化。


2.8 JButton:按钮

创建按钮:

JButton button = new JButton("登录");

注意:

创建按钮 ≠ 点击按钮后自动执行登录。

按钮只是:

GUI组件

还需要给它绑定:

事件监听器

才知道点击以后执行什么。


2.9 JTextField:单行文本输入框

JTextField usernameField =
        new JTextField(15);

获取输入:

String username =
        usernameField.getText();

修改内容:

usernameField.setText("");

2.10 JPasswordField:密码输入框

密码通常使用:

JPasswordField passwordField =
        new JPasswordField(15);

读取密码可以使用:

char[] password =
        passwordField.getPassword();

再根据实际业务进行处理。

它与普通 JTextField 的主要视觉差异是:

123456

显示时通常会变成:

••••••

2.11 常见 Swing 组件体系

当前阶段重点掌握:

| 组件 | 主要作用 | | ---------------- | --------------- | | JFrame | 顶级窗口 | | JPanel | 面板容器 | | JLabel | 文本/图片标签 | | JButton | 按钮 | | JTextField | 单行文本输入 | | JPasswordField | 密码输入 | | JTextArea | 多行文本 | | JTable | 表格 | | JScrollPane | 滚动容器 | | JOptionPane | 消息/确认对话框 |

现阶段没有必要把所有 Swing API 全背下来。

需要先掌握:

容器
组件
布局
事件

这四个核心概念。


2.12 什么是布局管理器

假设窗口中有三个按钮:

[添加] [删除] [修改]

它们应该:

  • 放在哪里?
  • 多大?
  • 谁在左边?
  • 谁在右边?
  • 窗口大小变化以后怎么调整?

这就是:

布局(Layout)

问题。

Java 提供:

布局管理器(Layout Manager)

自动管理组件的位置与尺寸。


2.13 为什么不建议完全依赖绝对坐标

一种 GUI 写法是:

button.setBounds(100, 50, 100, 40);

也就是直接规定:

x = 100
y = 50
width = 100
height = 40

这种方式叫:

绝对定位。

简单小游戏或者固定尺寸界面中有时会使用。

但是大型普通 GUI 如果全部这么干:

窗口尺寸改变
字体改变
系统缩放改变
文字长度改变
操作系统改变

都可能导致界面错位。

因此普通表单界面更适合:

布局管理器

2.14 FlowLayout

FlowLayout:

流式布局。

组件从左向右排列。

空间不足时继续换行。

类似:

[A] [B] [C] [D]
[E] [F]

示例:

JPanel panel =
        new JPanel(new FlowLayout());

panel.add(new JButton("添加"));
panel.add(new JButton("删除"));
panel.add(new JButton("修改"));

适合:

按钮栏
工具栏
简单操作区域

2.15 BorderLayout

BorderLayout:

边界布局。

将容器分为:

             NORTH
┌──────────────────────────┐
│                          │
├─────┬──────────────┬─────┤
│WEST │    CENTER    │EAST │
├─────┴──────────────┴─────┤
│          SOUTH           │
└──────────────────────────┘

五个区域:

BorderLayout.NORTH
BorderLayout.SOUTH
BorderLayout.WEST
BorderLayout.EAST
BorderLayout.CENTER

例如:

frame.setLayout(new BorderLayout());

frame.add(topPanel, BorderLayout.NORTH);
frame.add(contentPanel, BorderLayout.CENTER);
frame.add(bottomPanel, BorderLayout.SOUTH);

它非常适合:

一个窗口的大区域划分。

2.16 GridLayout

GridLayout:

网格布局。

例如:

new GridLayout(2, 3);

意味着:

2 行
3 列

效果:

┌─────┬─────┬─────┐
│  A  │  B  │  C  │
├─────┼─────┼─────┤
│  D  │  E  │  F  │
└─────┴─────┴─────┘

非常适合:

数字键盘
计算器按钮
棋盘
规则表单
网格式操作区

后面的石头迷阵本质上也天然具有:

二维网格

特征。


2.17 BoxLayout

BoxLayout 主要按照:

X_AXIS

或:

Y_AXIS

组织组件。

例如纵向:

用户名
[________]

密码
[________]

[ 登录 ]

它适合:

垂直菜单
表单
纵向工具区域

2.18 一个重要设计原则:组合布局

实际 GUI 通常不是:

整个程序只能选择一种布局。

而是:

JFrame:BorderLayout
│
├── NORTH:FlowLayout
│
├── CENTER:GridLayout
│
└── SOUTH:FlowLayout

也就是说:

不同 JPanel 可以拥有不同布局管理器。

这是构建复杂 GUI 最核心的方法之一。


三、使用方法

3.1 创建第一个 Swing 窗口

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;

public class FirstGuiDemo {

    public static void main(String[] args) {

        SwingUtilities.invokeLater(() -> {

            JFrame frame =
                    new JFrame("我的第一个 GUI");

            frame.setSize(500, 300);

            frame.setLocationRelativeTo(null);

            frame.setDefaultCloseOperation(
                    JFrame.EXIT_ON_CLOSE
            );

            frame.add(
                    new JLabel(
                            "Hello Swing!",
                            JLabel.CENTER
                    )
            );

            frame.setVisible(true);
        });
    }
}

程序运行后会出现:

┌─────────────────────────────┐
│ 我的第一个 GUI        ×      │
├─────────────────────────────┤
│                             │
│        Hello Swing!         │
│                             │
└─────────────────────────────┘

3.2 创建一个按钮

import javax.swing.*;

public class ButtonDemo {

    public static void main(String[] args) {

        SwingUtilities.invokeLater(() -> {

            JFrame frame =
                    new JFrame("按钮");

            JPanel panel =
                    new JPanel();

            JButton button =
                    new JButton("点我");

            panel.add(button);

            frame.add(panel);

            frame.setSize(400, 300);

            frame.setLocationRelativeTo(null);

            frame.setDefaultCloseOperation(
                    JFrame.EXIT_ON_CLOSE
            );

            frame.setVisible(true);
        });
    }
}

但是现在点击:

[点我]

什么也不会发生。

因为还没有:

监听器

3.3 事件驱动模型

GUI 事件可以建立如下模型:

事件源
  ↓
产生事件
  ↓
事件对象
  ↓
通知监听器
  ↓
执行处理方法

例如点击按钮:

JButton
   ↓
用户点击
   ↓
ActionEvent
   ↓
ActionListener
   ↓
actionPerformed()

四个核心概念:

事件源(Event Source)

谁产生事件。

例如:

JButton loginButton;

这里:

loginButton

就是事件源。


事件(Event)

发生了什么事情。

例如:

按钮被点击
键盘被按下
鼠标被移动

事件对象(Event Object)

Java 会把事件相关信息封装成对象。

例如:

ActionEvent
KeyEvent
MouseEvent

监听器(Listener)

监听某类事件,并规定事件发生以后:

执行什么代码。

例如:

ActionListener
KeyListener
MouseListener

3.4 ActionListener

按钮点击最常见:

button.addActionListener(
        new ActionListener() {

            @Override
            public void actionPerformed(
                    ActionEvent e) {

                System.out.println(
                        "按钮被点击了"
                );
            }
        }
);

流程:

用户点击 JButton
        ↓
产生 ActionEvent
        ↓
通知 ActionListener
        ↓
调用 actionPerformed()
        ↓
执行我们的代码

3.5 监听器写法一:单独实现类

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class LoginListener
        implements ActionListener {

    @Override
    public void actionPerformed(
            ActionEvent e) {

        System.out.println(
                "用户点击登录"
        );
    }
}

注册:

loginButton.addActionListener(
        new LoginListener()
);

优点:

事件逻辑可以单独组织
代码职责明确

缺点:

简单事件需要额外创建类

3.6 监听器写法二:匿名内部类

loginButton.addActionListener(
        new ActionListener() {

            @Override
            public void actionPerformed(
                    ActionEvent e) {

                System.out.println(
                        "登录"
                );
            }
        }
);

前面学习的:

匿名内部类

终于出现了一个非常典型的实际应用。


3.7 监听器写法三:Lambda

ActionListener 是函数式接口,因此可以:

loginButton.addActionListener(
        e -> System.out.println("登录")
);

如果事件处理逻辑较多:

loginButton.addActionListener(e -> {

    String username =
            usernameField.getText();

    System.out.println(
            "用户名:" + username
    );
});

这也是之前学习 Lambda 的真实应用场景。


3.8 完整登录窗口

下面将:

窗口
布局
标签
文本框
密码框
按钮
事件监听

组合起来。

import javax.swing.*;
import java.awt.*;

public class LoginFrame extends JFrame {

    private final JTextField usernameField =
            new JTextField(15);

    private final JPasswordField passwordField =
            new JPasswordField(15);

    private final JLabel resultLabel =
            new JLabel("请输入账号密码");

    public LoginFrame() {

        setTitle("登录系统");

        setSize(420, 250);

        setDefaultCloseOperation(
                JFrame.EXIT_ON_CLOSE
        );

        setLocationRelativeTo(null);

        initView();

        setVisible(true);
    }

    private void initView() {

        JPanel formPanel =
                new JPanel(
                        new GridLayout(
                                3,
                                2,
                                10,
                                10
                        )
                );

        formPanel.add(
                new JLabel("用户名:")
        );

        formPanel.add(
                usernameField
        );

        formPanel.add(
                new JLabel("密码:")
        );

        formPanel.add(
                passwordField
        );

        JButton loginButton =
                new JButton("登录");

        JButton clearButton =
                new JButton("清空");

        formPanel.add(loginButton);
        formPanel.add(clearButton);

        JPanel statusPanel =
                new JPanel();

        statusPanel.add(resultLabel);

        setLayout(
                new BorderLayout(
                        10,
                        10
                )
        );

        add(
                formPanel,
                BorderLayout.CENTER
        );

        add(
                statusPanel,
                BorderLayout.SOUTH
        );

        loginButton.addActionListener(
                e -> login()
        );

        clearButton.addActionListener(
                e -> clearInput()
        );
    }

    private void login() {

        String username =
                usernameField
                        .getText()
                        .trim();

        String password =
                new String(
                        passwordField
                                .getPassword()
                );

        if (
                "admin".equals(username)
                &&
                "123456".equals(password)
        ) {

            resultLabel.setText(
                    "登录成功"
            );

        } else {

            resultLabel.setText(
                    "用户名或密码错误"
            );
        }
    }

    private void clearInput() {

        usernameField.setText("");

        passwordField.setText("");

        resultLabel.setText(
                "请输入账号密码"
        );
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(
                LoginFrame::new
        );
    }
}

这段代码的调用关系:

main()
 ↓
创建 LoginFrame
 ↓
构造器
 ↓
initView()
 ↓
创建组件
 ↓
注册事件
 ↓
setVisible(true)
 ↓
等待用户事件

点击登录:

JButton
 ↓
ActionEvent
 ↓
Lambda监听器
 ↓
login()
 ↓
读取输入框
 ↓
验证账号
 ↓
修改 JLabel

3.9 JOptionPane 消息框

除了修改 JLabel,GUI 中经常弹出提示框:

JOptionPane.showMessageDialog(
        this,
        "登录成功"
);

错误:

JOptionPane.showMessageDialog(
        this,
        "用户名或密码错误",
        "登录失败",
        JOptionPane.ERROR_MESSAGE
);

确认框:

int result =
        JOptionPane.showConfirmDialog(
                this,
                "确定退出系统吗?",
                "确认",
                JOptionPane.YES_NO_OPTION
        );

然后:

if (
        result
        ==
        JOptionPane.YES_OPTION
) {

    System.exit(0);
}

3.10 键盘事件

GUI 不只有:

按钮点击

后面的石头迷阵会大量需要:

键盘事件

例如监听按键:

addKeyListener(
        new java.awt.event.KeyAdapter() {

            @Override
            public void keyPressed(
                    java.awt.event.KeyEvent e) {

                System.out.println(
                        "按键:" + e.getKeyCode()
                );
            }
        }
);

通过:

e.getKeyCode()

可以判断:

KeyEvent.VK_UP
KeyEvent.VK_DOWN
KeyEvent.VK_LEFT
KeyEvent.VK_RIGHT

于是:

↑
↓
←
→

就能够驱动程序逻辑。

这正是下一章石头迷阵的重要基础。


3.11 鼠标事件

鼠标相关事件可以使用:

MouseListener

或者更加方便的:

MouseAdapter

例如:

panel.addMouseListener(
        new MouseAdapter() {

            @Override
            public void mouseClicked(
                    MouseEvent e) {

                System.out.println(
                        "鼠标点击"
                );
            }
        }
);

常见事件:

mouseClicked
mousePressed
mouseReleased
mouseEntered
mouseExited

四、原理与进阶

4.1 为什么 GUI 程序不会在 main 结束后立即退出

你可能会产生疑问:

public static void main(String[] args) {
    new LoginFrame();
}

看起来:

new LoginFrame()

执行结束以后,main() 好像也结束了。

为什么窗口还活着?

因为 Swing GUI 建立以后,程序中还存在 GUI 事件处理相关线程。

程序进入类似:

等待事件
 ↓
处理事件
 ↓
继续等待

的状态。

GUI 的运行方式已经不同于简单的:

main()
 ↓
逐行执行
 ↓
结束

4.2 事件派发线程 EDT

Swing 中有一个非常重要的线程:

事件派发线程(Event Dispatch Thread, EDT)

它主要负责:

处理GUI事件
更新Swing组件
执行事件监听代码

例如:

loginButton.addActionListener(e -> {
    resultLabel.setText("登录成功");
});

用户点击按钮以后,这段监听代码通常就在:

EDT

中执行。


4.3 为什么使用 SwingUtilities.invokeLater

推荐这样启动 GUI:

SwingUtilities.invokeLater(
        LoginFrame::new
);

理解为:

main线程
   ↓
把“创建GUI”任务
提交给
   ↓
EDT
   ↓
EDT创建GUI

而不是:

public static void main(String[] args) {
    new LoginFrame();
}

直接让 main 线程创建界面。

在简单演示中后者经常看起来也能工作,但是建立正确 Swing 编程习惯时,应认识 EDT。


4.4 为什么不能在按钮事件中执行长任务

例如:

button.addActionListener(e -> {

    try {
        Thread.sleep(10000);
    } catch (InterruptedException ex) {
        throw new RuntimeException(ex);
    }

});

如果监听器运行在 EDT:

EDT
 ↓
sleep 10秒

那么这十秒内:

鼠标事件不能及时处理
按钮不能及时响应
窗口不能及时重绘

最终表现:

界面卡死。

所以必须形成一个重要认识:

短小GUI操作
→ EDT

耗时任务
→ 后台线程

结果更新GUI
→ EDT

这与前面学过的:

多线程

开始真正联系起来。


4.5 事件驱动与普通方法调用的关系

事件驱动并没有创造一种新的 Java 方法。

本质依旧是:

调用方法

只不过以前:

login();

是:

程序员主动调用。

现在:

button.addActionListener(
        e -> login()
);

变成:

用户触发事件以后,由事件系统间接调用。

所以:

面向对象
接口
匿名内部类
Lambda
方法调用
多线程

并没有消失。

它们只是被组合到 GUI 体系中了。


4.6 监听器为什么经常和 Lambda 联系起来

例如:

public interface ActionListener
        extends EventListener {

    void actionPerformed(ActionEvent e);
}

它只有一个抽象方法:

actionPerformed(...)

因此:

ActionListener

满足:

函数式接口。

所以:

new ActionListener() {

    @Override
    public void actionPerformed(
            ActionEvent e) {

        login();
    }
}

可以简化成:

e -> login()

进一步:

this::login

只有在参数形式能够匹配对应方法时才能使用方法引用,因此不要为了“炫技”机械改写。


五、实践应用

5.1 登录系统

登录界面非常适合练习:

JFrame
JPanel
JLabel
JTextField
JPasswordField
JButton
GridLayout
BorderLayout
ActionListener

流程:

用户名输入
      +
密码输入
      ↓
点击登录
      ↓
读取表单
      ↓
数据校验
      ↓
登录成功 / 失败

5.2 人事管理系统

原教学案例进一步使用:

JTable
DefaultTableModel
JTextField
JButton
JPopupMenu
MouseListener
ArrayList<Employee>

可以形成:

┌────────────────────────────────────┐
│ [姓名________] [搜索] [新增]       │
├────────────────────────────────────┤
│ ID │ 姓名 │ 性别 │ 年龄 │ 部门... │
│----│------│------│------│---------│
│ 1  │ 张三 │ 男   │ 24   │ 开发部  │
│ 2  │ 李四 │ 女   │ 25   │ 测试部  │
└────────────────────────────────────┘

此时:

GUI
+
ArrayList
+
OOP
+
事件驱动

已经组合起来。


5.3 石头迷阵

下一章会进一步组合:

JFrame
+
二维数组
+
图片
+
键盘事件
+
枚举
+
随机算法
+
文件
+
游戏状态

其中 GUI 的职责主要是:

把程序状态显示出来
        +
接收用户操作

而真正的游戏状态:

1  2  3  4
5  6  7  8
9 10 11 12
13 14 0 15

仍然应该由:

int[][]

等数据结构维护。

这里可以提前形成一个非常重要的设计意识:

界面只是数据和业务状态的表现形式,不要让 GUI 完全替代业务数据模型。


5.4 即时通信系统

后面的聊天室则会组合:

Swing GUI
+
Socket
+
IO
+
线程

用户点击:

[发送]

产生事件:

ActionListener

然后:

获取 JTextArea 内容
        ↓
Socket
        ↓
DataOutputStream
        ↓
服务器

这就是 JavaSE 各模块真正开始汇合的地方。


六、常见问题

6.1 创建了 JFrame 为什么看不到

最常见原因:

没有:

frame.setVisible(true);

创建对象只表示:

窗口对象存在

不代表:

窗口已经显示。

6.2 点击关闭按钮为什么程序没有退出

需要:

frame.setDefaultCloseOperation(
        JFrame.EXIT_ON_CLOSE
);

否则窗口关闭行为可能与你期待的“结束整个程序”不同。


6.3 JButton 为什么点了没有反应

因为:

JButton button =
        new JButton("登录");

只创建组件。

还必须注册:

button.addActionListener(...);

6.4 为什么所有组件挤在一起

很可能:

布局管理器使用不合理。

GUI 不是:

add()
add()
add()
add()

无限堆组件。

需要先设计:

窗口区域
 ↓
面板
 ↓
布局
 ↓
组件

6.5 JFrame 和 JPanel 有什么区别

可以粗略理解:

JFrame = 房子

JPanel = 房间

JButton/JLabel/JTextField
= 房间里的家具

通常:

JFrame
 ↓
JPanel
 ↓
组件

6.6 为什么事件监听器里可以直接访问输入框

例如:

loginButton.addActionListener(e -> {

    String username =
            usernameField.getText();
});

Lambda 可以捕获所在作用域中满足规则的变量。

如果:

usernameField

本身还是对象字段:

private JTextField usernameField;

事件处理方法自然可以直接访问。


6.7 为什么 GUI 卡住了

优先检查:

是否在事件处理方法中执行了耗时操作。

例如:

while (true) {
}

或者:

Thread.sleep(10000);

或者:

大型文件操作
耗时网络请求
复杂计算

如果这些直接占用 EDT:

GUI无法及时处理事件

于是表现为:

卡死
未响应

6.8 是不是必须背所有 Swing 组件

不是。

这一章真正需要建立的是:

GUI
 │
 ├── 容器
 │
 ├── 组件
 │
 ├── 布局
 │
 └── 事件

再进一步:

事件源
 ↓
事件对象
 ↓
监听器
 ↓
业务方法

有了这套模型,再查 API 就会非常容易。


6.9 Swing 现在是不是 Java 后端开发的核心技术

不是。

如果职业方向是:

Java Web
Spring
Spring Boot
微服务

那么 Swing 不会成为后端学习主线。

但是当前学习 Swing 的价值非常明确:

GUI编程
事件驱动
监听器模式
Lambda应用
组件化思维
多线程与UI线程
项目综合训练

特别是后面的:

石头迷阵
局域网聊天室

都需要 GUI。

因此本课程学习目标不是:

成为 Swing 专家。

而是:

通过 GUI 完成 JavaSE 综合能力闭环。


七、练习与验收

7.1 知识问答

  1. GUI 与 CLI 分别是什么?适用场景有什么不同?
  2. AWT 与 Swing 是什么关系?
  3. JFrameJPanelJLabelJButtonJTextFieldJPasswordField 分别承担什么职责?
  4. 什么是布局管理器?
  5. 为什么普通 GUI 不建议所有组件完全依赖绝对坐标?
  6. FlowLayoutBorderLayoutGridLayoutBoxLayout 分别适合什么场景?
  7. 什么是事件驱动编程?
  8. 什么是事件源?
  9. 什么是事件对象?
  10. 什么是事件监听器?
  11. 用户点击 JButton 后,从用户操作到 Java 方法执行,大致经历什么过程?
  12. ActionListener 为什么能够使用 Lambda?
  13. 什么是事件派发线程 EDT?
  14. 为什么 Swing 事件处理方法中不适合执行长时间任务?
  15. 为什么 GUI 程序可以看成“等待事件 → 处理事件 → 继续等待”?

7.2 代码阅读

阅读:

JButton button =
        new JButton("确定");

JLabel label =
        new JLabel("等待操作");

button.addActionListener(e -> {
    label.setText("已经点击");
});

禁止运行程序,回答:

  1. 事件源是谁?
  2. 监听器是什么?
  3. 用户点击按钮以后发生什么?
  4. e 大致代表什么?
  5. 哪一行真正修改了界面状态?
  6. 如果删除 addActionListener,按钮还能显示吗?
  7. 删除后点击按钮还会修改标签吗?

阅读:

JFrame frame =
        new JFrame();

frame.setLayout(
        new BorderLayout()
);

frame.add(
        new JButton("A"),
        BorderLayout.NORTH
);

frame.add(
        new JButton("B"),
        BorderLayout.CENTER
);

frame.add(
        new JButton("C"),
        BorderLayout.SOUTH
);

回答:

  1. 三个按钮分别位于什么区域?
  2. 哪一个通常占据最大空间?
  3. 如果再次向 CENTER 添加另一个普通组件,应该重点观察什么现象?
  4. 如果想在 SOUTH 同时排列三个按钮,应该如何增加 JPanel 进行组合布局?

7.3 手写代码

禁止复制本章示例。

任务一:第一个窗口

从零创建:

标题:星雨笔录
宽:500
高:400
居中显示
关闭窗口退出程序

任务二:登录窗口

要求包含:

标题
用户名
密码
登录按钮
取消按钮
状态标签

要求至少使用:

JFrame
JPanel
JLabel
JTextField
JPasswordField
JButton

任务三:布局实验

分别使用:

FlowLayout
BorderLayout
GridLayout
BoxLayout

制作四个小窗口。

记录:

组件排列规律
窗口缩放表现
适合场景

任务四:事件监听

创建:

[增加]

按钮。

每点击一次:

计数:0
 ↓
计数:1
 ↓
计数:2
 ↓
计数:3

要求界面直接显示当前次数。


任务五:匿名内部类改 Lambda

先写:

new ActionListener() {
    @Override
    public void actionPerformed(
            ActionEvent e) {

        // ...
    }
}

确认理解以后,再改成:

e -> {
    // ...
}

不得直接跳过第一版。


7.4 Debug

下面代码为什么无法显示正常窗口?

public class GuiDebug01 {

    public static void main(String[] args) {

        JFrame frame =
                new JFrame("Demo");

        frame.setSize(400, 300);

        frame.setDefaultCloseOperation(
                JFrame.EXIT_ON_CLOSE
        );
    }
}

要求:

  1. 找出问题。
  2. 说明问题属于编译错误还是逻辑问题。
  3. 修复代码。

分析:

button.addActionListener(e -> {

    while (true) {
        System.out.println("running");
    }

});

回答:

  1. 点击按钮后 GUI 可能发生什么?
  2. 为什么?
  3. 与 EDT 有什么关系?
  4. 应该如何重新设计长时间任务?

分析:

JPanel panel =
        new JPanel(
                new GridLayout(2, 2)
        );

panel.add(new JButton("1"));
panel.add(new JButton("2"));
panel.add(new JButton("3"));
panel.add(new JButton("4"));
panel.add(new JButton("5"));

在运行前先预测:

布局结果是否一定还是你想象中的严格 2×2?

实际运行后记录观察结果,并查阅对应 API 行为解释原因。


7.5 综合训练

实现一个:

简单人事管理 GUI

暂时不要求数据库。

使用:

ArrayList<Employee>

保存员工。

Employee 至少包含:

id
name
gender
age
department

GUI 至少具有:

员工列表
姓名搜索
新增员工
删除员工

建议:

JFrame
 │
 ├── NORTH
 │     ├── JTextField
 │     ├── 搜索按钮
 │     └── 新增按钮
 │
 ├── CENTER
 │     └── JTable
 │
 └── SOUTH
       └── 状态信息

项目重点不是界面美化,而是:

集合数据
      ↓
GUI展示
      ↓
用户事件
      ↓
修改集合
      ↓
刷新GUI

理解数据与界面之间的关系。


7.6 本章验收

本章达到以下标准,可以视为掌握。

概念能力

不查看资料能够解释:

GUI
CLI
AWT
Swing
JFrame
JPanel
布局管理器
事件驱动
事件源
事件对象
监听器
EDT

手写能力

能够独立写出:

JFrame
JPanel
JButton
JTextField
JLabel

并完成:

创建
添加
布局
显示
监听

事件能力

看到:

button.addActionListener(
        e -> doSomething()
);

能够立刻解释:

button
→ 事件源

点击
→ ActionEvent

ActionListener
→ 监听器

doSomething()
→ 实际业务逻辑

布局能力

能够根据需求选择:

FlowLayout
BorderLayout
GridLayout
BoxLayout

而不是所有界面:

setLayout(null);

然后疯狂:

setBounds(...)

综合能力

关闭本教程和 AI 自动补全,从空项目开始完成:

一个具有用户名、密码、登录、取消按钮和状态提示的 Swing 登录窗口。

并能将:

匿名内部类监听器

改写成:

Lambda监听器

最后能够完整口述:

用户点击按钮
      ↓
组件产生事件
      ↓
事件系统通知监听器
      ↓
监听器执行回调
      ↓
业务代码改变状态
      ↓
GUI更新

如果这条链路能够真正解释清楚,就已经具备进入石头迷阵 GUI 综合项目的基础。