JavaSwingGUI从小白到大神-5
背景
首先本文是一个系列的文章,起名为《Java Swing GUi从小白到大神》,主要介绍Java Swing相关概念和实践应用。目前JavaSwingGUI很少作为界面的开发框架,但是对于学习Java体系,以及新手将来学习后端之后,能快速从前后端整体去理解一套系统是非常有帮助的,所以是很有必要学习和掌握的。本篇是系列博文的第五篇,若想详细学习请点击首篇博文开始,让我们开始吧。
文章概览
- Swing与并发
 - Swing模型架构
 - Swing的其他特性
 
15. Swing与并发
1. SwingUtilities类介绍
先了解一下核心类SwingUtilities 是一个实用类,包含与 Swing GUI 相关的静态方法,以下是其三个常用方法及说明:
SwingUtilities 类的 3 个常用方法
- invokeLater(Runnable doRun)
 
- 作用:将一个任务(Runnable)调度到 事件分派线程(EDT) 上执行。
 - 用途:确保所有的 GUI 操作在 EDT 上运行,避免线程安全问题。
 - 示例:
 
SwingUtilities.invokeLater(() -> {
    JFrame frame = new JFrame("Example");
    frame.setSize(300, 200);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setVisible(true);
});- invokeAndWait(Runnable doRun)
 
- 作用:将一个任务调度到 EDT 上执行,并等待该任务完成。
 - 用途:用于需要在后台线程中执行某些 GUI 操作并确保同步完成。
 - 注意:如果当前线程已经是 EDT,调用此方法会导致 IllegalStateException。
 - 示例:
 
try {
    SwingUtilities.invokeAndWait(() -> System.out.println("Running on EDT"));
} catch (Exception e) {
    e.printStackTrace();
}- isEventDispatchThread()
 
- 作用:检查当前线程是否是 EDT。
 - 用途:在执行线程相关逻辑前验证线程类型,避免非线程安全操作。
 - 示例:
 
if (SwingUtilities.isEventDispatchThread()) {
    System.out.println("Running on EDT");
} else {
    SwingUtilities.invokeLater(() -> System.out.println("Switch to EDT"));
}2. wingWorker类介绍
SwingWorker 是一个抽象类,用于在后台线程中执行耗时任务,同时支持在事件分派线程中更新 GUI。
常用方法:
- doInBackground()
 - 作用:定义后台任务的主逻辑。
 - 运行线程:后台线程。
 - 返回值:任务完成后返回的结果(SwingWorker 的泛型参数 V)。
 - 示例:
 
@Override
protected Void doInBackground() {
    for (int i = 0; i < 100; i++) {
        setProgress(i);
        Thread.sleep(100);
    }
    return null;
}- publish(V... chunks)
 
- 作用:在后台线程中发布中间结果。
 - 运行线程:后台线程。
 - 用途:将中间结果发送给 process() 方法。
 - 示例:
 
publish(currentProgress);- process(List
chunks)  
- 作用:接收由 publish() 方法发送的中间结果。
 - 运行线程:事件分派线程(EDT)。
 - 用途:用于更新 GUI。
 - 示例:
 
@Override
protected void process(List chunks) {
    progressBar.setValue(chunks.get(chunks.size() - 1));
} - done()
 
- 作用:后台任务完成后调用。
 - 运行线程:事件分派线程(EDT)。
 - 用途:完成任务后的清理或结果处理。
 - 示例:
 
@Override
protected void done() {
    System.out.println("Task Completed");
}- setProgress(int progress)
 
- 作用:设置任务进度(范围 0-100)。
 - 运行线程:后台线程。
 - 用途:用于更新 progress 属性,并通过监听器通知。
 - 示例:
 
setProgress(50);- get()
 
- 作用:获取 doInBackground() 返回的结果。
 - 运行线程:任何线程。
 - 用途:阻塞线程直到任务完成,适用于获取计算结果。
 - 示例:
 
try {
    String result = myWorker.get();
} catch (Exception e) {
    e.printStackTrace();
}- execute()
 
- 作用:启动后台任务。
 - 运行线程:调用线程。
 - 用途:创建任务后,调用此方法开始执行。
 - 示例:
 
myWorker.execute();- cancel(boolean mayInterruptIfRunning)
 
- 作用:取消任务。
 - 运行线程:任何线程。
 - 用途:设置任务为取消状态,可选择是否中断正在运行的任务。
 - 示例:
 
myWorker.cancel(true);- isCancelled()
 
- 作用:检查任务是否已被取消。
 - 运行线程:任何线程。
 - 用途:在后台任务中动态检测取消状态。
 - 示例:
 
if (isCancelled()) break;- addPropertyChangeListener(PropertyChangeListener listener)
 
- 作用:添加属性变化监听器,监听 progress 或 state 的变化。
 - 用途:实现动态界面更新。
 - 示例:
 
myWorker.addPropertyChangeListener(evt -> {
    if ("progress".equals(evt.getPropertyName())) {
        progressBar.setValue((int) evt.getNewValue());
    }
});3. 事件分派线程
事件分派线程(Event Dispatch Thread, 简称 EDT) ,是 Swing 的核心线程之一,它专门负责处理事件队列中的任务,例如用户交互事件(鼠标点击、键盘输入等)、界面更新、绘制组件等。
事件分派线程的特点:
- 唯一性:在运行时,应用程序中只有一个事件分派线程,它是单一的线程模型。
 - Swing 操作必须在 EDT 上执行:所有涉及到 Swing 组件的操作(例如更新界面、添加组件)都需要在 EDT 上执行。这是因为 Swing 不是线程安全的,多线程访问可能导致 UI 渲染异常或线程竞争问题。
 - 自动启动:当 Swing 应用程序启动时(例如通过 SwingUtilities.invokeLater 或显示 JFrame),EDT 会自动创建。
 
事件分派线程的工作流程:
- 任务分派:EDT 维护一个任务队列,任务可以通过 SwingUtilities.invokeLater() 或 SwingUtilities.invokeAndWait() 提交到队列中。
 - 任务处理:EDT 按顺序处理队列中的任务。
 - 用户事件处理:当用户触发事件(例如鼠标点击、按键),这些事件会被放入任务队列,由 EDT 依次处理。
 
如何检测当前线程是否是事件分派线程:
可以通过 
SwingUtilities.isEventDispatchThread() 方法检查当前线程是否为事件分派线程。
示例:
if (SwingUtilities.isEventDispatchThread()) {
    System.out.println("Running on Event Dispatch Thread");
} else {
    System.out.println("Not on Event Dispatch Thread");
}如何确保代码运行在事件分派线程上:
如果代码需要操作 Swing 组件但可能运行在非 EDT 的线程中,应该使用以下方法将任务调度到 EDT:
- SwingUtilities.invokeLater(Runnable)
 
- 将任务提交给 EDT 异步执行,不阻塞调用线程。
 - 示例:
 
SwingUtilities.invokeLater(() -> {
    JLabel label = new JLabel("Hello, Swing!");
    frame.add(label);
});- SwingUtilities.invokeAndWait(Runnable)
 
- 将任务提交给 EDT 并等待其完成(阻塞调用线程)。
 - 示例:
 
try {
    SwingUtilities.invokeAndWait(() -> {
        JLabel label = new JLabel("Hello, Swing!");
        frame.add(label);
    });
} catch (Exception e) {
    e.printStackTrace();
}Swing多线程
public class ArithmeticAccumulatorDemo {
    private JFrame frame;
    private JLabel resultLabel, progressLabel;
    private JButton startButton, showResultButton, pauseButton, resumeButton, resetButton;
    private SwingWorker arithmeticWorker;
    private volatile boolean isPaused = false;
    private int accumulator = 0;
    public ArithmeticAccumulatorDemo() {
        // 创建主窗口
        frame = new JFrame("Arithmetic Accumulator Demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(500, 350);
        frame.setLayout(new BorderLayout());
        // 顶部: 显示累加器结果和进度
        JPanel displayPanel = new JPanel(new GridLayout(2, 1));
        resultLabel = new JLabel("Current Value: 0", SwingConstants.CENTER);
        resultLabel.setFont(new Font("Arial", Font.BOLD, 24));
        progressLabel = new JLabel("Progress: N/A", SwingConstants.CENTER);
        progressLabel.setFont(new Font("Arial", Font.PLAIN, 16));
        displayPanel.add(resultLabel);
        displayPanel.add(progressLabel);
        frame.add(displayPanel, BorderLayout.CENTER);
        // 底部: 按钮面板
        JPanel buttonPanel = new JPanel(new FlowLayout());
        startButton = new JButton("Start Accumulator");
        showResultButton = new JButton("Show Interim Result");
        pauseButton = new JButton("Pause");
        resumeButton = new JButton("Resume");
        resetButton = new JButton("Reset Accumulator");
        buttonPanel.add(startButton);
        buttonPanel.add(showResultButton);
        buttonPanel.add(pauseButton);
        buttonPanel.add(resumeButton);
        buttonPanel.add(resetButton);
        frame.add(buttonPanel, BorderLayout.SOUTH);
        // 按钮事件
        startButton.addActionListener(e -> startAccumulator());
        showResultButton.addActionListener(e -> showInterimResult());
        pauseButton.addActionListener(e -> pauseAccumulator());
        resumeButton.addActionListener(e -> resumeAccumulator());
        resetButton.addActionListener(e -> resetAccumulator());
        frame.setVisible(true);
    }
    // 启动累加器
    private void startAccumulator() {
        if (arithmeticWorker != null && !arithmeticWorker.isDone()) {
            JOptionPane.showMessageDialog(frame, "Accumulator is already running!", "Warning",
                    JOptionPane.WARNING_MESSAGE);
            return;
        }
        accumulator = 0; // 重置累加器
        isPaused = false;
        arithmeticWorker = new SwingWorker() {
            @Override
            protected Void doInBackground() throws Exception {
                while (!isCancelled()) {
                    if (!isPaused) {
                        accumulator++;
                        publish(accumulator); // 发布中间结果
                        setProgress(Math.min(accumulator, 100)); // 设置进度
                        Thread.sleep(500); // 模拟计算延迟
                    }
                }
                return null;
            }
            @Override
            protected void process(List chunks) {
                // 获取最后一个中间结果
                int latestResult = chunks.get(chunks.size() - 1);
                resultLabel.setText("Current Value: " + latestResult);
            }
            @Override
            protected void done() {
                JOptionPane.showMessageDialog(frame, "Accumulator task completed or stopped.", "Info",
                        JOptionPane.INFORMATION_MESSAGE);
            }
        };
        // 绑定属性变化监听器
        arithmeticWorker.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if ("progress".equals(evt.getPropertyName())) {
                    progressLabel.setText("Progress: " + evt.getNewValue() + "%");
                } else if ("state".equals(evt.getPropertyName())) {
                    progressLabel.setText("State: " + evt.getNewValue());
                }
            }
        });
        arithmeticWorker.execute();
        JOptionPane.showMessageDialog(frame, "Accumulator started in the background.", "Info",
                JOptionPane.INFORMATION_MESSAGE);
    }
    // 显示临时结果
    private void showInterimResult() {
        if (arithmeticWorker == null || arithmeticWorker.isDone()) {
            JOptionPane.showMessageDialog(frame, "Accumulator is not running!", "Warning", JOptionPane.WARNING_MESSAGE);
        } else {
            resultLabel.setText("Current Value: " + accumulator);
        }
    }
    // 暂停累加器
    private void pauseAccumulator() {
        if (arithmeticWorker == null || arithmeticWorker.isDone()) {
            JOptionPane.showMessageDialog(frame, "Accumulator is not running!", "Warning", JOptionPane.WARNING_MESSAGE);
        } else if (isPaused) {
            JOptionPane.showMessageDialog(frame, "Accumulator is already paused!", "Warning",
                    JOptionPane.WARNING_MESSAGE);
        } else {
            isPaused = true;
            JOptionPane.showMessageDialog(frame, "Accumulator paused.", "Info", JOptionPane.INFORMATION_MESSAGE);
        }
    }
    // 恢复累加器
    private void resumeAccumulator() {
        if (arithmeticWorker == null || arithmeticWorker.isDone()) {
            JOptionPane.showMessageDialog(frame, "Accumulator is not running!", "Warning", JOptionPane.WARNING_MESSAGE);
        } else if (!isPaused) {
            JOptionPane.showMessageDialog(frame, "Accumulator is already running!", "Warning",
                    JOptionPane.WARNING_MESSAGE);
        } else {
            isPaused = false;
            JOptionPane.showMessageDialog(frame, "Accumulator resumed.", "Info", JOptionPane.INFORMATION_MESSAGE);
        }
    }
    // 重置累加器
    private void resetAccumulator() {
        if (arithmeticWorker != null && !arithmeticWorker.isDone()) {
            arithmeticWorker.cancel(true);
        }
        accumulator = 0;
        resultLabel.setText("Current Value: 0");
        progressLabel.setText("Progress: N/A");
        JOptionPane.showMessageDialog(frame, "Accumulator reset. You can start again.", "Info",
                JOptionPane.INFORMATION_MESSAGE);
    }
    public static void main(String[] args) {
        SwingUtilities.invokeLater(ArithmeticAccumulatorDemo::new);
    }
}   16. Swing模型架构
Swing模型架构描述:
Swing 是基于 Model-View-Controller (MVC) 架构设计的轻量级 GUI 框架,它将数据模型、显示和用户交互逻辑分离,使组件更加灵活和可扩展。以下是 Swing 模型架构的主要概念和重点内容:
1. Swing 的 MVC 模型
Swing 中的大多数组件都基于一个简化的 MVC 模型,称为 “可分离的模型架构”。以下是其核心组成部分:
- Model(数据模型)
数据模型用于存储组件的数据和状态,并提供修改和访问数据的方法。 - 负责数据管理。
 - 通过监听器模式通知视图和控制器数据的变化。
 - 组件的数据模型可以是默认实现(如 DefaultTableModel),也可以通过接口自定义。
 - View(视图)
视图负责呈现组件的数据和外观。 - 负责绘制组件的外观。
 - Swing 组件通常共享一个默认的视图。
 - Controller(控制器)
控制器处理用户的输入并将其映射到模型和视图。 - Swing 的事件处理机制(事件监听器)充当控制器。
 - 常用的控制器接口包括 ActionListener, MouseListener 等。
 
2. Swing 的 MVC 特点
- 轻量级组件
Swing 组件通过 Java 代码实现,几乎不依赖原生平台,具备跨平台一致性。 - 可分离模型
 - 数据模型(Model)独立于视图(View)和事件处理(Controller)。
 - 某些组件(如 JTable 和 JList)允许用户使用自定义模型。
 - 单线程模型
Swing 的 UI 更新需要在 事件分派线程 (EDT) 上完成,确保线程安全。 - 灵活可定制
通过模型和渲染器,开发者可以轻松自定义组件的外观和行为。 
3. Swing 核心组件的模型架构
组件  | 数据模型  | 默认实现类  | 用途  | 
JButton  | ButtonModel  | DefaultButtonModel  | 管理按钮状态(启用、选中等)。  | 
JLabel  | 无数据模型  | 无  | 仅显示文本或图像,无需数据模型。  | 
JTextField  | Document  | PlainDocument  | 管理输入框的文本内容。  | 
JTable  | TableModel  | DefaultTableModel  | 管理表格数据。  | 
JList  | ListModel  | DefaultListModel  | 管理列表项数据。  | 
JTree  | TreeModel  | DefaultTreeModel  | 管理树形结构数据。  | 
JComboBox  | ComboBoxModel  | DefaultComboBoxModel  | 管理组合框的选项。  | 
4. 重点内容学习
- 数据模型
 
- 学习数据模型接口的定义(如 TableModel, ListModel)。
 - 掌握如何使用默认实现类(如 DefaultTableModel)进行简单开发。
 - 了解如何创建自定义数据模型以满足特定需求。
 
- 渲染器和编辑器
 
- 渲染器用于定制 Swing 组件的显示样式(如单元格颜色、字体等)。通过实现 TableCellRenderer 或扩展 DefaultTableCellRenderer。
 - 编辑器用于处理用户输入(如文本框、组合框等)。通过实现 TableCellEditor 或扩展 DefaultCellEditor。
 
- 事件处理
 
- 掌握事件监听器接口(如 ActionListener, MouseListener)。
 - 理解事件传播机制,以及如何在事件分派线程中处理事件。
 
- 线程安全
 
- 了解 Swing 的单线程规则。
 - 使用 SwingUtilities.invokeLater() 确保 UI 操作在事件分派线程上执行。
 
- 自定义组件
 
- 学习如何结合模型、渲染器和编辑器创建复杂的自定义组件。
 
- 性能优化
 
- 对于动态数据量较大的组件(如 JTable),通过虚拟化模型和延迟加载优化性能。
 
MVC 使用 JTable
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
public class SwingMVCExample {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            // 创建数据模型 (Model)
            DefaultTableModel tableModel = new DefaultTableModel(new Object[]{"ID", "Name", "Age"}, 0);
            // 添加数据
            tableModel.addRow(new Object[]{1, "Alice", 25});
            tableModel.addRow(new Object[]{2, "Bob", 30});
            tableModel.addRow(new Object[]{3, "Charlie", 35});
            // 创建视图 (View)
            JTable table = new JTable(tableModel);
            // 添加事件监听器 (Controller)
            table.getSelectionModel().addListSelectionListener(e -> {
                if (!e.getValueIsAdjusting()) {
                    int selectedRow = table.getSelectedRow();
                    if (selectedRow != -1) {
                        System.out.println("Selected Row Data: " +
                                tableModel.getValueAt(selectedRow, 1));
                    }
                }
            });
            // 显示 GUI
            JFrame frame = new JFrame("Swing MVC Example");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new JScrollPane(table));
            frame.pack();
            frame.setVisible(true);
        });
    }
}17. Swing的其他特性
1. 边框设置
并非所有组件都有默认边框。外边框通过 setBorder(Border border) 方法设置,所有继承自 JComponent 的组件均支持此方法。
- 有默认边框的组件:JTextField、JTextArea 通常有一个内置的边框。JScrollPane 也有默认的边框。
 - 没有默认边框的组件:JButton 和 JLabel 通常没有明显的边框。
 
JLabel label = new JLabel("Example Label");
label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); // 黑色边框,宽度为2像素2. 外边距设置
外边距是指组件与组件之间的距离,它通常用于调整组件之间的间距。
- 外部间距(边框): 通过 setBorder 设置边框内的间距。
 
component.setBorder(BorderFactory.createCompoundBorder(
    BorderFactory.createLineBorder(Color.RED, 2), // 外边框
    BorderFactory.createEmptyBorder(10, 15, 10, 15) // 外边距:上10px、左15px、下10px、右15px
));3. 内边距设置
对于某些组件(如按钮),内边距直接控制组件内容与其边框之间的距离,不同组件提供了不同的方法。
- 可以通过 setMargin 方法设置文字或内容与边界的距离。
 
JButton button = new JButton("Click Me");
button.setMargin(new Insets(10, 20, 10, 20)); // 上10px、左20px、下10px、右20px- 支持内边距的组件:
 
组件类型  | 方法  | 说明  | 
JButton  | setMargin(Insets margin)  | 设置按钮内容与边框的距离  | 
JTextField  | setMargin(Insets margin)  | 设置输入文本与边框的距离  | 
JTextArea  | setMargin(Insets margin)  | 设置多行文本与边框的距离  | 
JEditorPane  | setMargin(Insets margin)  | 设置编辑器内容与边框的距离  | 
JPasswordField  | setMargin(Insets margin)  | 设置密码输入框内容与边框的距离  | 
JTabbedPane  | setTabComponentInsets(Insets insets)  | 设置选项卡内容与边框的距离(需扩展方法)  | 
4. 布局管理器
- FlowLayout:通过构造函数或 setHgap 和 setVgap 方法设置组件之间的水平和垂直间距。
 
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 20, 10)); // 外边距- GridBagLayout:通过 Insets 指定组件的外边距。
 
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10); // 上、左、下、右外边距- BorderLayout:在每个区域使用 EmptyBorder 设置间距。
 
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));边框边距综合示例
public class SwingSpacingExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Swing Spacing Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);
        // 为 JFrame 设置红色边框(通过内容面板设置)
        JPanel frameContentPanel = (JPanel) frame.getContentPane();
        frameContentPanel.setBorder(BorderFactory.createLineBorder(Color.RED, 5)); // 外边框:红色
        // 创建主面板 JPanel
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout(FlowLayout.CENTER, 20, 20)); // 水平和垂直间距
        panel.setBorder(BorderFactory.createLineBorder(Color.GREEN, 3)); // 外边框:绿色
        // 创建按钮 JButton
        JButton button = new JButton("Button");
        button.setPreferredSize(new Dimension(120, 50));
        button.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(Color.BLUE, 2), // 外边框:蓝色
                BorderFactory.createEmptyBorder(10, 10, 10, 10) // 内边框:空白,10像素
        ));
        // 创建标签 JLabel
        JLabel label = new JLabel("Label");
        label.setPreferredSize(new Dimension(120, 50));
        label.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(Color.ORANGE, 2), // 外边框:橙色
                BorderFactory.createEmptyBorder(5, 5, 5, 5) // 内边框:空白,5像素
        ));
        // 添加组件到面板
        panel.add(button);
        panel.add(label);
        // 添加主面板到 JFrame
        frame.add(panel);
        frame.setVisible(true);
    }
}5. 如何在组件上使用HTML
直接使用HTML语句来代替文本字符串即可。
使用HTML语句
public class SwingHtmlExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Swing HTML Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);
        JPanel panel = new JPanel();
        JButton jButton1 = new JButton("Hello, World!
");
        JButton jButton2 = new JButton("Hello, World!
");
        panel.add(jButton1);
        panel.add(jButton2);
        frame.add(panel);
        frame.setVisible(true);
    }
}6. 设置按钮的图标
通过类ImageIcon来加载图片,并通过setIcon方法来设置按钮的图标。
ImageIcon示例
public class ImageIconExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Swing HTML Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);
        JPanel panel = new JPanel();
        ImageIcon icon1 = new ImageIcon("d://1.jpeg");
        ImageIcon icon2 = new ImageIcon("d://2.jpeg");
        JLabel jLabel1 = new JLabel(icon1);
        JButton jButton1 = new JButton(icon2);
        panel.add(jLabel1);
        panel.add(jButton1);
        frame.add(panel);
        frame.setVisible(true);
    }
}7. 获取焦点
拿按钮举例,让按钮组件在窗口被激活时,理解获得动作焦点。
JButton.requestFocusInWindow();8. 如何使用键绑定
在Java Swing中,可以为按钮绑定快捷键,通过设置键盘助记符(Mnemonic)或键绑定(Key Bindings)来实现。以下是两种常见的方法:
- 设置键盘助记符(Mnemonic) 键盘助记符允许用户按下 Alt + 字母 来激活按钮。
 
public class ButtonMnemonicExample {
    public static void main(String[] args) {
        // 创建 JFrame
        JFrame frame = new JFrame("按钮快捷键示例");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 200);
        
        // 创建按钮
        JButton button = new JButton("保存(S)");
        // 设置助记符 Alt+S
        button.setMnemonic('S');
        
        // 添加按钮到 JFrame
        frame.add(button);
        
        // 显示 JFrame
        frame.setVisible(true);
    }
}
- 设置键绑定(Key Bindings) 键绑定可以绑定任意键盘组合到按钮,不局限于 Alt + 字母。按下 Ctrl + Enter,按钮被触发,并打印消息或执行其他逻辑。
 
public class ButtonKeyBindingExample {
    public static void main(String[] args) {
        // 创建 JFrame
        JFrame frame = new JFrame("按钮快捷键示例");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 200);
        
        // 创建按钮
        JButton button = new JButton("提交");
        frame.add(button);
        
        // 获取按钮的输入映射和动作映射
        InputMap inputMap = button.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = button.getActionMap();
        
        // 绑定快捷键 Ctrl+Enter
        inputMap.put(KeyStroke.getKeyStroke("ctrl ENTER"), "submitAction");
        
        // 定义快捷键触发的行为
        actionMap.put("submitAction", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("按钮通过 Ctrl+Enter 触发!");
                JOptionPane.showMessageDialog(frame, "提交成功!");
            }
        });
        
        // 显示 JFrame
        frame.setVisible(true);
    }
}- 两种方法的区别
 
方法  | 适用场景  | 示例  | 
键盘助记符  | 简单快捷,通常用于菜单或按钮,激活键为 Alt + 字母  | button.setMnemonic('S');  | 
键绑定  | 更灵活,可以绑定任意键盘组合到组件,例如 Ctrl + Enter  | inputMap.put(KeyStroke.getKeyStroke("ctrl ENTER"), "action");  | 
9. 如何使用Modality
在 Java Swing 中,Modality 是用来控制对话框(JDialog)的模式行为。它定义了对话框在显示时是否会阻塞父窗口或应用程序的其他部分。常见的 Modality 类型包括:
- 模态对话框:阻塞父窗口。
 - 非模态对话框:不阻塞父窗口。
 - 应用程序模态:阻塞整个应用程序。
 - 工具模态:只阻塞当前窗口。
 
使用 JDialog 的 Modality
- 创建一个模态对话框
可以通过 JDialog 构造函数或 setModalityType 方法来设置对话框的模式。 
import javax.swing.*;
public class ModalityExample {
    public static void main(String[] args) {
        // 创建 JFrame
        JFrame frame = new JFrame("主窗口");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);
        
        // 创建按钮打开对话框
        JButton button = new JButton("打开模态对话框");
        frame.add(button);
        
        // 按钮点击事件
        button.addActionListener(e -> {
            // 创建模态对话框
            JDialog dialog = new JDialog(frame, "模态对话框", Dialog.ModalityType.APPLICATION_MODAL);
            dialog.setSize(200, 150);
            dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
            
            // 添加内容到对话框
            JLabel label = new JLabel("这是一个模态对话框", SwingConstants.CENTER);
            dialog.add(label);
            
            // 显示对话框
            dialog.setLocationRelativeTo(frame); // 对话框居中
            dialog.setVisible(true);
        });
        
        // 显示主窗口
        frame.setVisible(true);
    }
}- Modality 类型详解:Dialog.ModalityType 提供了以下几种模式:
 - APPLICATION_MODAL(默认)
阻塞整个应用程序的其他窗口,直到对话框关闭。 - DOCUMENT_MODAL
阻塞对话框所属的窗口及其子窗口,不阻塞其他窗口。 - TOOLKIT_MODAL
阻塞所有同一个 Toolkit 的窗口(通常是同一 JVM 的窗口)。 - MODELESS
非模态对话框,不阻塞任何窗口。 - 切换不同的 Modality 类型
 
button.addActionListener(e -> {
    JDialog dialog = new JDialog(frame, "模态对话框");
    dialog.setSize(200, 150);
    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    // 设置不同的 Modality 类型
    dialog.setModalityType(Dialog.ModalityType.DOCUMENT_MODAL);
    dialog.add(new JLabel("Document Modal 示例", SwingConstants.CENTER));
    dialog.setLocationRelativeTo(frame);
    dialog.setVisible(true);
});- 非模态对话框示例
 
将 ModalityType 设置为 MODELESS,使对话框非模态。效果,对话框显示时,用户仍然可以与主窗口交互。
button.addActionListener(e -> {
    JDialog dialog = new JDialog(frame, "非模态对话框");
    dialog.setSize(200, 150);
    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
    // 设置为非模态
    dialog.setModalityType(Dialog.ModalityType.MODELESS);
    dialog.add(new JLabel("这是一个非模态对话框", SwingConstants.CENTER));
    dialog.setLocationRelativeTo(frame);
    dialog.setVisible(true);
});10. 如何创建Splash Screen
在 Java 中,Splash Screen 是指在应用程序启动时显示的一个初始屏幕,用于展示启动画面或加载信息。Java 提供了两种实现方式:
- 通过 java.awt.SplashScreen 类
这是 Java 内置的方式,通常用于在应用程序启动之前显示一个启动画面。在应用程序启动时,会显示 splash.png,并绘制文字 “正在启动应用程序…",之后切换到主窗口。 - 步骤:
 - 在应用程序的 JAR 文件中添加一个启动图片(例如 splash.png)。
 - 在 MANIFEST.MF 文件中指定 SplashScreen-Image 属性。
 - 示例代码:
 - 添加 MANIFEST.MF 文件
 
Manifest-Version: 1.0
Main-Class: MainClass
SplashScreen-Image: splash.png- 手动或自动将该文件打jar文件,splash.png图片、MANIFEST.MF放入项目正确目录中。
 
public class SplashScreenExample {
    public static void main(String[] args) {
        // 显示启动画面
        showSplashScreen();
        // 模拟加载过程
        try {
            Thread.sleep(3000); // 等待 3 秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 隐藏启动画面并显示主窗口
        SwingUtilities.invokeLater(SplashScreenExample::showMainWindow);
    }
    private static void showSplashScreen() {
        // 获取当前的 SplashScreen
        SplashScreen splash = SplashScreen.getSplashScreen();
        if (splash != null) {
            // 获取 SplashScreen 的绘制画布
            Graphics2D g = splash.createGraphics();
            if (g != null) {
                // 绘制文本
                g.setColor(Color.BLACK);
                g.setFont(new Font("Microsoft YaHei", Font.BOLD, 16));
                g.drawString("正在启动应用程序...", 50, 180);
                // 更新 SplashScreen 的内容
                splash.update();
            }
        } else {
            System.out.println("无法加载启动画面(请确保 splash.png 已正确设置)");
        }
    }
    private static void showMainWindow() {
        // 创建主窗口
        JFrame frame = new JFrame("主窗口");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 300);
        // 设置主窗口内容
        JLabel label = new JLabel("欢迎使用主窗口", JLabel.CENTER);
        label.setFont(new Font("Microsoft YaHei", Font.BOLD, 24));
        frame.add(label);
        // 显示主窗口
        frame.setVisible(true);
    }
}- 通过自定义的 JWindow 或 JFrame
这种方式可以完全自定义启动画面,并添加更多的交互或动画。 
使用自定义 JWindow
如果需要更复杂的启动画面,比如动画或进度条,可以使用 JWindow 来实现。一个定制的窗口显示欢迎信息和进度条,3秒后切换到主窗口。
public class CustomSplashScreen {
    public static void main(String[] args) {
        // 创建一个 JWindow 作为启动画面
        JWindow splash = new JWindow();
        splash.setSize(400, 300);
        splash.setLocationRelativeTo(null);
        // 设置启动画面的内容
        JPanel panel = new JPanel(new BorderLayout());
        panel.setBackground(Color.WHITE);
        // 设置支持中文的字体,例如微软雅黑
        JLabel label = new JLabel("欢迎使用我的应用", JLabel.CENTER);
        label.setFont(new Font("Microsoft YaHei", Font.BOLD, 24)); // 设置字体为微软雅黑
        panel.add(label, BorderLayout.CENTER);
        JProgressBar progressBar = new JProgressBar();
        progressBar.setIndeterminate(true); // 显示不确定的进度条
        panel.add(progressBar, BorderLayout.SOUTH);
        splash.add(panel);
        // 显示启动画面
        splash.setVisible(true);
        // 模拟加载过程
        try {
            Thread.sleep(3000); // 等待3秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 隐藏启动画面并启动主窗口
        splash.dispose();
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("主窗口");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(400, 300);
            frame.setVisible(true);
        });
    }
}- 使用 JWindow 加载图片
 
如果需要展示一个图片作为启动画面,可以直接加载图片到 JWindow。
import javax.swing.*;
import java.awt.*;
public class SplashScreenWithImage {
    public static void main(String[] args) {
        // 创建 JWindow
        JWindow splash = new JWindow();
        splash.setSize(400, 300);
        splash.setLocationRelativeTo(null);
        // 设置图片作为启动画面
        JLabel imageLabel = new JLabel(new ImageIcon("splash.png"));
        splash.add(imageLabel);
        // 显示启动画面
        splash.setVisible(true);
        // 模拟加载过程
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 隐藏启动画面并启动主窗口
        splash.dispose();
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("主窗口");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setSize(400, 300);
            frame.setVisible(true);
        });
    }
}方法  | 优点  | 缺点  | 
java.awt.SplashScreen  | 简单易用,不需要额外代码或组件  | 样式受限,不能交互或自定义内容  | 
自定义 JWindow  | 完全可定制,支持交互、动画或进度条  | 需要更多代码  | 
选择合适的方法根据你的需求。如果需要简单的静态画面,用 SplashScreen;如果需要交互或动画,用自定义的 JWindow。
11. 如何使用System Tray
- 检查系统是否支持托盘: SystemTray.isSupported()
 - 获取 SystemTray 实例: SystemTray systemTray = SystemTray.getSystemTray()
 - 创建托盘图标: Image image = Toolkit.getDefaultToolkit().getImage(“icon.png”)
 - 将菜单绑定到托盘图标: TrayIcon trayIcon = new TrayIcon(image, “System Tray Example”, popupMenu)
 - 将托盘图标添加到系统托盘: systemTray.add(trayIcon)
 - 显示气泡通知: trayIcon.displayMessage(“标题”, “内容”, TrayIcon.MessageType.INFO)
 - 动态更改图标: trayIcon.setImage(newImage)
 
public class SystemTrayExample {
    public static void main(String[] args) {
        if (!SystemTray.isSupported()) {
            System.out.println("System Tray is not supported on your system.");
            return;
        }
        SystemTray systemTray = SystemTray.getSystemTray();
        Image image = Toolkit.getDefaultToolkit().getImage("d:\\11.png");
        PopupMenu popupMenu = new PopupMenu();
        MenuItem exitItem = new MenuItem("Exit");
        exitItem.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
        popupMenu.add(exitItem);
        TrayIcon trayIcon = new TrayIcon(image, "System Tray Example", popupMenu);
        trayIcon.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("Tray icon clicked");
            }
        });
        try {
            systemTray.add(trayIcon);
        } catch (AWTException e) {
            System.out.println("Error adding tray icon: " + e);
        }
    }
}12. 对齐
- GridBagConstraints.anchor 的作用 ,用于控制组件在其布局区域(由 gridwidth 和 gridheight 定义的单元格)内的对齐方式。设置为 EAST 时,组件会紧贴区域的右侧。
 - 常见对齐常量GridBagConstraints.WEST 左对齐GridBagConstraints.EAST 右对齐GridBagConstraints.CENTER 居中对齐(默认值)GridBagConstraints.NORTH 顶部对齐GridBagConstraints.SOUTH 底部对齐
 
public class GridBagExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("GridBagLayout 示例");
        frame.setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        // 创建一个标签,右对齐
        JLabel label = new JLabel("用户名:");
        gbc.anchor = GridBagConstraints.EAST; // 右对齐
        gbc.gridx = 0;
        gbc.gridy = 0;
        frame.add(label, gbc);
        // 创建一个文本框,左对齐
        JTextField textField = new JTextField(20);
        gbc.anchor = GridBagConstraints.WEST; // 左对齐
        gbc.gridx = 1;
        gbc.gridy = 0;
        frame.add(textField, gbc);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}总结
本章重点,Swing与并发、Swing的其他特性,知识点比较杂,结合例子多练习。