自定义弹窗 --- 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);
五、注意事项
- ComponentUtils 依赖:loading 方法中使用了 ComponentUtils.handleSwingWorker,已在第3篇中介绍
- CallbackProcessor 依赖:使用了 CallbackProcessor.run 和 CallbackProcessor.accept,已在第2篇中介绍
- 模态对话框:构造函数中 super(owner, title, true) 第三个参数为 true 表示模态弹窗,会阻塞父窗口
- 遮罩层:loading 方法会隐藏标题栏和内容面板,显示进度条,适合用于加载中场景
六、与 CusFrame 的区别
| 特性 | CusFrame | CusDialog |
|---|---|---|
| 窗口类型 | 主窗口 | 弹窗 |
| 模态 | 不支持 | 支持(可阻塞父窗口) |
| 遮罩层加载 | 不支持 | 支持 |
| 关闭回调 | 支持 | 支持 + 回调参数 |
| 窗口调整大小 | 支持 | 不支持(固定大小) |
| 最小化/最大化 | 支持 | 不支持 |
| 多屏幕适配 | 支持 | 支持 |