现在的位置: 首页 > 综合 > 正文

换一种方式弹出Swing Popup

2018年05月19日 ⁄ 综合 ⁄ 共 4777字 ⁄ 字号 评论关闭

 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内显示菜单先画顶层容器,再在该容器上形成动画效果的问题,我稍微做了一下修改。新版本的不再有这种问题。

 

抱歉!评论已关闭.