Java Swing 自定义组件库分享(三)

2 阅读11分钟

组件与表单工具 — ComponentUtils、FormPanelUtils

一、这两个类解决什么问题?

Swing 原生组件缺少一些常用的便捷方法,比如:

  • 统一文本框的边框和聚焦样式
  • 快速创建带边框的面板
  • 表单中常用的“标签 + 组件”一行添加
  • 安全执行后台任务(SwingWorker)

ComponentUtilsFormPanelUtils 就是对这类操作的封装。

两个类的职责划分:

职责
ComponentUtils通用组件操作:文本框创建、边框设置、Tab标签、SwingWorker封装
FormPanelUtils表单布局专用:标签+组件一行添加、表单元素快速创建

二、ComponentUtils 源码

import cn.hutool.core.util.ArrayUtil;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * 组件工具类
 * 封装 Swing 组件的常用操作
 *
 * 使用示例:
 * 1. 创建统一样式的文本框:
 *    JTextField textField = ComponentUtils.createTextField();
 * 2. 创建 Tab 标签:
 *    JLabel tab = ComponentUtils.createTabLabel("用户管理", 16, "#409EFF", true);
 * 3. 执行后台任务:
 *    ComponentUtils.handleSwingWorker(() -> service.getData(), result -> table.setData(result));
 */
public class ComponentUtils {

    // ==================== 鼠标光标 ====================

    /**
     * 给组件添加鼠标移入时变成手型光标的功能
     * @param component 目标组件
     */
    public static void addComponentCursor(Component component) {
        if (null == component) return;
        component.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
            }

            @Override
            public void mouseExited(MouseEvent e) {
                component.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
            }
        });
    }

    // ==================== 尺寸设置 ====================

    /**
     * 设置组件首选大小
     * @param component 目标组件
     * @param width 宽度
     * @param height 高度
     */
    public static void setSize(JComponent component, int width, int height) {
        Dimension size = new Dimension(width, height);
        component.setPreferredSize(size);
        component.setMinimumSize(size);
        component.setMaximumSize(size);
    }

    // ==================== 边框设置 ====================

    /**
     * 为组件设置带标题的边框和内边距
     * @param component 目标组件
     * @param title 边框标题
     * @param top 上内边距
     * @param left 左内边距
     * @param bottom 下内边距
     * @param right 右内边距
     */
    public static void setBorder(JComponent component, String title, int top, int left, int bottom, int right) {
        EmptyBorder emptyBorder = new EmptyBorder(top, left, bottom, right);
        TitledBorder titledBorder = new TitledBorder(title);
        Border compoundBorder = BorderFactory.createCompoundBorder(emptyBorder, titledBorder);
        component.setBorder(compoundBorder);
    }

    /**
     * 创建带标题的边框(仅边框,不设置到组件)
     * @param title 边框标题
     * @return 边框对象
     */
    public static Border createTitleEmptyBorder(String title) {
        TitledBorder titledBorder = BorderFactory.createTitledBorder(title);
        titledBorder.setBorder(BorderFactory.createEmptyBorder());
        // 这里的字体根据项目修改
        titledBorder.setTitleFont(new Font("Serif", Font.BOLD + Font.ITALIC, 16));
        Border paddingBorder = new EmptyBorder(5, 10, 5, 10);
        return BorderFactory.createCompoundBorder(titledBorder, paddingBorder);
    }

    // ==================== Tab 标签 ====================

    /**
     * 创建 Tab 标签
     * @param text 标签文字
     * @param fontSize 字体大小
     * @param hexColor 激活时的字体颜色(十六进制,如 "#409EFF")
     * @param active 是否激活
     * @return 标签组件
     */
    public static JLabel createTabLabel(String text, int fontSize, String hexColor, boolean active) {
        JLabel label = new JLabel(text);
        label.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
        label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        label.setFont(new Font("Microsoft YaHei", Font.PLAIN, fontSize));
        label.setHorizontalAlignment(SwingConstants.CENTER);
        label.setOpaque(true);
        if (active) {
            label.setForeground(Color.decode(hexColor));
            label.setBackground(Color.WHITE);
        } else {
            label.setForeground(Color.decode("#333333"));
            label.setBackground(Color.decode("#F2F3F5"));
        }
        return label;
    }

    /**
     * 创建可点击的 Tab 标签
     * @param text 标签文字
     * @param fontSize 字体大小
     * @param hexColor 激活时的字体颜色
     * @param active 是否激活
     * @param runnable 点击事件
     * @return 标签组件
     */
    public static JLabel createTabLabel(String text, int fontSize, String hexColor, boolean active, Runnable runnable) {
        JLabel label = createTabLabel(text, fontSize, hexColor, active);
        label.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                CallbackProcessor.run(runnable);
            }
        });
        return label;
    }

    // ==================== 文本框 ====================

    private static void setTextFieldBorder(JTextField textField) {
        Border border = BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(Color.LIGHT_GRAY, 1, true),
                BorderFactory.createEmptyBorder(0, 10, 0, 10)
        );
        textField.setBorder(border);
        textField.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                textField.setBorder(BorderFactory.createCompoundBorder(
                        BorderFactory.createLineBorder(Color.decode("#98C3EB"), 2, true),
                        BorderFactory.createEmptyBorder(0, 10, 0, 10)
                ));
            }

            @Override
            public void focusLost(FocusEvent e) {
                textField.setBorder(border);
            }
        });
    }

    /**
     * 创建统一样式的文本框
     * @return 文本框
     */
    public static JTextField createTextField() {
        JTextField textField = new JTextField();
        setTextFieldBorder(textField);
        return textField;
    }

    /**
     * 创建带默认文本的文本框
     * @param text 默认文本
     * @return 文本框
     */
    public static JTextField createTextField(String text) {
        JTextField textField = createTextField();
        textField.setText(text);
        return textField;
    }

    /**
     * 创建密码框
     * @return 密码框
     */
    public static JPasswordField createPasswordField() {
        JPasswordField passwordField = new JPasswordField();
        setTextFieldBorder(passwordField);
        return passwordField;
    }

    /**
     * 创建带默认文本的密码框
     * @param text 默认文本
     * @return 密码框
     */
    public static JPasswordField createPasswordField(String text) {
        JPasswordField passwordField = new JPasswordField(text);
        setTextFieldBorder(passwordField);
        return passwordField;
    }

    // ==================== 中文字符处理 ====================

    /**
     * 计算文本实际渲染宽度(中英文混合)
     * @param text 文本
     * @param fm 字体度量
     * @return 宽度(像素)
     */
    public static int calculateTextWidth(String text, FontMetrics fm) {
        int width = 0;
        for (char c : text.toCharArray()) {
            if (isChineseChar(c)) {
                width += fm.charWidth('中');
            } else if (Character.isLetterOrDigit(c)) {
                width += fm.charWidth(c);
            } else {
                width += fm.charWidth(c);
            }
        }
        return width;
    }

    /**
     * 判断字符是否为中文字符
     * @param c 字符
     * @return 是中文返回 true
     */
    public static boolean isChineseChar(char c) {
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
        return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION
                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS;
    }

    // ==================== 窗口切换 ====================

    /**
     * 平滑切换窗口(先显示新窗口,延迟隐藏旧窗口)
     * @param show 显示窗口的回调
     * @param hide 隐藏窗口的回调
     */
    public static void smoothSwitch(Runnable show, Runnable hide) {
        SwingUtilities.invokeLater(() -> {
            CallbackProcessor.run(show);
            Timer timer = new Timer(100, e -> CallbackProcessor.run(hide));
            timer.setRepeats(false);
            timer.start();
        });
    }

    // ==================== 标题标签 ====================

    /**
     * 创建标题标签
     * @param title 标题文字
     * @return 标题标签
     */
    public static JLabel createTitleLabel(String title) {
        return createTitleLabel(title, new Font("Microsoft YaHei", Font.BOLD, 20));
    }

    /**
     * 创建标题标签(可指定字体)
     * @param title 标题文字
     * @param font 字体
     * @return 标题标签
     */
    public static JLabel createTitleLabel(String title, Font font) {
        JLabel titleLabel = new JLabel(title);
        titleLabel.setFont(font);
        // 这里的图片就是获取一个ImageIcon
        // titleLabel.setIcon();
        return titleLabel;
    }

    // ==================== 包裹层面板 ====================

    /**
     * 创建带圆角背景的包裹面板
     * @param layout 布局管理器
     * @return 面板
     */
    public static JPanel createWrapPanel(LayoutManager layout) {
        JPanel wrapPanel = new JPanel(layout);
        // FlatLaf 圆角样式,不使用 FlatLaf 可注释
        wrapPanel.putClientProperty("FlatLaf.style", "arc: 30;");
        wrapPanel.setBackground(Color.WHITE);
        return wrapPanel;
    }

    /**
     * 创建带圆角背景的包裹面板(BorderLayout)
     * @return 面板
     */
    public static JPanel createWrapPanel() {
        return createWrapPanel(new BorderLayout());
    }

    /**
     * 创建固定高度的包裹面板
     * @param height 高度
     * @return 面板
     */
    public static JPanel createWrapPanel(int height) {
        JPanel wrapPanel = createWrapPanel(new BorderLayout());
        wrapPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, height));
        wrapPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, height));
        return wrapPanel;
    }

    /**
     * 创建固定高度的包裹面板(指定布局)
     * @param layout 布局管理器
     * @param height 高度
     * @return 面板
     */
    public static JPanel createWrapPanel(LayoutManager layout, int height) {
        JPanel wrapPanel = createWrapPanel(layout);
        wrapPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, height));
        wrapPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, height));
        return wrapPanel;
    }

    // ==================== SwingWorker 封装 ====================

    /**
     * 执行后台任务(有返回值),完成后在 EDT 执行回调
     * @param doInBackground 后台任务
     * @param doneAction 完成后的回调(在 EDT 执行)
     * @param <T> 返回值类型
     */
    public static <T> void handleSwingWorker(Supplier<T> doInBackground, Consumer<T> doneAction) {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            T result;

            @Override
            protected Void doInBackground() {
                Optional<T> optional = CallbackProcessor.get(doInBackground);
                result = optional.orElse(null);
                return null;
            }

            @Override
            protected void done() {
                CallbackProcessor.accept(doneAction, result);
            }
        };
        worker.execute();
    }

    /**
     * 执行后台任务(无返回值),完成后在 EDT 执行回调
     * @param doInBackground 后台任务
     * @param doneAction 完成后的回调(在 EDT 执行)
     */
    public static void handleSwingWorker(Runnable doInBackground, Runnable doneAction) {
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
            @Override
            protected Void doInBackground() {
                CallbackProcessor.run(doInBackground);
                return null;
            }

            @Override
            protected void done() {
                CallbackProcessor.run(doneAction);
            }
        };
        worker.execute();
    }

    // ==================== 透明设置 ====================

    /**
     * 递归设置组件及其所有子面板的透明属性
     * @param parent 父组件
     * @param opaque false 为透明,true 为不透明
     */
    public static void setOpaque(JComponent parent, boolean opaque) {
        Component[] components = parent.getComponents();
        if (ArrayUtil.isEmpty(components)) {
            parent.setOpaque(opaque);
            return;
        }
        for (Component component : components) {
            if (component instanceof JPanel) {
                ((JPanel) component).setOpaque(opaque);
                setOpaque((JPanel) component, opaque);
            }
        }
    }
}

三、FormPanelUtils 源码

import cn.hutool.core.util.StrUtil;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

/**
 * 表单面板工具类
 * 封装表单布局的常用操作(标签+组件一行添加、数字输入框等)
 *
 * 使用示例:
 * 1. 添加一行标签+组件:
 *    FormPanelUtils.addLabelAndTextField(panel, "用户名:", textField, null);
 * 2. 创建数字输入框:
 *    JTextField numberField = FormPanelUtils.getNumberField();
 */
public class FormPanelUtils {

    // ==================== 标签 ====================

    /**
     * 创建标签
     * @param text 标签文字
     * @return 标签
     */
    public static JLabel getLabel(String text) {
        return getLabel(text, null);
    }

    /**
     * 创建带图标的标签
     * @param text 标签文字
     * @param iconPath 图标路径
     * @return 标签
     */
    public static JLabel getLabel(String text, String iconPath) {
        JLabel label = new JLabel(text);
        if (null != iconPath) {
            // 需自己获取图片
            // label.setIcon();
        }
        return label;
    }

    /**
     * 创建指定尺寸的标签
     * @param text 标签文字
     * @param size 尺寸 [宽, 高]
     * @return 标签
     */
    public static JLabel getJLabel(String text, Integer... size) {
        JLabel label = new JLabel(text);
        int width = 85;
        int height = 36;
        if (null != size) {
            if (size.length > 0) width = size[0];
            if (size.length > 1) height = size[1];
        }
        ComponentUtils.setSize(label, width, height);
        return label;
    }

    // ==================== 文本框 ====================

    /**
     * 创建普通文本框(默认尺寸 180x36)
     * @return 文本框
     */
    public static JTextField getTextField() {
        JTextField text = new JTextField();
        ComponentUtils.setSize(text, 180, 36);
        return text;
    }

    /**
     * 创建指定尺寸的文本框
     * @param size 尺寸 [宽, 高]
     * @return 文本框
     */
    public static JTextField getTextField(Integer... size) {
        JTextField text = new JTextField();
        int width = 180;
        int height = 36;
        if (null != size) {
            if (size.length > 0) width = size[0];
            if (size.length > 1) height = size[1];
        }
        ComponentUtils.setSize(text, width, height);
        return text;
    }

    /**
     * 创建带默认文本的文本框
     * @param text 默认文本
     * @param size 尺寸 [宽, 高]
     * @return 文本框
     */
    public static JTextField getTextField(String text, Integer... size) {
        JTextField textField = getTextField(size);
        textField.setText(text);
        return textField;
    }

    /**
     * 创建可编辑/不可编辑的文本框
     * @param editable 是否可编辑
     * @param size 尺寸 [宽, 高]
     * @return 文本框
     */
    public static JTextField getTextField(Boolean editable, Integer... size) {
        JTextField text = getTextField(size);
        text.setEditable(editable);
        return text;
    }

		// ==================== 数字输入框 ====================

    /**
     * 创建数字输入框(支持小数)
     * @param size 尺寸 [宽, 高]
     * @return 文本框
     */
    public static JTextField getNumberField(Integer... size) {
        JTextField textField = getTextField(size);
        textField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                String key = "0123456789.";
                if (key.indexOf(e.getKeyChar()) < 0) {
                    e.consume();
                }
                if (e.getKeyChar() == '.' && textField.getText().indexOf(".") > 0) {
                    e.consume();
                }
            }
        });
        return textField;
    }

    /**
     * 创建带默认值的数字输入框
     * @param text 默认值
     * @param size 尺寸 [宽, 高]
     * @return 文本框
     */
    public static JTextField getNumberField(String text, Integer... size) {
        JTextField textField = getNumberField(size);
        if (StrUtil.isNotBlank(text)) {
            textField.setText(text);
        }
        return textField;
    }

    /**
     * 创建整数输入框(不支持小数点)
     * @param size 尺寸 [宽, 高]
     * @return 文本框
     */
    public static JTextField getIntegerField(Integer... size) {
        JTextField textField = getTextField(size);
        textField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                String key = "0123456789";
                if (key.indexOf(e.getKeyChar()) < 0) {
                    e.consume();
                }
            }
        });
        return textField;
    }

    // ==================== 表单布局 ====================

    /**
     * 向父面板添加组件(自动换行包装)
     * @param parentPanel 父面板
     * @param components 组件数组
     */
    public static void addComponent(JPanel parentPanel, JComponent... components) {
        JPanel subPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        subPanel.setBorder(new EmptyBorder(0, 0, 0, 0));
        for (JComponent component : components) {
            subPanel.add(component);
        }
        parentPanel.add(subPanel);
    }

    /**
     * 向父面板添加透明背景的组件
     * @param parentPanel 父面板
     * @param components 组件数组
     */
    public static void addTransparentComponent(JPanel parentPanel, JComponent... components) {
        JPanel subPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        subPanel.setOpaque(false);
        subPanel.setBorder(new EmptyBorder(0, 0, 0, 0));
        for (JComponent component : components) {
            subPanel.add(component);
        }
        parentPanel.add(subPanel);
    }

    /**
     * 向面板添加“标签 + 组件”一行(默认标签宽100,组件宽400)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param component 组件
     * @param gbc 布局约束(可为null)
     */
    public static void addLabelAndTextField(JPanel parentPanel, String text, JComponent component, GridBagConstraints gbc) {
        addLabelAndTextField(parentPanel, text, component, gbc, false);
    }

    /**
     * 向面板添加“标签 + 组件”一行(可选必填标记)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param component 组件
     * @param gbc 布局约束(可为null)
     * @param required 是否必填(显示红色星标)
     */
    public static void addLabelAndTextField(JPanel parentPanel, String text, JComponent component, GridBagConstraints gbc, boolean required) {
        addLabelAndTextField(parentPanel, text, 100, component, 400, gbc, required);
    }

    /**
     * 向面板添加“标签 + 组件”一行(指定宽度)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param labelWidth 标签宽度
     * @param component 组件
     * @param componentWidth 组件宽度
     * @param gbc 布局约束(可为null)
     */
    public static void addLabelAndTextField(JPanel parentPanel, String text, int labelWidth, JComponent component, int componentWidth, GridBagConstraints gbc) {
        addLabelAndTextField(parentPanel, text, labelWidth, component, componentWidth, gbc, false);
    }

    /**
     * 向面板添加“标签 + 组件”一行(完整参数)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param labelWidth 标签宽度
     * @param component 组件
     * @param componentWidth 组件宽度
     * @param gbc 布局约束(可为null)
     * @param required 是否必填
     */
    public static void addLabelAndTextField(JPanel parentPanel, String text, int labelWidth, JComponent component, int componentWidth, GridBagConstraints gbc, boolean required) {
        JPanel subPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
        if (StrUtil.isNotBlank(text)) {
            JLabel label = new JLabel(text);
            if (required) {
                // 需要自行获取图片
                // ImageIcon icon = xxxx;
                // label.setIcon(icon);
                label.setText(label.getText() + " *");
            }
            ComponentUtils.setSize(label, labelWidth);
            subPanel.add(label);
        }
        ComponentUtils.setSize(component, componentWidth);
        subPanel.add(component);
        if (null != gbc) {
            parentPanel.add(subPanel, gbc);
        } else {
            parentPanel.add(subPanel);
        }
    }

    /**
     * 向面板添加“标签 + 文本域”(自动带滚动条)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param textArea 文本域
     * @param gbc 布局约束
     */
    public static void addLabelAndTextArea(JPanel parentPanel, String text, JTextArea textArea, GridBagConstraints gbc) {
        addLabelAndTextArea(parentPanel, text, textArea, gbc, false);
    }

    /**
     * 向面板添加“标签 + 文本域”(可选必填)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param textArea 文本域
     * @param gbc 布局约束
     * @param required 是否必填
     */
    public static void addLabelAndTextArea(JPanel parentPanel, String text, JTextArea textArea, GridBagConstraints gbc, boolean required) {
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        addLabelAndTextField(parentPanel, text, scrollPane, gbc, required);
    }

    /**
     * 向面板添加“标签 + 文本域”(指定宽度)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param labelWidth 标签宽度
     * @param textArea 文本域
     * @param textAreaWidth 文本域宽度
     * @param gbc 布局约束
     */
    public static void addLabelAndTextArea(JPanel parentPanel, String text, int labelWidth, JTextArea textArea, int textAreaWidth, GridBagConstraints gbc) {
        addLabelAndTextArea(parentPanel, text, labelWidth, textArea, textAreaWidth, gbc, false);
    }

    /**
     * 向面板添加“标签 + 文本域”(完整参数)
     * @param parentPanel 父面板
     * @param text 标签文字
     * @param labelWidth 标签宽度
     * @param textArea 文本域
     * @param textAreaWidth 文本域宽度
     * @param gbc 布局约束
     * @param required 是否必填
     */
    public static void addLabelAndTextArea(JPanel parentPanel, String text, int labelWidth, JTextArea textArea, int textAreaWidth, GridBagConstraints gbc, boolean required) {
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        addLabelAndTextField(parentPanel, text, labelWidth, scrollPane, textAreaWidth, gbc, required);
    }

    // ==================== 表单标签 ====================

    /**
     * 创建表单标签(右对齐)
     * @param text 标签文字
     * @param required 是否必填
     * @return 标签
     */
    public static JLabel createFormLabel(String text, boolean required) {
        JLabel label = new JLabel(text);
        if (required) {
            // 需要自行获取图片
            // label.setIcon();
            label.setText(label.getText() + " *");
        }
        label.setFont(new Font("Microsoft YaHei", Font.PLAIN, 16));
        label.setHorizontalAlignment(SwingConstants.RIGHT);
        label.setVerticalAlignment(SwingConstants.TOP);
        return label;
    }

    /**
     * 创建指定高度的表单标签
     * @param text 标签文字
     * @param height 高度
     * @param required 是否必填
     * @return 标签
     */
    public static JLabel createFormLabel(String text, int height, boolean required) {
        JLabel label = createFormLabel(text, required);
        label.setPreferredSize(new Dimension(label.getPreferredSize().width, height));
        return label;
    }

    // ==================== 滚动文本域 ====================

    /**
     * 创建带滚动条的文本域
     * @param textArea 文本域(可为null,内部会创建)
     * @param height 高度
     * @return 滚动面板
     */
    public static JScrollPane createScrollTextArea(JTextArea textArea, int height) {
        if (null == textArea) {
            textArea = new JTextArea(5, 1);
        }
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        JScrollPane scrollPane = new JScrollPane(textArea, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        scrollPane.setPreferredSize(new Dimension(textArea.getPreferredSize().width, height));
        return scrollPane;
    }
}

四、使用示例

4.1 创建统一样式的文本框

// 普通文本框
JTextField nameField = ComponentUtils.createTextField();
nameField.setText("张三");
// 密码框
JPasswordField pwdField = ComponentUtils.createPasswordField();

4.2 创建 Tab 标签页

JLabel tab1 = ComponentUtils.createTabLabel("用户管理", 16, "#409EFF", true);
JLabel tab2 = ComponentUtils.createTabLabel("角色管理", 16, "#409EFF", false, () -> {
    System.out.println("切换到角色管理");
});

4.3 执行后台任务

ComponentUtils.handleSwingWorker(
    () -> userService.getUserList(),      // 后台执行(可能耗时)
    userList -> {                          // 完成后在 EDT 更新 UI
        table.setData(userList);
        statusLabel.setText("加载完成");
    }
);

4.4 表单布局

JPanel formPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5);

// 添加一行:标签 + 文本框
FormPanelUtils.addLabelAndTextField(formPanel, "用户名:", ComponentUtils.createTextField(), gbc);

// 添加一行:标签 + 数字输入框
FormPanelUtils.addLabelAndTextField(formPanel, "年龄:", FormPanelUtils.getIntegerField(), gbc, true);  // 必填

// 添加一行:标签 + 文本域
JTextArea remarkArea = new JTextArea(3, 20);
FormPanelUtils.addLabelAndTextArea(formPanel, "备注:", remarkArea, gbc);

4.5 创建标题标签

JLabel title = ComponentUtils.createTitleLabel("用户信息");
panel.add(title, BorderLayout.NORTH);

五、注意事项

  • 涉及图标的地方,需要自行实现或替换
  • FlatLaf 样式:createWrapPanel 中的 putClientProperty 是 FlatLaf 特有的,不使用 FlatLaf 可删除
  • 数字输入框:只做了简单的按键过滤,复杂校验建议配合文档监听器使用
  • 线程安全:handleSwingWorker 已正确处理 EDT 和后台线程的切换

六、小结

ComponentUtils 和 FormPanelUtils 是两个基础工具类,提供了 Swing 开发中最常用的组件操作封装。

  • ComponentUtils:通用组件能力(文本框、边框、Tab、SwingWorker)
  • FormPanelUtils:表单布局能力(标签+组件一行、数字输入框)