Skip to content

S14-02 GUI-Swing

[TOC]

Swing 概述

javax.swing 是 Java 编程语言中用于构建图形用户界面(GUI)的核心工具包。它是 Java 基础类(Java Foundation Classes, JFC)的一部分,旨在为开发者提供一套跨平台、功能丰富且高度可定制的 UI 组件

相比于早期的 java.awt(AWT)包,Swing 带来了革命性的改进,至今仍在许多企业级桌面应用、内部管理系统和工具软件中被广泛使用。

核心特点

核心特点

  1. 轻量级组件

    • AWT重量级的,它的组件依赖于底层操作系统的原生窗口部件(Peer)。这意味着在 Windows 上运行像 Windows,在 Mac 上运行像 Mac,且功能受限于各平台共有功能的“最大公约数”。
    • Swing轻量级的,绝大多数组件完全由 Java 代码在空白画布上“绘制”出来。它不依赖操作系统的原生组件,因此在所有平台上都能保证行为和外观的高度一致。
  2. 可插入的外观感觉

    Swing 允许程序在运行时动态切换界面的视觉风格。你可以让应用在 Windows 上展示 Windows 皮肤,在 Linux 上展示 Metal 或 Nimbus 皮肤,甚至可以使用第三方皮肤库(如 FlatLaf)。

  3. 基于 MVC 架构

    Swing 组件采用了模型-视图-控制器(MVC)设计模式的变体。组件的数据(Model)与它的视觉展现(View/Controller)是分离的。例如,JTable 的数据存储在 TableModel 中,这使得处理复杂数据和界面更新变得非常清晰。

体系结构

Swing 的组件层次结构非常清晰,几乎所有的 Swing 组件都继承自 JComponent(而 JComponent 又继承自 AWT 的 ContainerComponent)。

顶层容器

这些容器是不能嵌套在其他容器内的独立窗口,是 Swing 应用程序的基石:

  • JFrame:最常用的标准主窗口(带有最大化、最小化、关闭按钮)。
  • JDialog对话框窗口,通常用于弹出提示、输入信息或确认操作。

中间容器

这些容器用于组织和容纳其他基础组件,不能独立存在,必须放入顶层容器中

  • JPanel:最基础的空白面板,主要用于复杂的布局嵌套。
  • JScrollPane带滚动条的面板,用于包裹内容超出屏幕的组件(如大文本框、表格)。
  • JSplitPane拆分面板,将空间分为两部分,用户可拖动分隔条调整大小。
  • JTabbedPane选项卡面板,允许用户在多个标签页之间切换。

原子组件

直接与用户交互的独立 UI 元素:

组件类别常用类名用途描述
文本控制JLabel / JTextField / JTextArea显示文本、单行输入、多行输入
按钮触发JButton / JCheckBox / JRadioButton普通按钮、复选框、单选按钮
列表选择JComboBox / JList下拉选择框、列表选择框
高级数据JTable / JTree / JProgressBar表格数据展示、树状结构、进度条

布局管理器

Swing 中不建议使用绝对坐标(像素点)来定位组件,因为在窗体缩放或跨平台时界面会错位。Swing 通过布局管理器来自动调整组件的大小和位置:

  • FlowLayout流式布局。组件从左到右、从上到下依次排列,是 JPanel默认布局
  • BorderLayout边界布局。将容器划分为东、西、南、北、中五个区域,JFrame默认布局
  • GridLayout网格布局。将空间划分为等大小的行和列。
  • BoxLayout箱式布局。允许组件在单一轴线(垂直或水平)上线性排列。
  • GridBagLayout:最强大但也最复杂的布局,允许组件跨越多个行或列,并能精细控制缩放权值。

事件处理机制

Swing 采用事件监听器模型(Delegation Event Model)。当用户触发某个操作(如点击按钮)时,组件会产生一个事件对象,并将其发送给注册的监听器。

常用的监听器包括

  • ActionListener:处理按钮点击、菜单选择或文本框回车。
  • MouseListener / MouseMotionListener:处理鼠标点击、进入、拖拽等。
  • KeyListener:处理键盘按键输入。

非线程安全

⚠️ 核心铁律:Swing 是非线程安全的。

所有与 Swing 组件状态相关的修改(如更新文本、刷新表格、显示窗口)都必须在事件派发线程(Event Dispatch Thread, EDT) 中执行。

  • 启动应用时:必须将窗体的创建和显示打包送入 EDT。
  • 耗时操作时:如果点击按钮后需要进行网络请求或查询数据库,绝对不能在 EDT 中直接运行,否则界面会卡死。应该使用 SwingWorker 或另开线程处理,处理完后再通过 SwingUtilities.invokeLater 回到 EDT 更新界面。

注意事项

严禁在主线程(Main Thread)或自定义工作线程中直接调用 setVisible(true) 或改变组件状态,否则在多线程高并发访问时极易导致界面死锁或渲染紊乱。必须通过 SwingUtilities.invokeLater() 将任务投递至 EDT。

java
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class JFrameCoreLauncher {
  public static void main(String[] args) {
    // 正确的 EDT 启动方式
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        JFrame frame = new JFrame("EDT 规范启动窗体");
        frame.setSize(400, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
      }
    });
  }
}

示例:标准 Swing 应用

下面是一个标准的、符合线程规范的 Swing 应用程序示例:

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

public class SwingDemo {
  public static void main(String[] args) {
    // 确保 GUI 在事件派发线程(EDT)中创建
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        createAndShowGUI();
      }
    });
  }

  private static void createAndShowGUI() {
    // 1. 创建主窗口
    JFrame frame = new JFrame("Swing Demo");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 点击关闭时退出程序
    frame.setSize(400, 200); // 设置初始大小
    frame.setLocationRelativeTo(null); // 窗口居中显示

    // 2. 设置布局和组件
    frame.setLayout(new FlowLayout());

    JLabel label = new JLabel("欢迎来到 Java Swing 世界!");
    JButton button = new JButton("点击我");

    // 3. 为按钮添加点击事件监听器
    button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        label.setText("你好!按钮已被点击。");
      }
    });

    // 4. 将组件添加到窗口中
    frame.add(label);
    frame.add(button);

    // 5. 让窗口可见
    frame.setVisible(true);
  }
}

JFrame~

在 Java Swing Soft 界面开发中,javax.swing.JFrame 是最核心的组件之一。它是一个顶层容器(Top-Level Container),代表了操作系统中的一个独立主窗口(带有标题栏、最小化、最大化和关闭按钮)。

几乎所有独立的 Swing 桌面应用程序都需要以 JFrame 作为整个界面的“地基”。

内部结构

理解 JFrame 的关键在于:你不能直接把组件(如按钮、文本框)直接扔进 JFrame 的最外层。 JFrame 内部是由一个 JRootPane(根面板)管理的复杂结构,它包含以下几个层次:

  • 根面板 (JRootPane):管理所有其他面板的顶级容器。
  • 玻璃面板 (Glass Pane):最顶层,默认透明。常用于拦截鼠标事件或绘制全局动画。
  • 分层面板 (Layered Pane):允许组件在 Z 轴上叠加。
  • 内容面板 (Content Pane)最重要! 这是我们放置业务组件(按钮、标签、面板等)的实际地方。
  • 菜单栏 (Menu Bar):可选,位于窗口顶部。

💡 历史小贴士: 在旧版本的 Java(Java 1.4 及以前)中,必须显式调用 frame.getContentPane().add(component)。从 Java 5 开始,为了简化代码,直接调用 frame.add(component) 会在底层自动重定向到内容面板中。

API:JFrame

构造方法
  • JFrame JFrame()()无参构造方法。创建一个初始不可见、无标题的全新窗体。适用于后续动态计算并赋值标题的场景。

  • JFrame JFrame()(String title)带标题构造方法。创建一个指定标题且初始不可见的窗体。此方法是最常用的窗体初始化方式。

    java
    // 1. 调用无参构造函数后设置标题
    JFrame standardFrame = new JFrame();
    standardFrame.setTitle("动态标题窗体");
    
    // 2. 直接通过构造函数指定标题
    JFrame titledFrame = new JFrame("静态标题窗体");
窗口生命周期与显示控制
  • void setVisible()(boolean b)设置窗体可见性。参数为 true 时,底层宿主系统为其分配对等体(Peer)并将其显示在屏幕上;参数为 false 时执行隐藏。

  • void pack()()调整大小以适应组件。使当前窗体的大小刚好适合其子组件的首选大小(Preferred Size)和整体布局约束。

  • void setSize()(int width, int height)设置窗体大小。调整此组件的大小,使其宽度为 width,高度为 height。单位为像素。

  • void dispose()()释放窗体资源。销毁当前的 JFrame 实例,释放其占用的所有本机屏幕资源及其子组件的内存占用。

注意事项

调用 pack() 方法时,如果内容面板中没有任何组件,或者组件没有定义首选大小,窗体可能会缩减为一个极小的像素点。因此,通常应在向内容面板添加完所有子组件之后、调用 setVisible(true) 之前调用 pack()

java
JFrame controlFrame = new JFrame("控制窗体");
// 模拟添加组件后进行紧凑布局优化
controlFrame.pack();
controlFrame.setVisible(true);

// 在特定事件触发时(如任务结束),销毁窗口释放资源
controlFrame.dispose();
布局与组件管理
  • Container getContentPane()()获取内容面板。返回该窗体的 Container 对象,以便执行组件添加和布局更改。

  • void setContentPane()(Container contentPane)设置内容面板。用一个新的容器替换当前 JFrame 的默认内容面板。

  • Component add()(Component comp)添加组件。自 JDK 1.5 起,直接在 JFrame 实例上调用 add() 会被自动转发至内容面板,等价于 getContentPane().add(comp)

    java
    JFrame layoutFrame = new JFrame("布局示例");
    
    // 方式一:直接使用转发机制添加组件(推荐)
    layoutFrame.add(new JButton("简易添加"));
    
    // 方式二:显式通过获取内容面板进行细粒度配置
    Container panel = layoutFrame.getContentPane();
    panel.setBackground(Color.WHITE);
窗口属性与行为配置
  • void setDefaultCloseOperation()(int operation)设置默认关闭操作。规范用户点击右上角 "X" 关闭按钮时的系统行为。常用常量包括 JFrame.EXIT_ON_CLOSE(退出程序)、JFrame.DO_NOTHING_ON_CLOSE(不执行任何操作,通常交由监听器处理)。

  • void setLocationRelativeTo()(Component c)设置相对位置。将当前窗口相对于指定组件居中。若参数传入 null,则窗口将自动在当前屏幕的物理中央位置显示。

  • void setResizable()(boolean resizable)设置是否允许调整大小。控制用户是否能够通过拖拽边框来改变窗口的尺寸。

注意事项

  1. setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) 会在窗口关闭时直接调用 System.exit(0)。如果当前应用程序是多窗口应用,对其中一个子窗口使用该配置会导致整个应用程序彻底终止。在多窗口应用中,子窗口应当使用 JFrame.DISPOSE_ON_CLOSE
  2. setLocationRelativeTo(null) 必须在 setSize()pack() 之后调用。如果在确定窗口尺寸之前调用,系统无法正确计算居中偏移量,导致窗口出现定位偏差。
java
JFrame configFrame = new JFrame("配置示例");

configFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
configFrame.setSize(800, 600);

// 必须在 setSize 之后调用,保证居中计算准确
configFrame.setLocationRelativeTo(null);
// 锁定窗口尺寸,禁止用户拉伸边框
configFrame.setResizable(false);

四种关闭操作

当用户点击窗口右上角的 “X” 关闭按钮时,JFrame 默认不会退出程序,而只是隐藏窗口。你需要通过 setDefaultCloseOperation(int operation) 来明确指定其行为,operation 参数选项值如下:

常量名称行为描述适用场景
JFrame.DO_NOTHING_ON_CLOSE什么都不做。点击关闭按钮无效。需要弹出提示框询问“是否保存”的场景。
JFrame.HIDE_ON_CLOSE(默认值) 仅仅隐藏窗口,程序继续在后台运行。主界面不关闭,只关闭临时弹窗
JFrame.DISPOSE_ON_CLOSE销毁当前窗口,释放资源。如果它是最后一个窗口,程序可能退出。多窗口应用中的子窗口。
JFrame.EXIT_ON_CLOSE直接终止整个 Java 虚拟机(JVM)进程。应用程序的主窗口
java
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class JFrameLifecycleDemo {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
      JFrame frame = new JFrame("生命周期控制窗体");
      frame.setSize(400, 200);

      // 拦截关闭行为,设置为不做任何事,改由监听器接管
      frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

      frame.addWindowListener(new WindowAdapter() {
        @Override
        public void windowClosing(WindowEvent e) {
          int option = JOptionPane.showConfirmDialog(
              frame, "确定要退出系统吗?", "提示", JOptionPane.YES_NO_OPTION
          );
          if (option == JOptionPane.YES_OPTION) {
            frame.dispose(); // 销毁窗体资源
            System.exit(0); // 安全退出 JVM
          }
        }
      });

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

布局管理器

JFrame 的内容面板默认采用的是 BorderLayout(边界布局)

这意味着如果你连续直接向 JFrame 添加组件,新添加的组件会把旧的完全覆盖

  • 错误写法:直接 frame.add(button1); frame.add(button2);(只会看到 button2 填满整个窗口)。
  • 正确做法:指定方位(如 BorderLayout.NORTH),或者先将组件放入一个采用 FlowLayoutGridLayoutJPanel 中,再把该 JPanel 放入 JFrame

示例:标准 JFrame 应用

在实际开发中,推荐通过继承 JFrame 的方式来构建自定义窗口,这样代码结构更清晰、更易扩展。

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

public class MyMainFrame extends JFrame {

  public MyMainFrame() {
    // 1. 初始化窗口基本属性
    setTitle("企业管理系统 v1.0");
    setSize(500, 400);
    setLocationRelativeTo(null); // 居中
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 点击X退出程序

    // 2. 初始化并添加组件
    initComponents();
  }

  private void initComponents() {
    // 创建一个中间容器,默认是 FlowLayout
    JPanel panel = new JPanel();

    JLabel label = new JLabel("用户名:");
    JTextField textField = new JTextField(15);
    JButton loginButton = new JButton("登录");

    // 将组件组装到面板中
    panel.add(label);
    panel.add(textField);
    panel.add(loginButton);

    // 将面板放入 JFrame 的内容面板中(默认放中央)
    this.add(panel, BorderLayout.CENTER);
  }

  public static void main(String[] args) {
    // 安全地在事件派发线程(EDT)中启动 GUI
    SwingUtilities.invokeLater(() -> {
      MyMainFrame frame = new MyMainFrame();
      frame.setVisible(true); // 展现窗口
    });
  }
}

SwingUtilities~

javax.swing.SwingUtilities 是一个完全由 static(静态)方法组成的工具类。它不用于创建具体的 UI 组件,而是提供了一系列核心的辅助方法,用于解决 Swing 开发中最棘手的问题:线程安全、组件树遍历、坐标转换以及鼠标事件判断

EDT 线程调度

Swing 的核心设计遵循单线程模型(Single-Threaded Model)。除少数例外情况外,所有 Swing 组件的创建、状态修改和绘制逻辑都必须在唯一的事件派发线程(Event Dispatch Thread, EDT) 中顺序执行。

当非 EDT 线程(如后台工作线程、定时任务线程)直接操作 UI 组件时,会破坏组件内部的状态一致性,引发不可预知的画面撕裂或死锁。SwingUtilities 的核心原理在于充当了工作线程与事件队列(EventQueue)之间的通信桥梁。它通过将封装了 UI 操作的 Runnable 任务封装为 AWTEvent,投递到系统的中央事件队列中,由 EDT 线程异步或同步地取出并执行,从而确保多线程环境下 UI 操作的绝对安全性。

java
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import java.awt.FlowLayout;

public class SwingEDTPrincipleDemo {
  public static void main(String[] args) {
    // 在 EDT 中初始化界面
    SwingUtilities.invokeLater(() -> {
      JFrame frame = new JFrame("EDT 核心原理演示");
      JLabel label = new JLabel("等待后台线程更新...");
      frame.setLayout(new FlowLayout());
      frame.add(label);
      frame.setSize(300, 200);
      frame.setLocationRelativeTo(null);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setVisible(true);

      // 启动一个非 EDT 的工作线程
      new Thread(() -> {
        try {
          Thread.sleep(2000); // 模拟耗时计算
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        // 检查当前是否在 EDT 线程中
        boolean isEDTBefore = SwingUtilities.isEventDispatchThread();
        System.out.println("直接在工作线程中更新前,是否为EDT: " + isEDTBefore);

        // 错误示范:直连修改 label.setText(text) 会导致线程安全隐患

        // 正确做法:使用 invokeLater 桥接回 EDT
        SwingUtilities.invokeLater(() -> {
          boolean isEDTAfter = SwingUtilities.isEventDispatchThread();
          System.out.println("通过 invokeLater 回调内部,是否为EDT: " + isEDTAfter);
          label.setText("工作线程更新成功!");
        });
      }).start();
    });
  }
}

API:SwingUtilities

EDT 线程调度
  • void SwingUtilities.invokeLater()(Runnable doRun)异步推入事件队列。将任务排入事件派发线程(EDT)的队列中,在处理完所有挂起的事件后异步执行该任务。常用于工作线程需要非阻塞地更新 UI 的场景。

  • void SwingUtilities.invokeAndWait()(Runnable doRun)同步挂起执行:导致任务在 EDT 中执行,当前调用线程将会被阻塞挂起,直到任务在 EDT 中执行完毕。适用于工作线程必须等待 UI 更新完成或获取 UI 数据后才能继续后续逻辑的场景。

  • boolean SwingUtilities.isEventDispatchThread()()判定当前线程。如果当前调用此方法的线程是事件派发线程(EDT),则返回 true,否则返回 false。常用于防御性编程,确保代码在预期的线程环境中运行。

注意事项

  1. 严禁在 EDT 线程内直接调用 SwingUtilities.invokeAndWait()。因为该方法会阻塞调用线程并等待 EDT 完成任务,若此时调用线程本身就是 EDT,将会直接导致永久性死锁(Deadlock)
  2. SwingUtilities.invokeAndWait() 会抛出 InterruptedExceptionInvocationTargetException 编译期异常,调用时必须显式捕获或向上抛出。
java
import javax.swing.SwingUtilities;
import java.lang.reflect.InvocationTargetException;

public class SwingUtilitiesThreadApiDemo {
  public static void testThreadingMethods() {
    // 1. 异步提交 UI 任务
    SwingUtilities.invokeLater(() -> {
      System.out.println("异步任务在工作:" + SwingUtilities.isEventDispatchThread());
    });

    // 2. 在非 EDT 线程中同步提交任务
    new Thread(() -> {
      try {
        System.out.println("后台线程准备同步挂起...");
        SwingUtilities.invokeAndWait(() -> {
          System.out.println("同步 UI 任务执行中...");
        });
        System.out.println("后台线程继续向下执行。");
      } catch (InterruptedException | InvocationTargetException e) {
        e.printStackTrace();
      }
    }).start();
  }
}
组件树遍历
  • Window SwingUtilities.windowForComponent()(Component c)获取宿主窗口。返回组件 c 当前所在的顶级 Window(可能是 JFrameJDialog 等)。如果该组件未包含在任何窗口内部,则返回 null

  • Component SwingUtilities.getRoot()(Component c)获取根组件。遍历组件层次树,返回该组件树顶层的第一个 AWT 组件(通常为 WindowApplet)。

  • Container SwingUtilities.getAncestorOfClass()(Class<?> c, Component comp)按类型查找祖先。在组件 comp 的父级架构上溯查找,返回指定类 c 或其子类的第一个容器实例。常用于在深层嵌套的子组件中寻找特定的父面板。

注意事项

在组件尚未通过 add() 添加到容器中,或者容器尚未调用 setVisible(true) / pack() 从而未与底层原生系统关联(即未创建 Peer 对等体)前,上述查找方法可能返回 null

java
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.SwingUtilities;
import java.awt.Container;
import java.awt.Window;

public class SwingUtilitiesHierarchyDemo {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
      JFrame frame = new JFrame("层次遍历示例");
      JPanel mainPanel = new JPanel();
      JButton internalButton = new JButton("测试查找按钮");

      mainPanel.add(internalButton);
      frame.add(mainPanel);
      frame.setSize(300, 200);
      frame.setVisible(true);

      // 1. 查找按钮所在的顶级 Window
      Window hostWindow = SwingUtilities.windowForComponent(internalButton);
      System.out.println("宿主窗口标题: " + ((JFrame) hostWindow).getTitle());

      // 2. 查找最上层根组件
      Component root = SwingUtilities.getRoot(internalButton);
      System.out.println("根组件类型: " + root.getClass().getName());

      // 3. 向上搜寻特定类类型的容器
      Container ancestor = SwingUtilities.getAncestorOfClass(JPanel.class, internalButton);
      System.out.println("查找到的父 JPanel 是否匹配: " + (ancestor == mainPanel));
    });
  }
}
坐标转换
  • Point SwingUtilities.convertPoint()(Component source, Point aPoint, Component destination)点坐标转换。将 source 组件坐标系下的一个 Point 绝对坐标转换为 destination 组件坐标系下的对应坐标。如果 sourcedestinationnull,则代表以屏幕左上角作为视口坐标系。

  • void SwingUtilities.convertRectangle()(Component source, Rectangle aRectangle, Component destination)矩形区域转换。修改传入的 aRectangle,将其空间范围从 source 组件坐标系映射至 destination 组件坐标系中。

注意事项

进行坐标转换的两个组件必须同时处于同一可见的顶级窗口上下文中。如果其中一个组件未显示或未处于活动组件树中,转换出来的物理坐标将失去物理参考价值。

java
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.SwingUtilities;
import java.awt.Point;
import java.awt.Rectangle;

public class SwingUtilitiesCoordinateDemo {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
      JFrame frame = new JFrame("坐标转换示例");
      JPanel containerPanel = new JPanel();
      JButton childButton = new JButton("坐标源");

      containerPanel.setLayout(null); // 绝对布局以便测试
      childButton.setBounds(50, 50, 100, 30);
      containerPanel.add(childButton);
      frame.add(containerPanel);
      frame.setSize(400, 300);
      frame.setVisible(true);

      // 1. 转换按钮内 (0, 0) 点到父面板 containerPanel 的相对坐标
      Point pointInButton = new Point(0, 0);
      Point pointInPanel = SwingUtilities.convertPoint(childButton, pointInButton, containerPanel);
      System.out.println("按钮原点在父面板中的实际坐标: " + pointInPanel);

      // 2. 转换矩形区域到顶级框架
      Rectangle rectInButton = new Rectangle(0, 0, 100, 30);
      SwingUtilities.convertRectangle(childButton, rectInButton, frame);
      System.out.println("按钮在顶级窗口框架中的物理矩形区域: " + rectInButton);
    });
  }
}

实战:多线程任务进度条更新

本案例综合展示了在大型或复杂的 GUI 程序中,如何结合后台线程与 SwingUtilities 维持界面的响应性。本案例使用后台线程模拟计算密集型任务,并通过 SwingUtilities.invokeLater 安全地将计算结果及状态实时同步至前端界面的 JProgressBar(进度条)和 JLabel(文本标签)中。

java
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JProgressBar;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class TaskProgressMonitorApp {
  private JFrame mainFrame;
  private JProgressBar progressBar;
  private JLabel statusLabel;
  private JButton startButton;

  public static void main(String[] args) {
    // 严格在事件派发线程中初始化整体 UI
    SwingUtilities.invokeLater(() -> new TaskProgressMonitorApp().buildGUI());
  }

  public void buildGUI() {
    // 初始化主窗体
    mainFrame = new JFrame("高阶线程安全任务监控器");
    mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    mainFrame.setSize(450, 180);
    mainFrame.setLocationRelativeTo(null);

    // 初始化交互与数据展示组件
    progressBar = new JProgressBar(0, 100);
    progressBar.setStringPainted(true);
    statusLabel = new JLabel("系统就绪,等待执行任务...", JLabel.CENTER);
    startButton = new JButton("启动异步核心任务");

    // 构建中心控制区域
    JPanel centerPanel = new JPanel(new GridLayout(2, 1, 5, 5));
    centerPanel.add(statusLabel);
    centerPanel.add(progressBar);

    // 装配至内容面板
    mainFrame.getContentPane().add(centerPanel, BorderLayout.CENTER);
    mainFrame.getContentPane().add(startButton, BorderLayout.SOUTH);

    // 挂载按钮交互事件
    startButton.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        // 冻结当前按钮,防止用户重复提交任务
        startButton.setEnabled(false);
        statusLabel.setText("正在准备计算环境...");

        // 派生独立工作线程执行耗时操作,绝对不能阻塞 EDT 线程
        executeBackgroundProcess();
      }
    });

    mainFrame.setVisible(true);
  }

  private void executeBackgroundProcess() {
    new Thread(() -> {
      try {
        for (int progressValue = 1; progressValue <= 100; progressValue++) {
          // 模拟密集型数学计算或 IO 网络延时
          Thread.sleep(50);

          final int currentProgress = progressValue;

          // 核心关键点:必须使用 invokeLater 将组件数据刷新逻辑桥接回 EDT
          SwingUtilities.invokeLater(() -> {
            // 此处处于 EDT 内部,可安全操作所有 Swing 基础组件
            progressBar.setValue(currentProgress);
            statusLabel.setText("任务正在进行中,已完成: " + currentProgress + "%");
          });
        }

        // 任务彻底执行完毕后,执行收尾 UI 恢复
        SwingUtilities.invokeLater(() -> {
          statusLabel.setText("恭喜!所有后台数据处理完毕。");
          startButton.setEnabled(true);
        });

      } catch (InterruptedException ex) {
        SwingUtilities.invokeLater(() -> {
          statusLabel.setText("任务不幸被异常中断。");
          startButton.setEnabled(true);
        });
      }
    }).start();
  }
}