图形界面开发对于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 com.birosoft.liquid.LiquidLookAndFeel;
public class TestEvent extends JComponent
implements ComponentListener,WindowFocusListener {
private JFrame frame;
private boolean start = false;
private Image background;
private Point p;
// 获得当前屏幕快照
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();
}
}
// 将窗口掉离出屏幕以获得纯粹的背景图象
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) {
System.out.println("Hidden");
}
// 窗口移动时
public void componentMoved(ComponentEvent e) {
System.out.println("moved");
this.repaint();
}
// 窗口改变大小时
public void componentResized(ComponentEvent e) {
System.out.println("resized");
this.repaint();
}
public void componentShown(ComponentEvent e) {
System.out.println("shown");
}
// 窗口得到焦点后,用refresh()方法更新界面
public void windowGainedFocus(WindowEvent e) {
System.out.println("gainedFocus");
refresh();
start = false;
}
// 窗口失去焦点后,将其移出屏幕
public void windowLostFocus(WindowEvent e) {
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);
}
// 绘制外观,注意,其中 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) {
try {
// UIManager.setLookAndFeel("org.fife.plaf.Office2003.Office2003LookAndFeel");
// UIManager.setLookAndFeel("org.fife.plaf.OfficeXP.OfficeXPLookAndFeel");
// UIManager.setLookAndFeel("org.fife.plaf.OfficeXP.OfficeXPLookAndFeel");
UIManager.setLookAndFeel("com.birosoft.liquid.LiquidLookAndFeel");
LiquidLookAndFeel.setLiquidDecorations(true);
// LiquidLookAndFeel.setLiquidDecorations(true, "mac");
// 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