支持透明和不規則窗口已經成為 AWT 和 Swing 團隊長久以來夢寐以求的功能。盡管本機應用程序在主要操作系統上使用這項功能已經為時 已久,但在核心 Java 中還不能使用它。即將發布的 “Consumer JRE”正在進行修改,也就是對 Java SE 6 進行重大更新。Java SE 6 將為 創建不規則、全透明和每個像素透明的頂級窗口提供 API。
歷史
本機應用程序的開發人員通常在開發 UI 應用程序中享受了更高級的靈活性。但是為此而付出的代價是將應用程序限制在某一特定平台上, 在許多情況中,這種靈活性不如獲得更為豐富的 UI 體驗和桌面緊密集成那麼重要。從傳統上講,跨平台 UI 工具箱,例如 Swing、SWT、QT 和 wxWidgets 趨向於被動應付眾所周知的兩難問題。當只有某些目標平台支持所要求的功能時怎麼辦?在這種情況下,模擬缺失的功能可能只 會讓您南轅北轍。
不規則和透明窗口是跨平台 UI 工具箱局限性的最好例子。如果在特定目標平台不支持此項功能,那麼在該平台上就沒有什麼更多事情要做 了,此項功能可能用作強有力的參數向工具箱添加該項功能。但是,Swing 開發人員社區長久以來一直爭論主要目標平台不久就會提供這些功 能。事實上,Windows 自從 Windows 95 ( 參見 MSDN 上的 SetWindowRgn 文檔 )就已經支持不規則窗口了。在 X11 中匹配功能自從 1989 年 ( 參見 X Nonrectangular Window Shape Extension Library PDF 文檔 )就已經可用了。在 OS X 中您僅能在 JFrame 上設置透明的背 景顏色。
直到現在,對跨平台透明和不規則窗口有興趣的 Swing 應用程序有三種主要可選方式。
在顯示目標窗口之前使用 java.awt.Robot 捕獲桌面。這種方法在 Joshua Marinacci 和 Chris Adamson 編寫的 《 Swing Hacks 》 書中 的 第 41 章 中已經進行了評述。
使用 JNI 包裝目標平台的本機 API。
使用由 Timothy Wall 開發的 JNA 庫。該庫在 2007 年問世,Timothy 對於 不規則窗口 和 字母掩碼透明度 已經發表過博客。
第一種方法的主要問題是要使用 Robot 類。即使您有權限獲得屏幕截圖,您也必須在顯示窗口之前完成。此外,如何保持桌面後台同步?假 設在後台正在播放 YouTube 視頻。與窗口生成的事件不同( 調整大小,移動 ),AWT 並不在任何交叉窗口的重畫上提供注冊偵聽器的任何方 式。雖然 Chris 和 Joshua 通過在至少每秒內進行快照提供解決方法,這對於覆蓋後台視頻播放還不夠。而且在每次快照前需要對窗口加以隱 藏;這可能導致可見的閃爍。
使用 JNI 和 JNA 導致顯著的視覺保真性改進。純 JNI 會帶來開銷的急劇下降:您必須將目標平台的每一個相關的 API 綁定,還要捆綁本 機庫。JNA 為您分擔這項重任; 它捆綁主機庫並提供能在運行時提取並加載它們的類加載器。它支持 Linux、 OS X、 Windows、 Solaris 和 FreeBSD。
Consumer JRE
Java SE 6 Update N, 通常稱作 Consumer JRE, 是 Sun 公司的努力成果,為重新配置 Java 將其作為開發富桌面應用程序的可行方法 。在 Consumer JRE 中的新功能和主要改進列表相當廣泛,並將特別閃耀的寶石隱藏在最新一周構建代碼之一的發行說明中。Bug 6633275 被 簡單地賦予“需要支持不規則/透明窗口”的標題。但是該實現核心 JDK 新功能的可能性所帶給 Swing 開發人員的意義是深遠的。本文的剩余 部分將顯示能夠實現和如何實現該功能的幾個示例。
在進一步研究之前,有一個非常重要的注意事項。由於 Consumer JRE 被官方認為是對穩定 JDK 發行的一個次要更新,因此在“公共”包 中不能添加任何新的 API( 類、方法等等 ),例如 java.awt 或 javax.swing。在本文中討論的所有 API 在新 com.sun.awt.AWTUtilities 類中出現,該類不是官方支持的部分 API。它在 Java SE 7 中的位置最有可能發生改變,簽名方法可能在現在和最終的 Consumer JRE 發行之 間發生輕微變化。所以當這種改變發生時准備更改您自己的代碼。
AWTUtilities 類
我首先討論 com.sun.awt.AWTUtilities 類,請參見 在核心 Java 中的透明和不規則窗口 博客條目。首先我們從圖 1 中的簡單窗口入手 :
圖 1. 帶有控件的窗口
要使窗口透明,您可以使用 AWTUtilities.setWindowOpacity(Window, float) 方法,如圖 2 所示:
圖 2. 相同的窗口,但是有 50% 的不透明度
要使窗口不規則,您可以使用 AWTUtilities.setWindowShape(Window, Shape) 方法,如圖 3 所示:
圖 3. 相同的窗口,但是被一個橢圓剪裁
正如您從圖 3 中能看到的,不規則的窗口看起來不是很好。窗口的邊緣呈鋸齒狀並且整體印象也不是很干淨。要獲得不規則窗口的更佳視 覺效果,您必須使用 AWTUtilities.setWindowOpaque(Window, boolean) API,並使用柔性裁剪繪畫窗口背景。這在後續的 Swing 窗口的柔 性裁剪和每像素透明度 博客條目中進行了闡明。對於窗口的左上角和右上角,該條目采用 Chris Campbell 的 柔性裁剪教程 以及 Romain Guy 的 反射教程, 其中包括 Sebastien Petrucci 的改進。圖 4 顯示了每個像素透明的柔性裁剪窗口:
圖 4. 柔性裁剪和每個像素透明的窗口
現在我們手頭上已經有了這些 API,我們打算做些什麼呢?對它們進行探索這種可能性當然是另人好奇的,我們正打算看看幾個多樣混合的 示例。
工具提示
讓我們使應用工具提示變得透明怎麼樣?對於輕量級工具提示,實現這一目標是相當容易的,因為它們被作為 Swing 頂級窗口的一部分加 以繪畫。( 要獲得關於輕量級彈出菜單的詳細信息,請參見 玻璃窗格和輕量級彈出菜單 條目。)但是,一旦工具提示成為重量級並“打破” 窗口綁定,您必須繼續采用 Robot 或 JNI/JNA。現在讓我們看一看使用 AWTUtilities API 如何完成這項任務。
javax.swing.PopupFactory 是創建彈出菜單的廠。工具提示只是彈出功能的一個例子;其他例子包括組合框下拉列表和菜單。 PopupFactory.setSharedInstance API 可以被用於設置自定義彈出廠,這就是我們想要做的。當前的彈出廠被用於創建所有應用彈出窗口,我 們將在所有的工具提示上安裝自定義不透明廠。
核心彈出廠的實現是相當復雜的。首先嘗試創建輕量級彈出窗口,當要求創建重量級窗口時,系統要管理高速緩存以便重用先前創建的彈出 窗口。實現過程將創建一個新的重量級彈出窗口;在相對較新的膝上型電腦上運行不同的方案還未顯示任何突出的性能突破。讓我們從自定義 彈出廠著手研究:
public class TranslucentPopupFactory extends PopupFactory {
@Override
public Popup getPopup(Component owner, Component contents, int x, int y)
throws IllegalArgumentException {
// A more complete implementation would cache and reuse
// popups
return new TranslucentPopup(owner, contents, x, y);
}
}
TranslucentPopup 的實現相當簡單。構造器創建新的 JWindow,將工具提示的不透明度設置為 0.8,從 Looks 項目安裝提供拖放陰影的自 定義邊框:
TranslucentPopup(Component owner, Component contents, int ownerX, int ownerY) {
// create a new heavyweight window
this.popupWindow = new JWindow();
// mark the popup with partial opacity
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
(contents instanceof JToolTip) ? 0.8f : 0.95f);
// determine the popup location
popupWindow.setLocation(ownerX, ownerY);
// add the contents to the popup
popupWindow.getContentPane().add(contents, BorderLayout.CENTER);
contents.invalidate();
JComponent parent = (JComponent) contents.getParent();
// set the shadow border
parent.setBorder(new ShadowPopupBorder());
}
現在我們需要重寫 Popup 的 show() 方法來標記整個彈出窗口為透明樣式。這要求拖放陰影邊框的每個像素具有透明性。
@Override
public void show() {
this.popupWindow.setVisible(true);
this.popupWindow.pack();
// mark the window as non-opaque, so that the
// shadow border pixels take on the per-pixel
// translucency
com.sun.awt.AWTUtilities.setWindowOpaque(this.popupWindow, false);
}
hide() 方法只是隱藏並處置彈出窗口:
@Override
public void hide() {
this.popupWindow.setVisible(false);
this.popupWindow.removeAll();
this.popupWindow.dispose();
}
要安裝該彈出窗口,僅簡單調用
PopupFactory.setSharedInstance(new TranslucentPopupFactory());
圖 5 顯示了一個具有透明工具提示的示例幀。注意,與工具提示保持視覺(透明性和拖放陰影邊框)上的一致性跨越 Swing 幀綁定並擴展 到後台 Eclipse 窗口。
圖 5. 工具提示
現在我們做相同的動畫。當工具提示顯示時將顏色調淡些,當它被隱藏起來時把它的顏色漸隱如何?一旦您熟悉了 AWTUtilities API,上 述操作不難實現。下面給出 show() 方法的代碼:
@Override
public void show() {
if (this.toFade) {
// mark the popup with 0% opacity
this.currOpacity = 0;
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow, 0.0f);
}
this.popupWindow.setVisible(true);
this.popupWindow.pack();
// mark the window as non-opaque, so that the
// shadow border pixels take on the per-pixel
// translucency
com.sun.awt.AWTUtilities.setWindowOpaque(this.popupWindow, false);
if (this.toFade) {
// start fading in
this.fadeInTimer = new Timer(50, new ActionListener() {
public void actionPerformed(ActionEvent e) {
currOpacity += 20;
if (currOpacity <= 100) {
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
currOpacity / 100.0f);
// workaround bug 6670649 - should call
// popupWindow.repaint() but that will not repaint the
// panel
popupWindow.getContentPane().repaint();
} else {
currOpacity = 100;
fadeInTimer.stop();
}
}
});
this.fadeInTimer.setRepeats(true);
this.fadeInTimer.start();
}
}
這時我們用 0% 的不透明度標記彈出窗口。然後我們啟動重復計時器進行五次迭代。每一次跌代我們增加窗口不透明度 20% 並重新繪 畫。最後我們停止計時器。最終的視覺結果是工具提示外觀的平滑退色序列,這一序列持續大約 250 毫秒。
hide() 方法非常類似:
@Override
public void hide() {
if (this.toFade) {
// cancel fade-in if it's running.
if (this.fadeInTimer.isRunning())
this.fadeInTimer.stop();
// start fading out
this.fadeOutTimer = new Timer(50, new ActionListener() {
public void actionPerformed(ActionEvent e) {
currOpacity -= 10;
if (currOpacity >= 0) {
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
currOpacity / 100.0f);
// workaround bug 6670649 - should call
// popupWindow.repaint() but that will not repaint the
// panel
popupWindow.getContentPane().repaint();
} else {
fadeOutTimer.stop();
popupWindow.setVisible(false);
popupWindow.removeAll();
popupWindow.dispose();
currOpacity = 0;
}
}
});
this.fadeOutTimer.setRepeats(true);
this.fadeOutTimer.start();
} else {
popupWindow.setVisible(false);
popupWindow.removeAll();
popupWindow.dispose();
}
}
首先檢查退色序列是否仍在運行,根據需要將它刪除。然後,不立即隱藏窗口,而是將不透明度以 10% 的增量從 100% 改為 0(因此漸隱 序列是退色序列的兩倍)然後隱藏並處置彈出窗口。注意兩種方法參閱了 Boolean toFade 變量 —— 它在工具提示上被設置為 true。彈出窗 口的其他類型(菜單、組合框下拉列表)沒有退色動畫。
視頻反射
現在讓我們做些更為激動人心的事情。在 Romain Guy 的博客條目 重畫管理器演示(第 11 章) 中,它顯示了提供反射功能的 Swing 組 件。從他與 Chet Haase 合著的 《 骯髒的富客戶機 》 書中抽取一段測試應用程序,其中顯示該組件提供了 QuickTime 電影的實時反射。 在窗口綁定 之外 進行反射如何?
首先要有實際應用中的反射幀的屏幕截圖。圖 6 顯示了正在播放 “Get a Mac” 廣告的形狀規則的 Swing 幀( 使用嵌入式 QuickTime 播放器 ), 伴隨著覆蓋桌面的透明的實時反射:
圖 6. QuickTime 電影的反射
該實現重用了來自 Romain 的幾個構造塊並將它們擴展到“桢外”。它還有一個重畫管理器 ( 要了解關於重畫管理器方面的詳細信息,請 參見 使用重畫管理器的驗證覆蓋 條目 )以便將主桢內容與反射窗口保持同步。還需要在主桢上注冊組件偵聽器和窗口偵聽器以便確保反射窗 口與主窗口的可見性、位置和大小保持同步。除此之外,還要有一個自定義窗格將其內容繪畫到脫屏緩沖區。脫屏緩沖區被用於繪畫主桢和在反 射窗口內的反射。
讓我們看一下代碼。主類是擴展 JFrame 的 JReflectionFrame。構造器創建了反射窗口並向其中添加非雙重緩沖和透明的面板。還重寫了 面板的 paintComponent() 以便繪畫主桢內容的反射。在初始化反射桢的位置和大小後,我們安裝了一個自定義重畫管理器。
public JReflectionFrame(String title) {
super(title);
reflection = new JWindow();
reflectionPanel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
// paint the reflection of the main window
paintReflection(g);
}
};
// mark the panel as non-double buffered and non-opaque
// to make it translucent.
reflectionPanel.setDoubleBuffered(false);
reflectionPanel.setOpaque(false);
reflection.setLayout(new BorderLayout());
reflection.add(reflectionPanel, BorderLayout.CENTER);
// register listeners - see below
...
// initialize the reflection size and location
reflection.setSize(getSize());
reflection.setLocation(getX(), getY() + getHeight());
reflection.setVisible(true);
// install custom repaint manager to force re-painting
// the reflection when something in the main window is
// repainted
RepaintManager.setCurrentManager(new ReflectionRepaintManager());
}
下面是保持反射窗口與主桢同步的偵聽器:
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
reflection.setVisible(false);
}
@Override
public void componentMoved(ComponentEvent e) {
// update the reflection location
reflection.setLocation(getX(), getY() + getHeight());
}
@Override
public void componentResized(ComponentEvent e) {
// update the reflection size and location
reflection.setSize(getWidth(), getHeight());
reflection.setLocation(getX(), getY() + getHeight());
}
@Override
public void componentShown(ComponentEvent e) {
reflection.setVisible(true);
// if the reflection window is opaque, mark
// it as per-pixel translucent
if (com.sun.awt.AWTUtilities.isWindowOpaque(reflection)) {
com.sun.awt.AWTUtilities.setWindowOpaque(reflection, false);
}
}
});
this.addWindowListener(new WindowAdapter() {
@Override
public void windowActivated(WindowEvent e) {
// force showing the reflection window
reflection.setAlwaysOnTop(true);
reflection.setAlwaysOnTop(false);
}
});
重畫管理器相當簡單:它強制主桢的整個根窗格重畫,然後更新反射窗口。這樣可以最優化更新區域反射的同步,對於示例應用程序要達到 的目的,這點就足夠了。
private class ReflectionRepaintManager extends RepaintManager {
@Override
public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
Window win = SwingUtilities.getWindowAncestor(c);
if (win instanceof JReflectionFrame) {
// mark the entire root pane to be repainted
JRootPane rp = ((JReflectionFrame) win).getRootPane();
super.addDirtyRegion(rp, 0, 0, rp.getWidth(), rp.getHeight());
// workaround bug 6670649 - should call reflection.repaint()
// but that will not repaint the panel
reflectionPanel.repaint();
} else {
super.addDirtyRegion(c, x, y, w, h);
}
}
}
主桢 (脫屏緩沖區) 和反射窗口的繪圖代碼在 Romain 的 反射教程 中進行了詳細描述。
結束語
對這一結果我們期待已久,現在終於如願以償。盡管創建透明和不規則窗口的 API 還沒有官方支持的包,但是它們仍可用於創建可視的富 跨平台 UI。從 Romain 的博客 透明和不規則窗口( Extreme GUI Makeover ) 條目展示 JNA 項目,用於創建動畫的透明不規則窗口的可視 化競爭應用。現在您可以使用核心 JDK 做同樣的處理。本文全面介紹了顯示實際應用中的核心 JDK API 的三個示例。我確信您能想出更多的 例子。