背景

首先本文是一个系列的文章,起名为《Java Swing GUi从小白到大神》,主要介绍Java Swing相关概念和实践应用。目前JavaSwingGUI很少作为界面的开发框架,但是对于学习Java体系,以及新手将来学习后端之后,能快速从前后端整体去理解一套系统是非常有帮助的,所以是很有必要学习和掌握的。本篇是系列博文的第五篇,若想详细学习请点击首篇博文开始,让我们开始吧。

文章概览

  1. Swing与并发
  2. Swing模型架构
  3. Swing的气体特性

15. Swing与并发

先了解一下核心类SwingUtilities 是一个实用类,包含与 Swing GUI 相关的静态方法,以下是其三个常用方法及说明:

SwingUtilities 类的 3 个常用方法

  1. invokeLater(Runnable doRun)

    • 作用:将一个任务(Runnable)调度到 事件分派线程(EDT) 上执行。

    • 用途:确保所有的 GUI 操作在 EDT 上运行,避免线程安全问题。

    • 示例

      1
      2
      3
      4
      5
      6
      
      SwingUtilities.invokeLater(() -> {
          JFrame frame = new JFrame("Example");
          frame.setSize(300, 200);
          frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          frame.setVisible(true);
      });
      
  2. invokeAndWait(Runnable doRun)

    • 作用:将一个任务调度到 EDT 上执行,并等待该任务完成。

    • 用途:用于需要在后台线程中执行某些 GUI 操作并确保同步完成。

    • 注意:如果当前线程已经是 EDT,调用此方法会导致 IllegalStateException

    • 示例

      1
      2
      3
      4
      5
      
      try {
          SwingUtilities.invokeAndWait(() -> System.out.println("Running on EDT"));
      } catch (Exception e) {
          e.printStackTrace();
      }
      
  3. isEventDispatchThread()

    • 作用:检查当前线程是否是 EDT。

    • 用途:在执行线程相关逻辑前验证线程类型,避免非线程安全操作。

    • 示例

      1
      2
      3
      4
      5
      
      if (SwingUtilities.isEventDispatchThread()) {
          System.out.println("Running on EDT");
      } else {
          SwingUtilities.invokeLater(() -> System.out.println("Switch to EDT"));
      }
      

SwingWorker 类的方法及说明

SwingWorker 是一个抽象类,用于在后台线程中执行耗时任务,同时支持在事件分派线程中更新 GUI。

常用方法:

  1. doInBackground()

    • 作用:定义后台任务的主逻辑。

    • 运行线程:后台线程。

    • 返回值:任务完成后返回的结果(SwingWorker 的泛型参数 V)。

    • 示例

      1
      2
      3
      4
      5
      6
      7
      8
      
      @Override
      protected Void doInBackground() {
          for (int i = 0; i < 100; i++) {
              setProgress(i);
              Thread.sleep(100);
          }
          return null;
      }
      
  2. publish(V... chunks)

    • 作用:在后台线程中发布中间结果。

    • 运行线程:后台线程。

    • 用途:将中间结果发送给 process() 方法。

    • 示例

      1
      
      publish(currentProgress);
      
  3. process(List<V> chunks)

    • 作用:接收由 publish() 方法发送的中间结果。

    • 运行线程:事件分派线程(EDT)。

    • 用途:用于更新 GUI。

    • 示例

      1
      2
      3
      4
      
      @Override
      protected void process(List<Integer> chunks) {
          progressBar.setValue(chunks.get(chunks.size() - 1));
      }
      
  4. done()

    • 作用:后台任务完成后调用。

    • 运行线程:事件分派线程(EDT)。

    • 用途:完成任务后的清理或结果处理。

    • 示例

      1
      2
      3
      4
      
      @Override
      protected void done() {
          System.out.println("Task Completed");
      }
      
  5. setProgress(int progress)

    • 作用:设置任务进度(范围 0-100)。

    • 运行线程:后台线程。

    • 用途:用于更新 progress 属性,并通过监听器通知。

    • 示例

      1
      
      setProgress(50);
      
  6. get()

    • 作用:获取 doInBackground() 返回的结果。

    • 运行线程:任何线程。

    • 用途:阻塞线程直到任务完成,适用于获取计算结果。

    • 示例

      1
      2
      3
      4
      5
      
      try {
          String result = myWorker.get();
      } catch (Exception e) {
          e.printStackTrace();
      }
      
  7. execute()

    • 作用:启动后台任务。

    • 运行线程:调用线程。

    • 用途:创建任务后,调用此方法开始执行。

    • 示例

      1
      
      myWorker.execute();
      
  8. cancel(boolean mayInterruptIfRunning)

    • 作用:取消任务。

    • 运行线程:任何线程。

    • 用途:设置任务为取消状态,可选择是否中断正在运行的任务。

    • 示例

      1
      
      myWorker.cancel(true);
      
  9. isCancelled()

    • 作用:检查任务是否已被取消。

    • 运行线程:任何线程。

    • 用途:在后台任务中动态检测取消状态。

    • 示例

      1
      
      if (isCancelled()) break;
      
  10. addPropertyChangeListener(PropertyChangeListener listener)

    • 作用:添加属性变化监听器,监听 progressstate 的变化。

    • 用途:实现动态界面更新。

    • 示例

      1
      2
      3
      4
      5
      
      myWorker.addPropertyChangeListener(evt -> {
          if ("progress".equals(evt.getPropertyName())) {
              progressBar.setValue((int) evt.getNewValue());
          }
      });
      

事件分派线程(Event Dispatch Thread, 简称 EDT)

是 Swing 的核心线程之一,它专门负责处理事件队列中的任务,例如用户交互事件(鼠标点击、键盘输入等)、界面更新、绘制组件等。

事件分派线程的特点:

  1. 唯一性:在运行时,应用程序中只有一个事件分派线程,它是单一的线程模型。
  2. Swing 操作必须在 EDT 上执行:所有涉及到 Swing 组件的操作(例如更新界面、添加组件)都需要在 EDT 上执行。这是因为 Swing 不是线程安全的,多线程访问可能导致 UI 渲染异常或线程竞争问题。
  3. 自动启动:当 Swing 应用程序启动时(例如通过 SwingUtilities.invokeLater 或显示 JFrame),EDT 会自动创建。

事件分派线程的工作流程:

  1. 任务分派:EDT 维护一个任务队列,任务可以通过 SwingUtilities.invokeLater()SwingUtilities.invokeAndWait() 提交到队列中。
  2. 任务处理:EDT 按顺序处理队列中的任务。
  3. 用户事件处理:当用户触发事件(例如鼠标点击、按键),这些事件会被放入任务队列,由 EDT 依次处理。

如何检测当前线程是否是事件分派线程:

可以通过 SwingUtilities.isEventDispatchThread() 方法检查当前线程是否为事件分派线程。

示例

1
2
3
4
5
if (SwingUtilities.isEventDispatchThread()) {
    System.out.println("Running on Event Dispatch Thread");
} else {
    System.out.println("Not on Event Dispatch Thread");
}

如何确保代码运行在事件分派线程上:

如果代码需要操作 Swing 组件但可能运行在非 EDT 的线程中,应该使用以下方法将任务调度到 EDT:

  1. SwingUtilities.invokeLater(Runnable)

    • 将任务提交给 EDT 异步执行,不阻塞调用线程。

    • 示例:

      1
      2
      3
      4
      
      SwingUtilities.invokeLater(() -> {
          JLabel label = new JLabel("Hello, Swing!");
          frame.add(label);
      });
      
  2. SwingUtilities.invokeAndWait(Runnable)

    • 将任务提交给 EDT 并等待其完成(阻塞调用线程)。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      
      try {
          SwingUtilities.invokeAndWait(() -> {
              JLabel label = new JLabel("Hello, Swing!");
              frame.add(label);
          });
      } catch (Exception e) {
          e.printStackTrace();
      }
      
代码swing与多线程
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
public class ArithmeticAccumulatorDemo {

    private JFrame frame;
    private JLabel resultLabel, progressLabel;
    private JButton startButton, showResultButton, pauseButton, resumeButton, resetButton;
    private SwingWorker<Void, Integer> 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<Void, Integer>() {
            @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<Integer> 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)。
    • 某些组件(如 JTableJList)允许用户使用自定义模型。
  • 单线程模型
    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. 重点内容学习

  1. 数据模型
  • 学习数据模型接口的定义(如 TableModel, ListModel)。
  • 掌握如何使用默认实现类(如 DefaultTableModel)进行简单开发。
  • 了解如何创建自定义数据模型以满足特定需求。
  1. 渲染器和编辑器
  • 渲染器用于定制 Swing 组件的显示样式(如单元格颜色、字体等)。
    • 通过实现 TableCellRenderer 或扩展 DefaultTableCellRenderer
  • 编辑器用于处理用户输入(如文本框、组合框等)。
    • 通过实现 TableCellEditor 或扩展 DefaultCellEditor
  1. 事件处理
  • 掌握事件监听器接口(如 ActionListener, MouseListener)。
  • 理解事件传播机制,以及如何在事件分派线程中处理事件。
  1. 线程安全
  • 了解 Swing 的单线程规则。
  • 使用 SwingUtilities.invokeLater() 确保 UI 操作在事件分派线程上执行。
  1. 自定义组件
  • 学习如何结合模型、渲染器和编辑器创建复杂的自定义组件。
  1. 性能优化
  • 对于动态数据量较大的组件(如 JTable),通过虚拟化模型和延迟加载优化性能。
代码MVC 使用 JTable
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 通常没有明显的边框。
1
2
JLabel label = new JLabel("Example Label");
label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 2)); // 黑色边框,宽度为2像素
  • 外边距:
    外边距是指组件与组件之间的距离,它通常用于调整组件之间的间距。

    • 外部间距(边框): 通过 setBorder 设置边框内的间距。
1
2
3
4
component.setBorder(BorderFactory.createCompoundBorder(
    BorderFactory.createLineBorder(Color.RED, 2), // 外边框
    BorderFactory.createEmptyBorder(10, 15, 10, 15) // 外边距:上10px、左15px、下10px、右15px
));
  • 内边距:
    对于某些组件(如按钮),内边距直接控制组件内容与其边框之间的距离,不同组件提供了不同的方法。
    • 可以通过 setMargin 方法设置文字或内容与边界的距离。
1
2
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) 设置选项卡内容与边框的距离(需扩展方法)
  • 布局管理器:

  • FlowLayout:通过构造函数或 setHgap 和 setVgap 方法设置组件之间的水平和垂直间距。

1
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 20, 10)); // 外边距
  • GridBagLayout:通过 Insets 指定组件的外边距。
1
2
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10); // 上、左、下、右外边距
  • BorderLayout:在每个区域使用 EmptyBorder 设置间距。
1
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
代码边框边距综合示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
    }
}
  1. 如何在组件上使用HTML
    直接使用HTML语句来代替文本字符串即可。
代码使用HTML语句
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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("<html><body><h1><font color=blue>Hello, World!</font></h1></body></html>");
        JButton jButton2 = new JButton("<html><body><h1>Hello, World!</h1></body></html>");

        panel.add(jButton1);
        panel.add(jButton2);

        frame.add(panel);

        frame.setVisible(true);
    }
}
  1. 如何使用图标 通过类ImageIcon来加载图片,并通过setIcon方法来设置按钮的图标。
代码ImageIcon示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
    }
}
  1. 获取焦点
    拿按钮举例,让按钮组件在窗口被激活时,理解获得动作焦点。
1
JButton.requestFocusInWindow();
  1. 如何使用键绑定
    在Java Swing中,可以为按钮绑定快捷键,通过设置键盘助记符(Mnemonic)或键绑定(Key Bindings)来实现。以下是两种常见的方法:
    1. 设置键盘助记符(Mnemonic) 键盘助记符允许用户按下 Alt + 字母 来激活按钮。
代码键盘助记符示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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);
    }
}
    1. 设置键绑定(Key Bindings) 键绑定可以绑定任意键盘组合到按钮,不局限于 Alt + 字母。按下 Ctrl + Enter,按钮被触发,并打印消息或执行其他逻辑。
代码键绑定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
    }
}
    1. 两种方法的区别
      方法 适用场景 示例
      键盘助记符 简单快捷,通常用于菜单或按钮,激活键为 Alt + 字母 button.setMnemonic('S');
      键绑定 更灵活,可以绑定任意键盘组合到组件,例如 Ctrl + Enter inputMap.put(KeyStroke.getKeyStroke("ctrl ENTER"), "action");
  1. 如何使用Modality
    在 Java Swing 中,Modality 是用来控制对话框(JDialog)的模式行为。它定义了对话框在显示时是否会阻塞父窗口或应用程序的其他部分。常见的 Modality 类型包括:
  • 模态对话框:阻塞父窗口。
  • 非模态对话框:不阻塞父窗口。
  • 应用程序模态:阻塞整个应用程序。
  • 工具模态:只阻塞当前窗口。

使用 JDialog 的 Modality

    1. 创建一个模态对话框
      可以通过 JDialog 构造函数或 setModalityType 方法来设置对话框的模式。
代码基本模态对话框
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
    }
}
    1. Modality 类型详解Dialog.ModalityType 提供了以下几种模式:
    • APPLICATION_MODAL(默认)
      阻塞整个应用程序的其他窗口,直到对话框关闭。

    • DOCUMENT_MODAL
      阻塞对话框所属的窗口及其子窗口,不阻塞其他窗口。

    • TOOLKIT_MODAL
      阻塞所有同一个 Toolkit 的窗口(通常是同一 JVM 的窗口)。

    • MODELESS
      非模态对话框,不阻塞任何窗口。

    1. 切换不同的 Modality 类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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);
});
    1. 非模态对话框示例

ModalityType 设置为 MODELESS,使对话框非模态。效果,对话框显示时,用户仍然可以与主窗口交互。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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);
});
  1. 如何创建Splash Screen
    在 Java 中,Splash Screen 是指在应用程序启动时显示的一个初始屏幕,用于展示启动画面或加载信息。Java 提供了两种实现方式:
    1. 通过 java.awt.SplashScreen
      这是 Java 内置的方式,通常用于在应用程序启动之前显示一个启动画面。在应用程序启动时,会显示 splash.png,并绘制文字 “正在启动应用程序…",之后切换到主窗口。

    步骤:

    • 在应用程序的 JAR 文件中添加一个启动图片(例如 splash.png)。
    • MANIFEST.MF 文件中指定 SplashScreen-Image 属性。

    示例代码:

    • 添加 MANIFEST.MF 文件
    1
    2
    3
    
    Manifest-Version: 1.0
    Main-Class: MainClass
    SplashScreen-Image: splash.png
    
    • 手动或自动将该文件打jar文件,splash.png图片、MANIFEST.MF放入项目正确目录中。
代码通过java.awt.SplashScreen类
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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);
    }
}
    1. 通过自定义的 JWindowJFrame
      这种方式可以完全自定义启动画面,并添加更多的交互或动画。

    使用自定义 JWindow
    如果需要更复杂的启动画面,比如动画或进度条,可以使用 JWindow 来实现。一个定制的窗口显示欢迎信息和进度条,3秒后切换到主窗口。

示例代码:

代码使用自定义JWindow
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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);
        });
    }
}
    1. 使用 JWindow 加载图片

    如果需要展示一个图片作为启动画面,可以直接加载图片到 JWindow

代码使用JWindow加载图片
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

  1. 如何使用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)
代码SystemTray示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
        }
    }
}

总结

本章重点,Swing与并发、Swing的其他特性,知识点比较杂,结合例子多练习。