Translucent and Shaped Swing WindowsSupport for translucent and shaped windows has been a long-standing request for the AWT and Swing teams. Even though native applications have had access to this functionality on all major operating systems for quite a few years, it has not been accessible in core Java. This is changing in the upcoming "Consumer JRE," a major update to Java SE 6 which provides an API to create shaped, globally translucent, and per-pixel translucent top-level windows.
Developers of native applications usually enjoy a greater level of flexibility in developing UI applications. While this comes at the price of restricting an application to a particular platform, in many cases it is outweighed by a richer UI experience and tighter integration with the desktop. Traditionally, cross-platform UI toolkits such as Swing, SWT, QT, and wxWidgets tend to suffer from a well-known dilemma: what to do when only some of the target platforms support a requested feature. In such a case, emulating missing functionality can only get you so far.
Shaped and translucent windows are perfect examples of the
limitations of cross-platform UI toolkits. If a specific target
platform does not support this functionality, there is not much you
can do, and this can be used as a strong argument against adding
this feature to the toolkit. However, the Swing developer community
has long since argued that the major target platforms have provided
these features for quite some time. In fact, Windows has supported
shaped windows since Windows 95 (see the
SetWindowRgn documentation on MSDN). The matching functionality
in X11 has been available since 1989 (see the X Nonrectangular
Window Shape Extension Library PDF document). In OS X, you can
just set a non-opaque background color on a
JFrame.
Up until now, there were three major alternatives available to Swing applications interested in cross-platform translucent and shaped windows:
java.awt.Robot
to capture the desktop prior to showing the target window. This
approach has been documented in
chapter 41 of Swing Hacks
by Joshua Marinacci and Chris Adamson.The main problem with the first approach is the very use of the
Robot class. Even when you have screen capture
permission, it must be done before you show your window. In
addition, how do you keep the desktop background in sync? Suppose
you have a YouTube video playing in the background. Unlike the
window-generated events (resize, move), AWT doesn't provide any way
to register a listener on repaint of intersected windows. While
Chris and Joshua provide a workaround by taking a snapshot at least
every second, it is not enough for overlaying background video
playback. Furthermore, your window needs to be hidden before every
snapshot; this can result in visible flickers.
Using JNI and JNA results in significant visual fidelity improvements. Pure JNI comes with a steep price: you need to bind to the relevant APIs of each one of the target platforms and bundle the native libraries. JNA does the heavy lifting for you; it bundles the native libraries and provides a class loader that is capable of extracting and loading them at runtime. It supports Linux, OS X, Windows, Solaris, and FreeBSD.
Java SE 6 Update N, commonly known as Consumer JRE, is an effort by Sun to reposition Java as a viable alternative for developing rich desktop applications. The list of new features and major improvements in Consumer JRE is quite extensive, and a particularly shiny gem is hidden inside the release notes of one of its latest weekly builds. Bug 6633275 is titled simply "Need to support shaped/translucent windows." However, the possibilities that this new core JDK feature brings to Swing developers are quite far-reaching. The remainder of this article will show just a few examples of what can be done, and how.
Before continuing any further, there is a very important note.
Since Consumer JRE is officially considered a minor update to a
stable JDK release, it cannot add any new APIs (classes, methods,
etc.) in the "public" packages, such as java.awt or
javax.swing. All the APIs discussed in this article
appear in the new com.sun.awt.AWTUtilities class,
which is not a part of officially supported API. Most probably its
location will change in Java SE 7, and the methods signatures might
change slightly between now and the final Consumer JRE release. So
be ready to change your own code when that happens.
AWTUtilities ClassI first talked about the com.sun.awt.AWTUtilities
class in my Translucent and shaped
windows in core Java blog entry. Let's start from a simple
window in Figure 1:

Figure 1. A window with few controls
To make it translucent, you can use the
AWTUtilities.setWindowOpacity(Window, float) method, as
illustrated in Figure 2:

Figure 2. Same window, but with 50 percent opacity
To make it shaped, you can use the
AWTUtilities.setWindowShape(Window, Shape) method as
illustrated in Figure 3:

Figure 3. Same window, but clipped by an oval
As you can see from Figure 3, a shaped window does not look very
good. Its edges are aliased, and the overall impression is not very
clean. To achieve better visuals for shaped windows you need to use
the AWTUtilities.setWindowOpaque(Window, boolean) API
and paint the background of the window with soft clipping. This is
illustrated in the follow-up Soft clipping and per-pixel
translucency for Swing windows blog entry. This entry uses
Chris Campbell's
tutorial on soft clipping for the top-left and top-right
corners of the window and Romain Guy's tutorial on
reflection, including the improvement by Sebastien Petrucci.
Figure 4 shows a soft-clipped window with per-pixel
translucency:

Figure 4. A window with soft clipping and per-pixel
translucency
Now that we have these APIs at our fingertips, what are we going to do? The possibilities are quite intriguing, and to explore them we're going to take a look at a few assorted examples.
How about making application tooltips translucent? This is quite
easy to achieve today for lightweight tooltips
since they are painted as part of the Swing top-level window. (See the Glass panes and lightweight
pop-up menus entry for more information on lightweight pop-ups.)
However, once the tooltip becomes heavyweight and "breaks" the
window bounds, you need to fall back on either Robot
or JNI/JNA. Let's see how it can be done with the
AWTUtilities API.
The javax.swing.PopupFactory is the factory for creating pop-ups. A tooltip is just one example of pop-up functionality; other examples include combo-box drop-down lists and menus. The PopupFactory.setSharedInstance API can be used to set a custom pop-up factory, and this is what we will do. The current pop-up factory is used to create all application pop-ups, and we will install a custom opacity factor on all tooltips.
The core pop-up factory implementation is quite complex. It first tries to create a lightweight pop-up, and when a heavyweight pop-up is required, it manages a cache to reuse previously created pop-up windows. Our implementation will always create a new heavyweight pop-up; running different scenarios on a relatively recent laptop has not revealed any noticeable performance hit. Let's start with a custom pop-up factory:
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);
}
}
The implementation of TranslucentPopup is quite
simple. The constructor creates a new JWindow, sets
its opacity to 0.8 for tooltips, and installs a custom border from
the Looks project that
provides a drop-shadow effect:
TranslucentPopup(Component owner, Component contents, int ownerX, int ownerY) {
// create a new heavyweight window
this.popupWindow = new JWindow();
// mark the popup with partial opacity
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
(contents instanceof JToolTip) ? 0.8f : 0.95f);
// determine the popup location
popupWindow.setLocation(ownerX, ownerY);
// add the contents to the popup
popupWindow.getContentPane().add(contents, BorderLayout.CENTER);
contents.invalidate();
JComponent parent = (JComponent) contents.getParent();
// set the shadow border
parent.setBorder(new ShadowPopupBorder());
}
Now we need to override Popup's show()
method to mark the entire pop-up window as non-opaque. This is
needed for the drop shadow border that has per-pixel
translucency.
@Override
public void show() {
this.popupWindow.setVisible(true);
this.popupWindow.pack();
// mark the window as non-opaque, so that the
// shadow border pixels take on the per-pixel
// translucency
com.sun.awt.AWTUtilities.setWindowOpaque(this.popupWindow, false);
}
The hide() method just hides and disposes the pop-up
window:
@Override
public void hide() {
this.popupWindow.setVisible(false);
this.popupWindow.removeAll();
this.popupWindow.dispose();
}
To install this pop-up factory, simply call
PopupFactory.setSharedInstance(new TranslucentPopupFactory());
Figure 5 shows a sample frame with the translucent tooltip. Note the consistency of the visuals (translucency and drop shadow border) as the tooltip crosses the Swing frame bounds and extends into the background Eclipse window:

Figure 5. Translucent tooltip
Now let's do some animation. How about fading in the tooltip
when it's shown and fading it out when it's hidden? Once you're
familiar with the AWTUtilities APIs, it's not
difficult to do. Here is the code for the show()
method:
@Override
public void show() {
if (this.toFade) {
// mark the popup with 0% opacity
this.currOpacity = 0;
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow, 0.0f);
}
this.popupWindow.setVisible(true);
this.popupWindow.pack();
// mark the window as non-opaque, so that the
// shadow border pixels take on the per-pixel
// translucency
com.sun.awt.AWTUtilities.setWindowOpaque(this.popupWindow, false);
if (this.toFade) {
// start fading in
this.fadeInTimer = new Timer(50, new ActionListener() {
public void actionPerformed(ActionEvent e) {
currOpacity += 20;
if (currOpacity <= 100) {
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
currOpacity / 100.0f);
// workaround bug 6670649 - should call
// popupWindow.repaint() but that will not repaint the
// panel
popupWindow.getContentPane().repaint();
} else {
currOpacity = 100;
fadeInTimer.stop();
}
}
});
this.fadeInTimer.setRepeats(true);
this.fadeInTimer.start();
}
}
Here, we mark the pop-up window with zero percent opacity. Then we start a repeating timer for five iterations. At every iteration, we increase the window opacity by 20 percent and repaint it. Finally, we stop the timer. The end visual result is a smooth fade-in sequence of the tooltip appearance; this sequence lasts for about 250 milliseconds.
The hide() method is very similar:
@Override
public void hide() {
if (this.toFade) {
// cancel fade-in if it's running.
if (this.fadeInTimer.isRunning())
this.fadeInTimer.stop();
// start fading out
this.fadeOutTimer = new Timer(50, new ActionListener() {
public void actionPerformed(ActionEvent e) {
currOpacity -= 10;
if (currOpacity >= 0) {
com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
currOpacity / 100.0f);
// workaround bug 6670649 - should call
// popupWindow.repaint() but that will not repaint the
// panel
popupWindow.getContentPane().repaint();
} else {
fadeOutTimer.stop();
popupWindow.setVisible(false);
popupWindow.removeAll();
popupWindow.dispose();
currOpacity = 0;
}
}
});
this.fadeOutTimer.setRepeats(true);
this.fadeOutTimer.start();
} else {
popupWindow.setVisible(false);
popupWindow.removeAll();
popupWindow.dispose();
}
}
First it checks whether the fade-in sequence is still running,
and cancels it as needed. Then, instead of immediately hiding the
window, it changes its opacity from 100 percent to zero percent in increments of 10
(so that the fade-out sequence is twice as slow as the fade-in),
and only then hides and disposes of the pop-up window. Note that both
methods consult the Boolean toFade variable -- it is
set to true on tooltips only. Other types of pop-ups
(menus, combo box drop-downs) do not have fade animations.
Now let's do something a little bit more exciting. In his Repaint Manager Demos (Chapter 11) blog entry, Romain Guy showed a Swing component that provides reflection capabilities. Taken from the book Filthy Rich Clients, which he co-authored with Chet Haase, the test application shows this component providing realtime reflection of a QuickTime movie. How about taking the reflection outside the window bounds?
First, here is a screenshot of our reflection frame in action. Figure 6 shows a regular Swing frame that plays one of "Get a Mac" ads (using the embedded QuickTime player), along with a translucent realtime reflection that overlays the desktop:

Figure 6. Reflection of a QuickTime movie
This implementation reuses a few building blocks from Romain and extends them to the "out of frame" world. It has a custom repaint manager (see the Validation overlays using repaint manager entry for more information on repaint managers) to keep the reflection window in sync with the main frame contents. It also registers a component listener and window listener on the main frame to make sure that the reflection window is kept in sync with the visibility, location, and size of the main window. In addition, it has a custom root pane that paints its contents to an offscreen buffer. This offscreen buffer is then used to paint both the main frame and its reflection in the reflection window.
Let's see some code. The main class is
JReflectionFrame which extends JFrame. The
constructor creates the reflection window and adds a non-double-buffered, non-opaque panel to it. It also overrides the
paintComponent() of that panel to paint the reflection
of the main frame contents. After initializing the location and
size of the reflection frame, we install a custom repaint
manager.
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 non-double buffered and non-opaque
// to make it translucent.
reflectionPanel.setDoubleBuffered(false);
reflectionPanel.setOpaque(false);
reflection.setLayout(new BorderLayout());
reflection.add(reflectionPanel, BorderLayout.CENTER);
// register listeners - see below
...
// initialize the reflection size and location
reflection.setSize(getSize());
reflection.setLocation(getX(), getY() + getHeight());
reflection.setVisible(true);
// install custom repaint manager to force re-painting
// the reflection when something in the main window is
// repainted
RepaintManager.setCurrentManager(new ReflectionRepaintManager());
}
Here are the listeners that keep the reflection window in sync with the main frame:
this.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
reflection.setVisible(false);
}
@Override
public void componentMoved(ComponentEvent e) {
// update the reflection location
reflection.setLocation(getX(), getY() + getHeight());
}
@Override
public void componentResized(ComponentEvent e) {
// update the reflection size and location
reflection.setSize(getWidth(), getHeight());
reflection.setLocation(getX(), getY() + getHeight());
}
@Override
public void componentShown(ComponentEvent e) {
reflection.setVisible(true);
// if the reflection window is opaque, mark
// it as per-pixel translucent
if (com.sun.awt.AWTUtilities.isWindowOpaque(reflection)) {
com.sun.awt.AWTUtilities.setWindowOpaque(reflection, false);
}
}
});
this.addWindowListener(new WindowAdapter() {
@Override
public void windowActivated(WindowEvent e) {
// force showing the reflection window
reflection.setAlwaysOnTop(true);
reflection.setAlwaysOnTop(false);
}
});
The repaint manager is quite simple: it enforces the repaint of the entire root pane of the main frame and then updates the reflection window. This can be optimized to only sync the reflection of the updated region; for the purposes of our sample application (full-frame video) it is enough:
private class ReflectionRepaintManager extends RepaintManager {
@Override
public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
Window win = SwingUtilities.getWindowAncestor(c);
if (win instanceof JReflectionFrame) {
// mark the entire root pane to be repainted
JRootPane rp = ((JReflectionFrame) win).getRootPane();
super.addDirtyRegion(rp, 0, 0, rp.getWidth(), rp.getHeight());
// workaround bug 6670649 - should call reflection.repaint()
// but that will not repaint the panel
reflectionPanel.repaint();
} else {
super.addDirtyRegion(c, x, y, w, h);
}
}
}
The painting code of the main frame (offscreen buffer) and reflection window was described at length in Romain's tutorial on reflection.
It has been long awaited, and now it's finally here. Even though the APIs for creating translucent and shaped windows are not in the officially supported packages, they can still be used for creating visually rich cross-platform UIs. The Translucent-Shaped Windows (Extreme GUI Makeover) entry from Romain's blog showcases the JNA project to create a visually compelling use of an animated translucent shaped window. Now you can do the same with the core JDK. This article walked through three sample examples that show the new core JDK APIs in action. I'm sure that you can think of more.
Robot class to emulate
transparent windows, from Swing Hacks by Joshua
Marinacci and Chris AdamsonKirill Grouchnikov has been writing software since he was in junior high school, and after finishing his BSc in computer science, he happily continues doing it for a living. His main fields of interest are desktop applications, imaging algorithms, and advanced UI technologies.
|
|