Support 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.
History
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:
Use the 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.
Use JNI to wrap the native APIs of the target platform(s).
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.
Consumer JRE
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.
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.
Tooltips
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();
}
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.
Video Reflection
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.
Conclusion
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.
Overview of video reflection component from the book Filthy Rich Clients
by Chet Haase and Romain Guy
Kirill 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.
a bug with buffered windows?
2008-06-26 11:48:28 gr5
[Reply | View]
I found what seems like a bug using just normal swing JPanel. The workaround was to turn off doubleBuffering in *all* my JPanels. The bug seems to be related to toolTips. I *only* used the transparent feature of awtUtilities and I simply wanted a background with rounded corners. A panel within a panel within my frame had strange bugs but only if I had a few tooltips popup first. Go figure. Are tooltips, double buffering, and awtUtilities sharing the same memory? What did the bug look like you wonder? Basically all JLabel text was transparent (you can see through into applications below) instead of black. It's very obvious if you run into it.
Suggestion and comments
2008-05-03 14:25:05 pwillis
[Reply | View]
Not that I'm complaining....but,
Instead of just masking away part of the window to make it transparent, wouldn't
it be better to be able to apply a spline curve to the axes of window components and simply *BEND* the components to the desired shape?
Masking is a nice quick and dirty eye candy thing but,
personally, I'd like to make my scroll bars wavy, my windows
round like portholes, and then map textures to the components **WITHOUT** having to skin a whole bunch
of bitmaps over a fudge-work of mouse events.
Window masking and transparency are kind of standard fare....aren't they.
Maybe I'm too critical. I'm sure this took a great deal of work to accomplish. It's a very nice addition to the available tools.
There are two reasons. One - i needed only a very simple animation sequence. Two - the future of these two projects is not very clear now that Chet has left to work for Adobe.
applets also don't have the desktop as a background but the browser window. At most you'd be able to get transparency to show a background image or colour selected in the containing html file, but IMO anyone using background images for web pages should be shot on sight :) and solid colours can be set in the applet directly as a canvas backgroun colour.
WOW, it's fantastic. The reflection outside the window bounds is just incredible. I can think of all sorts of applications for this, balloon tips, OSX style docks, popups that come from the try area and fade in and out. Great article and thank you to the 6u10 people.