图形界面开发对于Java来说并非它的长项,开发者经常会碰到各种各样的限制,比如,如何打造一款任意形状的窗口如何可以透过窗口显示它覆盖下的内容

考虑到Java并没有被设计成支持以上的功能,所以,你能得到的永远是方方正正的窗口,毫无新意,当然,我们可以通过JNI调用本地代码来完成,但是这就失去了java可移植性的意义,那么,用纯粹的java代码如何实现以上两种功能呢?

下文提供了一个实现的参考

预备知识
1.java.awt.Robot,这个类是一个功能非常强大的类,通过它我们可以控制鼠标和键盘的响应,不过这里,我们只需要用到它的一个功能--截屏,也就是得到当前屏幕的快照(screenshot)
2.我们可以通过调用一个swing组件的paintComponent(Graphics g)方法用特定的Graphics为其定制特殊的外观

首 先声明的一点是,本文中的实现方法只是一个欺骗的手法,因为要想实现前文中的功能,我们几乎的重写一套Swing出来,这里,简便的做法是,我们通过 robot类来获得当前屏幕的快照,然后贴在我们需要的窗口上,这样,不就实现了透明的功能了吗?顺便提一下,几年前日本发明的隐形衣也是依靠这种机制, 这种衣服在身后附带一套摄像机,然后即时的将拍下的内容显示在衣服的正面,因此,当别人看过来时,仿佛就通过人和衣服看到了身后的场景^_^

另外,还要感谢Joshua Chris leniz的Swing Hack一书,以上充满创新的方法正来自他的书中

好咯,让我们具体看一下细节的处理:
第一,我们要得到当前屏幕的快照,保存到一个Image对象[color=Red]background[/color]中:
  public void updateBackground() {
  try {
   Robot rbt = new Robot();
   Toolkit tk = Toolkit.getDefaultToolkit();
   Dimension dim = tk.getScreenSize();
   [color=Red]background [/color]= rbt.createScreenCapture(new Rectangle(0, 0, (int) dim
     .getWidth(), (int) dim.getHeight()));
  } catch (Exception ex) {
   }
 }

第二,我们需要让窗口显示这个图像,也就是说,让窗口的背景图像是这副图像,以达到透明的欺骗效果:

public void paintComponent(Graphics g) {
  Point pos = this.getLocationOnScreen();
  Point offset = new Point(-pos.x, -pos.y);
  g.drawImage([color=Red]background[/color], offset.x, offset.y, null);
 }

在swing hack一书中,作者也给出了他的实现,然而,运行结果表明该实现存在很大的问题:窗口经常无法即时更新,往往背景变了,窗口里显示的却还是以前的背景。 仔细研究了他的代码,我发现了问题的根结,同时也是这种实现技术的关键要素--如何让窗口在正确的时机里更新显示,下面,我们会讨论这一点

第三,更新窗口外观
前两步的操作,只能得到一副当前屏幕的快照,一旦背景变化了,或者窗口移动了,变化大小了,那么我们制作的窗口将永远无法和和屏幕背景联合成整体,也就失去了透明的效果;同时,我们也不可能每时每刻都调用
updateBackground() 方法获得最新的背景,可行的方法是,通过对事件的监听来选择适当的时间来更新外观

我们应该可以达到这三点共识
1。窗口移动或改变大小时,背景图像一般是不会发生变化的,我们不需要得到新的屏幕快照,只用将以前得到的背景中适当的部分贴到窗口上,调用repaint()方法就足已
2。要获得最新的屏幕快照,必须先将窗口隐藏起来,调用updateBackground() 得到图像后再把窗口显示,我们可以用refresh方法来表示
 refresh(){
    frame.hide();
    updateBackground() ;
   frame.show();
}
3。如果背景改变了,那么一定是别的windows程序获得了或失去了事件焦点,也就是说我们关注的窗口将触发焦点得失事件,这个时候需要调用refresh() 得到最新的屏幕快照


看到这里,你或许认为已经没有技术难点了,然而,此时才是我们[color=Red]最需要关注的地方[/color]:
参 看第三点,我们需要在窗口得失焦点时调用refresh() 方法;参看第一点,我们调用refresh() 方法时需要先将窗口隐藏,然后再显示。于是问题来了,在隐藏和显示窗口时,同样会触发得失焦点事件,得失焦点事件又将触发新的隐藏和显示窗口事件(为了得 到新的屏幕快照),这就使程序陷入了死循环中,我们必须加以控制,使得第二次触发焦点得失事件时不调用refresh()方法

作者的办法是加一个线程来控制,通过判断时间间隔长短来决定是否调用refresh()方法,可是,这个条件是程序非常的不稳定,因为往往调用时间会根据系统繁忙度而改变,使得需要更新时不能更新,不需要更新的时候反而更新了
因此,我决定采取新的解决方案,能不能隐藏/显示窗口时不触发得失焦点事件呢?
解 决方法很简单,我抛开了传统的setVisible()或者show(),hide()方法,而是使用setLocation()方法,因为调用 setLocation()方法时窗口不会失去焦点,同时,只要用类似setLocation(-2000,-2000)方法也同样可以轻松的让窗口在屏 幕中消失
下面是我的全部代码:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.UIManager;

import org.jvnet.substance.SubstanceLookAndFeel;
import org.jvnet.substance.theme.SubstanceLightAquaTheme;

import ch.randelshofer.quaqua.QuaquaLookAndFeel;

public class TestEvent extends JComponent implements ComponentListener,
  WindowFocusListener {
 private JFrame frame;

 private Boolean isHiding = false, isShowing = false, start = false;

 private Image background;

 private Point p;
                    
                    [color=Red]//获得当前屏幕快照[/color]
 public void updateBackground() {
  try {
   Robot rbt = new Robot();
   Toolkit tk = Toolkit.getDefaultToolkit();
   Dimension dim = tk.getScreenSize();
   background = rbt.createScreenCapture(new Rectangle(0, 0, (int) dim
     .getWidth(), (int) dim.getHeight()));
  } catch (Exception ex) {
   // p(ex.toString());
   // 此方法没有申明过 ,因为无法得知上下文 。因为不影响执行效果 ,先注释掉它 ex.printStackTrace();
  }

 }

                   [color=Red]  //将窗口掉离出屏幕以获得纯粹的背景图象[/color]
 public void refresh() {
  if (start == true) {
   this.updateBackground();
   frame.setLocation(p);
   if (p.x < 0 || p.y < 0)
    frame.setLocation(0, 0);
   this.repaint();
  }
 }

 public void componentHidden(ComponentEvent e) {
  // TODO Auto-generated method stub
  System.out.println("Hidden");
 }

                     [color=Red] //窗口移动时[/color]
 public void componentMoved(ComponentEvent e) {
  // TODO Auto-generated method stub
  System.out.println("moved");
  this.repaint();
 }

                    [color=Red] //窗口改变大小时[/color]
 public void componentResized(ComponentEvent e) {
  // TODO Auto-generated method stub
  System.out.println("resized");
  this.repaint();
 }

 public void componentShown(ComponentEvent e) {
  // TODO Auto-generated method stub
  System.out.println("shown");
 }

                    [color=Red] //窗口得到焦点后,用refresh()方法更新界面[/color]
 public void windowGainedFocus(WindowEvent e) {
  // TODO Auto-generated method stub
  System.out.println("gainedFocus");
  refresh();
  start = false;
 }

                    [color=Red] //窗口失去焦点后,将其移出屏幕[/color]
 public void windowLostFocus(WindowEvent e) {
  // TODO Auto-generated method stub
  System.out.println("lostFocus");
  if (frame.isShowing() == true) {
   System.out.println("visible");
  } else {
   System.out.println("invisible");
  }
  start = true;
  p = frame.getLocation();
  frame.setLocation(-2000, -2000);
 }

 public TestEvent(JFrame frame) {
  super();
  this.frame = frame;
  updateBackground();
  this.setSize(200, 120);
  this.setVisible(true);
  frame.addComponentListener(this);
  frame.addWindowFocusListener(this);

  // TODO Auto-generated constructor stub
 }

                    //绘制外观,注意,其中 pos,offset 是为了将特定部分的图象贴到窗口上
 public void paintComponent(Graphics g) {
  Point pos = this.getLocationOnScreen();
  Point offset = new Point(-pos.x, -pos.y);
  g.drawImage(background, offset.x, offset.y, null);
 }

 /**
  * @param args
  */
 public static void main(String[] args) {
  // TODO Auto-generated method stub
  try {
   // UIManager.setLookAndFeel("org.fife.plaf.Office2003.Office2003LookAndFeel");
   // UIManager.setLookAndFeel("org.fife.plaf.OfficeXP.OfficeXPLookAndFeel");
   // UIManager.setLookAndFeel("org.fife.plaf.OfficeXP.OfficeXPLookAndFeel");
   UIManager.setLookAndFeel(new SubstanceLookAndFeel());
   //UIManager.setLookAndFeel(new SmoothLookAndFeel());
   //UIManager.setLookAndFeel(new QuaquaLookAndFeel());
   UIManager.put("swing.boldMetal", false);
   if (System.getProperty("substancelaf.useDecorations") == null) {
    JFrame.setDefaultLookAndFeelDecorated(true);
    //JDialog.setDefaultLookAndFeelDecorated(true);
   }
   System.setProperty("sun.awt.noerasebackground", "true");
   SubstanceLookAndFeel.setCurrentTheme(new SubstanceLightAquaTheme());

   // UIManager.setLookAndFeel("org.fife.plaf.VisualStudio2005.VisualStudio2005LookAndFeel");
  } catch (Exception e) {
   System.err.println("Oops!  Something went wrong!");
  }

  JFrame frame = new JFrame("Transparent Window");
  TestEvent t = new TestEvent(frame);
  t.setLayout(new BorderLayout());
  JButton button = new JButton("This is a button");
  t.add("North", button);
  JLabel label = new JLabel("This is a label");
  t.add("South", label);
  frame.getContentPane().add("Center", t);
  frame.pack();
  frame.setSize(150, 100);
  frame.show();
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  // t.start=true;
 }

}


当然,以上代码还存在许多不足,比如在移动窗口时性能会有影响,应该可以通过线程来控制其更新的频率来提升效率,希望大家能一起研究一下,改好了记得通知我哦

ps:你也许还会说,e?怎么没讲任意形状窗口如何打造?
       呵呵,其实只要把窗口按上述方法设置成透明,再去掉边框,再换上自己的边框,不就完成了马?
     相信聪明的各位一定很容易就办到咯

另外,我还在下面附带了作者的大论,想了解更多的同学也可以去看看

http://www.matrix.org.cn/resource/article/44/44186_Swing.html