在O'Reilly的主页上看到的一篇文章,关于Swing实现透明窗体的。很喜欢,收藏!
Editor's note: The following example from Swing Hacks
is one of the book's most visually daring hacks--mimicking the
arbitrarily shaped window you might see in an native MP3 player skin.
The hack here is necessitated by the fact that Java
doesn't support non-rectangular windows, so the only option to make
this work is for the Java window to be aware of what's under it, and to
handle the imaging of areas within the window's rectangle but not
within its arbitrary shape. Read on for how this is accomplished.
Create translucent and shaped windows, while avoiding native code, with clever use of a screenshot.
One of the most commonly requested Swing features is transparent windows. Also called shaped windows, these are windows that have transparent portions, allowing the desktop
background and other programs to shine through. Java doesn't provide
any way of creating transparent windows without using the Java Native
Interface (JNI) (and even then the native platform must support
transparency as well), but that's not going to stop us. We can cheat
using one of my favorite techniques, the screenshot.
The process of faking a transparent window is basically:
-
Take a screenshot before the window is shown.
-
Use that screenshot as the background of the window.
-
Adjust the position so that the screenshot and the real screen line up, creating the illusion of transparency.
This is the easy part. The hard part is updating the screenshot when the window moves or changes.
To start off, create a JPanel subclass that can capture the screen and paint it as the background, as shown in Example 6-1
Example 6-1. A transparent background component
public
class
TransparentBackground
extends
Jcomponent {
private
JFrame frame;
private
Image background;
public
TransparentBackground(JFrame frame) {
this
.frame
=
frame;
updateBackground( );
}
public
void
updateBackground( ) {
try
{
Robot rbt
=
new
Robot( );
Toolkit tk
=
Toolkit.getDefaultToolkit( );
Dimension dim
=
tk.getScreenSize( );
background
=
rbt.createScreenCapture(
new
Rectangle(
0
,
0
,(
int
)dim.getWidth( ),
(
int
)dim.getHeight( )));
}
catch
(Exception ex) {
p(ex.toString( ));
ex.printStackTrace( );
}
}
public
void
paintComponent(Graphics g) {
Point pos
=
this
.getLocationOnScreen( );
Point offset
=
new
Point(
-
pos.x,
-
pos.y);
g.drawImage(background,offset.x,offset.y,
null
);
}
First, the constructor saves a reference to the parent JFrame; then it calls updateBackground( ), which captures the entire screen using java.awt.Robot. createScreenCapture( ), and saves the capture in the background variable. paintComponent(
) gets the panel's absolute position on screen and then fills the panel
with the background image, shifted to account for the panel's location.
This makes the fake background image line up with the real background,
giving the appearance of transparency.
You can run this with a simple main( ) method, dropping a few components onto the panel and putting it into a frame:
public
static
void
main(String[] args) {
JFrame frame
=
new
JFrame(
"
Transparent Window
"
);
TransparentBackground bg
=
new
TransparentBackground(frame);
bg.setLayout(
new
BorderLayout( ));
JButton button
=
new
JButton(
"
This is a button
"
);
bg.add(
"
North
"
,button);
JLabel label
=
new
JLabel(
"
This is a label
"
);
bg.add(
"
South
"
,label);
frame.getContentPane( ).add(
"
Center
"
,bg);
frame.pack( );
frame.setSize(
150
,
100
);
frame.show( );
}
The code produces a window that looks like Figure 6-1.
The code is pretty simple,
but it has two big flaws. First, if the window is moved, the background
won't be refreshed automatically. paintComponent( ) only
gets called when the user resizes the window. Second, if the screen
ever changes, it won't match up with the background anymore.
You really don't want to update the screenshot often, though,
because that involves hiding the window, taking a new screenshot, and
then reshowing the window—all of which is disconcerting to the user.
Actually detecting when the rest of the desktop
changes is almost impossible, but most changes happen when the
foreground window changes focus or moves. If you accept this idea (and
I do), then you can watch for those events and only update the
screenshot when that happens:
public
class
TransparentBackground
extends
JComponent
implements
ComponentListener, WindowFocusListener,
Runnable {
private
JFrame frame;
private
Image background;
private
long
lastupdate
=
0
;
public
boolean
refreshRequested
=
true
;
public
TransparentBackground(JFrame frame) {
this
.frame
=
frame;
updateBackground( );
frame.addComponentListener(
this
);
frame.addWindowFocusListener(
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( ); }
First, make the panel, TransparentWindow, implement ComponentListener, WindowFocusListener, and Runnable.
The listener interfaces will let the panel catch events indicating that
the window has moved, been resized, or the focus changes. Implementing Runnable will let the panel create a thread to handle custom repaint( )s.
The implementation of ComponentListener involves the four methods beginning with component. They each simply call repaint(
) so that the background will be updated whenever the user moves or
resizes the window. Next are the two window focus handlers, which just
call refresh( ), as shown here:
public
void
refresh( ) {
if
(frame.isVisible( )) {
repaint( );
refreshRequested
=
true
;
lastupdate
=
new
Date( ).getTime( );
}
}
public
void
run( ) {
try
{
while
(
true
) {
Thread.sleep(
250
);
long
now
=
new
Date( ).getTime( );
if
(refreshRequested
&&
((now
-
lastupdate)
>
1000
)) {
if
(frame.isVisible( )) {
Point location
=
frame.getLocation( );
frame.hide( );
updateBackground( );
frame.show( );
frame.setLocation(location);
refresh( );
}
lastupdate
=
now;
refreshRequested
=
false
;
}
}
}
catch
(Exception ex) {
p(ex.toString( ));
ex.printStackTrace( );
}
}
refresh( ) ensures that the frame is visible and schedules a repaint. It also sets the refreshRequested boolean to true and saves the current time, which will become very important shortly.
The run( ) method sleeps constantly, waking up every
quarter-second to see if a refresh has been requested, and whether it
has been more than a second since the last refresh. If more than a
second has passed and the frame is actually visible, then run( ) will save the frame location, hide it, update the background, then put the frame back in place and call refresh( ). This ensures that the background is never updated more than needed.
So, why all of this rigmarole about using a thread to control
refreshing? One word: recursion. The event handlers could simply call updateBackground( ) and repaint(
) directly, but hiding and showing the window to generate the
screenshot would cause more focus-changed events. These would then
trigger another background update, causing the window to hide again,
and so on, creating an infinite loop. The new focus events are
generated a few milliseconds after refresh( ) is processed, so simply checking for an isRecursing flag wouldn't stop a loop.
Additionally, any user action that would change the screen will probably create lots of events, not just one. It's just the last event that should trigger updateBackground(
), not the first. To handle all these issues, the code creates a thread
that watches for repaint requests and only processes a new screenshot
if it hasn't already been done in the last 1,000 milliseconds. If the
user generates events continuously for five seconds (searching for that
lost browser window, for example), then only when everything else has
settled down for a second will the refresh actually happen. This
ensures that users won't have a window disappear out from under them
while they are moving things around.
Another annoyance is that the window still has its border, which
sort of ruins the effect of having a transparent background.
Unfortunately, removing the borders with setUndecorated(true)
would also remove the titlebar and window controls. This probably isn't
too much of a problem, though, because the types of applications that
typically use shaped windows usually have draggable backgrounds [Hack #34].
Here's a simple test program to put this into action:
public
static
void
main(String[] args) {
JFrame frame
=
new
JFrame(
"
Transparent Window
"
);
frame.setUndecorated(
true
);
TransparentBackground bg
=
new
TransparentBackground(frame);
bg.snapBackground( );
bg.setLayout(
new
BorderLayout( ));
JPanel panel
=
new
JPanel( ) {
public
void
paintComponent(Graphics g) {
g.setColor(Color.blue);
Image img
=
new
ImageIcon(
"
mp3.png
"
).getImage( );
g.drawImage(img,
0
,
0
,
null
);
}
};
panel.setOpaque(
false
);
bg.add(
"
Center
"
,panel);
frame.getContentPane( ).add(
"
Center
"
,bg);
frame.pack( );
frame.setSize(
200
,
200
);
frame.setLocation(
500
,
500
);
frame.show( );
}
The code creates a faux MP3 player interface using a JPanel subclass and a PNG image with transparency. Note the call to frame.setUndecorated(true), which turns off the border and titlebar. The call to panel.setOpaque(false)
turns off the default background (usually plain gray), allowing the
screenshot background to shine through the transparent parts of the
image (Figure 6-2). This produces a window that looks like Figure 6-3—a
vision of Java programs to come?
Figure 6-2. Template for an MP3 player
Figure 6-3. Running the MP3 player