支持透明和不规则窗口已经成为 AWT 和 Swing 团队长久以来梦寐以求的功能尽管本机应用程序在主要操作系统上使用这项功能已经为时已久但在核心 Java 中还不能使用它即将发布的 Consumer JRE正在进行修改也就是对 Java SE 进行重大更新Java SE 将为创建不规则全透明和每个像素透明的顶级窗口提供 API 历史本机应用程序的开发人员通常在开发 UI 应用程序中享受了更高级的灵活性但是为此而付出的代价是将应用程序限制在某一特定平台上在许多情况中这种灵活性不如获得更为丰富的 UI 体验和桌面紧密集成那么重要从传统上讲跨平台 UI 工具箱例如 SwingSWTQT 和 wxWidgets 趋向于被动应付众所周知的两难问题当只有某些目标平台支持所要求的功能时怎么办?在这种情况下模拟缺失的功能可能只会让您南辕北辙 不规则和透明窗口是跨平台 UI 工具箱局限性的最好例子如果在特定目标平台不支持此项功能那么在该平台上就没有什么更多事情要做了此项功能可能用作强有力的参数向工具箱添加该项功能但是Swing 开发人员社区长久以来一直争论主要目标平台不久就会提供这些功能事实上Windows 自从 Windows (参见 MSDN 上的 SetWindowRgn 文档 )就已经支持不规则窗口了在 X 中匹配功能自从 年 ( 参见 X Nonrectangular Window Shape Extension Library PDF 文档 )就已经可用了在 OS X 中您仅能在 JFrame 上设置透明的背景颜色 直到现在对跨平台透明和不规则窗口有兴趣的 Swing 应用程序有三种主要可选方式 在显示目标窗口之前使用 javaawtRobot 捕获桌面这种方法在 Joshua Marinacci 和 Chris Adamson 编写的 《 Swing Hacks 》 书中的 第 章 中已经进行了评述 使用 JNI 包装目标平台的本机 API使用由 Timothy Wall 开发的 JNA 库该库在 年问世Timothy 对于 不规则窗口 和 字母掩码透明度 已经发表过博客 第一种方法的主要问题是要使用 Robot 类即使您有权限获得屏幕截图您也必须在显示窗口之前完成此外如何保持桌面后台同步?假设在后台正在播放 YouTube 视频与窗口生成的事件不同( 调整大小移动 )AWT 并不在任何交叉窗口的重画上提供注册侦听器的任何方式虽然 Chris 和 Joshua 通过在至少每秒内进行快照提供解决方法这对于覆盖后台视频播放还不够而且在每次快照前需要对窗口加以隐藏这可能导致可见的闪烁 使用 JNI 和 JNA 导致显着的视觉保真性改进纯 JNI 会带来开销的急剧下降您必须将目标平台的每一个相关的 API 绑定还要捆绑本机库JNA 为您分担这项重任 它捆绑主机库并提供能在运行时提取并加载它们的类加载器它支持 Linux OS X Windows Solaris 和 FreeBSD Consumer JRE Java SE Update N 通常称作 Consumer JRE 是 Sun 公司的努力成果为重新配置 Java将其作为开发富桌面应用程序的可行方法在 Consumer JRE 中的新功能和主要改进列表相当广泛并将特别闪耀的宝石隐藏在最新一周构建代码之一的发行说明中Bug 被简单地赋予需要支持不规则/透明窗口的标题但是该实现核心 JDK 新功能的可能性所带给 Swing 开发人员的意义是深远的本文的剩余部分将显示能够实现和如何实现该功能的几个示例 在进一步研究之前有一个非常重要的注意事项由于 Consumer JRE 被官方认为是对稳定 JDK 发行的一个次要更新因此在公共包中不能添加任何新的 API( 类方法等等 )例如 javaawt 或 javaxswing在本文中讨论的所有 API 在新 comsunawtAWTUtilities 类中出现该类不是官方支持的部分 API它在 Java SE 中的位置最有可能发生改变签名方法可能在现在和最终的 Consumer JRE 发行之间发生轻微变化所以当这种改变发生时准备更改您自己的代码 AWTUtilities 类我首先讨论 comsunawtAWTUtilities 类请参见 在核心 Java 中的透明和不规则窗口 博客条目首先我们从图 中的简单窗口入手 图 带有控件的窗口 要使窗口透明您可以使用 AWTUtilitiessetWindowOpacity(Window float) 方法如图 所示 图 相同的窗口但是有 % 的不透明度 要使窗口不规则您可以使用 AWTUtilitiessetWindowShape(Window Shape) 方法如图 所示 图 相同的窗口但是被一个椭圆剪裁 正如您从图 中能看到的不规则的窗口看起来不是很好窗口的边缘呈锯齿状并且整体印象也不是很干净要获得不规则窗口的更佳视觉效果您必须使用 AWTUtilitiessetWindowOpaque(Window boolean) API并使用柔性裁剪绘画窗口背景这在后续的 Swing窗口的柔性裁剪和每像素透明度 博客条目中进行了阐明对于窗口的左上角和右上角该条目采用 Chris Campbell 的 柔性裁剪教程 以及 Romain Guy 的 反射教程 其中包括 Sebastien Petrucci 的改进图 显示了每个像素透明的柔性裁剪窗口 图 柔性裁剪和每个像素透明的窗口 现在我们手头上已经有了这些 API我们打算做些什么呢?对它们进行探索这种可能性当然是另人好奇的我们正打算看看几个多样混合的示例 工具提示让我们使应用工具提示变得透明怎么样?对于轻量级工具提示实现这一目标是相当容易的因为它们被作为 Swing 顶级窗口的一部分加以绘画( 要获得关于轻量级弹出菜单的详细信息请参见 玻璃窗格和轻量级弹出菜单 条目)但是一旦工具提示成为重量级并打破窗口绑定您必须继续采用 Robot 或 JNI/JNA现在让我们看一看使用 AWTUtilities API 如何完成这项任务 javaxswingPopupFactory 是创建弹出菜单的厂工具提示只是弹出功能的一个例子其他例子包括组合框下拉列表和菜单PopupFactorysetSharedInstance 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将工具提示的不透明度设置为 从 Looks 项目安装提供拖放阴影的自定义边框 TranslucentPopup(Component owner Component contents int ownerX int ownerY) { // create a new heavyweight window thispopupWindow = new JWindow() // mark the popup with partial opacity comsunawtAWTUtilitiessetWindowOpacity(popupWindow (contents instanceof JToolTip) ? f f) // determine the popup location popupWindowsetLocation(ownerX ownerY) // add the contents to the popup popupWindowgetContentPane()add(contents BorderLayoutCENTER) contentsinvalidate() JComponent parent = (JComponent) contentsgetParent() // set the shadow border parentsetBorder(new ShadowPopupBorder()) }现在我们需要重写 Popup 的 show() 方法来标记整个弹出窗口为透明样式这要求拖放阴影边框的每个像素具有透明性 @Override public void show() { thispopupWindowsetVisible(true) thispopupWindowpack() // mark the window as nonopaque so that the // shadow border pixels take on the perpixel // translucency comsunawtAWTUtilitiessetWindowOpaque(thispopupWindow false) }hide() 方法只是隐藏并处置弹出窗口 @Override public void hide() { thispopupWindowsetVisible(false) thispopupWindowremoveAll() thispopupWindowdispose() }要安装该弹出窗口仅简单调用 PopupFactorysetSharedInstance(new TranslucentPopupFactory()) 图 显示了一个具有透明工具提示的示例帧注意与工具提示保持视觉(透明性和拖放阴影边框)上的一致性跨越 Swing 帧绑定并扩展到后台 Eclipse 窗口 图 工具提示 现在我们做相同的动画当工具提示显示时将颜色调淡些当它被隐藏起来时把它的颜色渐隐如何?一旦您熟悉了 AWTUtilities API上述操作不难实现下面给出 show() 方法的代码 @Override public void show() { if (thistoFade) { // mark the popup with % opacity thiscurrOpacity = comsunawtAWTUtilitiessetWindowOpacity(popupWindow f) } thispopupWindowsetVisible(true) thispopupWindowpack() // mark the window as nonopaque so that the // shadow border pixels take on the perpixel // translucency comsunawtAWTUtilitiessetWindowOpaque(thispopupWindow false) if (thistoFade) { // start fading in thisfadeInTimer = new Timer( new ActionListener() { public void actionPerformed(ActionEvent e) { currOpacity += if (currOpacity <= ) { comsunawtAWTUtilitiessetWindowOpacity(popupWindow currOpacity / f) // workaround bug should call // popupWindowrepaint() but that will not repaint the // panel popupWindowgetContentPane()repaint() } else { currOpacity = fadeInTimerstop() } } }) thisfadeInTimersetRepeats(true) thisfadeInTimerstart() } }这时我们用0%的不透明度标记弹出窗口然后我们启动重复计时器进行五次迭代每一次跌代我们增加窗口不透明度 % 并重新绘画最后我们停止计时器最终的视觉结果是工具提示外观的平滑退色序列这一序列持续大约 毫秒 hide() 方法非常类似 @Override public void hide() { if (thistoFade) { // cancel fadein if its running if (thisfadeInTimerisRunning()) thisfadeInTimerstop() // start fading out thisfadeOutTimer = new Timer( new ActionListener() { public void actionPerformed(ActionEvent e) { currOpacity = if (currOpacity >= ) { comsunawtAWTUtilitiessetWindowOpacity(popupWindow currOpacity / f) // workaround bug should call // popupWindowrepaint() but that will not repaint the // panel popupWindowgetContentPane()repaint() } else { fadeOutTimerstop() popupWindowsetVisible(false) popupWindowremoveAll() popupWindowdispose() currOpacity = } } }) thisfadeOutTimersetRepeats(true) thisfadeOutTimerstart() } else { popupWindowsetVisible(false) popupWindowremoveAll() popupWindowdispose() } }首先检查退色序列是否仍在运行根据需要将它删除然后不立即隐藏窗口而是将不透明度以 % 的增量从 % 改为 (因此渐隐序列是退色序列的两倍)然后隐藏并处置弹出窗口注意两种方法参阅了 Boolean toFade 变量 —— 它在工具提示上被设置为 true弹出窗口的其他类型(菜单组合框下拉列表)没有退色动画 视频反射现在让我们做些更为激动人心的事情在 Romain Guy 的博客条目 重画管理器演示(第 章) 中它显示了提供反射功能的 Swing 组件从他与 Chet Haase 合着的 《骯髒的富客户机》 书中抽取一段测试应用程序其中显示该组件提供了 QuickTime 电影的实时反射在窗口绑定 之外 进行反射如何? 首先要有实际应用中的反射帧的屏幕截图图 显示了正在播放 Get a Mac 广告的形状规则的 Swing 帧( 使用嵌入式 QuickTime 播放器 ) 伴随着覆盖桌面的透明的实时反射 图 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 nondouble buffered and nonopaque // to make it translucent reflectionPanelsetDoubleBuffered(false) reflectionPanelsetOpaque(false) reflectionsetLayout(new BorderLayout()) reflectionadd(reflectionPanel BorderLayoutCENTER) // register listeners see below …… // initialize the reflection size and location reflectionsetSize(getSize()) reflectionsetLocation(getX() getY() + getHeight()) reflectionsetVisible(true) // install custom repaint manager to force repainting // the reflection when something in the main window is // repainted RepaintManagersetCurrentManager(new ReflectionRepaintManager()) }下面是保持反射窗口与主桢同步的侦听器 thisaddComponentListener(new ComponentAdapter() { @Override public void componentHidden(ComponentEvent e) { reflectionsetVisible(false) } @Override public void componentMoved(ComponentEvent e) { // update the reflection location reflectionsetLocation(getX() getY() + getHeight()) } @Override public void componentResized(ComponentEvent e) { // update the reflection size and location reflectionsetSize(getWidth() getHeight()) reflectionsetLocation(getX() getY() + getHeight()) } @Override public void componentShown(ComponentEvent e) { reflectionsetVisible(true) // if the reflection window is opaque mark // it as perpixel translucent if (comsunawtAWTUtilitiesisWindowOpaque(reflection)) { comsunawtAWTUtilitiessetWindowOpaque(reflection false) } } }) thisaddWindowListener(new WindowAdapter() { @Override public void windowActivated(WindowEvent e) { // force showing the reflection window reflectionsetAlwaysOnTop(true) reflectionsetAlwaysOnTop(false) } }) 重画管理器相当简单它强制主桢的整个根窗格重画然后更新反射窗口这样可以最优化更新区域反射的同步对于示例应用程序要达到的目的这点就足够了 private class ReflectionRepaintManager extends RepaintManager { @Override public void addDirtyRegion(JComponent c int x int y int w int h) { Window win = SwingUtilitiesgetWindowAncestor(c) if (win instanceof JReflectionFrame) { // mark the entire root pane to be repainted JRootPane rp = ((JReflectionFrame) win)getRootPane() superaddDirtyRegion(rp rpgetWidth() rpgetHeight()) // workaround bug should call reflectionrepaint() // but that will not repaint the panel reflectionPanelrepaint() } else { superaddDirtyRegion(c x y w h) } } }主桢 (脱屏缓沖区) 和反射窗口的绘图代码在 Romain 的 反射教程 中进行了详细描述 结束语对这一结果我们期待已久现在终于如愿以偿尽管创建透明和不规则窗口的 API 还没有官方支持的包但是它们仍可用于创建可视的富跨平台 UI从 Romain 的博客 透明和不规则窗口( Extreme GUI Makeover ) 条目展示 JNA 项目用于创建动画的透明不规则窗口的可视化竞争应用现在您可以使用核心 JDK 做同样的处理本文全面介绍了显示实际应用中的核心 JDK API 的三个示例我确信您能想出更多的例子 |