Creating Custom Desktop Components
Pages: 1, 2
Implementing paintComponent()
Old AWT components had to implement the paint() method for their
painting. The Swing's JComponent overrides this method and defines
three new methods: paintComponent(), paintBorder(),
and paintChildren(). Custom Swing components may override any of
these new methods, which are called by paint(), but in most situations
you only need to implement paintComponent(). For more details on
this subject, you should read "Painting
in AWT and Swing" on The
Swing Connection.
The paintComponent() method of the PaintView component:
- Obtains the annotated image with the
getBackImage()method of the data model. - Draws a black rectangle if
getBackImage()returnsnull. - Passes the zoom factor to the
scale()method of the graphics context. - Draws the annotated image (if present).
- Sets the background color of the graphics context.
- Iterates over the tool objects, calling their
paint()method in order to draw all image annotations.
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Image backImage = model.getBackImage();
if (backImage == null) {
g.setColor(Color.black);
Dimension size = getSize();
g.drawRect(0, 0, size.width-1, size.height-1);
}
Graphics2D g2 = (Graphics2D) g;
float zoomFactor = model.getZoomFactor();
g2.scale(zoomFactor, zoomFactor);
if (backImage != null)
g2.drawImage(backImage, 0, 0, this);
g2.setBackground(getBackground());
Iterator iterator = model.getToolIterator();
while (iterator.hasNext()) {
AbstractTool tool
= (AbstractTool) iterator.next();
tool.paint(g2, backImage);
}
}
When the user finishes drawing an annotation and releases the mouse button,
the mouseReleased() method described earlier calls repaint(),
requesting AWT to execute the paint() method, which invokes paintComponent().
The custom component is also repainted when its content needs to be refreshed.
This happens, for example, when the application's window is resized or maximized.
User Interface Operations
Typical custom Swing components need to do two things: implement the painting
operation that visualizes the data model, and handle the user actions (mouse
and/or keyboard events) in order to update the data model. These operations
are specific to each component, but as you've seen in the previous section,
AWT and Swing provide generic mechanisms such as the paint() and
paintComponent() methods that must be overridden by the custom
components, and the registration mechanism for your event listeners. Simple
GUI components can implement both operations in the same class. Complex components,
however, should split the functionality into specialized classes, making the
code more maintainable. For example, Swing's JTable uses cell renderers
and cell editors for the two user interface operations. The PaintView
component delegates its UI operations to a set of "tool" classes that paint
lines, rectangles, ellipses, text notes, and so on. This makes the application
extensible, allowing you to add support for new annotations without having to
change the application's architecture. Note that these classes are specific
to JImaging, but you'll probably find them useful when developing other drawing
components or applications. Figure 2 shows JImaging's tools.
Figure 2. The tool classes are selected from the toolbar
Painting Image Annotations
Each image annotation has a set of properties that define its shape and specify
the color and other rendering attributes. The AbstractTool class
groups these properties using java.awt.Shape, java.awt.Color,
and java.awt.Stroke:
package com.devsphere.articles.desktop.paint.tools;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Stroke;
import java.awt.Shape;
public abstract class AbstractTool {
public static final int DRAW_STYLE = 1;
public static final int FILL_STYLE = 2;
private Shape shape;
private Color color;
private Stroke stroke;
private int paintStyle;
...
}
The AbstractTool class has get and set methods for each of its
properties: shape, color, stroke, and
paintStyle.
public Shape getShape() {
return shape;
}
public void setShape(Shape shape) {
this.shape = shape;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
public Stroke getStroke() {
return stroke;
}
public void setStroke(Stroke stroke) {
this.stroke = stroke;
}
public int getPaintStyle() {
return paintStyle;
}
public void setPaintStyle(int paintStyle) {
this.paintStyle = paintStyle;
}
The abstract action() method must be implemented by the subclasses
of AbstractTool in order to handle the user actions, such as the
mouse events generated while the user draws an annotation on screen.
public abstract void action(int eventID,
float x, float y, Graphics2D g);
The paint() method has two parameters: the graphics context and
the annotated image. These parameters are passed to paintImpl()
along with the tool properties: shape, color, stroke,
and paintStyle. The paintImpl() method sets the rendering
attributes of the graphics context and calls draw() or fill(),
depending on the value of the paintStyle property:
public void paint(Graphics2D g, Image backImage) {
paintImpl(g, backImage, shape, color,
stroke, paintStyle);
}
protected void paintImpl(Graphics2D g,
Image backImage, Shape shape, Color color,
Stroke stroke, int paintStyle) {
g.setColor(color);
g.setStroke(stroke);
switch (paintStyle) {
case DRAW_STYLE:
g.draw(shape);
break;
case FILL_STYLE:
g.fill(shape);
break;
}
}
Handling User Actions
The PencilTool class handles the mouse events that occur when
the user draws something using the application's pencil. The PencilTool()
constructor creates a java.awt.geom.GeneralPath instance and sets
the shape and paintStyle properties:
package com.devsphere.articles.desktop.paint.tools;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
public class PencilTool extends AbstractTool {
private GeneralPath path;
public PencilTool() {
path = new GeneralPath();
setShape(path);
setPaintStyle(DRAW_STYLE);
}
...
}
The action() method takes four parameters: the ID of a mouse event,
the x and y coordinates of the mouse cursor, and the
graphics context. Note that x and y are float numbers
because they may have a fractional part if the zoom factor isn't 1.
These coordinates are calculated in the toolAction() method of
the PaintView class.
When the user presses the mouse button, the action() method calls
the moveTo() method of the GeneralPath object, passing
the x and y coordinates. When the user drags the mouse,
action() calls the lineTo() method of the GeneralPath
object and creates a java.awt.geom.Line2D that is painted with
the draw() method of the graphics context. Therefore, the action()
method does two things:
- Draws small lines between the points where the mouse events are generated.
- Stores all of these points into a
GeneralPathobject so that the shape can be redrawn at a later time.
public void action(int eventID, float x, float y,
Graphics2D g) {
if (eventID == MouseEvent.MOUSE_PRESSED)
path.moveTo(x, y);
else if (eventID == MouseEvent.MOUSE_DRAGGED) {
Point2D startPoint = path.getCurrentPoint();
Point2D endPoint = new Point2D.Float(x, y);
Line2D l = new Line2D.Float(startPoint, endPoint);
g.setColor(getColor());
g.setStroke(getStroke());
g.draw(l);
path.lineTo(x, y);
}
}
Summary
When developing desktop GUIs, most of the time you use existing components
as they are. But sometimes you have to customize them, and there are situations
where you have to build your own components from scratch, which requires a thorough
understanding of the internal mechanisms of AWT and Swing. The example component
of this article implements the basic operations of the typical UI components:
painting and event handling. Not so common is the painting of the graphic objects
outside of the paint() method, but this is a very useful technique
when developing drawing applications. It can make the difference between a slow
app and one that works very fast, no matter what API you use: Swing, AWT, SWT,
Win32, .NET, or something else.
Resources
- Download the source code of the JImaging prototype
- "Java Desktop Development"
- "Prototyping Desktop Applications"
- "Data Models for Desktop Apps"
Andrei Cioroianu is the founder of Devsphere and an author of many Java articles published by ONJava, JavaWorld, and Java Developer's Journal.
Return to ONJava.com