Java Tips Weblog

  • Blog Stats

    • 2,571,044 hits
  • Categories

  • Archives

Playing With Shapes

Posted by Rob Camick on May 13, 2013

Custom painting, as we all know, should be done by overriding the paintComponent(…) method of a Swing component. Chances are that when you first start to experiment with custom painting you will use one or more of the following Graphics painting methods:

  • Graphics.fillRect(…) – to paint a square or rectangle
  • Graphics.fillOval(…) – to paint a circle or oval
  • Graphics.fillPolygon(…) – to paint an arbitrary hape like a triangle, hexagon or octagon

These painting methods are easy to use, but not very flexible:

  • the painting code is programmed in the paintComponent() method of your component so it can’t be shared or reused by other components
  • you need to know which Graphics method to invoke to do the painting
  • the location where the painting is done is specified as a parameter of the Graphics method
  • the Color needs to be specified

The Graphics2D class provides another painting method that is more powerful than the Graphics painting methods:

  • Graphics2D.fill(Shape)

Shape is an interface that has many default implementations. To use fill(…) in your paintComponent() method you need to have access to the Graphics2D object. Swing Graphics objects are actually Graphics2D objects so you could just cast the Graphics object to a Graphics2D, but a better approach is to use the Graphics.create() method. You should also use the Graphics.dispose() method when you are finished painting. Then you must create a Shape using one of the Shape implementations. For example:


@Override
protected void paintComponent(Graphics g)
{
    super.paintComponent( g );
    Graphics2D g2d = (Graphics2D)g.create();

    Polygon triangle = new Polygon();
    triangle.addPoint(0, 0);
    triangle.addPoint(15, 30);
    triangle.addPoint(30, 0);
    g2d.setColor( Color.RED );
    g2d.fill( triangle );

    Shape rectangle = new Rectangle(40, 0, 50, 30);
    g2d.setColor( Color.BLUE );
    g2d.fill( rectangle );

    Shape oval = new Ellipse2D.Double(100, 0, 50, 30);
    g2d.setColor( Color.GREEN );
    g2d.fill( oval );

    g2d.dispose();
}

This approach still suffers from some of the problems of using the individual Graphics methods:

  • the painting code is in the paintComponent() method
  • the location needs to be specified
  • the Color needs to be specified

However, there are important differences. We have eliminated the need to know which method is used to paint the Shape. Also, the Shape interface supports a contains(…) method. This method can be valuable if you ever need to do “hit detection” to determine, for example, which Shape was clicked on by a mouse. Both of these features will allow us to start creating more reusable code.

For example, we could now create a ShapeInfo class to store all the information (shape, location, color) needed to paint a Shape. Then we could add multiple ShapeInfo instances to a List. The paintComponent() method could then use this List to paint all the Shapes. Using this approach, the code to paint two circles beside one another might be something like:


// Create List containing Shapes to be painted

Shape circle = new Ellipse2D.Double(0, 0, 30, 30);
List<ShapeInfo> shapes = new ArrayList<ShapeInfo>();
shapes.add( new ShapeInfo(circle, new Point(0, 0), Color.RED) );
shapes.add( new ShapeInfo(circle, new Point(30, 0), Color.BLUE) );

// The custom painting code might look like:

protected void paintComponent(Graphics g)
{
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D)g.create();

    for (ShapeInfo info : shapes)
    {
        Point location = info.getLocation();
        g2d.translate(location.x, location.y);
        g2d.setColor( info.getColor() );
        g2d.fill( info.getShape() );
        g2d.translate(-location.x, -location.y)
    }

    g2d.dispose();
}

So now the component that does the custom painting can be reused. All you need to do is pass the List containing the ShapeInfo to the component. You could even add methods to the component to dynamically update the List.

Although using the fill() method for custom painting, as suggested above, is an improvement over using the Graphics methods to paint shapes, both approaches still suffer the drawbacks of any component that does custom painting:

  • a custom painted Shape doesn’t respond to events
  • you can’t distinguish one Shape (when you click on it) from another
  • the preferred size of the component, where the shapes are painted, is unknown

It would be relatively easy to implement solutions for the last two issues since all the shape information is contained in the List. However, why not just use the Shape in a real Swing component, then we don’t have to worry about any of these issues?

Shape Icon

Maybe the easiest way to use a Shape is to make it an Icon. The ShapeIcon class does just this. You specify the Shape and Color when you create the ShapeIcon. Then you can add the Icon to any component that knows how to paint an Icon. For example, to create a JLabel that uses a ShapeIcon you could do:


Shape rectangle = new Rectangle(0, 0, 50, 30);
ShapeIcon rectangleIcon = new ShapeIcon(rectangle, Color.RED);
JLabel rectanglelLabel = new JLabel( rectangleIcon );

We don’t need to specify the location since the component will determine the location of the Icon within the component. The layout manager will in turn determine the location of the label within the panel. The rectangle can be used to create another ShapeIcon and the Icon can be used by other components. This provides for lots of reusability.

Swing components respond to events and have a preferred size so we have addressed the issues with custom painting by using the ShapeIcon.

Shape Component

ShapeComponent is a component that paints a Shape by doing the custom painting for you. The main difference between using a JLabel with a ShapeIcon and creating a ShapeComponent is that the ShapeComponent supports “shape detection”. That is, the ShapeComponent will only recognize mouse events that occur within the actual bounds of the Shape as oppose to the bounds of the rectangle that fully contains the Shape.

Creation of a ShapeComponent is as follows:


Shape oval = new Ellipse2D.Double(0, 0, 50, 30);
ShapeComponent ovalComponent = new ShapeComponent(oval, Color.GREEN);

Shape Utils

Now that custom painting is done by using a Shape it is possible to create utility methods that work with shapes. The ShapeUtils class contains some static methods you may find handy to use:

  • radiusShape – This method (inspired by this answer) is used to create a Shape. You specify the number of points you want to create for the Shape and the radius of each point. Each radius will be repeated in order when the required number of points is greater than the number of radii specified. A few examples might help you better understand the usage of this method:
    
        Shape hexagon = ShapeUtils.radiusShape(6, 20);
        Shape simpleStar = ShapeUtils.radiusShape(16, 20, 8);
        Shape doubleStar = ShapeUtils.radiusShape(16, 20, 8, 14, 8);
        Shape dancingStar = ShapeUtils.radiusShape(15, 20, 6, 12);
  • rotate – This method will rotate a Shape about its center by the specified degrees:
    
        Shape rotated = ShapeUtils.rotate(doubleStar, 45);
    

The image below shows how these Shapes would look on a JLabel as an Icon:

PlayingWithShapes

Shape Outlines

The Graphics2D class also supports a draw(Shape) method. This method will draw an outline of the Shape. The default is to draw a single pixel outline, although the thickness of the outline can be changed by creating a BasicStroke object with the desired thickness.

The OutlineIcon and OutlineComponent classes can be used to paint the outline of a Shape. The functionality of these two classes is the same as the respective ShapeIcon and ShapeComponent classes. The only difference is an additional thickness parameter to control the thickness of the outline.

So, for example, to create an outline of the hexagon shape defined above the code would be:

OutlineIcon hexagonIcon = new OutlineIcon(hexagon, Color.RED, 3);
JLabel hexagonLabel = new JLabel( hexagonIcon );

The following image shows how the Shapes defined above would be painted with a thickness of 3:

OutlineIcon

Hopefully the ShapeIcon, ShapeComponent, OutlineIcon and OutlineComponent classes will provide you with an alternative to custom painting. They should simplify your code since you can take advantage of all the normal Swing features like event handling, layout management and default painting.

Get The Code

ShapeIcon.java
ShapeComponent.java
ShapeUtils.java
OutlineIcon.java
OutlineComponent.java

Related Reading

Java API: java.awt.Shape

9 Responses to “Playing With Shapes”

  1. Jack said

    Cool !!!!

  2. mjquake said

    Great one as usual, have always had issues with the paint class from Java for most of the reasons you mentioned above

  3. Neil Cochrane said

    Nice ideas.
    It’d be useful to push it further and have the component or icon take multiple shapes, like is mentioned near the start of the post. Then we’ll have a real ‘painter’ component that can render complex designs without subclassing.
    There’s a nice class called Area to which all these shapes can be added, hit detection and and sizing are then just one call away.

    • Rob Camick said

      Thanks. I actually have had this idea in the back of my mind. I have also been looking at the GeneralPath.append() method in addition to the Area class. My understanding when using the various methods in these classes is that each Shape needs to know some information about one another before they can be conbined. For example, lets say you have a circle and a square that are both 30 pixels in width and height and you want to display them right to left. As I understand it you need to create the shapes like new Circle(0, 0, 30, 30) and new Square(30, 0, 30, 30) (ie. the square needs to know the width of the circle). I would like to be able to create the Shape with X/Y values of 0 and then just specify the relative location of each Shape (above, below, before, after). I should also mention that the CompoundIcon (from this blog) can already be used to combine shapes in this way and has the added advantage that each Shape can be a different color, however you do loose the hit detection feature. I may look into pursing this further when I have time.

      In the meantime there is no reason why you can’t use the Area or GeneralPath classes to combine multiple Shapes into a single Shape. Then you can use any of the classes presented here to display the combined Shape. I don’t plan an changing the classes to accept multiple Shapes, I just plan on making it easier to combine multiple shapes into one.

      • Neil Cochrane said

        Hmm, once we get into the realm of shapes being above, left, right, etc; then they might as well be in separate components in a panel and wrangled with a layout manager. Perhaps there would be some handy utility or panel subclass that makes listening to all components for hit detection simple.

  4. Joshua said

    I love this and it has helped so much. I did notice an error in ShapeUtils rotate function. double anchorX = bounds.width / 2; shouud actually be double anchorX = bounds.getX() + bounds.width / 2; in order to rotate around the center otherwise it rotates around some point.

    • Rob Camick said

      Thanks for pointing this out. I updated the website with the current version from my machine. Note that both the anchorX/Y should be changed. A new translate(…) method has also been added.

Leave a comment