Swing中Popup结构是Swing所有弹出窗口的基础类。Swing的弹出式窗口包括菜单、JComboBox下拉框、JToolTip等,它们都使用PopupFactory.getPopup来获得Popup窗口。目前Swing弹出窗口总是瞬时显示,没有像滚动弹出和淡入淡出等动画效果。但在一些操作系统上,弹出窗口可以被配置为动画弹出模式的。比如在Windows中,通过桌面属性->外观->效果的对话框,可以为菜单和工具设置位滚动效果。如下图所示:
如果你选择这种效果,Windows桌面应用程序的菜单和提示都会滚动弹出。但即使在Windows启动了这一效果,Swing仍然使用瞬时弹出模式,对于追求完美的人来说总让人觉得不满意。
幸运的是Swing的PopupFactory工厂提供了使用自定义PopupFactory的方法,你可以编写自己的PopupFactory类,来实现这种滚动弹出窗口,并在程序开始调用PopupFactory.setSharedInstance(PopupFactory factory)来替换Swing缺省的
PopupFactory,从而使得swing的所有弹出窗口都具有这种效果。
如何编写PopupFactory呢?首先要了解Swing应用程序的Popup窗口类型。为提高效率,Swing会尽可能地使用轻量级的弹出窗口,这在前面的一篇文章《如何混排Swing和AWT组件》中提到过。Swing弹出窗口分为三种类型:轻量级弹出窗口、中量级弹出窗口及重量级弹出窗口。下面简要叙述一下它们适用范围。
轻量级弹出是窗口是继承自JComponent的,它们实际上是添加到顶层容器的JLayeredPane浮动层的普通组件。由于被添加到的层高于ContentPane的层,因此在显示时会覆盖其他Swing组件。这种轻量级组件只能在弹出窗口边界不超出顶层容器的边界时使用,否则显示的内容由于剪裁作用就会不完全。
重量级组件是使用JWindow等重量级顶层容器实现的弹出时窗口。这样做的目的是为了实现边界超出顶层容器边界的弹出窗口。比如多级长菜单经常会超出JFrame的边界,这时它所使用的弹出式窗口就是重量级弹出窗口。
中量级组件是使用AWT/Canvas实现的介于轻量级组件和重量级组件之间的弹出窗口。这类窗口的边界同轻量级组件一样不超出顶层容器的边界,但由于某些原因,比如Swing和AWT混排的窗口,由于AWT组件的Z-order通常要高于Swing组件所依赖的顶层AWT容器组件的Z-order,所以Swing的可扩展组件如菜单就可能被AWT组件所遮盖,这时就要求使用AWT组件来实现弹出窗口。这种情况一般使用AWT/Canvas作为弹出窗口的实现。
因此轻量级弹出窗口的内容组件通常就是弹出窗口本身,而重量级弹出窗口的通常是内容组件的顶层容器JWindow对象,而中量级组件通常是它的父容器AWT/Canvas对象。另外JToolTip是个特殊的依赖于特定组件的组件,当它弹出窗口是轻/中量级时,它的弹出窗口是它的父容器。
明白这些原理后,我们只需要继承Swing的PopupFactory,重载它的getPopup方法,提供自己的Popup。自定义的Popup实现实际上是一个Popup的代理类,该代理类将Popup的显示和关闭方法重载,在显示时启动动画,进行滚动,在关闭前,结束可能的滚动时钟。这个类是这样定义的:
/** * 这个类是一个Popup代理,将真实Popup的显示过程动画弹出 */ class PopupProxy extends Popup implements ActionListener{ //一些常量 private static final int ANIMATION_FRAME_INTERVAL=10; private static final int ANIMATION_FRAMES=10; //被代理的弹出式窗口,这个弹出式窗口是从缺省工厂那儿获得的。 private Popup popupProxy; //当前组件 private Component topComponent; //弹出式窗口最终尺寸 private Dimension fullSize; //动画时钟 private Timer timer; //动画的当前帧 private int frameIndex; public PopupProxy(Popup popup, Component component){ popupProxy=popup; topComponent=component; } /** * 覆盖show方法启动动画线程 */ @Override public void show() { //代理窗口显示 popupProxy.show(); //获取显示后窗口的最终大小。 fullSize=topComponent.getSize(); //设置窗口的初始尺寸 topComponent.setSize( horizontalExtending?0:fullSize.width, verticalExtending?0:fullSize.height); //初始化为第一帧 frameIndex=1; //启动动画时钟 timer=new Timer(ANIMATION_FRAME_INTERVAL, this); timer.start(); } /** * 重载hide,关闭可能的时钟 */ @Override public void hide() { if(timer!=null&&timer.isRunning()){ //关闭时钟 timer.stop(); timer=null; } //代理弹出窗口关闭 popupProxy.hide(); } //动画时钟事件的处理,其中一帧 public void actionPerformed(ActionEvent e) { //设置当前帧弹出窗口组件的尺寸 topComponent.setSize( horizontalExtending? fullSize.width*frameIndex/ANIMATION_FRAMES: fullSize.width, verticalExtending? fullSize.height*frameIndex/ANIMATION_FRAMES: fullSize.height); if(frameIndex==ANIMATION_FRAMES){ //最后一帧,关闭时钟 timer.stop(); timer=null; }else //前进一帧 frameIndex++; } }
其构造函数从PopupFactory获取一个Popup及顶层弹出窗口组件。显示时,启动时钟,设置最小尺寸。每一动画帧计算当前尺寸并更新弹出窗口的大小。最后一帧关闭时钟。在关闭弹出窗口时,关闭有可能正在进行动画滚动的时钟。
自定义的ScrollablePopupFactory是这样定义的:
public class ScrollablePopupFactory extends PopupFactory{ //是否横向滚动,缺省不 private boolean horizontalExtending; //是否垂直滚动,缺省不 private boolean verticalExtending; /** * Creates a new instance of ScrollablePopupFactory */ public ScrollablePopupFactory(){ } /** * @param h 是否横向滚动 * @param v 是否垂直滚动 */ public ScrollablePopupFactory(boolean h, boolean v) { horizontalExtending=h; verticalExtending=v; } /** * 覆盖PopupFactory.getPopup方法提供自定义Popup代理 * */ @Override public Popup getPopup(Component owner, Component contents, int x, int y) throws IllegalArgumentException { //获取缺省的Popup Popup popup = super.getPopup(owner, contents, x, y); //如果纵横都不滚动,直接使用缺省的弹出式窗口 if(!(horizontalExtending||verticalExtending)) return popup; //目前没有好办法判断弹出窗口是何种类型,只好用类名字来判断 String name=popup.getClass().getName(); if(name.equals("javax.swing.PopupFactory$HeavyWeightPopup")){ //重量级的弹出窗口,其顶层容器为JWindow return new PopupProxy( popup, SwingUtilities.getWindowAncestor(contents)); }else{ //轻量级的弹出窗口 if(contents instanceof JToolTip) //如果组件是JToolTip,则其父亲容器就是顶层容器 return new PopupProxy( popup, contents.getParent()); else //其他弹出式窗口则组件本身就是顶层容器 return new PopupProxy( popup, contents); } } /** * 这个类是一个Popup代理,将真实Popup的显示过程动画弹出 */ class PopupProxy extends Popup implements ActionListener{ ...... } }
ScrollablePopupFactory.getPopup使用父类方法的getPopup获取Popup对象,并根据其类型计算出不同弹出式窗口的顶层组件,然后将Popup该顶层组件封装成PopupProxy对象返回给调用者。
如何使用这个ScrollablePopupFactory?只需要在程序开始时设置一下就可以了:
public static void main(String args[]) { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception ex) {} //设置自定义的PopupFactory,注意纵横都扩展,可以改变布尔值只横向或纵向扩展,或者没有动画 PopupFactory.setSharedInstance(new ScrollablePopupFactory(true, true)); EventQueue.invokeLater(new Runnable() { public void run() { new ScrollablePopupDemo().setVisible(true); } }); }
下面是演示程序的抓图,菜单正在滚动展开,下面有两个checkbox指定滚动展开方式。另外可以拖动窗口尺寸,以便让菜单、下拉框和ToolTip的窗口边界超过外层窗口的边界,测试重量级弹出窗口的效果。
同样原理可以实现弹出式窗口的淡入淡出动画效果。该演示的源码可以在这儿下载,其主类是dyno.swing.beans.test.ScrollablePopupDemo,下载后导入NetBeans进行编译运行。
更新:针对有的网友提出的JInternalFrame内显示菜单先画顶层容器,再在该容器上形成动画效果的问题,我稍微做了一下修改。新版本的不再有这种问题。