Saturday, April 08, 2006

Decorating/Overpainting Swing Components


Have you ever wanted to change the way an existing component appears, just a little, without having to go and rewrite it or subclass it?

The image at right demonstrates several implementations of an abstract base class which puts an arbitrary decoration over an existing component. This technique allows you to do any of the following:

  • Put labels on a scrolled component which stay fixed relative to the scroll pane rather than the scrolled component (useful for labeling horizontal lines on a panned display).
  • Put an icon badge anywhere on a component, such as a stop sign over invalid text in a text field
  • Put tooltips on a component that you didn't write (like adding annotations to someone else's work).
  • Dim the entire painted image to gray, or fade it into the background color to make the component look disabled.
  • Change the appearance of component's background (gradients, stripes, or polka-dots).
  • Apply animation effects to the component.
  • Marching ants/Rubber band selection rectangle (xor is so Seventies).
  • Highlight all or part of an existing component to indicate the effective target of a drop operation (changing the selection isn't always the best solution).

to see a few of these decorations in action.

Note that the code for each of the following decorations consists of the painting logic and optionally positioning logic, which is all you should be concerned about.

The implementation for drawing a badge:
class Warning extends AbstractComponentDecorator {
    private final int SIZE = 16;
    public Warning(JTextField f) {
        super(f);
    }
    /** Position the badge at the right-most edge. */
    public Rectangle getDecorationBounds() {
        Rectangle r = super.getDecorationBounds();
        Insets insets = getComponent().getInsets();
        r.x += r.width - SIZE - 1;
        r.y += (r.height - SIZE) / 2;
        if (insets != null) {
            r.x -= insets.right;
        }
        return r;
    }
    public void paint(Graphics graphics) {
        Rectangle r = getDecorationBounds();
        Graphics2D g = (Graphics2D)graphics;
        GeneralPath triangle = new GeneralPath();
        triangle.moveTo(r.x + SIZE/2, r.y);
        triangle.lineTo(r.x + SIZE-1, r.y + SIZE-1);
        triangle.lineTo(r.x, r.y + SIZE-1);
        triangle.closePath();
        g.setColor(Color.yellow);
        g.fill(triangle);
        g.setColor(Color.black);
        g.draw(triangle);
        g.drawLine(r.x + SIZE/2, r.y + 3, r.x + SIZE/2, r.y + SIZE*3/4 - 2);
        g.drawLine(r.x + SIZE/2, r.y + SIZE*3/4+1, r.x + SIZE/2, r.y + SIZE - 4);
    }
}
The implementation for dimming:

class Dimmer extends AbstractComponentDecorator {
    public Dimmer(JComponent target) {
        super(target);
    }
    /** Paint using a transparent version of the bg color. */
    public void paint(Graphics g) {
        Color bg = getComponent().getBackground();
        g.setColor(new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), 200));
        Rectangle r = getDecorationBounds();
        g.fillRect(r.x, r.y, r.width, r.height);
    }
}
The implementation for semi-scrolling labels:

   private static final int SIZE = 1000;
   private static final int BLOCK = 20;
   private static final int LINE = 20;

   private static class Labeler extends AbstractComponentDecorator {
       public Labeler(JComponent target) {
           super(target);
       }
       /** Ensure the label stays at the left-most visible
        * edge of the component.
        */
       public Rectangle getDecorationBounds() {
           Rectangle r = super.getDecorationBounds();
           Rectangle visible = getComponent().getVisibleRect();
           if (r.x < visible.x)
              r.x = visible.x;
           return r;
       }
       public void paint(Graphics g) {
           Rectangle r = getDecorationBounds();
           for (int i=0;i < SIZE;i+= LINE) {
               g.drawString("label " + (i/LINE + 1),
                            r.x, r.y + i + g.getFontMetrics().getAscent() + 2);
           }
       }
   } 


This class started out as a way to highlight different drop targets in a tree or table, and a method for the Costello editor to highlight areas of a component to be captured as an image (without actually setting the target component's background). I've subsequently used it for other decorations as noted above.

The whole idea would be trivial if Swing provided a hook into a component's paint method. There is no such explicit hook, and no implicit ones either without getting into a lot of complexity (perhaps by writing your own repaint manager). Since writing a repaint manager seems totally orthogonal to decorating a single component, we take a different tack.

My first implementation was to simply add a sibling or child component into the hierarchy on top of the first (and indeed, that implementation is still in use by the Costello editor). Unfortunately, the normal component hierarchy doesn't really consider z ordering, so the results are not always consistent. You might get things painted properly 90% of the time, but occasionally have your decoration occluded by a scroll pane, or the fact that there's an extra component in the hierarchy causes the layout manager to occasionally freak out. So it's okay for a proof of concept, but kinda lame in practice.

There is, however, a Swing component that knows about z ordering. Most Swing components will reside somewhere below a JLayeredPane (see the javadoc for javax.swing.RootPaneContainer for details). This little component has the capability of painting things in layers, which is just the capability we're looking for. The JLayeredPane even has predefined layers and was explicitly designed for stacking components in a known Z order (it also has a sub-layer ordering referred to as "position", but this doesn't seem to work).

So the basic idea is that for any decoration, we establish a painting component as a child of the JLayeredPane which is positioned in a layer above the decorated component. The Swing painting mechanism will automatically ensure that our decoration gets painted whenever the decorated component is painted, and that the decoration happens after the target component is painted.

A few details to note:

  • The decorator must maintain proper position and size with respect to the target component. If the decoration is in the lower right corner of the component, it should stay there if the component grows.
  • The decorator must clip its painting according to how much of the target component is visible. If the component is partially obscured within a scroll pane, the decorator should be clipped in a similar fashion.
  • The decorator needs to track the target component's visibility. If the component is on a tabbed pane and a different tab is displayed, the decoration needs to be hidden.
  • Painting under the decorated component is a bit more tricky. The current implementation for decorating component backgrounds assumes the current LAF doesn't change how ComponentUI paints the default background (gtk/synth probably won't work well). I haven't really looked into this much.
  • It's possible to composite with the existing graphics (so you could, for instance, paint only where there's already a blue pixel), but this capability varies by platform and VM version.


Full source for the demo and AbstractComponentDecorator class.

22 comments:

Anonymous said...

Hi.

I like this component over painting.

Would it be possible for me to use AbstractComponentDecorator.java in my application?

I suppose my real question is, can you add a license comment to the sample, something like Apache license would be good.

Cheers

Peter Henderson.

technomage said...

Sources (with license) may be obtained at http://sf.net/projects/furbelow.

Anonymous said...

Great Work. I love your Blog

Swing Components Library said...

Thanx for a nice post

very informative and catchy

Regards
Iksanika

Anonymous said...

I actually have a question more than a comment. Although I really like your framework. It says that I can paint outside the component? How can this be done? I have a JButton and I want to paint an image to the left that extends out over the edge of the button?

email: mwannamaker@propertyspot.ca

Thanks
Mike

technomage said...

If you override the bounds (which default to the decorated component's bounds), you can adjust to include space outside the original component.

You may need to adjust the clip mask, since the default clips to the decorated component's bounds.

Anonymous said...

Thats what I thought and tried various things but I didn't have success, I'm not really a great graphics guy ;) I will experiment somemore but do you have an example?

When I got the bounds the x,y=0, however I want them to be shifted left and up outside the button, say like x,y=-20. If you have an example it would be greatly appreciated.

Thanks
Mike

Anonymous said...

Hi Me Again,

You state:

The decorator needs to track the target component's visibility. If the component is on a tabbed pane and a different tab is displayed, the decoration needs to be hidden.

Does this mean that I have to do this? I'm seeing odd behaviour when in a tabbed pane.

1) Gradient & Dynamic decorators that paint to background don't work, it actually shows on all tabs.

2) I have a banner component that when it's height reaches > 69, it's paintComponent() method is called repeatedly? Only happens when I have the decorators.

What I did was make your decorator demo an jpanel and put that in a JTabbedPane.


...... Some time passes .......


Okay I fixed #1 I removed attach(); from componentMoved() and componentResized() and replaced with synch();

To fix the marquee issue I added attach(); in BackgroundDecorator c'tor.

public BackgroundPainter(JLayeredPane p, int layer)
{
super(p, 0, TOP);
this.layer = layer;
key = key(layer);
p.putClientProperty(key, this);
// Reattach
attach();
}

I still have #2.

And BackgroundDecorator, when in a tabbed pane, doesn't work. It doesn't paint, it just shows the background color of the tabbed pane.

I can send you my tabbedpanedemo if you like?

technomage said...

The background "decoration" is kinda hacky and probably not the best general solution. You're better off to put your component into a JPanel with bg painting customization or just customize the component itself. This solution was mainly intended for cases where you can't or don't want to do either. It's probably possible to tweak it to properly handle the case of tabbed panes, though.

The decorator *does* pay attention to component visibility. It's possible that in the background decorator case that provision is missing (background decoration is handled specially). A quick glance at the BackgroundPainter internal class looks like it doesn't check component visibility/showing, when it should.

Did you obtain source from the furbelow project or from a blog post? The furbelow source would be more up-to-date.

Contributions are welcome, but please post them to the furbelow project on sourceforge.

As for changing the decorator bounds, you override getDecorationBounds(). Within it, call super.getDecorationBounds() to get the original rectangle, then you can offset x,y by negative amounts if you want. All bounds are interpreted relative to the decorated component. Clipping should automatically account for your claimed decoration bounds.

I don't know what marquee issue you're referring to.

Anonymous said...

I worked the whole day and get very excited to see that things were working.

I got the AbstractComponentDecorator from "http://www.java2s.com/Open-Source/Java-Document/Testing/abbot-1.0.1/abbot.editor.widgets.htm" and used the highlighter and Warning that I have modified to display icons outside the component using your "getDecorationBounds()" trick.
I was happy... until I resized my component and tabbed-out. The highlighter did not follow the resize and the icons either fails to be redraw or show-up on the wrong tab.
I whent to the "http://sf.net/projects/furbelow" and found no jars or zip to download. Then I tried to install the TortoiseSVN client to be warned that it was not supported on Vista and I found their "on your own risk" too scary.
Now I am stuck. Does your "background "decoration" is kinda hacky" statement implies that this thing is not working? Do I need to derive from JComboBox, Buttons, JTextFields and do this myself? Is there any help for me here?

Pierre

technomage said...

You can download directly from SF SVN by following the "Browse SVN" links (or go to http://furbelow.svn.sourceforge.net/viewvc/furbelow/trunk/src/furbelow/).

The latest version of SVN has some fixes that affect tabbed panes. If that doesn't fix your problem, try to be a little more descriptive and clear about what the problem is.

Heico said...

Hi,
nice implementation. However there a several shortcommings when using al decorated icon in the lower/left corner, half outside of component. Because the Icon is in front of component clipping problems arise. Can this be solved, or did I forget to override some functions?

technomage said...

If you mean that the icon is clipped by the component bounds, then you need to change the decoration bounds, which default to the component bounds. Any painting will be clipped by the decoration bounds.

The bounds are specified relative to the component origin.

Heico said...

Thnx for posting answer. I wasn't 100% clear, forgot to tell I already override setDecorationBounds(and paint ofcourse). In default situation painting a status icon outside bounds was no problem. (Status Icon in Lower Left Corner)
Clipping problems arise when component(textfield) is in jScrollPane. At the bounds of viewPort problems arise. (textfield is clipped, but decoration wasn't clipped as expected).
After few tweaks in you abstract class it's solved.
Maybe still need to override some other functions (tried setPainterBounds). Or I can send you my "hack"...

technomage said...

Normally a decoration will be clipped by an ancestor scroll pane if the decorated component is clipped.

Are you saying the decoration should have been clipped and wasn't?

Heico said...

Yes I do. It's not clipped as expected to be in a scrollpane.
Is there some emailadres I can send my code and screenshot?

technomage said...

twall AT
users DOT sf DOT net

Paul Andrews said...

I was trying to use this in conjunction with your animated icon classes to paint a waiting indicator on a table column header, but it doesn't work. Could it be modified to work in that situation too?

Paul Andrews said...

I know this is an old article, but I was hoping to use this in combination with you animated icon code to overlay a wait indicator on a table cell header. It doesn't work, I guess because the coordinate systems are out of sync. Is there any way it could be modified to work?

technomage said...

I don't know why it wouldn't work in a table header; the general method of re-using a JLabel in that case is the same, but you are correct that perhaps the calculation of the area to be repainted is incorrect. Check whether the animation repaint area calculated corresponds with the actual coordinates of the table header.

Alex Lui said...

Hi,

We find your frame work very useful and save us in development time, especially AbstractComponentDecorator.java and GhostedDragImage.java. GhostedDragImage.java is from “koder.com”.

My question is would it possible to in cooperate the two classes in our production application without the LGPL license? That is a special license grant or purchases the right to link the two classes into our production application.

Regards,

Hee Lui

Timothy Wall said...

Sure, Hee/Alex Lui, go right ahead. All I ask is for appropriate attribution/thanks in your application "About" box or equivalent.