 |
Unleash Your Creativity with Swing and the Java 2D API!
"I wish I could do that!"
2002 was a great year at the JavaOne conference for developers of rich client applications. Multiple sessions looked at the endless possibilities available when using the Java platform to build user interfaces. The number one question that we've received in feedback to our sessions is "How do I make my apps look like that?" This article begins to answer that question by showing how to incorporate rich visual effects, or "eye-candy", into your application.
|
 |
|
Many developers look at the beautiful GUIs of some applications, imagining extreme complexity, and say "I wish I could do that!" In fact, you can achieve many of the striking effects seen in today's advanced GUIs by integrating a few lines of 2D code into your Swing application. Ahead you'll find three sections on this topic, each one bringing you closer to the point where developers will make similar exclamations about your applications.
- Where to Paint shows you how to plug your custom painting into the Swing paint system.
- What to Paint gives you some cool effects to try.
- Pulling It Together presents an example application, with complete code for you to play with!
Where to Paint
Before continuing, please read the excellent article "Painting in AWT and Swing". In particular, you'll benefit from understanding the section on opacity under "Additional Paint Properties" and the section "The Paint Methods." This last section describes how Swing factors paint calls into three overridable methods: paintComponent(Graphics g), paintChildren(Graphics g), and paintBorder(Graphics g).
In this article, we'll play around with the first two of these methods. We'll discuss how paintComponent can be
overridden to customize the look of your components, and then we'll discuss a trick that uses paintChildren to
paint custom content on top of the component and its children.
Customizing the Look of Components
One of the most important methods for customizing the painting of your Swing components is paintComponent. It's the method called by the Swing paint system to have the component paint its content. The default JComponent implementation simply calls update on the associated UI delegate to paint the built-in visuals of the component. Your implementation of paintComponent can be as creative as you like! This is where Swing gives you the opportunity to customize the look of your component. You can include the built-in visuals at any time with a call to super.paintComponent.
For example, to paint entirely on top of a component, you first invoke the superclass behavior and then make your custom paint calls. This is illustrated by a code sample showing how you might paint a small image on top of a JButton:
public class ImageButton extends JButton {
protected void paintComponent(Graphics g) {
// Paint the default look of the button.
super.paintComponent(g);
// Your custom painting here.
g.drawImage(foregroundImage, x, y, this);
}
}
|
Now what if you want to completely replace a component's visuals? A JPanel, for instance, is often used as a painting surface, such as in a drawing program or an image viewer. In a case like this there is no need for the component's built-in visuals, since the application dictates everything that is displayed, so you can leave out the call to super.paintComponent. But before doing so, it is very important that you familiarize yourself with the concept of opacity as it applies to JComponents.
JComponent has an opaque property that is accessed through two methods: setOpaque(boolean o) and isOpaque(). This property is a contract with the Swing paint system that reads: "If I am opaque, then I guarantee that I will fill every pixel within my boundaries with an opaque color." Non-opaque components, on the other hand,
promise nothing about what pixels they fill. The reason that this property exists is to aid in the performance of Swing painting.
When a Swing component is painted, it's possible that it will not fill every pixel within its bounds (if it has translucent or unfilled areas). This means that when it is repainted, whatever lies beneath must be repainted first. Without optimization, this back-to-front paint operation would require the Swing paint system to traverse up the containment hierarchy to find the first heavyweight ancestor from which to begin.
The opaque property keeps this traversal to a minimum, allowing Swing to detect whether the painting of a component requires additional painting of underlying ancestors. Since opaque components guarantee to fill their bounds entirely, there's no need to repaint whatever is behind them; the traversal can stop at the first opaque component.
A common bug for developers customizing the painting of their components is forgetting to honor the opaque property. If a component claims to be opaque but fails to fill every pixel within its bounds, the result is often garbage in the unfilled areas.
The UI delegates responsible for the default painting of components were designed to honor the property for you by filling the bounds of a component with its own background color if and only if it is opaque. There is confusion around this issue in that developers often think that the opacity property was designed to control the transparency of a component's background. In
most cases this assumption works; setting the opacity of a component to false causes the UI to skip the painting of its background. However, it is possible for components to use opacity in a different way. For example, a round component may use the opaque property to let Swing know that it doesn't fill the pixels in its corners; yet it still fills the
majority of its background.
When you override paintComponent without invoking the superclass behavior, the responsibility to honor the opaque property becomes yours. There are multiple ways to do this: You can check the value of isOpaque in paintComponent and take the appropriate action, you can override isOpaque to return a constant known value, or you can always fill the entire bounds and ignore the opacity (a perfectly legal way to satisfy the opacity contract).
The following example shows a JPanel that has been customized to always fill its background in four sections of alternating red and white. Not needing the component's built-in visuals, it leaves out a call to the superclass behavior. Since it always fills its entire bounds opaquely, it already satisfies the opacity contract. Even better, it overrides isOpaque to return true, allowing its painting to be optimized.
public class PaintSurface extends JPanel {
// Since we're always going to fill our entire
// bounds, allow Swing to optimize painting of us
public boolean isOpaque() {
return true;
}
protected void paintComponent(Graphics g) {
// We'll do our own painting, so leave out
// a call to the superclass behavior
// We're telling Swing that we're opaque, and
// we'll honor this by always filling our
// our entire bounds with solid colors.
int w = getWidth();
int h = getHeight();
int leftW = w / 2;
int topH = h / 2;
// Paint the top left and bottom right in red.
g.setColor(Color.RED);
g.fillRect(0, 0, leftW, topH);
g.fillRect(leftW, topH, w - leftW, h - topH);
// Paint the bottom left and top right in white.
g.setColor(Color.WHITE);
g.fillRect(0, topH, leftW, h - topH);
g.fillRect(leftW, 0, w - leftW, topH);
}
}
|
Here is a screenshot of what this panel would look like when used as the content pane in an application:
Now that you know how opacity works, we can look at an example that modifies the property, in conjunction with overriding paintComponent, to produce a rounded JTextField. What we wish to accomplish in this example is to customize the background of a component while delegating to the superclass to paint the foreground (in this case, the text, caret, and highlights of the JTextField). The approach is simple:
- Add a constructor to the subclass that sets the opacity to
false. This serves two purposes: to notify Swing that the component does not fill its entire bounds and to ensure that invoking the superclass behavior does not cause the background to be filled.
- Also in the constructor, replace the rectangle border that is installed by default on
JTextFields. Setting it to null is one option but you'll quickly notice that it looks funny when the caret occupies the region at either end of the field; it paints outside the rounded corners. We can compensate for that by adding an empty border with a few pixels of space.
- Provide a custom implementation of
paintComponent that does two things: paints the rounded background in the component's background color, and then invokes the superclass behavior to paint the foreground content.
Here is an example of following these three steps:
public class RoundField extends JTextField {
public RoundField(int cols) {
super(cols);
// We must be non-opaque since we won't fill all pixels.
// This will also stop the UI from filling our background.
setOpaque(false);
// Add an empty border around us to compensate for
// the rounded corners.
setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 5));
}
protected void paintComponent(Graphics g) {
int width = getWidth();
int height = getHeight();
// Paint a rounded rectangle in the background.
g.setColor(getBackground());
g.fillRoundRect(0, 0, width, height, height, height);
// Now call the superclass behavior to paint the foreground.
super.paintComponent(g);
}
}
|
The following screenshot shows two of these fields with different background colors. Notice that a third field and the containing panel have been further customized to paint their backgrounds with a nice fading color transition. We'll discuss this technique later in the section on gradients.
Painting on Top of Children
This section discusses a trick that allows you to paint custom content on top of a component and its children. It is referred to as a trick since it is an atypical usage of a Swing paint method and it often requires further customization of child components.
However, with certain preparations it works out quite nicely! Let's proceed by considering an example of how this might be used. No doubt you've come across web pages, or even television stations, that float their logo in a fixed position on top of all moving content. A similar effect is possible with JScrollPane in Swing; we can customize an instance to float an image on top of the scrolling component.
JScrollPane is actually a composite of multiple components JScrollBars and a JViewport and it is the JViewport that provides the window through which you see the visible region of the scrolling component (the view). JViewport is therefore the class that can be extended to paint an image on top of that region. By now you should be familiar with the high-level steps to do this, since they are similar to the previous
section: extend the component and provide a custom implementation for one of the paint methods. This time, however, we will choose a different method. When a component has finished painting its content and border, it calls paintChildren to continue painting further down the containment hierarchy. paintChildren does this by invoking paint on each of the component's children. We can override and add behavior to this method by having it first invoke the superclass behavior (to paint the children) and then do our custom painting on top. In the case we're discussing, we'll paint an image on top of the JViewport's single child component, the view component.
As mentioned earlier, often further steps need to be taken to ensure that painting works properly; this applies here as well. It is important to make sure that the handling of every repaint request on the child component includes repainting of the JViewport. If the request were to be handled directly by the component, without going through our JViewport's paintChildren method, we wouldn't get the image overlay. As you might recall, this type of self-contained painting is exactly what opaque components do. Therefore we should set the opacity of any component used with this JViewport to false to force the painting to originate with the next highest opaque component in the component hierarchy, our JViewport.
Unfortunately, this has one side effect: it stops the view's UI from filling the background with its background color, and we end up with the scrolling content on top of the JViewport's background. If this is undesirable, as it is in our example, we can easily counteract it. We can either customize the paintComponent method of the view component to fill its own background, or we can make sure that our JViewport uses the view's color when it fills its background. This can be accomplished by overriding getBackground to delegate to the view component. We'll take the latter approach since it results in one less subclass. Here is the final code, including an example that uses the new class:
public class LogoViewport extends JViewport {
public void paintChildren(Graphics g) {
// Invoke the superclass behavior first
// to paint the children.
super.paintChildren(g);
// Paint the logo image on top of the
// bottom right corner.
g.drawImage(logoImage,
getWidth() - logoImage.getWidth(null),
getHeight() - logoImage.getHeight(null),
this);
}
// Return the view component's background color
// so that our UI will use that color to fill
// the background.
public Color getBackground() {
return getView() == null
? super.getBackground()
: getView().getBackground();
}
}
// Example code that uses this custom JViewport:
// Create the view component to be
// scrolled in the JScrollPane.
JTextArea ta = new JTextArea();
// Make it non-opaque so painting will
// go through the viewport.
ta.setOpaque(false);
LogoViewport vp = new LogoViewport();
// Install the JTextArea as the view.
vp.setView(ta);
JScrollPane sp = new JScrollPane();
// Install the custom viewport into JScrollPane.
sp.setViewport(vp);
|
The following screenshot shows this custom viewport in action. The Duke image remains fixed in the bottom right, on top of the text component, regardless of how we scroll it. This might look even better if we could partially see through the image. You'll
learn how to accomplish this in the section on translucency.
|
The technique in this section is interesting and gives a neat effect (it is particularly well suited to JScrollPane), but it is not for every occasion. It can become increasingly difficult to perfect when there are multiple levels of children below your customized component in the hierarchy, since the components at every one of these levels must be non-opaque. This makes composites such as JComboBox poor choices for children. Also, remember that Swing has ways to optimize painting of opaque components. We've already discussed one of them previously, and another actually occurs with JViewport itself.
It has a special scroll mode that allows it to speed up painting by simply copying graphics areas and moving them on the screen. This mode has to be disabled when a non-opaque component is used. The point is, too many non-opaque components, and your application could suffer in painting performance.
What to Paint
Now that you're familiar with the ease of plugging into the Swing paint system, it's time to get to the fun stuff! This section looks at a few techniques that allow you to achieve cool effects with your custom painting. We'll start with a couple of notes about the Graphics object, and then dive into images, translucency, and gradients. Lastly we'll touch on how to specify preferences with respect to quality versus performance in different rendering areas.
About the Graphics Object
As you might have noticed, all the Swing component paint methods are called with a single parameter, an object of type java.awt.Graphics. It's by setting properties and invoking methods on this object that you'll achieve onscreen visuals. An interesting point about the object passed to this method is that (as of 1.2) it's actually an instance of java.awt.Graphics2D, an extension of Graphics that provides a richer drawing API. To take advantage of this API, you need only cast the object to type Graphics2D.
Another important note about the usage of the Graphics object is that the same instance is passed to all three of the paint methods. Through paintChildren, it is also passed down the component hierarchy to each child (where a copy is made to be used in painting that child).
As a result, permanent changes made to the Graphics object in one place may be visible in other areas of painting. This is acceptable with font and color, since other code is likely to set these before painting. However, changing other properties (the clip region, transform, or composite mode, for example) can lead to undesirable results. As a rule, your custom paint methods should not have the side effect of modifying the Graphics object passed into them. You'll also want to be careful about invoking any superclass behavior with a modified Graphics object. Two coding techniques allow you to defend against making permanent changes.
The first technique stores the old values of any properties before setting them, and then restores them when painting is complete. For example:
// Store original property values.
Foo oldFoo = g.getFoo();
// Set properties.
g.setFoo(newFoo);
// Paint something.
g.somePaintMethods();
// Restore original property values.
g.setFoo(oldFoo);
|
The second technique creates a copy of the Graphics object and uses it instead. Changing properties on the copy does not affect the original and therefore they do not need to be reset. The copy should be disposed of to release resources before returning from the method. This second technique is typically easier to code but incurs the expense of creating and disposing of the Graphics object. Here's an example using the second technique:
// Create a copy of the graphics object.
Graphics gCopy = g.create();
// Set properties on the copy.
gCopy.setFoo(newFoo);
// Paint something using the copy.
gCopy.somePaintMethods();
// IMPORTANT: Dispose of the copy to release resources.
gCopy.dispose();
|
Working with Images
One of the customizations that can help your GUIs to really shine is the tasteful use of carefully chosen images. With some creative graphic design work and only a few lines of code, you can add incredible personality to your applications. This section looks at some ways of incorporating images into the painting of components. First, here's a quick note on loading them.
Two recommended ways of loading a static image (as opposed to an animated image, such as a multi-frame GIF) shelter you from any details of its loading. By blocking until the image is completely available, these approaches hide the fact that image data is
often loaded asynchronously.
The first approach is to let ImageIcon do the work for you. Conveniently, ImageIcon contains code to track the loading of an image and its constructor does not return until the image is fully loaded. One of the benefits of
loading images this way is that the image returned may be hardware accelerated on some platforms (the details are not within the scope of this article):
Image image = new ImageIcon(urlToImage).getImage();
|
The second way is to take advantage of the new Java Image I/O API. This powerful API provides many new objects and methods to deal with input/output of images. In particular, we're interested in the read method of class javax.imageio.ImageIO, which returns a java.awt.image.BufferedImage:
try {
Image image = javax.imageio.ImageIO.read(urlToImage);
} catch (IOException ioe) {
// ...handle exception...
}
|
Note:
While testing sample code for this article, we encountered exceptions and image display problems when using ImageIO to load images from compressed JAR files under Java Web Start. This problem is likely related to bugs 4764639 and 4675817. Workarounds include using ImageIcon instead of ImageIO to load the image, loading the image directly from the server or file system, or by creating your JAR file uncompressed (by passing the "-0" option to the jar tool).
|
|
Once you've obtained an image, you can paint it using one of the various drawImage methods on the Graphics object. The simplest of these methods paints an entire image at its natural size, at specified co-ordinates:
Graphics.drawImage(Image image, int x, int y,
ImageObserver observer)
|
The code snippet below shows the paintComponent method of a component that uses drawImage to tile an image in its background:
public void paintComponent(Graphics g) {
int width = getWidth();
int height = getHeight();
int imageW = tileImage.getWidth(this);
int imageH = tileImage.getHeight(this);
// Tile the image to fill our area.
for (int x = 0; x < width; x += imageW) {
for (int y = 0; y < height; y += imageH) {
g.drawImage(tileImage, x, y, this);
}
}
}
|
The following two images illustrate the use of this code:
Another drawImage method has additional parameters to specify the size at which the image should be drawn. This method draws the entire image, like the previous method, but scales the image to fit inside the specified rectangle. Here is an example:
Graphics.drawImage(Image image,
int x, int y, int width, int height,
ImageObserver observer)
|
This method is quite useful, allowing for the presentation of a single image at different levels of magnification. It comes in handy when building an image viewer application, to display thumbnails or close-ups, or when you wish to use the same image in painting multiple components of differing sizes.
Yet another drawImage method is even more powerful, with many possible applications. It allows you to specify both a
destination rectangle to draw into and a source rectangle to draw from. Only the specified part of the image is drawn, scaled if necessary to fit into the destination rectangle. Note that the rectangles are specified differently than in the other two drawImage methods; they are defined by two points on opposing corners. For example:
Graphics.drawImage(Image img,
int dx1, int dy1, int dx2, int dy2,
int sx1, int sy1, int sx2, int sy2,
ImageObserver observer)
|
One neat application of this method might be a virtual magnifying glass, as suggested by the image above. The user could use the mouse to drag a rectangle around an image on the screen, and be presented with a close-up of the selected region. (This effect was demonstrated at the JavaOne 2002 conference.)
Another excellent use of this method is loading images to be used for animation. One typical animation technique is to animate by loading multiple images, each representing a single frame, and then present them in sequence. With this drawImage at our service, we can modify the technique to load a single image comprised of multiple frames, and then display only the relevant frame's region on each step through the animation. This saves on the cost of loading multiple images, whose overall size is often larger than their combination. Here's an example image that could be used in animating Duke:
Translucency
Translucency is an effect that is appearing more and more frequently in modern GUIs. As developers master the skill, using it to add flash and sparkle to their applications, it quickly moves high on their list of favorite techniques. Simply stated, when something is painted translucently it allows some level of whatever is behind it to show through; it is partially see-through. Translucency is an excellent technique for the presentation of multiple layers of information, such as palettes and annotations, and also allows for some beautiful fade transitions. Swing offers three easy routes to achieving translucent painting: by using images with built-in translucency, by using translucent colors, or by explicitly setting translucency on a Graphics2D object.
The Java platform has built-in support for two image formats with translucency information: GIF and PNG. GIF allows each pixel to be specified as either fully transparent or fully opaque. PNG has a full alpha channel; the creator can specify the level of translucency for each pixel. Swing honors translucency information when painting images, allowing appropriate regions of the background to show through.
In addition to support for translucent images, Swing allows for painting with translucent colors. The java.awt.Color class has two constructors that accept an alpha value a number that specifies how see-through the color should be:
Color(int r, int g, int b, int a)
Color(float r, float g, float b, float a)
|
In the first case, the color components are specified as integers in the range (0 - 255). In the second, they are floats in the range (0.0 - 1.0). In both cases, a lower number for the alpha value indicates a more translucent color, with the bottom of the range being fully transparent and the top fully opaque.
By setting a Color with custom alpha information on a Graphics object, you can paint items such as text and shapes with translucency. The following code snippet shows a custom paintComponent method that illustrates some of what we've discussed. It first paints a PNG containing translucency information (a transparent background and translucent edges around Duke allow the image to seamlessly blend with whatever is behind it) and then it draws text and a square on top in a translucent color. Here's the code:
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Paint a PNG having translucency information.
g.drawImage(dukePNG, 70, 27, this);
// Paint a rectangle with a translucent color.
g.setColor(new Color(128, 255, 128, 56));
g.fillRect(20, 20, 80, 80);
// Paint a solid black rectangular outline.
g.setColor(Color.BLACK);
g.drawRect(20, 20, 80, 80);
// Paint a string with a translucent color.
g.setColor(new Color(128, 128, 255, 128));
g.setFont(new Font("Dialog", Font.BOLD, 20));
g.drawString("Swing!", 25, 26);
}
|
The following screenshot shows what this would look like when used to paint on a white panel.
The third translucent painting technique is the most powerful, allowing for arbitrary painting to be done translucently. By invoking setComposite on a Graphics2D object with an instance of java.awt.AlphaComposite,
you can specify a translucency level to be used in subsequent paint calls. An appropriate AlphaComposite is obtained by invoking the following static method:
AlphaComposite.getInstance(int rule, float alpha)
|
The first parameter, which represents the compositing rule, should be AlphaComposite.SRC_OVER. The second parameter specifies the level of translucency between 0.0 (fully transparent) and 1.0 (fully opaque). The following
code snippet shows how this technique is typically set up in the context of a paintComponent method:
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Cast to Graphics2D so we can set composite information.
Graphics2D g2d = (Graphics2D)g;
// Save the original composite.
Composite oldComp = g2d.getComposite();
// Create an AlphaComposite with 50% translucency.
Composite alphaComp = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 0.5f);
// Set the composite on the Graphics2D object.
g2d.setComposite(alphaComp);
// Invoke arbitrary paint methods, which will paint
// with 50% translucency.
g2d.somePaintMethods();
// Restore the old composite.
g2d.setComposite(oldComp);
}
|
Here are two final notes about translucency: First, you should be aware that translucency is not yet hardware accelerated, and mixing it with other acceleration-capable operations may cause them to also be done without acceleration. Second, keep in mind the rules of component opacity when dealing with translucent painting. Since translucent items allow pixels behind them to show through, you'd be breaking the rules if you tried to fill the background of an opaque component with translucent painting (even by simply calling setBackground with a translucent color).
Gradients
In an earlier section you saw a screenshot of a text field and panel that painted their backgrounds with a fading transition between two different colors. This type of transition, called a gradient, is very pretty, adding a nice dimensionality to components. It can be used in the painting of both shapes and text and is easily achieved. Most of the work is done by the java.awt.GradientPaint class; you need only create an instance describing the desired gradient and set it on a Graphics2D object by way of the setPaint method (remembering to restore the old paint when finished). Here is
a relevant constructor:
GradientPaint(float x1, float y1, Color color1,
float x2, float y2, Color color2,
boolean cyclic)
|
The first six parameters are the two colors and two points describing the line along which the color transition should take place. The last parameter describes how areas past the ends of the line should be painted, whether the gradient should be cycled or if the color at the respective end of the line should be used. Read the
API documentation of GradientPaint for further details. The following screenshot shows the painting of four squares and some text, all done with different gradients. The black lines have been drawn to show the lines describing each gradient. From left to right, the first two squares show non-cyclic gradients and the second two, cyclic ones. The text was done with a cyclic gradient between a solid and translucent version of the same white.
Rendering Hints
Rendering hints allow you to specify preferences as to the algorithms used in various areas of rendering. Many of these areas have multiple algorithms available, providing different benefits such as higher quality or better performance. To suggest which algorithm be used, you invoke one of the following methods to set rendering hints (as key-value pairs) on an instance of Graphics2D before using it to paint.
Graphics2D.setRenderingHint(RenderingHints.Key hintKey,
Object hintValue)
Graphics2D.setRenderingHints(Map hints)
|
Two hint keys are particularly interesting in the context of this article. The first, RenderingHints.KEY_INTERPOLATION, suggests the algorithm to be used for image scaling. The following values can be used with this key:
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR
RenderingHints.VALUE_INTERPOLATION_BILINEAR
RenderingHints.VALUE_INTERPOLATION_BICUBIC
|
The first value, currently the default, suggests the algorithm that provides the quickest scaling. Each subsequent value suggests an algorithm that sacrifices some of this speed in the name of smoother scaling quality.
The second interesting key suggests whether antialiasing should be used in rendering. By default, antialiasing is turned off for higher performance. When it is turned on, the edges of shapes and text appear much smoother. The key is RenderingHints.KEY_ANTIALIASING, and the possible values are:
RenderingHints.VALUE_ANTIALIAS_OFF
RenderingHints.VALUE_ANTIALIAS_ON
|
Pulling It Together
When trying to design a sample application for this article, we wanted to produce something that was both cool, illustrating many of the effects discussed earlier, and useful. The end result builds on an earlier example to produce a set of reusable classes that make customizing JViewport even easier! WatermarkViewport is a custom JViewport implementation that supports pluggable painting of its background and foreground. Its paintComponent and paintChildren methods have both been customized to first invoke the superclass behavior and then delegate to a plug-in that is an instance of WatermarkPainter. When used with a JScrollPane, the resulting effect is that the view component appears to float in space between whatever the painters fix in the background and foreground.
Four WatermarkPainter subclasses have been provided two background painters and two foreground painters to illustrate four of the effects discussed in this article: image tiling, gradients, translucency, and animation. A runnable
main class has been included to demonstrate the use of these classes, showing how they might be used with both a JTable and a JTextArea. Menus let you choose between different background and foreground painters, and choose
any options on those painters.
On a machine with Java Web Start installed, you can click this button to launch the demo now:
Otherwise, please have a look at these two screenshots:
Since the source code for these classes is heavily documented and we've already covered most of the techniques, little discussion is needed here. However, one issue is worth touching on, and that is how the translucent cell and text selection was achieved on the two components. For JTextArea, it was as simple as calling setSelectionColor with a translucent color. For JTable on the other hand, it was a little more complex, requiring a custom renderer to be written and
set on the table by way of its setDefaultRenderer method. Fortunately, there was a convenient base class to start with, DefaultTableCellRenderer (a subclass of JLabel). Surely by now you can guess at least one of the methods that needed to be customized.
Now it's time to get the code! The source files are available under the BSD+ license.
They, along with the necessary image files, are available in a single zip file. To run the application, place all files in the same directory, compile *.java (with 1.4 or later), and then execute WatermarkDemo. The following table describes each file required by the demo.
|
The main runnable class. |
|
A custom JViewport implementation that supports pluggable
painting of its background and foreground. |
|
The abstract base class for objects that know how to paint a
WatermarkViewport. |
|
A WatermarkPainter implementation that paints the entire region with a tiled image. Designed to be used as a background painter. |
|
A WatermarkPainter implementation that paints the entire region with a white to blue color gradient. Designed to be used as a background painter. |
|
A WatermarkPainter implementation that translucently paints an image in the bottom right corner. Designed to be used as a foreground painter. |
|
A WatermarkPainter implementation that animates a waving duke in the bottom right corner. Designed to be used as a foreground painter. |
|
The images required by the demo application. |
Documentation for the classes is available in javadoc format, also downloadable for offline use.
Final Words
The goal of this article was to demonstrate the ease of creating beautiful user interfaces with Swing and the Java 2D API. Hopefully by now you're convinced. You've learned how simple it is to plug into the Swing paint system and discovered
some of the many advanced rendering techniques that hinge on just a few lines of code. The rest is up to your imagination!
|
|