问题背景

最近在写 LiveDock 时,我需要实现一个无边框 + 圆角 + 可拖动的 Swing 窗口。

接触 FlatLaf 之后,我一开始还是很乐观的,我以为用 JFrame.setUndecorated(true) 配合 setShape(new RoundRectangle2D...)就能完美实现我想要的效果。

结果却让我大失所望:

这是什么鬼?告诉你坝这圆角圆在哪?

一个粗糙圆角的无边框窗口

初步尝试抗锯齿

这是上面提到的初始代码:

frame.setUndecorated(true);
frame.setBackground(new Color(0, 0, 0, 0));
frame.setShape(new RoundRectangle2D.Double(0, 0, w, h, arc, arc));

尝试求助了ChatGPT,他贴心地给我提供了一个 Graphics2D 的抗锯齿方案:

一个放入了 fillRoundRect() 的自定义 JPanel

protected void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g.create();
    g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    g2.setColor(Color.DARK_GRAY);
    g2.fillRoundRect(0, 0, getWidth(), getHeight(), arc, arc);
    g2.dispose();
}

正常情况下,这段代码应该能得到一个光滑的圆角窗口,但经过我的尝试,它的效果和上面展示的图片并没有什么区别……

那问题出在哪里呢?

来自 setShape() 的坑

先说结论:

setShape() 是一个 AWT 级别的窗口裁切方法,它会把窗口的形状裁切成指定的形状,并且会把所有的绘制内容都限制在这个形状内。

同样的,如果使用 Graphics2D 绘制出了一个光滑的圆角,但 setShape() 的锯齿裁切更“向内收”,导致绘制出的光滑圆角(那一圈渐变)再次被可恶的 setShape() 裁切掉了。

而这一切都在我误打误撞中发现:

我随手把 setShape() 里的 arc 设置成了 15,但 fillRoundRect() 里的圆角仍然是 40,结果窗口突然看起来很光滑,而且没有出现任何锯齿,正好符合我想要的效果。

经过一番尝试,我发现只要把 setShape() 的 arc 设置得比 fillRoundRect() 的 arc 稍小,就能得到一个光滑的圆角窗口。

最终解决方案

int visualArc = 40;
int shapeArc = 35; // 较小的裁切圆角

frame.setUndecorated(true);
frame.setBackground(new Color(0, 0, 0, 0));
frame.setShape(new RoundRectangle2D.Double(0, 0, w, h, shapeArc, shapeArc));

JPanel content = new JPanel() {
    protected void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D) g.create();
        g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
        g2.setColor(new Color(30, 30, 30));
        g2.fillRoundRect(0, 0, getWidth(), getHeight(), visualArc, visualArc);
        g2.dispose();
    }
};
content.setOpaque(false);

这个解决方案还可以进一步优化,通过寻找 visualArcshapeArc 之间的差值与它们之间的关系,可以做到:

  • 窗体边框圆角抗锯齿
  • 窗体方角区域与圆角区域之间的透明区域,不属于窗体的一部分(即不参与事件监听等)

最后封装成统一的工具类,比如 WindowFrameHelper.createFramelessWindow(...) 之类的,即可实现抗锯齿圆角无边框窗体的快速创建了。

效果如下:

一个光滑圆角的无边框窗口

总结

通过以上探索,我们发现 Swing 虽然缺乏现代窗口的一些特性(如 GPU 加速渲染、原生圆角等),但通过巧妙运用 setShape()Graphics2D 的配合,依然能实现令人满意的无边框圆角窗口效果。

关键在于理解 setShape() 的本质是 AWT 层面的裁切工具,并利用不同的圆角参数来达到视觉上的平滑效果。有时候,技术的突破不一定需要新工具,而是对已有工具的巧妙运用。

希望这个小方案能帮助到路过的你!如果对你有帮助,不妨为本 仓库 点个⭐~