不管你信不信,“如何用Swing做不规则窗体”是近10年来Swing被最常问到的几个问题之一。Google上搜索“Swing 不规则 窗体”有将近10万个中文结果,而且从1999年就有人在致力于这一问题的解决方法,至今甚至已经出现多种不同“流派”的解决方案。
第一招:抓屏法
最早出现的比较靠谱的一个解决方法出现在《Swing Hacks》一书中。《Swing Hacks》一书是著名的图书出版商O’Reilly在2007年出版的Swing技巧图书,作者是Swing开发组成员Joshua Marinacci,他也是目前JavaFX开发组成员。在这本书的41章中介绍了一种制作透明窗体和不规则窗体的方法。由于当时的JDK功能所限,Java本身并未提供任何直接的API来制作半透明或不规则窗体,所以这一技巧是利用Robot来截取屏幕原有内容生成内存图片,然后将图片显示在Swing的窗体中作为背景,“欺骗”大家的眼睛误以为是窗口“透”过去了。这一招确实体现了程序员的聪明才智。
不过这一方法的缺陷是,当窗口被移动一下后就会露馅,所谓“透明”区域的抓图不知道位置变化,也不会随之改变。所以在该书的例子程序中,作者又将JFrame的setUndecorated设置为true去掉了标题栏,让你无法移动窗口;再启动了一个线程,每250毫秒就重新抓屏一次,更新Swing窗口的图片背景。不过由于Swing窗口显示出来后,它本身又遮挡了屏幕后面的物体,作者只好先frame.hide()把窗口隐藏一下,然后马上抓图,然后再frame.show恢复窗口显示。透明效果是勉强出来了,但是程序在那里有事没事的一直忽隐忽现,真是够怪异的,效率、实时性也都惨不忍睹。
以下是相关代码:
Java代码
- public void run( ) {
- try {
- while(true) {
- Thread.sleep(250);
- long now = new Date( ).getTime( );
- if(refreshRequested &&
- ((now - lastupdate) > 1000)) {
- if(frame.isVisible( )) {
- Point location = frame.getLocation( );
- frame.hide( );
- updateBackground( );
- frame.show( );
- frame.setLocation(location);
- refresh( );
- }
- lastupdate = now;
- refreshRequested = false;
- }
- }
- } catch (Exception ex) {
- p(ex.toString( ));
- ex.printStackTrace( );
- }
- }
《Swing Hacks》中介绍的方法代码片段
第二招:AWTUtilities.setWindowShape法
随着Sun公司对JavaFX技术的猛醒和大力持续投入,JDK从6就开始从底层为JavaFX的未来做好准备,提供更多底层功能支撑。作为“酷”、“炫”的UI技术的先锋,窗口透明、不规则窗口自然是将来JavaFX不可缺少的元素和特性。所以,Sun在JDK6中提供了几个新的函数,用来支持窗口透明度、窗口任意形状:
Java代码
- void setWindowOpacity(Window window, float opacity) //设置窗口透明度
- void setWindowShape(Window window, Shape shape) //设置窗口形状
这里有官方的具体介绍:
http://java.sun.com/developer/technicalArticles/GUI/translucent_shaped_windows/
setWindowOpacity方法提供了官方的、彻底的方法来生成不规则形状窗体。不过依旧有以下几个问题:
- AWTUtilities并非JDK公开类,将来可能会发生变化。当然除了编译时的一个不爽的警告外,也不用过度担心,即使将来API发生变化,相信Sun和Oracle也会妥善处理好。
- 用Shape形状定义的窗口边缘粗糙,显示效果差。使用setWindowShape函数对窗口设定形状后,其窗口切割的边缘并未做抗锯齿(anti-alias)处理,也没有相应的函数或参数进行控制,导致显示效果粗糙。看看Sun自己做出来的例子:http://java.sun.com/developer/technicalArticles/GUI/translucent_shaped_windows/9.jpg,一个简单的椭圆Shape,其边缘就已经粗糙不堪。更不用说更复杂的透明图片边缘了。本人经过好几天的反复尝试,发现其效果始终不甚理想(如图),无论对图片的透明边缘如何精细处理,甚至直接new Shape,都完全达不到美工设计出来的效果图。
- 透明PNG图片的边缘Shape不好获取。如果我们的窗体不是一个规则的、可定义的几何形状Shape,而是一个任意透明PNG图片,该如何获取图片的透明边缘Shape,进而设置window的不规则形状呢?这确实是一个难题。在网上有人专门讨论这一算法,基本上是读取PNG图片的每一个像素,获得像素透明边界点,对边界点进行不断的合并与逼近,最后形成一个最终Shape。TWaver的TWaverUtil工具类中就有一个getImageShape方法用来获得任意图片的边缘shape。经反复测试验证,就是采用了这种算法。不过这种算法的缺点很明显:边缘必须是连续的,甚至必须是“外凸”的;如果png图片中间有一个透明的“洞”,甚至边缘有一个凹陷透明区域,生成的Shape都无法准确反映出来。
第三招:终极解决之道
经过反复的研究探索,终于获得了一个完美的解决方法:不用shape、不用抓图、不用workaround,真正的、彻底的、完全的、随意的在桌面上任意绘图、涂鸦、撒野,真正的属于程序员的Freedom!下面就来一起揭开这层窗户纸吧!
在程序中依次设置以下几个参数:
- 设置窗口完全透明:AWTUtilities.setWindowOpaque(frame, false);
- 设置窗口无边缘:frame.setUndecorated(true);
- 设置窗口的ContentPane为要显示的Pane:frame.setContentPane(myPane);
- 在myPane中放置具体要显示的内容,也可以重载paint方法进行Java2D绘制。这些paint会直接发生在桌面背景上。
- 接下来,就是见证奇迹的时刻!
(不好意思,暴露我的桌面了)
通过上面方法,可以做一个任意大小、任意位置的window,在相应的桌面位置上随意显示Swing组件,或做任意Java2D画图。比如下面小例子可以在屏幕上直接画一个红色的立体矩形,而没有显示窗口:
Java代码
- import com.sun.awt.AWTUtilities;
- import java.awt.Color;
- import java.awt.Graphics;
- import javax.swing.JFrame;
- import javax.swing.JPanel;
-
- public class Test {
-
- public static void main(String[] args) {
- JFrame frame = new JFrame();
- frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- frame.setUndecorated(true);
- frame.setBounds(500, 500, 300, 300);
- AWTUtilities.setWindowOpaque(frame, false);
-
- JPanel pane = new JPanel() {
-
- @Override
- public void paint(Graphics g) {
- super.paint(g);
-
- g.setColor(Color.red);
- g.fill3DRect(10, 10, 100, 100, true);
- }
- };
-
- frame.setContentPane(pane);
-
- frame.setVisible(true);
- }
- }
运行效果如下图:
窗口的拖拽移动
窗口不再规则,窗口标题栏不再出现,如何移动窗口?按照其他类似软件的习惯做法,应当允许用鼠标直接拖拽窗体任意位置进行窗口移动。做一个鼠标监听器对窗口中的元素进行拖动监听,对窗口进行相应拖动距离的移动:
Java代码
- private MouseAdapter moveWindowListener = new MouseAdapter() {
-
- private Point lastPoint = null;
-
- @Override
- public void mousePressed(MouseEvent e) {
- lastPoint = e.getLocationOnScreen();
- }
-
- @Override
- public void mouseDragged(MouseEvent e) {
- Point point = e.getLocationOnScreen();
- int offsetX = point.x - lastPoint.x;
- int offsetY = point.y - lastPoint.y;
- Rectangle bounds = FreeLoginUI.this.getBounds();
- bounds.x += offsetX;
- bounds.y += offsetY;
- FreeLoginUI.this.setBounds(bounds);
- lastPoint = point;
- }
- };
对窗体上的组件安装这一listener,就可以对窗口中任意元素进行拖拽,直接拖动窗体四处晃悠了。
图片的切割
要做好的界面,需要一个耐心、有创意的美工大力协助,例如图片的切割就很重要。下图展示了如何从效果图进行具体切割素材:
制作渐变背景Panel
仔细观察中间的输入区域部分,其背景是有渐变设计的。其制作方法也很简单:首先让美工帮助制作一个一个像素宽、整个panel高度的小图片作为素材;然后用这个图片创建纹理Paint;最后用这个纹理对真个panel进行fill。
Java代码
- private JPanel inputPane = new JPanel() {
-
- private String backgroundImageURL = FreeUtil.getImageURL("login_background.png");
- private TexturePaint paint = FreeUtil.createTexturePaint(backgroundImageURL);
-
- @Override
- protected void paintComponent(Graphics g) {
- super.paintComponent(g);
- Graphics2D g2d = (Graphics2D) g;
- g2d.setPaint(paint);
- g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
- }
- };
肆虐你的桌面:六月飘雪!
既然窗户纸捅破了,在桌面上就随意折腾吧。这几天窗外酷热难耐,咱们就来个桌面飘雪,也许可以望梅止渴,带来丝丝清凉吧!
先准备一个雪花的png透明图片,然后在桌面上随机生成50个雪花坐标,每次paint让每个雪花的左右略微抖一下(snowX[i] += TWaverUtil.getRandomInt(5) - 3),垂直距离下坠5像素(snowY[i] += 5),再旋转个小角度。然后,用一个线程不停的repaint窗口。
雪花png图片:
程序代码如下:
Java代码
- public class TestSnow {
-
- public static void main(String[] args) {
- final JFrame frame = new JFrame();
- frame.setAlwaysOnTop(true);
- frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
- frame.setUndecorated(true);
- frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
- AWTUtilities.setWindowOpaque(frame, false);
-
- final JPanel pane = new JPanel() {
-
- private int[] snowX = null;
- private int[] snowY = null;
- private int[] angles = null;
- private int count = 50;
-
- @Override
- public void paint(Graphics g) {
- super.paint(g);
- Rectangle bounds = frame.getBounds();
- if (snowX == null) {
-
- snowX = new int[count];
- for (int i = 0; i < snowX.length; i++) {
- snowX[i] = TWaverUtil.getRandomInt(bounds.width);
- }
- snowY = new int[count];
- for (int i = 0; i < snowY.length; i++) {
- snowY[i] = TWaverUtil.getRandomInt(bounds.height);
- }
- angles = new int[count];
- for (int i = 0; i < snowY.length; i++) {
- angles[i] = TWaverUtil.getRandomInt(360);
- }
- }
-
- Graphics2D g2d = (Graphics2D) g;
- Image image = TWaverUtil.getImage("/free/test/snow.png");
- for (int i = 0; i < count; i++) {
- snowX[i] += TWaverUtil.getRandomInt(5) - 3;
- snowY[i] += 5;
- angles[i] += i / 5;
- snowY[i] = snowY[i] > bounds.height ? 0 : snowY[i];
- angles[i] = angles[i] > 360 ? 0 : angles[i];
- int x = snowX[i];
- int y = snowY[i];
- int angle = angles[i];
- g2d.translate(x, y);
- double angleValue = Math.toRadians(angle);
- g2d.rotate(angleValue);
- g2d.drawImage(image, 0, 0, null);
- g2d.rotate(-angleValue);
- g2d.translate(-x, -y);
- }
- }
- };
-
- frame.setContentPane(pane);
- frame.setVisible(true);
- Thread thread = new Thread() {
-
- @Override
- public void run() {
- while (true) {
- try {
- Thread.sleep(10);
- } catch (Exception ex) {
- ex.printStackTrace();
- }
- pane.repaint();
- }
- }
- };
-
- thread.start();
- }
- }
快快运行代码,让雪花飘起来吧!
如果愿意折腾,还可以修改代码中的:
- private int count = 50,调整雪花的数量;
- 修改angles[i] += i / 5,调整雪花翻滚的速度;
- 修改snowY[i] += 5,调整雪花下坠的速度;
- 修改snowX[i] += TWaverUtil.getRandomInt(5) – 3,调整雪花左右摆动的速度;
别说你不知道怎么结束程序啊,不会Alt+F4的话,你这个程序员肯定不合格了。
秘密背后的秘密
当把透明窗口Frame设置特别大以后(例如10000*10000),你会发现不但界面变得极其缓慢,而且还会内存溢出。Sun的秘密不言自明了:还是使用了BufferedImage。否则,鼠标点击你画的椭圆或桌面的图标,它如何知道是点击了窗体,还是操作了桌面?只能生成内存图片,在里面进行像素判断了。要挖掘再深入的秘密,我也不清楚了,自己继续探索吧!