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

自定义弹窗 --- CusDialog

一、背景

Swing 原生对话框 JDialog 和 JFrame 一样,标题栏是系统自带的,样式陈旧且无法自定义。同时,原生对话框不支持圆角、自定义标题栏图标、拖拽移动等功能。 CusDialog 的作用就是:移除系统装饰,自绘标题栏,实现与 CusFrame 风格统一的弹窗。同时支持模态/非模态、可拖拽移动、圆角边框、遮罩层加载效果、回调事件等功能。

二、类源码

java 复制代码
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.json.JSONObject;
import lombok.Getter;

import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.RoundRectangle2D;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 1. 自定义 Dialog 弹窗
 2. 实现圆角弹窗和自定义标题栏,支持拖拽移动、模态/非模态、遮罩层加载、回调事件
 3.  4. 使用示例:
 5. CusDialog dialog = new CusDialog(frame, "弹窗标题");
 6. dialog.setSize(500, 400);
 7. dialog.showDialog();
 */
public class CusDialog extends JDialog {
    private static Integer WIDTH = 1300;
    private static Integer HEIGHT = 800;
    private final int cornerRadius = 15;
    private boolean isRoundEnabled = true;
    private JPanel titlePanel;
    private JLabel leftIcon;
    private JLabel titleLabel;
    private JLabel rightIcon;
    @Getter
    private JLabel closeButton;
    /** 关闭按钮事件 */
    private Runnable closeAction;
    private Point initialClick;
    private boolean isDragEnabled = true;
    private JPanel contentPanel;
    private boolean paintBorder = true;
    private boolean isLoading = false;
    /** 父窗口引用,用于多显示器支持 */
    private final Frame owner;
    /** 对话框关闭回调事件 */
    private Consumer<JSONObject> callBack;

    /**
     * 默认构造函数
     */
    public CusDialog() {
        this("");
    }

    /**
     * 指定父窗口的构造函数
     * @param owner 父窗口
     */
    public CusDialog(Frame owner) {
        this(owner, "");
    }

    /**
     * 带标题的构造函数
     * @param title 弹窗标题
     * @param sizes 尺寸 [宽, 高]
     */
    public CusDialog(String title, Integer... sizes) {
        super((Frame) null, title, true);
        if (ArrayUtil.isNotEmpty(sizes)) {
            if (sizes.length == 1) WIDTH = sizes[0];
            if (sizes.length > 1) {
                WIDTH = sizes[0];
                HEIGHT = sizes[1];
            }
        }
        this.owner = null;
        setSize(WIDTH, HEIGHT);
        initialized();
    }

    /**
     * 指定父窗口和标题的构造函数
     * @param owner 父窗口
     * @param title 弹窗标题
     */
    public CusDialog(Frame owner, String title) {
        super(owner, title, true);
        this.owner = owner;
        isLoading = false;
        setSize(WIDTH, HEIGHT);
        initialized();
        addWindowFocusListener(new WindowAdapter() {
            @Override
            public void windowGainedFocus(WindowEvent e) {
                repaint();
            }
            @Override
            public void windowLostFocus(WindowEvent e) {
                repaint();
            }
        });
    }

    /**
     * 初始化弹窗
     */
    private void initialized() {
        setUndecorated(true);
        // 标题栏容器
        titlePanel = createTitlePanel();
        // 左侧区域(图标+标题+右侧图标)
        JPanel leftArea = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 8));
        leftArea.setOpaque(false);
        // 左侧图标
        leftIcon = new JLabel();
        leftArea.add(leftIcon);
        // 标题文本
        titleLabel = new JLabel(getTitle());
        titleLabel.setFont(new Font("Microsoft YaHei", Font.PLAIN, 16));
        leftArea.add(titleLabel);
        // 标题右侧图标
        rightIcon = new JLabel();
        leftArea.add(rightIcon);
        titlePanel.add(leftArea, BorderLayout.WEST);
        // 关闭按钮(保持在最右侧)
        closeButton = createCloseButton();
        titlePanel.add(closeButton, BorderLayout.EAST);
        // 中间弹性空间
        titlePanel.add(Box.createHorizontalGlue(), BorderLayout.CENTER);
        // 添加拖动支持
        addDragListeners();
        // 布局设置
        getContentPane().add(titlePanel, BorderLayout.NORTH);
        contentPanel = createContentPanel();
        getContentPane().add(contentPanel, BorderLayout.CENTER);
        // 设置对话框形状为圆角矩形
        setRoundEnable(true);
    }

    /**
     * 获取父窗口所在的屏幕设备
     * @return 屏幕设备
     */
    private GraphicsDevice getOwnerScreenDevice() {
        if (owner instanceof CusFrame) {
            return ((CusFrame) owner).getCurrentScreenDevice();
        } else if (owner != null) {
            GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
            GraphicsDevice[] screens = ge.getScreenDevices();
            Rectangle ownerBounds = owner.getBounds();
            int centerX = ownerBounds.x + ownerBounds.width / 2;
            int centerY = ownerBounds.y + ownerBounds.height / 2;
            for (GraphicsDevice screen : screens) {
                Rectangle screenBounds = screen.getDefaultConfiguration().getBounds();
                if (screenBounds.contains(centerX, centerY)) {
                    return screen;
                }
            }
        }
        return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
    }

    /**
     * 在父窗口屏幕居中显示
     */
    private void setLocationToOwnerScreen() {
        GraphicsDevice ownerScreen = getOwnerScreenDevice();
        if (ownerScreen != null) {
            Rectangle screenBounds = ownerScreen.getDefaultConfiguration().getBounds();
            int x = screenBounds.x + (screenBounds.width - getWidth()) / 2;
            int y = screenBounds.y + (screenBounds.height - getHeight()) / 2;
            setLocation(x, y);
        }
    }

    /**
     * 创建标题栏
     * @return 标题栏面板
     */
    private JPanel createTitlePanel() {
        if (null == titlePanel) {
            titlePanel = new JPanel(new BorderLayout());
        }
        titlePanel.setOpaque(false);
        return titlePanel;
    }

    /**
     * 设置标题栏下划线(默认颜色)
     */
    public void setUnderLine() {
        setUnderLine(new Color(0, 183, 195));
    }

    /**
     * 设置标题栏下划线颜色
     * @param color 下划线颜色
     */
    public void setUnderLine(Color color) {
        setUnderLine(color, 1);
    }

    /**
     * 设置标题栏下划线
     * @param color 下划线颜色
     * @param thickness 下划线厚度
     */
    public void setUnderLine(Color color, int thickness) {
        titlePanel.setBorder(BorderFactory.createMatteBorder(0, 0, thickness, 0, color));
    }

    /**
     * 设置是否允许拖动对话框
     * @param enabled true=允许拖动,false=禁止拖动
     */
    public void setDragEnabled(boolean enabled) {
        if (this.isDragEnabled != enabled) {
            this.isDragEnabled = enabled;
            updateDragListeners();
        }
    }

    /**
     * 更新拖动事件监听器状态
     */
    private void updateDragListeners() {
        MouseListener[] mouseListeners = titlePanel.getMouseListeners();
        MouseMotionListener[] motionListeners = titlePanel.getMouseMotionListeners();
        for (MouseListener listener : mouseListeners) {
            titlePanel.removeMouseListener(listener);
        }
        for (MouseMotionListener listener : motionListeners) {
            titlePanel.removeMouseMotionListener(listener);
        }
        if (isDragEnabled) {
            addDragListeners();
        }
    }

    /**
     * 添加拖动事件监听器
     */
    private void addDragListeners() {
        titlePanel.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                initialClick = e.getPoint();
            }
        });
        titlePanel.addMouseMotionListener(new MouseAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                if (isDragEnabled) {
                    int deltaX = e.getX() - initialClick.x;
                    int deltaY = e.getY() - initialClick.y;
                    setLocation(getX() + deltaX, getY() + deltaY);
                }
            }
        });
    }

    /**
     * 设置左侧图标
     * @param icon 图标
     */
    public void setIcon(Icon icon) {
        leftIcon.setIcon(icon);
        leftIcon.setVisible(null != icon);
    }

    /**
     * 设置左侧图标及点击事件
     * @param icon 图标
     * @param runnable 点击事件
     */
    public void setIcon(Icon icon, Runnable runnable) {
        leftIcon.setIcon(icon);
        leftIcon.setVisible(null != icon);
        addMouseClicked(leftIcon, runnable);
    }

    /**
     * 设置标题右侧图标
     * @param icon 图标
     */
    public void setRightIcon(Icon icon) {
        rightIcon.setIcon(icon);
        rightIcon.setVisible(null != icon);
    }

    /**
     * 设置标题右侧图标及点击事件
     * @param icon 图标
     * @param runnable 点击事件
     */
    public void setRightIcon(Icon icon, Runnable runnable) {
        rightIcon.setIcon(icon);
        rightIcon.setVisible(null != icon);
        addMouseClicked(rightIcon, runnable);
    }

    /**
     * 设置点击事件
     * @param component 组件
     * @param runnable 点击事件
     */
    private static void addMouseClicked(Component component, Runnable runnable) {
        component.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        component.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
            		// 详情见第二篇
                CallbackProcessor.run(runnable);
            }
        });
    }

    /**
     * 创建关闭按钮
     * @return 关闭按钮
     */
    private JLabel createCloseButton() {
        JLabel close = new JLabel("×");
        close.setFont(new Font("Microsoft YaHei", Font.PLAIN, 32));
        close.setForeground(new Color(102, 102, 102));
        close.setBorder(BorderFactory.createEmptyBorder(-5, 10, 0, 10));
        addMouseClicked(close, () -> {
            if (null != closeAction) {
                closeAction.run();
            } else {
                dispose();
            }
        });
        return close;
    }

    /**
     * 关闭按钮监听事件
     * @param closeAction 关闭事件
     */
    public void addCloseAction(Runnable closeAction) {
        this.closeAction = closeAction;
    }

    /**
     * 修改关闭按钮样式
     * @param icon 图标
     */
    public void setCloseButtonStyle(Icon icon) {
        closeButton.setIcon(icon);
        closeButton.setText("");
        closeButton.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (null != closeAction) {
                    closeAction.run();
                } else {
                    dispose();
                }
            }
        });
    }

    /**
     * 关闭按钮监听事件(带图标)
     * @param icon 图标
     * @param runnable 关闭事件
     */
    public void addCloseAction(Icon icon, Runnable runnable) {
        if (null == icon) {
            closeButton.setText("×");
        } else {
            closeButton.setIcon(icon);
            closeButton.setText("");
        }
        this.closeAction = runnable;
    }

    /**
     * 设置标题文字
     * @param title 标题文字
     */
    @Override
    public void setTitle(String title) {
        super.setTitle(title);
        titleLabel.setText(title);
    }

    /**
     * 设置标题文字和字体
     * @param title 标题文字
     * @param font 字体
     */
    public void setTitle(String title, Font font) {
        super.setTitle(title);
        titleLabel.setText(title);
        titleLabel.setFont(font);
    }

    /**
     * 设置标题字体
     * @param font 字体
     */
    public void setTitleFont(Font font) {
        titleLabel.setFont(font);
    }

    /**
     * 设置标题栏样式
     * @param operate 自定义操作
     */
    public void setTitleStyle(BiConsumer<JPanel, JLabel> operate) {
        if (null != operate) {
            operate.accept(titlePanel, titleLabel);
        }
    }

    /**
     * 创建内容部分
     * @return 内容面板
     */
    private JPanel createContentPanel() {
        if (null == contentPanel) {
            contentPanel = new JPanel(new BorderLayout());
        }
        contentPanel.setBackground(Color.WHITE);
        return contentPanel;
    }

    /**
     * 启用圆角
     * @param enable 是否启用圆角
     */
    private void setRoundEnable(boolean enable) {
        isRoundEnabled = enable;
        setShape(createShape(enable));
        repaint();
    }

    /**
     * 创建对话框形状
     * @param enable 是否是圆角
     * @return 形状
     */
    private Shape createShape(boolean enable) {
        return enable ? new RoundRectangle2D.Double(0, 0, getWidth(), getHeight(), cornerRadius, cornerRadius) : null;
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        if (paintBorder) {
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.setColor(new Color(0, 183, 195));
            g2d.setStroke(new BasicStroke(1));
            g2d.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, cornerRadius, cornerRadius);
            g2d.dispose();
        }
    }

    @Override
    public void setSize(int width, int height) {
        super.setSize(width, height);
        SwingUtilities.invokeLater(() -> setShape(createShape(isRoundEnabled)));
    }

    @Override
    public Component add(Component comp) {
        return null == contentPanel ? super.add(comp) : contentPanel.add(comp);
    }

    @Override
    public Component add(String name, Component comp) {
        return null == contentPanel ? super.add(name, comp) : contentPanel.add(name, comp);
    }

    @Override
    public Component add(Component comp, int index) {
        return null == contentPanel ? super.add(comp, index) : contentPanel.add(comp, index);
    }

    @Override
    public void add(Component comp, Object constraints) {
        if (null == contentPanel) {
            super.add(comp, constraints);
        } else {
            contentPanel.add(comp, constraints);
        }
    }

    @Override
    public void add(Component comp, Object constraints, int index) {
        if (null == contentPanel) {
            super.add(comp, constraints, index);
        } else {
            contentPanel.add(comp, constraints, index);
        }
    }

    /**
     * 设置内容面板边框
     * @param border 边框
     */
    public void setBorder(Border border) {
        if (null != contentPanel) {
            contentPanel.setBorder(border);
        }
    }

    /**
     * 打开对话框,并在父窗口屏幕居中
     */
    public synchronized void showDialog() {
        if (!isLoading) {
            setLocationToOwnerScreen();
        }
        setVisible(true);
    }

    /**
     * 隐藏对话框
     */
    public void hideDialog() {
        setVisible(false);
    }

    /**
     * 关闭对话框(带回调参数)
     * @param jsonObject 回调参数
     */
    public void hideDialog(JSONObject jsonObject) {
        setVisible(false);
        // 详情见第二篇
        CallbackProcessor.accept(callBack, jsonObject);
    }

    /**
     * 设置回调事件
     * @param callBack 回调事件
     * @return 当前对话框实例
     */
    public CusDialog setCallBack(Consumer<JSONObject> callBack) {
        this.callBack = callBack;
        return this;
    }

    /**
     * 运行后台任务并显示加载对话框
     * @param tip 提示语句
     * @param parentBounds 父窗口的边界
     * @param doInBackground 后台任务
     * @param doneAction 后台任务完成回调
     */
    public <T> void loading(String tip, Rectangle parentBounds, Supplier<T> doInBackground, Consumer<T> doneAction) {
        loading(tip, parentBounds);
        // 详情见第三篇
        ComponentUtils.handleSwingWorker(doInBackground, result -> {
            hideDialog();
            // 详情见第二篇
            CallbackProcessor.accept(doneAction, result);
        });
        SwingUtilities.invokeLater(this::showDialog);
    }

    /**
     * 运行后台任务并显示加载对话框(指定尺寸)
     * @param tip 提示语句
     * @param width 宽
     * @param height 高
     * @param parentBounds 父窗口的边界
     * @param doInBackground 后台任务
     * @param doneAction 后台任务完成回调
     */
    public <T> void loading(String tip, int width, int height, Rectangle parentBounds, Supplier<T> doInBackground, Consumer<T> doneAction) {
        loading(tip, width, height, parentBounds);
        // 详情见第三篇
        ComponentUtils.handleSwingWorker(doInBackground, result -> {
            hideDialog();
            // 详情见第二篇
            CallbackProcessor.accept(doneAction, result);
        });
        SwingUtilities.invokeLater(this::showDialog);
    }

    /**
     * 打开遮罩层(根据父窗口边界)
     * @param tip 提示语句
     * @param parentBounds 父窗口的边界
     */
    public void loading(String tip, Rectangle parentBounds) {
        setSize(parentBounds.width - 20, parentBounds.height - 80);
        initLoading(tip, parentBounds);
    }

    /**
     * 打开遮罩层(指定尺寸)
     * @param tip 提示语句
     * @param width 宽
     * @param height 高
     * @param parentBounds 父窗口的边界
     */
    public void loading(String tip, int width, int height, Rectangle parentBounds) {
        setSize(width, height);
        initLoading(tip, parentBounds);
    }

    /**
     * 初始化遮罩层
     * @param tip 提示语句
     * @param parentBounds 父窗口的边界
     */
    public void initLoading(String tip, Rectangle parentBounds) {
        paintBorder = false;
        isLoading = true;
        setBackground(new Color(0, 0, 0, 0.5f));
        titlePanel.setVisible(false);
        contentPanel.setOpaque(false);
        // 创建进度条
        JProgressBar progressBar = new JProgressBar();
        progressBar.setIndeterminate(true);
        progressBar.setString(tip);
        progressBar.setStringPainted(true);

        JPanel reservePanel = new JPanel(new BorderLayout());
        reservePanel.setOpaque(false);
        add(reservePanel, BorderLayout.CENTER);

        JPanel progressPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
        progressPanel.setPreferredSize(new Dimension(getSize().width, getSize().height / 5 * 3));
        progressPanel.setOpaque(false);
        progressPanel.add(progressBar);
        add(progressPanel, BorderLayout.SOUTH);
        int x = parentBounds.x + (parentBounds.width - getWidth()) / 2;
        int y = parentBounds.y + (parentBounds.height - getHeight()) / 2 + 20;
        setLocation(x, y);
    }
}

三、核心功能说明

弹窗装饰:

  • setUndecorated(true):移除系统默认标题栏
  • setShape(new RoundRectangle2D.Double(...)):设置圆角矩形形状
  • paint(Graphics g):绘制圆角边框

自定义标题栏:

  • 左侧:图标 + 标题文字 + 右侧图标
  • 右侧:关闭按钮
  • 支持拖拽移动弹窗

标题栏样式:

  • setUnderLine():设置标题栏下划线
  • setTitleStyle():自定义标题栏样式
  • setIcon() / setRightIcon():设置标题栏图标

遮罩层加载:

  • loading(String tip, Rectangle parentBounds, Supplier, Consumer):执行后台任务并显示加载弹窗
  • 加载时显示不确定进度条,任务完成后自动关闭
  • 弹窗背景半透明,形成遮罩效果

多屏幕适配:

  • getOwnerScreenDevice():检测父窗口所在的屏幕
  • setLocationToOwnerScreen():在父窗口屏幕居中显示

关闭回调:

  • addCloseAction(Runnable):设置关闭按钮点击事件
  • setCallBack(Consumer):设置关闭时回调,可传递参数

四、使用示例

4.1 创建基本弹窗

java 复制代码
CusDialog dialog = new CusDialog(frame, "提示");
dialog.setSize(400, 300);
dialog.setTitle("操作确认");
dialog.showDialog();

4.2 设置标题栏样式

java 复制代码
dialog.setTitleStyle((titlePanel, titleLabel) -> {
    titlePanel.setBackground(new Color(50, 50, 80));
    titleLabel.setForeground(Color.WHITE);
});
dialog.setUnderLine(new Color(100, 150, 200), 2);

4.3 设置标题栏图标

java 复制代码
// 注:图片获取需自行实现
ImageIcon icon = xxx;
dialog.setIcon(icon);

4.4 设置关闭回调

java 复制代码
dialog.addCloseAction(() -> {
    System.out.println("弹窗已关闭");
});

4.5 遮罩层加载效果

java 复制代码
Rectangle bounds = frame.getBounds();
dialog.loading("正在加载数据,请稍候...", bounds,
    () -> {
        // 后台执行耗时任务
        Thread.sleep(2000);
        return "加载完成";
    },
    result -> {
        // 任务完成,更新UI
        System.out.println(result);
    }
);

4.6 带回调参数的关闭

java 复制代码
JSONObject result = new JSONObject();
result.set("code", 200);
dialog.setCallBack(json -> {
    System.out.println("回调参数:" + json);
});
dialog.hideDialog(result);

五、注意事项

  1. ComponentUtils 依赖:loading 方法中使用了 ComponentUtils.handleSwingWorker,已在第3篇中介绍
  2. CallbackProcessor 依赖:使用了 CallbackProcessor.run 和 CallbackProcessor.accept,已在第2篇中介绍
  3. 模态对话框:构造函数中 super(owner, title, true) 第三个参数为 true 表示模态弹窗,会阻塞父窗口
  4. 遮罩层:loading 方法会隐藏标题栏和内容面板,显示进度条,适合用于加载中场景

六、与 CusFrame 的区别

特性 CusFrame CusDialog
窗口类型 主窗口 弹窗
模态 不支持 支持(可阻塞父窗口)
遮罩层加载 不支持 支持
关闭回调 支持 支持 + 回调参数
窗口调整大小 支持 不支持(固定大小)
最小化/最大化 支持 不支持
多屏幕适配 支持 支持
相关推荐
啷里格啷2 小时前
第二章 Fast-DDS 整体架构与分层框架
后端·架构
DolphinDB2 小时前
漫长人工,耗费存储?用 BackupRestore 模块一站式解决跨环境数据同步难题
运维·后端·架构
Sam_Deep_Thinking2 小时前
连锁门店的外卖订单平台对接
java·微服务·架构·系统架构
钟智强2 小时前
硬核自研|HunTianDB 混天DB:Rust原生工业级时序安全数据库全技术拆解
后端
_遥远的救世主_3 小时前
从一次结果集密集型查询 OOM 看 Java 服务的稳定性架构治理
java·后端
代码丰3 小时前
基于数据库字段实现可续期分布式锁:从任务抢占到心跳续约
后端
用户8356290780513 小时前
Python 操作 PowerPoint 页眉与页脚指南
后端·python
一楼的猫3 小时前
从工具链视角对比:番茄作家助手 vs 第三方写作辅助方案
java·服务器·开发语言·前端·学习·chatgpt·ai写作
苍何3 小时前
从 0-1 跑通 AI 产品出海,没那么难
后端