要生成一个半透明的成形窗口而又要避免使用本地的编码唯有灵活地应用screenshot(屏幕快照) 半透明窗口是大众对Swing最为渴求的特性之一 也可以称之为定形窗口这种窗口有一部分是透明的可以透过它看到桌面背景和其它的程序如果不通过JNI(Java Native Interface 本地接口)Java是无法为我们生成一个半透明的窗口的(即使我们可以那样做还得本地操作平台好支持半透明窗口才行)然而这些现状无法阻止我们对半透明窗口的渴求通过一个我最喜欢的手段screenshot我们可以欺骗性地实现这个目的 仿造这样一个的半透明窗口的过程主要的通过以下几点: 在窗口显示之前先获得一个screenshot; 把上一步获取的屏幕快照作为窗口的背景图 调整位置以便于我们捕获的screenshot和实际当前的屏幕完美结合制造出一种半透明的假象 刚刚说到的部分只是小儿科重头戏在于如何在移动或变化半透明窗口时及时地更新screenshot也就是及时更新半透明窗口的背景 在开始我们的旅行之前先生成一个类让它继承 JPanel我们用这个继承类来捕获屏幕并把捕获的照片作为背景 类的具体代码如下例 例 - 半透明背景组件 public class TransparentBackground extends Jcomponent { private JFrame frame; private Image background; public TransparentBackground(JFrame frame) { thisframe = frame; updateBackground( ); } /** * @todo 获取屏幕快照后立即更新窗口背景 */ public void updateBackground( ) { try { Robot rbt = new Robot( ); Toolkit tk = ToolkitgetDefaultToolkit( ); Dimension dim = tkgetScreenSize( ); background = rbtcreateScreenCapture( new Rectangle((int)dimgetWidth( ) (int)dimgetHeight( ))); } catch (Exception ex) { //p(extoString( )); // 此方法没有申明过因为无法得知上下文因为不影响执行效果先注释掉它 exprintStackTrace( ); } } public void paintComponent(Graphics g) { Point pos = thisgetLocationOnScreen( ); Point offset = new Point(posxposy); gdrawImage(backgroundoffsetxoffsetynull); } } 首先构造方法把一个reference保存到父的JFrame然后调用updateBackground()方法在这个方法中我们可以利用javaawtRobot类捕获到整个屏幕并把捕获到的图像保存到一个定义了的放置背景的变量中 paintComponent()方法可以帮助我们获得窗口在屏幕上的绝对位置并用刚刚得到的背景作为panel的背景图同时这个背景图会因为panel位置的不同而作对应的移动以使panel的背景和panel覆盖的那部分屏幕图像无缝重叠在一起同时也就使panel和周围的屏幕关联起来 我们可以通过下面这个main方法简单的运行一下随便放置一些组件到panel上再把panel放置到frame中显示 public static void main(String[] args) { JFrame frame = new JFrame(Transparent Window); TransparentBackground bg = new TransparentBackground(frame); bgsetLayout(new BorderLayout( )); JButton button = new JButton(This is a button); bgadd(Northbutton); JLabel label = new JLabel(This is a label); bgadd(Southlabel); framegetContentPane( )add(Centerbg); framepack( ); framesetSize(); frameshow( ); } 通过这段代码运行出的效果如下图-所示 图 展示中的半透明窗口 这段代码相当简单却带有两个不足之处首先如果移动窗口panel中的背景无法自动的更新而paintComponent()只在改变窗口大小时被调用其次如果屏幕曾经发生过变化那么我们制作的窗口将永远无法和和屏幕背景联合成整体 谁也不想时不时地跑去更新screenshot想想看要找到隐藏于窗口后的东西要获得一份新的screenshot还要时不时的用这些screenshot来更新我们的半透明窗口这些事情足以让用户无法安心工作事实上想要获取窗口之外的屏幕的变化几乎是不太可能的事但多数变动都是发生在foreground窗口发生焦点变化或被移动之时如果你接受这的观点(至少我接受这个观点)那么你可以只监控下面提到的几个事件并只需在这几个事件被触发时去更新screenshot public class TransparentBackground extends JComponent implements ComponentListener WindowFocusListener Runnable { private JFrame frame; private Image background; private long lastupdate = ; public boolean refreshRequested = true; public TransparentBackground(JFrame frame) { thisframe = frame; updateBackground( ); frameaddComponentListener(this); frameaddWindowFocusListener(this); new Thread(this)start( ); } public void componentShown(ComponentEvent evt) { repaint( ); } public void componentResized(ComponentEvent evt) { repaint( ); } public void componentMoved(ComponentEvent evt) { repaint( ); } public void componentHidden(ComponentEvent evt) { } public void windowGainedFocus(WindowEvent evt) { refresh( ); } public void windowLostFocus(WindowEvent evt) { refresh( ); } 首先让我们的半透明窗口即panel实现ComponentListener接口 WindowFocusListener接口和Runnable接口Listener接口可以帮助我们捕获到窗口的移动大小变化和焦点变化实现Runnable接口可以使得panel生成一个线程去控制定制的repaint()方法 ComponentListener接口带有四个component开头的方法它们都可以很方便地调用repaint()方法所以窗口的背景也就可以随着窗口的移动大小的变化而相应地更新还有两个是焦点处理的它们只调用refresh()如下示意 public void refresh( ) { if(frameisVisible( )) { repaint( ); refreshRequested = true; lastupdate = new Date( )getTime( ); } } public void run( ) { try { while(true) { Threadsleep(); long now = new Date( )getTime( ); if(refreshRequested && ((now lastupdate) > )) { if(frameisVisible( )) { Point location = framegetLocation( ); framehide( ); updateBackground( ); frameshow( ); framesetLocation(location); refresh( ); } lastupdate = now; refreshRequested = false; } } } catch (Exception ex) { p(extoString( )); exprintStackTrace( ); } } refresh()可以保证frame可见并适时得调用repaint()它也会对refreshRequest变量置真(true)同时保存当前时间值现在所做的这些对接下来要做的事是非常重要的铺垫 除了每四分之一秒被唤醒一次用来检测是否有新的刷新的要求或者是否离上次刷新时间超过了一秒方法run()一般地处于休眠状态如果离上次刷新超过了一秒并且frame是可见的那么run()将保存frame的位置隐藏frame获取一个screenshot更新frame背景再根据隐藏frame时保存的位置信息重新显示已经更新了背景的frame接着调用refresh()方法通过这样的控制使得背景更新不至于比需要的多太多 那么我们为什么要对用一个线程控制刷新如此长篇大论呢?一个词递归事件处理可以直接轻松地调用repaint()但是隐藏和显示窗口已便于获取screenshot 却交替了很多得焦和失焦事件所有这些都会触发一个新的背景更新导致窗口再次被隐藏如此往返将导致永无止境的循环一个新的得焦事件将在执行refresh()几毫秒之后被调用所以简单地检测isRecursing标志是无法阻止循环的继续 另外用户任意一个改变屏幕的动作将会随之引出一堆的事件来而不仅仅是简单一个应该是最后一个事件去触发updateBackground()而不是第一个为了全面解决这些问题代码产生一个线程然后用这个线程去监控重画(repaint)要求并保证当前的执行动作是发生在过去的毫秒内没有发生过此动作如果一个客户每五秒不间断地产生事件(比如寻找丢失的浏览窗口)那么只有在其它所有工作在一秒内完成才执行更新这样就避免了用户不至于在移动东西时窗口却消失不见了的尴尬 另一件烦恼的事就是我们的窗口仍旧有边框这条边框使得我们无法完美和背景融为一体更为痛苦的是使用setUndecorated(true)移除边框时我们的标题栏和窗口控制栏也跟着移除了可是这也算不上是什么大问题因为那类使用定形窗口的应用程序一般都具有可拖动的背景【Hack#】 接下来我们在下面这个简单的测试程序中把所讲的东西落实进去 public static void main(String[] args) { JFrame frame = new JFrame(Transparent Window); framesetUndecorated(true); TransparentBackground bg = new TransparentBackground(frame); bgsnapBackground( ); bgsetLayout(new BorderLayout( )); JPanel panel = new JPanel( ) { public void paintComponent(Graphics g) { gsetColor(Colorblue); Image img = new ImageIcon(mppng)getImage( ); gdrawImage(imgnull); } }; panelsetOpaque(false); bgadd(Centerpanel); framegetContentPane( )add(Centerbg); framepack( ); framesetSize(); framesetLocation(); frameshow( ); } 这段代码通过继承JPanel加上一个透明的PNG格式图片人工生成一个mp播放器界面注意使用了setUndecorated()来隐藏边框和标题栏调用setOpaque(false)将隐藏默认的背景(一般为灰色)这样screenshot的背景就可以和图片中透明的部分合成一个整体去配合程序窗口周围的屏幕背景(如图)通过一系列的努力就可以看到图-的效果是不是很让人惊诧?会不会感歎Java的新版本腾空出世? 图 - mp 播放器外观模板 图 运行中的mp播放器 |