Java中透明和不规则Swing窗口

支持透明和不规则窗口已经成为 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 的三个示例。我确信您能想出更多的 例子。

你是自由的,不仅是身体上的自由,

Java中透明和不规则Swing窗口

相关文章:

你感兴趣的文章:

标签云: