Swing第六刀:老婆不能换,窗户框可以

Easter79
• 阅读 644

闲话

到了第六刀,这股刚刚被掀起的Swing学习热情,似乎正如天上飘过的这朵小乌云,在狂暴的烈日暴晒中,已迅速消散殆尽。刚才驻足抬首、啧啧称奇的人群已经迅速消散,继续在每天忙忙碌碌中烦躁,浮躁中无聊。写程序的生活,似乎总少那么一丝颜色,一股激情,一抹精彩。

谈论别人的精彩使我们永恒的话题。可是精彩却从未在我们身上发生,这似乎成了我们普通人的宿命。程序员有一个聪明的大脑和颗追求精彩的年轻的心,却不一定有强健的双腿和马拉松一样的耐力。因此那块精彩的大馅饼未砸在自己头上,也就似乎不难理解了。谈论别人的精彩,让自己永远是一个看客。我们何不利用闲暇时间,自己动手,勤学苦练、笔耕不辍,打造巧夺天工、鬼斧神工的编程手艺呢?本来,程序员就是一名现代“手工匠”。

那些满嘴之乎者也、非“框架”不编程、干过多少项目、跳过多少槽、写过多少书,而连synchronized、transient等关键字都没用过、甚至不用金山词霸捣鼓半天都拼不出来这些单词的人,我相信我们身边始终不乏这类“大牛人”。让一个根本不知道地基怎么挖、砖头怎么烧、水泥怎么浇、钢筋怎么焊、墙体怎么垒的建筑师来负责设计陆家嘴的金茂大厦,那是不可思议的事。而在我们软件行业,这似乎是再平常不过的事。怎么,你的老大在会上怒吼“周末一定要发布”、“月底打死要验收”的同时,他很清楚你正在修改的“订单编号要允许任意修改”的“客户合理需求”要导致数据库、PO、VO、EJB、通讯层、界面、设计文档、测试用例、用户手册等所有地方都要修改吗?他理解这需要2天甚至20天而不是2个小时吗?如果答案是肯定的,无疑你很Lucky,别抱怨了,偷着乐吧。

Swing第六刀:老婆不能换,窗户框可以

言归正传

三年前,微软发布了新一代操作系统Vista。Vista最大的卖点,无疑是那个具有半透明效果的窗口系统:Aero界面风格。虽然直到现在性能稳定性更加的Win 7都已经上市,但坚守在Windows XP上的朋友还有相当大的比例。不过没有人否认的是,Aero风格的半透明界面风格,甚是非常讨人喜欢。

Swing大刀砍刀这里,大家肯定知道我们想做什么了:Swing砍一个类似Vista的Aero窗口风格。如果你还坚守在Windows XP阵营,那么不妨用Swing给你的程序换个彩色的、半透明的“窗户框”,透透新鲜空气和阳光,让我们死板的编程生活也多一点精彩!先来一张效果图。如果有点审美疲劳,那就注意看窗口的边框,而不是花里胡哨的内部。这个窗户框可不一般:它不是操作系统的,而是Swing活生生画出来的。

Swing第六刀:老婆不能换,窗户框可以

看到窗口新的窗户框的变化了吗?下图是一个经典Windows窗体图,对比一下吧!告诉我,Swing真的很丑吗?

Swing第六刀:老婆不能换,窗户框可以

没错,这是Swing做的,而且可以运行在XP、Vista、Win 7之上,效果完全相同。窗框虽小,用到的技术却很多。希望对你来说,这不仅仅是一个“趣味程序”,而是一个充满了Swing知识和技巧的“大程序”。当然,这个程序需要Java 6。这个对于大多数人来说,应当不是问题。

基本思路

要用Swing实现这种风格的窗户框,要解决的问题不少。

  • 首先要解决去掉原来窗户框的问题。这个在以前的《Swing大刀》系列文章中提到具体做法,不难;

  • 其次,仔细观察Vista风格的窗体,其拐角是透明的圆角,而不是直角。要做到这一点,要让窗体透明。这个在以前的《Swing大刀》系列文章中也提到了具体做法,同样不难;

  • 自己绘制四周边框。这个可以用一些JLabel之类的组件放置在四周,并用美工制作的素材图打底。罗嗦一点,但是同样可以做到;

  • 窗口的操作。包括resize、move、标题栏双击等动作,都要统统自己来。还有,右上角的最小化、最大化、关闭窗口,也要自己动手;

  • 半透明。这个是关键,也是难点。我们用半透明的图片来解决,还要配上程序控制的半透明底色的动态渲染,以做到颜色动态的“千变万化”。例如,仔细观察Vista和Win7,窗口在active和inactive时候,边框的颜色、透明度等都是有变化的。我们Swing怎么能含糊呢?

  • 窗口标题的模糊背景。仔细观察Vista的窗口标题背景,有白色的一抹光晕。这个弄模拟出来吗?当然!既然Swing是大刀,就要刀刀见血、刀刀致命!当然这个也是本文中技术难度最大的一块,消耗了我一个周六上午才弄出来;

  • 难题还没结束。在具体写代码过程中,还遭遇了一个JDK的bug,几乎断送了所有的努力。最终,凭借在吃中午饭的路上差点被一辆飞驰的搅拌车kill的一瞬间迸发出的大量肾上腺激素,得到一个超凡脱俗的idea,几行代码,绕过了这个bug,终于应来了最终的春天。这个技术不难,思路却很诡异,有兴趣的请继续看。

下面,我们一起来用Swing这把大刀,给你的程序换上一张全新的“自制窗户框”吧!

Swing第六刀:老婆不能换,窗户框可以

窗户的筹备

筹备工作主要是让操作系统的窗户框消失,并且要支持透明。在《Swing第三刀》中我们介绍过相关技术用法。这里直接给出使用方法:

this.setUndecorated(true);
AWTUtilities.setWindowOpaque(this, false);

这两句话可以让窗框小时,并且支持窗口透明。接下来,我们就要自己来绘制窗户框了。我们假设用一个自定义的JPanel来管理整个窗口的内容,并负责窗口绘制,它叫做ShellWindowBorderPane.java。那么,我们需要用下面代码来设置整个窗口的ContentPane。

private ShellWindowBorderPane windowBorderPane = new ShellWindowBorderPane();
//...
this.setContentPane(windowBorderPane);

然后,把真正要显示的窗口的内容,都放在windowBorderPane的CENTER位置(它是一个BorderLayout的JPanel)就行了。

Swing第六刀:老婆不能换,窗户框可以

我们用ShellWindowBorderPane封装了所有窗户框的外观和行为。接下来,我们认真思考该如何实现这个窗户框吧。

窗户框的绘制

有两个思路来制作ShellWindowBorderPane。最开始,考虑过扩展一个Border,自己绘制四个边框和四个角。而且也动手实现了一版。不过后来发现,放置右上角的按钮、鼠标事件等操作,Border并不好处理。最后还是决定改为用JPanel来做。

如果用JPanel做整个内容区域,则窗框自然可以用一堆JLabel+图片来绘制。四条边、四个角,共8个JLabel分别各负其责。用JLabel而不直接用paint的原因是,这些JLabel还承担者接收鼠标事件、显示不同的resize鼠标光标等作用,用JComponent自然更加方便。

窗框的四个角和四个边,分别需要8张图片素材。4个角很好处理;4个边由于是没有复杂图形,所以让美工切一个单像素高(顶、底)或单像素宽(左、右)的素材图,由JLabel负责绘制。绘制图片的时候,强行把图片拉伸至全高或全宽,这样就可以实现边框的绘制了。下面的代码定义了一个基类:

    private class BorderLabel extends JLabel {
        protected Image image = null;
        protected Image inactiveImage = null;
        public BorderLabel(String imageURL) {
            this.image = getImage(imageURL, true);
            this.inactiveImage = getImage(imageURL, false);
        }
        @Override
        public void paint(Graphics g) {
            super.paint(g);
            Graphics2D g2d = (Graphics2D) g;
            Window window = getFrameWindow();
            if (window.isActive()) {
                g2d.drawImage(image, 0, 0, getWidth(), getHeight(), this);
            } else {
                g2d.drawImage(inactiveImage, 0, 0, getWidth(), getHeight(), this);
            }
        }
    }

Swing第六刀:老婆不能换,窗户框可以
然后就可以用这个基类定义这8个窗户框元素了。注意观察不同位置的JLabel要定义好其对应的PreferredSize。

private JLabel lbBottom = new BorderLabel("window_border_bottom.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(super.getPreferredSize().width, BORDER_SIZE);
    }
};
private JLabel lbLeft = new BorderLabel("window_border_left.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(BORDER_SIZE, super.getPreferredSize().height);
    }
};
private JLabel lbRight = new BorderLabel("window_border_right.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(BORDER_SIZE, super.getPreferredSize().height);
    }
};
private JLabel lbLeftTop = new BorderLabel("window_border_left_top.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(BORDER_SIZE, TITLE_HEIGHT);
    }
};
private JLabel lbRightTop = new BorderLabel("window_border_right_top.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(BORDER_SIZE, TITLE_HEIGHT);
    }
};
private JLabel lbLeftBottom = new BorderLabel("window_border_left_bottom.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(BORDER_SIZE, BORDER_SIZE);
    }
};
private JLabel lbRightBottom = new BorderLabel("window_border_right_bottom.png") {
    @Override
    public Dimension getPreferredSize() {
        return new Dimension(BORDER_SIZE, BORDER_SIZE);
    }
};

细心的同学会发现一个细节:每个label中都有image和inactiveImage两个图片。为什么呢?这是因为,当窗口在活动和非活动(当前窗口不是操作系统的激活、选中、有焦点的窗口),Vista和Win7的显示颜色和透明是有区别的。为了模拟这种情况,我们根据美工的素材,用程序动态生成了2个不同透明度和颜色的图片。具体请看源码中的相关处理方法。

添加窗户框的动作

窗户框上的动作有这么几个:

  • Resize。当鼠标放在窗口的4条边和4个角,都会显示不同的鼠标光标,并且相应拖拽事件对窗口进行调节大小。

  • 双击标题栏。双击标题栏的动作是最大化/回复窗口。

  • 双击logo,关闭窗口;单击logo,弹出窗口系统菜单。

  • 点击按钮“最大化、最小化、关闭”,响应相应动作。

其中,“点击logo弹出系统菜单”这个有难度。如果自己写一个Swing的弹出菜单弹出,自然没难度,不过太啰嗦,懒得写,风格也和Windows格格不入。怎么办?这里用了一个非常诡异的做法,实现了弹出真正的Windows的窗口系统菜单。请看下图:

Swing第六刀:老婆不能换,窗户框可以

不知道你猜到具体做法没有。如果你现在浏览本文章,请按一下“ALT+空格”这个组合键,再看看下面代码,相信你就会明白了。

@Override
public void mouseReleased(MouseEvent e) {
    //popup menu.
    if (isClickLogo(e)) {
        if (robot == null) {
            try {
                robot = new Robot();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        if (robot != null) {
            //send a "ALT+SPACE" keystroke to popup window menu.
            robot.keyPress(KeyEvent.VK_ALT);
            robot.keyPress(KeyEvent.VK_SPACE);
            robot.keyRelease(KeyEvent.VK_ALT);
        }
    }
}
@Override
public void mouseClicked(MouseEvent e) {
    if (isClickLogo(e)) {
        if (e.getClickCount() > 1) {
            System.exit(0);
        }
    }
}

Swing第六刀:老婆不能换,窗户框可以
其他动作都比较简单。比如resize窗口,主要是处理鼠标的拖拽处理。用一个mouse listener实例,安装在所有的JLabel上来监听事件:

private MouseInputAdapter mouseHandler = new MouseInputAdapter() {
    @Override
    public void mousePressed(MouseEvent e) {
        lastPoint = e.getLocationOnScreen();
    }
    @Override
    public void mouseClicked(MouseEvent e) {
        handleClick(e);
    }
    @Override
    public void mouseDragged(MouseEvent e) {
        handleDrag(e);
    }
    @Override
    public void mouseMoved(MouseEvent e) {
        if (e.getSource() == lbTop) {
            if (e.getPoint().y < 5 && !isWindowMaxmized()) {
                lbTop.setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
            } else {
                lbTop.setCursor(Cursor.getDefaultCursor());
            }
        }
    }
};

其中关于if (e.getSource() == lbTop) {和if (e.getPoint().y < 5 && !isWindowMaxmized()) {的判断,为了特殊处理窗口顶部的resize事件。和其他三个方向不同,title上只有最靠近窗口边缘的地带,才认为是要resize窗口,所以这里判断了一下鼠标y坐标。另外,在窗口最大化以后,所有的resize事件应当屏蔽,不再显示对应的鼠标光标,程序中也做了响应处理,有兴趣的可以看一下。下图是全屏时候的显示效果和鼠标效果。

Swing第六刀:老婆不能换,窗户框可以

JDK关于窗口最大化的一个BUG

日常写程序的过程中你也许很少碰到JDK的bug。大多时候,无论你多么烦躁、抓狂,程序的问题都是你自己造成的,不要轻易怀疑JDK的问题。不过也不尽然,在Swing、Java2D方面的质量确实要问题多一些,这从Sun的bug库数据统计,以及每次JDK更新列表中一大堆的Swing bug fix也能看出来。这次就碰到了一个:当窗口取消了操作系统窗户框、使用了透明以后,JFrame在最大化后,会盖住操作系统的任务栏,明显是没有计算好的原因。

如果你怀疑我说的话,那么可以自己看一下Sun官方的Bug库:

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4737788

惊人的是,这个bug的提交时间是2002年8月27日,已经整整8年过去了,Sun还没有fix。看来Sun真的是没钱了,要不也不会贱卖给了Oracle。不过地下不少牛人出招如何wordaround,甚至都考虑到了双屏幕切换时候的问题。最终,我采用了最后一位大牛的方法:重载了一下JFrame的setExtendedState函数。

/**
 * Fix the bug "jframe undecorated cover taskbar when maximized".
 * See:
 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4737788
 *
 * @param state
 */
@Override
public void setExtendedState(int state) {
    if ((state & java.awt.Frame.MAXIMIZED_BOTH) == java.awt.Frame.MAXIMIZED_BOTH) {
        Rectangle bounds = getGraphicsConfiguration().getBounds();
        Rectangle maxBounds = null;
        // Check to see if this is the 'primary' monitor
        // The primary monitor should have screen coordinates of (0,0)
        if (bounds.x == 0 && bounds.y == 0) {
            Insets screenInsets = getToolkit().getScreenInsets(getGraphicsConfiguration());
            maxBounds = new Rectangle(screenInsets.left, screenInsets.top,
                    bounds.width - screenInsets.right - screenInsets.left,
                    bounds.height - screenInsets.bottom - screenInsets.top);
        } else {
            // Not the primary monitor, reset the maximized bounds...
            maxBounds = null;
        }
        super.setMaximizedBounds(maxBounds);
    }
    super.setExtendedState(state);
}

标题光晕效果

Vista窗口的标题文字下方有一篇模糊的光晕。本来这不是一个很大的事情,Swing模拟不出来这个也没啥,毕竟这是Vista的看家本事。不过本着“要做就要做到很变态”的原则,还是模拟了一下。

private Image activeTitleShadow = null;
private Image inactiveTitleShadow = null;

这两个图片用来存储光晕图片。之所以两个图片,同样,也是因为active和非active时候的效果略有差异。有了这两个图片,在绘制标题的时候,就可以分为三步进行:1、绘制光晕;2、绘制文字;3、绘制logo。代码如下:

            //1. title shadow.
            g2d.setFont(FreeUtil.FONT_14_BOLD);
            if (activeTitleShadow == null) {
                activeTitleShadow = FreeUtil.createWindowTitleShadowImage(g2d, window.getTitle(), true);
                inactiveTitleShadow = FreeUtil.createWindowTitleShadowImage(g2d, window.getTitle(), false);
            }
            Image titleShadow = activeTitleShadow;
            if (!window.isActive()) {
                titleShadow = inactiveTitleShadow;
            }
            int shadowY = (titleShadow.getHeight(null) - TITLE_HEIGHT) / 2;
            g2d.drawImage(titleShadow, -10, -shadowY, null);
            //2. title text.
            g2d.setColor(windowTitleColor);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.drawString(window.getTitle(), 25 + leadingX, 17);
            //3. draw logo.
            g.drawImage(logo, leadingX, 4, null);

那么,光晕图片又是如何生成的呢?思路又是如何呢?细节有点复杂。不过思路是这样的:

  • 获得标题文字的矢量边缘形状;

  • 用一个很粗的画笔Stroke(这里是15像素粗)重新绘制这个文字形状;

  • 生成一个内存图片,用白色填充形状,绘制在内存图片内;

  • 用这个内存图片作为种子,生成一个高度模糊图,作为阴影。同时,在blur的同时,给一个略微透明的灰度颜色作为种子(这里根据是否active来给出两个不同种子,以便生成透明度和光晕度略有区别的光晕图,例如new Color(200, 200, 200, 200))

代码中还有一个很小的细节:在生成光晕图时候,所用的字符串比window的原始字符串增加了一个短横线字符“-”。其目的是什么呢?

GlyphVector vector = g2d.getFont().createGlyphVector(context, title +"-");

看看以下两个图片的区别就能体会其作用了:

Swing第六刀:老婆不能换,窗户框可以

更多图片处理细节,比较复杂,感兴趣的同学还是仔细研究以下代码吧!很多东西说清楚就没意思了。

又一个JDK BUG带来的超级难题

在即将大功告成之际,突然发现了一个现象:自绘窗框+透明窗体的JFrame,此时内部的所有组件的文字,都出现了比较明显的锯齿和失真变形!

Swing第六刀:老婆不能换,窗户框可以

通过反复追查,就是这句话引起的:

AWTUtilities.setWindowOpaque(this, false);

没招了,直觉上这又是JDK的问题。搜遍网络,总算发现一个老外也在问这个问题。不过没人回答。

http://efreedom.com/Question/1-2975380/AWTUtilities-setWindowOpaque-is-causing-some-text-painting-issues

Sun就是这样:增加一个小小的setWindowOpaque,反过来破坏一堆Swing的原有效果。抱怨也没有用了,这样的当头一棒让我的前面工作几乎前功尽弃:总不能为了一个花哨的窗户框,换来全界面文字的变形失真吧,哪怕只有一点点,这个代价也太大了。也就是这时,凭借在吃中午饭的路上差点被一辆飞驰的搅拌车kill的一瞬间迸发出的大量肾上腺激素,得到一个超凡脱俗的idea,几行代码,绕过了这个bug,终于应来了最终的春天。思路是这样的:

  • 再做一个Dialog,以窗框所在JFrame为父窗口弹出。注意不适用模式,Modality=false;

  • Dialog设置为无窗框;

  • 让JFrame的窗框中放入一个透明的JPanel在CENTER,作为“参照物”使用;

  • 真正的界面内容,不放在fakePane中,而放在dialog中;

  • 监听JFrame的尺寸变化和位置变化,让Dialog的大小和位置始终保持和“参照物”始终保持绝对同步;

  • 对了,还要记得,JFrame和Dialog要同生同灭,同隐同现;

不知道表达清楚了没有,也就是说,JFrame不再放内容,只是一个空的“窗户框”;真正的内容在其上面的一个无框的Dialog中放置,并保持Dialog和JFrame的内容区域的位置、大小保持完全一致。

Swing第六刀:老婆不能换,窗户框可以

这样,完全骗过了用户的眼睛,用两个Window叠加,模拟了一个Window。这样,成功的绕过了JDK带来的“透明窗体导致字体失真”的bug。下图看看具体变化(好好擦亮眼睛,仔细观察):

Swing第六刀:老婆不能换,窗户框可以

至此,终于大功告成!最终再来多欣赏几张美图:

Swing第六刀:老婆不能换,窗户框可以

Swing第六刀:老婆不能换,窗户框可以

Swing第六刀:老婆不能换,窗户框可以

源代码下载

老规矩,有福同享有难我当。源代码再次更新,源代码压缩包在这里下载最新twaver.jar在这里。同样老规矩:代码仅供参考学习,请勿直接使用源码用于其他商业用途(多少你得修改修改哈)。下载后,执行其中Shell.java文件即可。本例子需要twaver.jar和Java 6。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k