要生成一個半透明的成形窗口,而又要避免使用本地的編碼,唯有靈活地應用screenshot(屏幕快照).
半透明窗口是大眾對Swing最為渴求的特性之一. 也可以稱之為定形窗口,這種窗口有一部分是透明的,可以透過它看到桌面背景和其它的程序.如果不通過JNI(Java Native Interface 本地接口)Java是無法為我們生成一個半透明的窗口的(即使我們可以那樣做,還得本地操作平台好支持半透明窗口才行).然而這些現狀無法阻止我們對半透明窗口的渴求,通過一個我最喜歡的手段screenshot,我們可以欺騙性地實現這個目的.
仿造這樣一個的半透明窗口的過程,主要的通過以下幾點:
1.在窗口顯示之前,先獲得一個screenshot;
2.把上一步獲取的屏幕快照,作為窗口的背景圖
3.調整位置,以便於我們捕獲的screenshot和實際當前的屏幕完美結合,制造出一種半透明的假象.
剛剛說到的部分只是小兒科,重頭戲在於,如何在移動或變化半透明窗口時,及時地更新screenshot,也就是及時更新半透明窗口的背景.
在開始我們的旅行之前,先生成一個類,讓它繼承 JPanel,我們用這個繼承類來捕獲屏幕,並把捕獲的照片作為背景. 類的具體代碼如下例6-1
例 6-1 。 半透明背景組件
public class TransparentBackground extends Jcomponent {
private JFrame frame;
private Image background;
public TransparentBackground(JFrame frame) {
this.frame = frame;
updateBackground( );
}
/**
* @todo 獲取屏幕快照後立即更新窗口背景
*/
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 paintComponent(Graphics g) {
Point pos = this.getLocationOnScreen( );
Point offset = new Point(-pos.x,-pos.y);
g.drawImage(background,offset.x,offset.y,null);
}
}
首先,構造方法把一個reference保存到父的JFrame,然後調用updateBackground()方法,在這個方法中,我們可以利用java.awt.Robot類捕獲到整個屏幕,並把捕獲到的圖像保存到一個定義了的放置背景的變量中. paintComponent()方法可以幫助我們獲得窗口在屏幕上的絕對位置,並用剛剛得到的背景作為panel的背景圖,同時這個背景圖會因為panel位置的不同而作對應的移動,以使panel的背景和panel覆蓋的那部分屏幕圖像無縫重疊在一起,同時也就使panel和周圍的屏幕關聯起來.
我們可以通過下面這個main方法簡單的運行一下,隨便放置一些組件到panel上,再把panel放置到frame中顯示.
public static void main(String[] args) {
JFrame frame = new JFrame("Transparent Window");
TransparentBackground bg = new TransparentBackground(frame);
bg.setLayout(new BorderLayout( ));
JButton button = new JButton("This is a button");
bg.add("North",button);
JLabel label = new JLabel("This is a label");
bg.add("South",label);
frame.getContentPane( ).add("Center",bg);
frame.pack( );
frame.setSize(150,100);
frame.show( );
}
通過這段代碼,運行出的效果如下圖6-1所示:
圖6-1 展示中的半透明窗口
這段代碼相當簡單,卻帶有兩個不足之處。首先,如果移動窗口,panel中的背景無法自動的更新,而paintComponent()只在改變窗口大小時被調用;其次,如果屏幕曾經發生過變化,那麼我們制作的窗口將永遠無法和和屏幕背景聯合成整體。
誰也不想時不時地跑去更新screenshot,想想看,要找到隱藏於窗口後的東西,要獲得一份新的screenshot,還要時不時的用這些screenshot來更新我們的半透明窗口,這些事情足以讓用戶無法安心工作。事實上,想要獲取窗口之外的屏幕的變化幾乎是不太可能的事,但多數變動都是發生在foreground窗口發生焦點變化或被移動之時。如果你接受這的觀點(至少我接受這個觀點),那麼你可以只監控下面提到的幾個事件,並只需在這幾個事件被觸發時,去更新screenshot。
public class TransparentBackground extends JComponent
implements ComponentListener, WindowFocusListener,
Runnable {
private JFrame frame;
private Image background;
private long lastupdate = 0;
public boolean refreshRequested = true;
public TransparentBackground(JFrame frame) {
this.frame = frame;
updateBackground( );
frame.addComponentListener(this);
frame.addWindowFocusListener(this);
new Thread(this).start( );
}
public void componentShown(ComponentEvent evt) { repaint( ); }
public void componentResized(ComponentEvent evt) { repaint( ); }
public void componentMoved(ComponentEvent evt) { repaint( ); }
public void componentHidden(ComponentEvent evt) { }
public void windowGainedFocus(WindowEvent evt) { refresh( ); }
public void windowLostFocus(WindowEvent evt) { refresh( ); }
首先,讓我們的半透明窗口即panel實現ComponentListener接口,
WindowFocusListener接口和Runnable接口。Listener接口可以幫助我們捕獲到窗口的移動,大小變化,和焦點變化。實現Runnable接口可以使得panel生成一個線程去控制定制的repaint()方法。
ComponentListener接口帶有四個component開頭的方法。它們都可以很方便地調用repaint()方法,所以窗口的背景也就可以隨著窗口的移動,大小的變化而相應地更新。還有兩個是焦點處理的,它們只調用refresh(),如下示意:
public void refresh( ) {
if(frame.isVisible( )) {
repaint( );
refreshRequested = true;
lastupdate = new Date( ).getTime( );
}
}
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( );
}
}
refresh()可以保證frame可見,並適時得調用repaint()。它也會對refreshRequest變量置真(true),同時保存當前時間值,現在所做的這些對接下來要做的事是非常重要的鋪墊。
除了每四分之一秒被喚醒一次,用來檢測是否有新的刷新的要求或者是否離上次刷新時間超過了一秒,方法run()一般地處於休眠狀態。如果離上次刷新超過了一秒並且frame是可見的,那麼run()將保存frame的位置,隱藏frame,獲取一個screenshot,更新frame背景,再根據隱藏frame時保存的位置信息,重新顯示已經更新了背景的frame,接著調用refresh()方法。通過這樣的控制,使得背景更新不至於比需要的多太多。
那麼我們為什麼要對用一個線程控制刷新如此長篇大論呢?一個詞:遞歸。事件處理可以直接輕松地調用repaint(),但是隱藏和顯示窗口已便於獲取screenshot 卻交替了很多“得焦”和“失焦”事件。所有這些都會觸發一個新的背景更新,導致窗口再次被隱藏,如此往返,將導致永無止境的循環。一個新的“得焦”事件,將在執行refresh()幾毫秒之後被調用,所以簡單地檢測isRecursing標志是無法阻止循環的繼續。
另外,用戶任意一個改變屏幕的動作,將會隨之引出一堆的事件來,而不僅僅是簡單一個。應該是最後一個事件去觸發updateBackground(),而不是第一個。為了全面解決這些問題,代碼產生一個線程,然後用這個線程去監控重畫(repaint)要求,並保證當前的執行動作是發生在過去的1000毫秒內沒有發生過此動作。如果一個客戶每五秒不間斷地產生事件(比如,尋找丟失的浏覽窗口),那麼只有在其它所有工作在一秒內完成才執行更新。這樣就避免了,用戶不至於在移動東西時,窗口卻消失不見了的尴尬。
另一件煩惱的事就是,我們的窗口仍舊有邊框,這條邊框使得我們無法完美和背景融為一體。更為痛苦的是使用setUndecorated(true)移除邊框時,我們的標題欄和窗口控制欄也跟著移除了。可是這也算不上是什麼大問題,因為那類使用定形窗口的應用程序一般都具有可拖動的背景【Hack#34】
接下來,我們在下面這個簡單的測試程序中把所講的東西落實進去:
public static void main(String[] args) {
JFrame frame = new JFrame("Transparent Window");
frame.setUndecorated(true);
TransparentBackground bg = new TransparentBackground(frame);
bg.snapBackground( );
bg.setLayout(new BorderLayout( ));
JPanel panel = new JPanel( ) {
public void paintComponent(Graphics g) {
g.setColor(Color.blue);
Image img = new ImageIcon("mp3.png").getImage( );
g.drawImage(img,0,0,null);
}
};
panel.setOpaque(false);
bg.add("Center",panel);
frame.getContentPane( ).add("Center",bg);
frame.pack( );
frame.setSize(200,200);
frame.setLocation(500,500);
frame.show( );
}
這段代碼通過繼承JPanel,加上一個透明的PNG格式圖片,人工生成一個mp3播放器界面。注意使用了setUndecorated()來隱藏邊框和標題欄。調用setOpaque(false),將隱藏默認的背景(一般為灰色),這樣screenshot的背景就可以和圖片中透明的部分合成一個整體,去配合程序窗口周圍的屏幕背景。(如圖6-2)通過一系列的努力,就可以看到圖6-3的效果。是不是很讓人驚詫?會不會感歎Java的新版本騰空出世?
圖 6-2. mp3 播放器外觀模板
圖 6-3. 運行中的mp3播放器