ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Creating Custom Desktop Components Creating Custom Desktop Components

by Andrei Cioroianu
08/11/2004

Swing provides a complete set of standard GUI components, ranging from simple buttons and text fields to feature-rich tables, trees, and text editors. These components are fully customizable, but you might find that Swing's built-in components don't offer everything you need. For example, financial and monitoring applications use charts to present their data graphically. Of course, before starting to build your own chart components, you should evaluate some of the existing chart frameworks, in case someone has already created the component you need. Sometimes, this isn't the case, or perhaps the licensing terms are not acceptable, which means that you have to develop the custom component required by your application yourself. This article presents a drawing component used by an image-annotation application named JImaging. Some of the JImaging code has already been described in two other articles, titled "Prototyping Desktop Applications" and "Data Models for Desktop Apps."

Developing the Component Class

Related Reading

Java Swing
By Marc Loy, Robert Eckstein, Dave Wood, James Elliott, Brian Cole

JImaging's PaintView class extends javax.swing.JComponent, like any regular Swing component. The JComponent class provides many features shared by all Swing components, such as the support for double buffering, which eliminates the flashing effect that occurs when the graphic objects are painted directly onto the screen. With double buffering, the UI components are painted into a buffer, and when the painting is done, the buffer is copied to your screen very quickly.

The painting mechanisms of AWT and Swing are based on a paint() method that can draw a whole component. This works very well for the standard GUI components, such as text boxes, trees, and tables, that don't need to be repainted at a high rate. But when you draw something on screen, using a virtual pencil, hundreds or thousands of mouse events are generated while you move the mouse. It is not efficient to repaint the whole component for each event. It is much better to draw small lines directly on the screen, outside of the AWT/Swing paint() method, as you'll see later in this article.

Figure 1 shows the main frame of the application, which has a toolbar and a main panel (based on Swing's JDesktopPane) that contains the PaintView component. Text notes (based on Swing's JInternalFrame) are painted on top of our custom component.


Figure 1. The GUI components of the JImaging prototype

The Component's Data Model

Swing components obtain the data they visualize from the model objects that are updated when the user types characters, clicks on something like a list item, or does some other action with the mouse or the keyboard. In the previous article in this series, "Data Models for Desktop Apps,") I presented the PaintModel class, which manages the information about all annotations that are painted by the PaintView component. The separation between the data model and the GUI that visualizes the data has many benefits: the components of the application can be changed or enhanced independently, the application's data is kept in one place, and it becomes easier to reuse the application's code. An instance of the PaintModel class is created in the PaintView() constructor. Other classes may obtain a reference to the model object with the getModel() method. This method is not specified by any Swing interface or superclass, but all standard Swing components have such a method. For example, JTable's getModel() returns a TableModel instance, JTree's getModel() returns a TreeModel instance, and so on. Following this pattern, the getModel() method of PaintView returns a PaintModel object.

package com.devsphere.articles.desktop.paint;
 
 import com.devsphere.articles.desktop.paint.tools.AbstractTool;
 
 import javax.swing.JComponent;
 import javax.swing.SwingUtilities;
 
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.Image;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseMotionAdapter;
 
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 
 import java.util.Iterator;
 
 public class PaintView extends JComponent {
     private PaintModel model;
     private AbstractTool currentTool;
 
     public PaintView() {
         model = new PaintModel(this);
         registerListeners();
     }
 
     public PaintModel getModel() {
         return model;
     }
     ...
 }

Registering Event Listeners

In order to be able to respond the user's actions, most GUI components register mouse listeners and/or keyboard listeners with the addMouseListener(), addMouseMotionListener(), and addKeyListener() methods inherited from java.awt.Component. Note that there are components, such as JLabel, that don't handle mouse nor keyboard events.

The PaintView component has a method named registerListeners() that registers a mouse listener and a mouse motion listener. The methods implemented by these listeners are called when the user presses or releases a mouse button or when he drags the mouse with a button pressed, respectively. Only the events generated with the left mouse button are processed with the toolAction() method, which is presented below. The mousePressed() method creates a tool object that is passed to the data model in the mouseReleased() method. In addition to the two mouse listeners, registerListeners() creates a PropertyChangeListener that is registered to the data model. The propertyChange() method is called when the value of a model property is changed, which means that the PaintView component needs to be repainted:


protected void registerListeners() {
     addMouseListener(new MouseAdapter() {
         public void mousePressed(MouseEvent e) {
             if (SwingUtilities.isLeftMouseButton(e)) {
                 requestFocus();
                 currentTool = model.createTool(
                     AbstractTool.DRAW_STYLE);
                 toolAction(e);
             }
         }

         public void mouseReleased(MouseEvent e) {
             if (SwingUtilities.isLeftMouseButton(e)) {
                 toolAction(e);
                 model.setLastTool(currentTool);
                 currentTool = null;
                 repaint();
             }
         }
     });

     addMouseMotionListener(new MouseMotionAdapter() {
         public void mouseDragged(MouseEvent e) {
             if (SwingUtilities.isLeftMouseButton(e))
                 toolAction(e);
         }
     });

     model.addPropertyChangeListener(
             new PropertyChangeListener() {
         public void propertyChange(PropertyChangeEvent e) {
             if (isShowing())
                 repaint();
         }
     });
 }

The toolAction() method consumes mouse events, forwarding them to the current tool object for processing. In addition, this method:

protected void toolAction(MouseEvent e) {
     e.consume();
     Graphics2D g2 = (Graphics2D) getGraphics();
     float zoomFactor = model.getZoomFactor();
     g2.scale(zoomFactor, zoomFactor);
     float x = e.getX() / zoomFactor;
     float y = e.getY() / zoomFactor;
     currentTool.action(e.getID(), x, y, g2);
     g2.dispose();
 }

Note that toolAction() is called from the mouse event handlers, which are executed in an AWT thread that also invokes the paint() method inherited by the UI component from JComponent. The image annotations are shown on screen, but neither AWT nor Swing keeps them in memory after they are painted. The application must be prepared to repaint the annotations at any time. If the application wants to refresh the content of the PaintView component, it must call repaint(), which generates an AWT PaintEvent. All UI events, including the mouse and paint events, are queued. These events are processed by the AWT thread that invokes paint(), mousePressed(), mouseReleased(), mouseDragged(), and so on.

Non-AWT events such as PropertyChangeEvent are not queued. As explained in the previous article, propertyChange() is called by the firePropertyChange() methods of the data model. This has serious implications in multithreaded applications, which is not the case with JImaging. A discussion about the AWT event model or about thread safety is beyond the scope of this article. If you aren't familiar with these notions, you should read The Java Tutorial.

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:

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:


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

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

Copyright © 2009 O'Reilly Media, Inc.