2007-08-29

Transparent Java Windows on X11

Transparent windows don't really work in Java. You have four choices, and they all suck:

1. Use non-opaque Color instances. Apple even supports setting a non-opaque background Color, but you need all the components in your UI to behave, and that really makes this awkward. And slow. This may be the method of the future, but at the moment, not enough pieces are there for it to work.

2. Use undocumented/unsupported Java methods. The beauty of this is that it's easy. The trouble with this is that it's not really what you want. Start GNOME Terminal, for example, and drag the transparency slider in the preferences dialog. (You need to be running Compiz.) You'll notice that although the terminal's transparency changes, the window decorations supplied by the system and the menu bar and scroll bar supplied by the application all keep the default fully-opaque level of transparency. (More subtly than that, you'll notice that it's really just the default background that's transparent. Anything rendered on top of that is rendered normally.)

3. Native code. This is currently your only choice on MS Windows. I've never explored this because I don't care enough about transparency (or Windows) to go to the trouble.

4. Swing Hacks's embarrassing copy-the-background trick. I sometimes think that book was on a secret mission to make Java look bad. (Then again, when GNOME Terminal switched to real transparency from a similarly ugly hack, some people complained because they could no longer see XEarth through the whole window stack; they were using "transparent" terminals specifically because they wanted to see the root window. It takes all sorts.)

Here, I'm talking about method 2 for X11 (Linux and Solaris). The Mac OS equivalent is well-known, and can be found every so often on Apple's java-dev mailing list, or around the web if you search for "CWindow.setAlpha". I've yet to see the X11 equivalent, though, so that's what I'm going to show you today.

The code's really just these two lines:

long windowId = peer.getWindow();
sun.awt.X11.XAtom.get("_NET_WM_WINDOW_OPACITY").setCard32Property(windowId, value);

Things are made more complicated by the fact that Component's peer field is only accessible within its package (or via a deprecated method), and XAtom isn't necessarily available on all platforms, and will win you a warning even on Sun's Unix JDK. So it's the usual reflection dance:

import java.awt.*;
import java.lang.reflect.*;
import javax.swing.*;
import javax.swing.event.*;

public class TransparencyTest extends JFrame implements ChangeListener {
private JSlider slider;

public TransparencyTest() {
super("Transparency Test");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

slider = new JSlider(0, 255);
slider.setValue(255);
slider.addChangeListener(this);

JPanel panel = new JPanel(new BorderLayout());
panel.add(slider, BorderLayout.CENTER);
setContentPane(panel);
setSize(new Dimension(640, 480));
setLocationRelativeTo(null);
}

public void stateChanged(ChangeEvent e) {
double value = ((double) slider.getValue())/((double) slider.getMaximum());
setWindowOpacity(this, value);
}

public void setWindowOpacity(Frame frame, double opacity) {
long value = (int) (0xff * opacity) << 24;
try {
// long windowId = peer.getWindow();
Field peerField = Component.class.getDeclaredField("peer");
peerField.setAccessible(true);
Class<?> xWindowPeerClass = Class.forName("sun.awt.X11.XWindowPeer");
Method getWindowMethod = xWindowPeerClass.getMethod("getWindow", new Class[0]);
long windowId = ((Long) getWindowMethod.invoke(peerField.get(frame), new Object[0])).longValue();

// sun.awt.X11.XAtom.get("_NET_WM_WINDOW_OPACITY").setCard32Property(windowId, value);
Class<?> xAtomClass = Class.forName("sun.awt.X11.XAtom");
Method getMethod = xAtomClass.getMethod("get", String.class);
Method setCard32PropertyMethod = xAtomClass.getMethod("setCard32Property", long.class, long.class);
setCard32PropertyMethod.invoke(getMethod.invoke(null, "_NET_WM_WINDOW_OPACITY"), windowId, value);
} catch (Exception ex) {
// Boo hoo! No transparency for you!
ex.printStackTrace();
return;
}
}

public static void main(String[] args) {
new TransparencyTest().setVisible(true);
}
}

I'm not sure how useful this is, but at least you can no longer complain you weren't aware you had the option.

There are cases where you might actually want this kind of transparency, the sole example amongst things I've written was an application that monitored what iTunes was playing and, whenever it changed, faded in a transparent output-only window containing huge text telling me what I was now listening to.

Such uses, though, are probably few and far between.