ORIGINAL DRAFT

Some readers may recognize this column, based on earlier work I’ve published elsewhere in the past. The column centers on visual component development, typically with Swing, and interface widget design. It tries to communicate effective techniques for developing flexible elements with good value and a focus on reusability. Good design and effective use of patterns and interfaces ensure flexibility, and applicability to a variety of circumstances, rather than limiting components to a single project’s scope. Reading good code is one of the best way to learn about Java, I hope this column serves you well and encourage your feedback.

This installment explores a basic report widget. JReport implements an interface in which you can drag and drop field elements onto a grid, move and resize those elements, rename or remove them, and print a series of records with headers and footers using the new Java 2 printing API. This is a lot to compress into a small column, yet the whole thing fits nicely into about a dozen classes and interfaces, a testament to the power of Java and Swing.

Figure 1: JReport in Action.

Figure 1: JReport in Action.

Figure 1 shows the what a typical JReport window might look like. The rulers at the top left of each grid panel track the current selection with a shaded rectangle. Along with the snap-to-grid feature, this makes it much easier to line things up the way you like. Report configurations can be saved to file and loaded anytime. We’ll implement some useful utility fields which can resolve the date, time and page number while printing, and you can edit label fields all you like. JReport uses a RecordSet interface which allows you to expose fields and values from a database or other record-based data source.

Drag and Drop

To support the ability to drop an element onto the report grid, we’ll use the Drag and Drop facilities in Java

  1. The DropLabel class in Listing 1 implements the DragSourceListener, DragGestureListener and Transferable interfaces. We don’t actually have to do much with the methods in the DragSourceListener interface, though they need to be present, but we have to provide a DragSourceListener as an argument to the startDrag method in the DragGestureEvent. Our interest is primarily in implementing the DragGestureListener so that we can start dragging our object.

We implement the dragGestureRecognized method to support the DragGestureListener interface, passing on the require information through the event object. The Transferable interface comes into play when we start dragging the object, so we need to implement the three methods that support a suitable data flavor. We don’t expect to drag and drop between anything outside our program. That means we’re safe with a custom flavor, based on the DropLabel class. This flavor is declared statically, so that we can reference it from anywhere without creating new instances.

Most of the code in Listing 1 is fairly self-explanatory, primarily an implementation of the drag and drop interfaces in a JLabel class. We set up a default drag source and gesture recognizer in the constructor to support the copy and move events. Once we’re done, our object can be dragged from its origin onto any suitable drop target.

We’ll be hosting a number of DropLabel objects, one for each of the field labels and data elements in a record, along with a few goodies like date, time, page number and customizable labels you may want to use to put comments or other things in your report pages. Form elements are either editable or not. The field names and the custom element are editable, but the field data and special elements are not. Just to be sure they are visually recognizable, we’ll use an icon with the fields which are not editable.

Listing 2 shows the DragPanel class, which is primarily responsible for showing general, label and value panels. There should be a one to one mapping between these and the field names derived from the RecordSet interface. The DragPanel extends JToolBar, so it can also be undocked.

Record Sets

Listing 3 shows the RecordSet interface, which defines three simple methods. The getFieldNames methods lets us collect a set of field names as a String array. The getNextRecord method lets us retrieve a Properties object with the content of a given record. The Properties object is expected to have the record values stored under the same keys returned by the getFieldNames method. We implement a reset method to start the data collection from the beginning, so we can print reports more than once.

Listing 4 shows a TestRecordSet class implementation, which uses simple records that hold a first and last name, along with an email address. The complexity of these records is completely arbitrary and you can create any suitable implementation, so long as it implements the RecordSet interface. You can find a DBRecordSet class, which uses JDBC, online at the JavaPro site if you’re interested.

Manipulating Elements

In order to properly organize our layout, we’ll use a JLayeredPane and a JPanel extension called FormElement, which you can find in Listing 5. The FormElement class does a fair amount of work. It must be moveable and resizable, and may have the keyboard focus. What’s more, it may be editable so we have to implement a number of listeners. The FocusListener lets us determine how we should draw the border. The ActionListener is used to implement a delete function so a FormElement can be removed. The MouseListener and MouseMotionListener interfaces allow us to move and resize the element. The DocumentListener lets us watch for edit changes, so we can save those changes when we store the form on disk.

Before we take a deeper look at the FormElement, its worth taking a brief excursion to Listing 6, the DragBorder class, which implements the Swing Border interface. Figure 2 shows the border up close.

Figure 2: DragBorder implementation.

Figure 2: DragBorder implementation.

We make use of the BasicGraphicsUtils class in the plugable look and feel library, and the drawDashedRect method, to draw a dotted rectangle around the component. Because these are nested, each internal drawing is offset by exactly one pixel so we end up with a single-pixel checkerboard pattern when the thickness is more than one. The corner and side anchors are just white-filled squares, with dimensions based on the border thickness. The side anchors are placed exactly halfway along each respective side of the border.

While the DragBorder creates the look we need, the actual functionality is in the FormElement class. The four corners and fours sides are identified by a set of constants at the top of Listing 5. When the mousePressed event occurs, we check the position of the mouse by using a set of utility methods. These let us determine whether the mouse is in one of the anchor points, in the border, or within the component’s drawing area. We reuse these methods again in the mouseMoved method to select an appropriate Cursor, watching for the mouseExited event to restore the original cursor.

The mousePressed event is the one which sets the anchor point. We store the x and y values to be sure we drag relative to the offset from the component position. When we start dragging, the work is taken over by the mouseDragged event. When we see the the mouseReleased event, we set the drag flag to NONE. If we’re in the border, but not on an anchor point, we’re moving the FormElement within the parent window, so we use the convertPoint method from the SwingUtilities class to get the relative position and set the new coordinates.

We set the new bounds with the setGridBounds method to constrain resizing and movement. This is what lets us implement the snap-to-grid feature and keep the component within the drawing space. If you don’t like the snap-to-grid feature, you can set the gridSnap variable to one. The default setting is 8 pixels.

When a FormElement gets the focus, we change the border and register component listeners with the ruler objects so they can track the position of an active FormElement. To update the visual display, we call the refreshTracking methods directly and move the FormElement to the top of the JLayeredPane display. When we lose the focus, we unregister the ComponentListener.

To support persistence, we implement a toString method, which creates a comma-delimited string with the value, editable flag and the FormElement’s position and dimensions. We can recreate a FormElement using the fromString method, which interprets the same string format.

Finally, the FormElement object supports a PopupMenu implementation which lets you right click to delete the object from its parent. When we get an action event from the menu, we queue up a repaint event ten milliseconds into the future before actually deleting the component, so we can avoid null pointer exceptions, given that the object will be removed by the time the paint executes. This trick might be an issue on slower computers, so you’ll want to keep an eye out for trouble and increment the value if it causes problems.

Layered Grid

The GridPanel class in Listing 7 is receptacle for drag-and-drop events. It implements the DropTargetListener interface, paints the grid background and allows the FormElements to be saved and loaded. Handling the drop events involves accepting the drag operation in the dragEnter and drag methods and verifying the data flavor in the drag method before completing the operation.

The drag method receives the information it needs to create a FormElement from the DropLabel. We use a helper method called addElement to actually add the element to our container. I had a major problem with system hangs when I was developing this and finally tracked it down to the setLocation method. After replacing it with setBounds and trying a multitude of variations, to no avail, I finally gave up an kludged together a LocationThread (Listing 8) that defers the event to a point after the drag event handler was exited. If you come up with a better solution, I’d love to hear about it.

The two rulers are implemented in the same GridRuler class from Listing 9. They draw the tick marks and tracking areas vertically or horizontally, depending on the orientation setting you chose. This class implements the ComponentListener interface, so we can tell when a tracked component changes position or size. The constructor requires a height or width argument. Both the GridRuler and GridPanel uses a 72 dot per inch setting, in keeping with the default printing format.

The GridForm class in Listing 10 wraps the GridPanel and GridRulers into a single scroll panel. We take advantage of the setColumnHeaderView and setRowHeaderView methods in JScrollPane to work with the rulers and implement a save and load method which ties together the underlying implementation and does its own stream handling. As you may recall, FormElement can parse and build a parsable string. The FormPanel can save to and load from a stream. The GridForm can save to and load from a file.

JReport

The ReportPrinter class in Listing 11 implements the Printable interface. The print method with no arguments is there from convenience. It fetches a PrinterJob and clears the settings before starting the print operation. The print method with arguments is our implementation of the Printable interface. It uses a few support methods to measure the height of a single record, to draw text in a given rectangle and to actually paint the FormElement components in the right position, resolving all the special and field value records before rendering the output.

The only complication is that the print method may be called multiple times on the same page. To make sure we don’t increment our record position on every call (calling the RecordSet getNextRecord method), we use a Properties array to cache the entries when the page count changes. We also use the page count to determine whether we are finished printing, returning NO_SUCH_PAGE when we’re done.

Figure 3: Printed Report Example.

Figure 3: Printed Report Example.

Listing 12 shows the JReport class, which ties it all together. Implemented as a JPanel extension, you can drop it into a dialog box as easily as a JFrame or other container. JReport creates three GridForm areas, accounting for the header, footer and a record area in which you can organize the FormElements. You can drag elements from the DragPanel onto any grid area and use the mouse to move and resize them to your heart’s content. Some fields are editable, so you can click or tab to them and just start typing. The blue border will tell you which element has the focus. Once you are satisfied with the arrangement, you can save or print and later load the results for further printing or editing.

Figure 4: JReportTest with Undocked ToolBar.

Figure 4: JReportTest with Undocked ToolBar.

To demonstrate the JReport class, Listing 13 shows the JReportTest class. This class implements four buttons to Load, Save, Print and Quit, handling the various action events by calling into the JReport methods. Figure 4 shows the initial layout after the user dragged the DragPanel off the main JFrame to undock it.

Summary

The JReport component allows you to introduce reporting capabilities in your application with minimal effort. With it, you can provide the power to configure, as well as store and retrieve form configurations to your users. Reports can be based on any record collection which implements the RecordSet interface, maximizing flexibility. User can move and resize value elements and even customize labels to suit their needs. This widget demonstrates numerous techniques for form-handling, drag-and-drop, printing, effective use of interfaces and more. I hope you learned as much from this as I did, and I look forward to the next installment of our Visual Components column.

Listing 1

import java.awt.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.swing.border.*;

public class DropLabel extends JLabel
  implements DragSourceListener, DragGestureListener, Transferable
{
  public static DataFlavor dropLabelFlavor =
    new DataFlavor(DropLabel.class, "DropLabel");
  
  public DropLabel(String text, Icon icon)
  {
    super(text, icon, JLabel.LEFT);
    setBorder(new EtchedBorder(EtchedBorder.RAISED));
    DragSource source = DragSource.getDefaultDragSource();
    DragGestureRecognizer recognizer =
      source.createDefaultDragGestureRecognizer(
        this, DnDConstants.ACTION_COPY_OR_MOVE, this);
  }

  public void dragGestureRecognized(DragGestureEvent event)
  {
    event.startDrag(null, this, this);
  }
    
  public Object getTransferData(DataFlavor flavor)
  {
    if (flavor.equals(dropLabelFlavor))
      return(this);
    return null;
  }

  public DataFlavor[] getTransferDataFlavors()
  {
    DataFlavor[] flavors = { dropLabelFlavor };
    return flavors;
  }

  public boolean isDataFlavorSupported(DataFlavor flavor)
  {
    return flavor.equals(dropLabelFlavor);
  }

  public void dragDropEnd(DragSourceDropEvent event) {}
  public void dragEnter(DragSourceDragEvent event) {}
  public void dragExit(DragSourceEvent event) {}
  public void dragOver(DragSourceDragEvent event) {}
  public void dropActionChanged(DragSourceDragEvent event) {}

}

Listing 2

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class DragPanel extends JToolBar
{
  public static final Icon dataIcon =
    new ImageIcon("ValueIcon.gif");

  public DragPanel(RecordSet set)
  {
    JPanel panel = new JPanel();
    panel.setLayout(new BorderLayout());
    panel.add(BorderLayout.NORTH, generalPanel());
    panel.add(BorderLayout.CENTER, labelPanel(set.getFieldNames()));
    panel.add(BorderLayout.SOUTH, valuePanel(set.getFieldNames()));
    
    setLayout(new BorderLayout());
    add(BorderLayout.NORTH, panel);
  }
  
  public JPanel generalPanel()
  {
    JPanel panel = new JPanel();
    panel.setBorder(new TitledBorder("General"));
    panel.setLayout(new GridLayout(4, 1));
    panel.add(new DropLabel("DATE", dataIcon));
    panel.add(new DropLabel("TIME", dataIcon));
    panel.add(new DropLabel("PAGE", dataIcon));
    panel.add(new DropLabel("LABEL", null));
    return panel;
  }
  
  public JPanel labelPanel(String[] fieldNames)
  {
    JPanel panel = new JPanel();
    panel.setBorder(new TitledBorder("Labels"));
    panel.setLayout(new GridLayout(fieldNames.length, 1));
    for (int i = 0; i < fieldNames.length; i++)
      panel.add(new DropLabel(fieldNames[i], null));
    return panel;
  }

  public JPanel valuePanel(String[] fieldNames)
  {
    JPanel panel = new JPanel();
    panel.setBorder(new TitledBorder("Values"));
    panel.setLayout(new GridLayout(fieldNames.length, 1));
    for (int i = 0; i < fieldNames.length; i++)
      panel.add(new DropLabel(fieldNames[i], dataIcon));
    return panel;
  }
}

Listing 3

import java.util.*;

public interface RecordSet
{
  public String[] getFieldNames();
  public Properties getNextRecord();
  public void reset();
}

Listing 4

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.print.*;
import javax.swing.*;
import javax.swing.border.*;

public class JReportTest extends JFrame
  implements ActionListener
{
  protected static JButton save, load, print, quit;
  protected JReport report;
  
  public JReportTest(PageFormat pageFormat, RecordSet recordSet)
  {
    report = new JReport(pageFormat, recordSet);

    JPanel buttons = new JPanel(new FlowLayout());
    buttons.add(save = new JButton("Save"));
    save.addActionListener(this);
    buttons.add(load = new JButton("Load"));
    load.addActionListener(this);
    buttons.add(print = new JButton("Print"));
    print.addActionListener(this);
    buttons.add(quit = new JButton("Quit"));
    quit.addActionListener(this);
    
    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(BorderLayout.CENTER, report);
    getContentPane().add(BorderLayout.SOUTH, buttons);
  }
  
  public void actionPerformed(ActionEvent event)
  {
    Object source = event.getSource();
    if (source == save)
    {
      try
      {
        report.save("JReport");
      }
      catch (IOException e)
      {
        e.printStackTrace();
      }
    }
    if (source == load)
    {
      try
      {
        report.load("JReport");
      }
      catch (IOException e)
      {
        e.printStackTrace();
      }
    }
    if (source == print)
    {
      report.printer.print();
    }
    if (source == quit)
    {
      System.exit(0);
    }
  }
  
  public static void main(String[] args)
  {
    PLAF.setNativeLookAndFeel(true);
    
    PageFormat pageFormat = new PageFormat();
    RecordSet recordSet = new TestRecordSet();
    
    JFrame frame = new JReportTest(pageFormat, recordSet);
    frame.pack();
    Rectangle bounds = frame.getBounds();
    bounds.height += 100;
    frame.setBounds(bounds);
    frame.setVisible(true);
  }
}

Listing 5

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class FormElement extends JPanel
  implements FocusListener, ActionListener,
    MouseListener, MouseMotionListener,
    DocumentListener, SwingConstants
{
  public static final int DRAG_NONE = 0;
  public static final int DRAG_MOVE = 1;
  public static final int DRAG_NORTH = 2;
  public static final int DRAG_SOUTH = 3;
  public static final int DRAG_EAST = 4;
  public static final int DRAG_WEST = 5;
  public static final int DRAG_NORTHWEST = 6;
  public static final int DRAG_NORTHEAST = 7;
  public static final int DRAG_SOUTHWEST = 8;
  public static final int DRAG_SOUTHEAST = 9;

  protected static Icon icon =
    new ImageIcon("ValueIcon.gif");
  protected JPopupMenu popup;
  protected JMenuItem delete;

  protected int xDot, yDot;
  protected int mode = DRAG_NONE;
  protected JComponent child;
  protected String value;
  protected boolean editable;
  protected int alignment = LEFT;
  
  protected Border focusBorder =
    new DragBorder(Color.blue, 5, true, true);
  protected Border normalBorder =
    new DragBorder(Color.black, 5, true, true);
  
  public FormElement(String value, boolean editable)
  {
    this.value = value;
    this.editable = editable;
    setBorder(normalBorder);
    setLayout(new BorderLayout());
    if (editable)
    {
      JTextField field =new JTextField(value);
      field.getDocument().addDocumentListener(this);
      child = field;
    }
    else child = new JLabel(value, icon, JLabel.LEFT);
    child.setBorder(null);
    add(BorderLayout.CENTER, child);
    addMouseMotionListener(this);
    addMouseListener(this);
    child.addMouseListener(this);
    child.addFocusListener(this);
    addFocusListener(this);
    popup = createPopup();
  }
  
  protected boolean isNorthWest(int x, int y)
  {
    return
      x < getInsets().left &&
      y < getInsets().top;
  }
  
  protected boolean isNorthEast(int x, int y)
  {
    return
      x > getSize().width - getInsets().right &&
      y < getInsets().top;
  }
  
  protected boolean isSouthWest(int x, int y)
  {
    return
      x < getInsets().left &&
      y > getSize().height - getInsets().bottom;
  }
  
  protected boolean isSouthEast(int x, int y)
  {
    return
      x > getSize().width - getInsets().right &&
      y > getSize().height - getInsets().bottom;
  }
  
  protected boolean isNorth(int x, int y)
  {
    int left = (getSize().width - getInsets().left) / 2;
    int right = left + getInsets().left;
    return
      x > left && x < right &&
      y < getInsets().top;
  }
  
  protected boolean isSouth(int x, int y)
  {
    int left = (getSize().width - getInsets().left) / 2;
    int right = left + getInsets().left;
    return
      x > left && x < right &&
      y > getSize().height - getInsets().bottom;
  }
  
  protected boolean isEast(int x, int y)
  {
    int top = (getSize().height - getInsets().top) / 2;
    int bottom = top + getInsets().top;
    return
      x > getSize().width - getInsets().right &&
      y > top && y < bottom;
  }
  
  protected boolean isWest(int x, int y)
  {
    int top = (getSize().height - getInsets().top) / 2;
    int bottom = top + getInsets().top;
    return
      x < getInsets().left &&
      y > top && y < bottom;
  }
  
  protected boolean isMove(int x, int y)
  {
    return
      x < getInsets().left ||
      x > getSize().width - getInsets().right ||
      y < getInsets().top || 
      y > getSize().height - getInsets().bottom;
  }
  
  public boolean isFocusTraversable()
  {
    return true;
  }
  
  public void changedUpdate(DocumentEvent event) {}
  
  public void insertUpdate(DocumentEvent event)
  {
    value = ((JTextField)child).getText();
  }
  
  public void removeUpdate(DocumentEvent event)
  {
    value = ((JTextField)child).getText();
  }	
  
  public void mouseClicked(MouseEvent event)
  {
    if (SwingUtilities.isRightMouseButton(event))
    {
      popup.show(this, event.getX(), event.getY());
    }
  }

  public void mousePressed(MouseEvent event)
  {
    if (SwingUtilities.isRightMouseButton(event))
      return;
    int x = event.getX();
    int y = event.getY();
    xDot = x; yDot = y;
    if (isNorthWest(x, y))
      mode = DRAG_NORTHWEST;
    else if (isNorthEast(x, y))
      mode = DRAG_NORTHEAST;
    else if (isSouthWest(x, y))
      mode = DRAG_SOUTHWEST;
    else if (isSouthEast(x, y))
      mode = DRAG_SOUTHEAST;
    else if (isNorth(x, y))
      mode = DRAG_NORTH;
    else if (isSouth(x, y))
      mode = DRAG_SOUTH;
    else if (isEast(x, y))
      mode = DRAG_EAST;
    else if (isWest(x, y))
      mode = DRAG_WEST;
    else if (isMove(x, y))
      mode = DRAG_MOVE;
    else mode = DRAG_NONE;
    if (editable) child.requestFocus();
    else requestFocus();
  }

  public void mouseReleased(MouseEvent event)
  {
    mode = DRAG_NONE;
  }

  public void mouseEntered(MouseEvent event) {}
  public void mouseMoved(MouseEvent event)
  {
    int x = event.getX();
    int y = event.getY();
    int cursor = Cursor.DEFAULT_CURSOR;
    if (isNorthWest(x, y))
      cursor = Cursor.NW_RESIZE_CURSOR;
    else if (isNorthEast(x, y))
      cursor = Cursor.NE_RESIZE_CURSOR;
    else if (isSouthWest(x, y))
      cursor = Cursor.SW_RESIZE_CURSOR;
    else if (isSouthEast(x, y))
      cursor = Cursor.SE_RESIZE_CURSOR;
    else if (isNorth(x, y))
      cursor = Cursor.N_RESIZE_CURSOR;
    else if (isSouth(x, y))
      cursor = Cursor.S_RESIZE_CURSOR;
    else if (isEast(x, y))
      cursor = Cursor.E_RESIZE_CURSOR;
    else if (isWest(x, y))
      cursor = Cursor.W_RESIZE_CURSOR;
    else if (isMove(x, y))
      cursor = Cursor.MOVE_CURSOR;
    setCursor(Cursor.getPredefinedCursor(cursor));
  }
  
  public void mouseExited(MouseEvent event)
  {
    setCursor(Cursor.getPredefinedCursor(
      Cursor.DEFAULT_CURSOR));
  }
  
  public void mouseDragged(MouseEvent event)
  {
    int x = event.getX();
    int y = event.getY();
    int left = getBounds().x;
    int top = getBounds().y;
    int width = getBounds().width;
    int height = getBounds().height;
    if (mode == DRAG_NONE) return;
    if (mode == DRAG_MOVE)
    {
      Point point = SwingUtilities.convertPoint(
        this, x - xDot, y - yDot, getParent());
      setGridBounds(false, point.x, point.y, width, height);
    }
    if (mode == DRAG_EAST)
      setGridBounds(true, left, top, x, height);
    if (mode == DRAG_SOUTH)
      setGridBounds(true, left, top, width, y);
    if (mode == DRAG_NORTH)
      setGridBounds(true, left, top + y, width, height - y);
    if (mode == DRAG_WEST)
      setGridBounds(true, left + x, top, width - x, height);
    if (mode == DRAG_SOUTHEAST)
      setGridBounds(true, left, top, x, y);
    if (mode == DRAG_SOUTHWEST)
      setGridBounds(true, left + x, top, width - x, y);
    if (mode == DRAG_NORTHWEST)
      setGridBounds(true, left + x, top + y, width - x, height - y);
    if (mode == DRAG_NORTHEAST)
      setGridBounds(true, left, top + y, x, height - y);
  }

  protected void setGridBounds(boolean resizing,
    int x, int y, int w, int h)
  {
    int gridSnap = ((GridPanel)getParent()).snapToGridSize;
    int width = getParent().getPreferredSize().width;
    int height = getParent().getPreferredSize().height;
    if (x < 0)
    {
      if (resizing) w += x;
      x = 0;
    }
    else
    {
      if (x + w > width)
      {
        if (resizing) w = width - x;
        else x = width - w;
      }
    }
    if (y < 0)
    {
      if (resizing) h += y;
      y = 0;
    }
    else
    {
      if (y + h > height)
      {
        if (resizing) h = height - y;
        else y = height - h;
      }
    }
    int r = x + w;
    int b = y + h;
    x = (x / gridSnap) * gridSnap;
    y = (y / gridSnap) * gridSnap;
    w = (r / gridSnap) * gridSnap - x;
    h = (b / gridSnap) * gridSnap - y;
    setBounds(x, y, w, h);
    revalidate();
  }
  
  public void focusGained(FocusEvent event)
  {
    if (getParent() instanceof GridPanel)
    {
      GridPanel grid = (GridPanel)getParent();
      addComponentListener(grid.horz);
      addComponentListener(grid.vert);
      grid.horz.refreshTracking(this);
      grid.vert.refreshTracking(this);
      grid.moveToFront(this);
    }
    setBorder(focusBorder);
    repaint();
  }
  
  public void focusLost(FocusEvent event)
  {
    if (getParent() instanceof GridPanel)
    {
      GridPanel grid = (GridPanel)getParent();
      removeComponentListener(grid.horz);
      removeComponentListener(grid.vert);
    }
    setBorder(normalBorder);
    repaint();
  }
  
  public String toString()
  {
    Rectangle rect = getBounds();
    return value + "," + editable + "," +
      rect.x + "," + rect.y + "," +
      rect.width + "," + rect.height;
  }
  
  public static FormElement fromString(String text)
  {
    StringTokenizer tokenizer =
      new StringTokenizer(text, ",", false);
    String value = tokenizer.nextToken().trim();
    boolean editable = Boolean.valueOf(
      tokenizer.nextToken()).booleanValue();
    int x = Integer.parseInt(tokenizer.nextToken());
    int y = Integer.parseInt(tokenizer.nextToken());
    int w = Integer.parseInt(tokenizer.nextToken());
    int h = Integer.parseInt(tokenizer.nextToken());
    FormElement element = new FormElement(value, editable);
    element.setBounds(x, y, w, h);
    return element;
  }
  
  protected JPopupMenu createPopup()
  {
    JPopupMenu menu = new JPopupMenu();
    menu.add(delete = new JMenuItem("Delete"));
    delete.addActionListener(this);
    menu.add(new JMenuItem("Cancel"));
    return menu;
  }	
  
  public void actionPerformed(ActionEvent event)
  {
    if (event.getSource() == delete)
      remove();
  }
  
  protected void remove()
  {
    getParent().repaint(10);
    getParent().remove(this);
  }
}

Listing 6

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.plaf.basic.*;

public class DragBorder implements Border
{
  protected Color color;
  protected int thickness = 1;
  protected boolean corners = true;
  protected boolean sides = true;
    
  public DragBorder()
  {
    this(Color.black, 1, false, false);
  }

  public DragBorder(Color color)
  {
    this(color, 1, false, false);
  }

  public DragBorder(Color color, int thickness)
  {
    this(color, thickness, false, false);
  }

  public DragBorder(Color color, int thickness,
    boolean corners, boolean sides)
  {
    this.color = color;
    this.thickness = thickness;
    this.corners = corners;
    this.sides = sides;
  }

  public boolean isBorderOpaque()
  {
    return false;
  }

  public Insets getBorderInsets(Component component)
  {
    return new Insets(thickness, thickness, thickness, thickness);
  }
  
  private void drawAnchor(Graphics g, int x, int y)
  {
    int t = thickness - 1;
    g.setColor(Color.white);
    g.fillRect(x, y, t, t);
    g.setColor(Color.black);
    g.drawRect(x, y, t, t);
  }
  
  public void paintBorder(Component component,
    Graphics g, int x, int y, int w, int h)
  {
    g.setColor(color);
    for (int i = 0; i < thickness; i++)
    {
      BasicGraphicsUtils.drawDashedRect(g, 
        x + i, y + i, w - (i * 2), h - (i * 2));
    }
    int r = w - thickness;
    int b = h - thickness;
    if (corners)
    {
      drawAnchor(g, x, y);
      drawAnchor(g, r, y);
      drawAnchor(g, x, b);
      drawAnchor(g, r, b);
    }
    int c = x + (w - thickness) / 2;
    int m = y + (h - thickness) / 2;
    if (sides)
    {
      drawAnchor(g, x, m);
      drawAnchor(g, r, m);
      drawAnchor(g, c, y);
      drawAnchor(g, c, b);
    }
  }
}

Listing 7

import java.io.*;
import java.awt.*;
import java.awt.dnd.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;

public class GridPanel extends JLayeredPane
  implements DropTargetListener
{
  protected GridRuler horz, vert;
  protected int dotsPerInch = 72;
  protected int snapToGridSize = 8;
  protected int divisions = 9;
  
  public GridPanel(GridRuler horz, GridRuler vert)
  {
    DropTarget droptarget = new DropTarget(this, this);
    this.horz = horz;
    this.vert = vert;
  }
  
  private void addElement(int x, int y, DropLabel dropLabel)
  {
    FormElement element = new FormElement(
      dropLabel.getText(), dropLabel.getIcon() == null);
    element.setBounds(dropLabel.getBounds());
    element.setVisible(false);
    add(element);
    new LocationThread(element, x, y);
  }

  public void dragEnter(DropTargetDragEvent event)
  {
    if (isValidDragDrop(event.getDropAction(),
      event.getCurrentDataFlavors()))
    {
      event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
    }
    else event.rejectDrag();
  }

  public void dragExit(DropTargetEvent event) {}
  public void dragOver(DropTargetDragEvent event) {}
  public void dropActionChanged(DropTargetDragEvent event) {}
 
  public void drop(DropTargetDropEvent event)
  {
    if (isValidDragDrop(event.getDropAction(),
      event.getCurrentDataFlavors()))
    {
      event.acceptDrop(event.getDropAction());
      try
      {
        Transferable xfer = event.getTransferable();
        Object obj = xfer.getTransferData(DropLabel.dropLabelFlavor);
        if(obj instanceof DropLabel)
        {
          DropLabel label = (DropLabel)obj;
          int x = (int)event.getLocation().getX();
          int y = (int)event.getLocation().getY();
          addElement(x, y, label);
        }
        event.dropComplete(true);
      }
      catch(Exception e)
      {
        e.printStackTrace();
        event.dropComplete(false);
      }      
    }
    else
    {
      event.rejectDrop();
    }
  }

  private boolean isValidDragDrop(
    int dropAction, DataFlavor flavors [])
  {
    if ((dropAction & DnDConstants.ACTION_COPY_OR_MOVE) != 0)
    {
      for (int i = 0; i < flavors.length; i++)
      {
        if(flavors[i].getPrimaryType().equals("application")
          && flavors[i].getSubType().equals(
            "x-java-serialized-object"))
        {
          return true;
        }
      }
    }
    return false;
  }

  public void paintComponent(Graphics g)
  {
    int w = getPreferredSize().width;
    int h = getPreferredSize().height;
    g.drawRect(0, 0, w, h);
    double unit = (double)dotsPerInch / (double)(divisions);
    for (double x = 0; x < w; x += unit)
    {
      for (double y = 0; y < h; y += unit)
      {
        int xx = (int)Math.round(x);
        int yy = (int)Math.round(y);
        g.drawLine(xx, yy, xx, yy);
      }
    }
    for (int x = 0; x < w; x += dotsPerInch)
    {
      g.drawLine(x, 0, x, h);
    }
    for (int y = 0; y < h; y += dotsPerInch)
    {
      g.drawLine(0, y, w, y);
    }
  }

  public void save(PrintWriter writer)
    throws IOException
  {
    Component[] elements = getComponents();
    for (int i = 0; i < elements.length; i++)
    {
      writer.println(elements[i].toString());
    }
  }
  
  public void load(BufferedReader reader)
    throws IOException
  {
    removeAll();
    while(true)
    {
      String line = reader.readLine();
      if (line == null) break;
      add(FormElement.fromString(line));
    }
    revalidate();
    repaint();
  }
}

Listing 8

import java.awt.*;
import javax.swing.*;

public class LocationThread extends Thread
{
  protected Component frame;
  protected int x, y;
  
  public LocationThread(Component frame, int x, int y)
  {
    this.frame = frame;
    this.x = x;
    this.y = y;
    start();
  }
  
  public void run()
  {
    try
    {
      Thread.sleep(10);
    }
    catch(InterruptedException e) {}
    frame.setLocation(x, y);
    frame.setVisible(true);
  }

}

Listing 9

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;

public class GridRuler extends JPanel
  implements SwingConstants, ComponentListener
{
  protected int orientation, length;
  protected int divisions = 9;
  protected int dotsPerInch = 72;
  protected int from = 0, to = 0;
  
  public GridRuler(int orientation, int length)
  {
    this.orientation = orientation;
    this.length = length;
    setBorder(new BevelBorder(BevelBorder.LOWERED));
  }
  
  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;
    double unit = (double)dotsPerInch / (double)(divisions);

    g.setColor(getBackground());
    g.fillRect(0, 0, w, h);
    g.setColor(Color.gray);

    if (orientation == HORIZONTAL)
    {
      int center = h / 2;
      g.fillRect(from, 0, to, h);
      g.setColor(getForeground());
      for (double x = 0; x < w; x += unit)
      {
        int xx = (int)Math.round(x);
        g.drawLine(xx, center - 2, xx, center + 2);
      }
      for (int x = 0; x < w; x += dotsPerInch)
      {
        g.fillRect(x - 2, center - 2, 5, 5);
      }
    }
    else
    {
      int center = w / 2;
      g.fillRect(0, from, w, to);
      g.setColor(getForeground());
      for (double y = 0; y < h; y += unit)
      {
        int yy = (int)Math.round(y);
        g.drawLine(center - 2, yy, center + 2, yy);
      }
      for (int y = 0; y < h; y += dotsPerInch)
      {
        g.fillRect(center - 2, y - 2, 5, 5);
      }
    }
  }
  
  public Dimension getPreferredSize()
  {
    return new Dimension(
      orientation == HORIZONTAL ? length : 16,
      orientation == HORIZONTAL ? 16 : length);
  }

  public void refreshTracking(Component comp)
  {
    if (orientation == HORIZONTAL)
    {
      from = comp.getX();
      to = comp.getWidth();
    }
    else
    {
      from = comp.getY();
      to = comp.getHeight();
    }
    repaint();
  }
  
  public void componentMoved(ComponentEvent event)
  {
    refreshTracking(event.getComponent());
  }
  
  public void componentResized(ComponentEvent event)
  {
    refreshTracking(event.getComponent());
  }

  public void componentShown(ComponentEvent event) {}
  public void componentHidden(ComponentEvent event) {}
}

Listing 10

import java.io.*;
import java.awt.*;
import javax.swing.*;

public class GridForm extends JPanel
{
  protected GridRuler horz, vert;
  protected GridPanel gridPanel;

  public GridForm(int width, int height)
  {
    horz = new GridRuler(GridRuler.HORIZONTAL, width);
    vert = new GridRuler(GridRuler.VERTICAL, height);
    gridPanel = new GridPanel(horz, vert);
    gridPanel.setPreferredSize(new Dimension(width, height));
  
    JScrollPane scroll = new JScrollPane(gridPanel);
    scroll.setColumnHeaderView(horz);
    scroll.setRowHeaderView(vert);
    
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, scroll);
  }
  
  public void setDotsPerInch(int dotsPerInch)
  {
    vert.dotsPerInch = dotsPerInch;
    horz.dotsPerInch = dotsPerInch;
    gridPanel.dotsPerInch = dotsPerInch;
  }
  
  public void setInchDivisions(int divisions)
  {
    vert.divisions = divisions;
    horz.divisions = divisions;
    gridPanel.divisions = divisions;
  }
  
  public void setSnapToGridSize(int snapToGridSize)
  {
    gridPanel.snapToGridSize = snapToGridSize;
  }
  
  public void save(String filename)
    throws IOException
  {
    PrintWriter writer = new PrintWriter(
      new FileWriter(filename));
    gridPanel.save(writer);
    writer.close();
  }

  public void load(String filename)
    throws IOException
  {
    BufferedReader reader = new BufferedReader(new FileReader(filename));
    gridPanel.load(reader);
    reader.close();
  }
}

Listing 11

import java.awt.*;
import java.text.*;
import java.util.*;
import java.awt.print.*;

import java.io.*;
import java.awt.image.*;
import com.sun.image.codec.jpeg.*;

public class ReportPrinter implements Printable
{
  protected GridPanel header, record, footer;
  protected RecordSet recordSet;
  protected int donePage = Integer.MAX_VALUE;
  protected int currentPage = -1;
  protected Date now = new Date();
  protected Properties[] records;
  
  public static final DateFormat dateFormatter =
    DateFormat.getDateInstance(DateFormat.MEDIUM);
  public static final DateFormat timeFormatter =
    DateFormat.getTimeInstance(DateFormat.MEDIUM);

  public ReportPrinter(RecordSet recordSet,
    GridPanel header, GridPanel record, GridPanel footer)
  {
    this.recordSet = recordSet;
    this.header = header;
    this.record = record;
    this.footer = footer;
  }

  public void preview() throws IOException
  {
    BufferedImage image = new BufferedImage(
      (int)(8.5 * 72), 11 * 72, BufferedImage.TYPE_INT_RGB);
    Graphics g = image.getGraphics();
    print(g, new PageFormat(), 1);
    g.dispose();
    FileOutputStream file = new FileOutputStream("capture.jpg");
    JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(file);
    encoder.encode(image);
    file.close();
  }
  
  public int print(Graphics g, PageFormat pageFormat, int pageIndex)
  {
    int page = pageIndex + 1;
    if (page > donePage) return NO_SUCH_PAGE;
    
    int x = (int)pageFormat.getImageableX();
    int y = (int)pageFormat.getImageableY();
    int w = (int)pageFormat.getImageableWidth();
    int h = (int)pageFormat.getImageableHeight();
    int recordHeight = getHeight(record);
    int height = h - (header.getSize().height +
       header.getSize().height);
    int count = (int)(height / recordHeight);
    g.setColor(Color.black);
    
    // Print header
    printForm(g, header, null, page, x, y);

    // Printing may take several passes,
    // so collect the records only once.
    if (page > currentPage)
    {
      currentPage = page;
      records = new Properties[count];
      for (int i = 0; i < count; i++)
      {
        records[i] = recordSet.getNextRecord();
      }
    }
    // print the records
    for (int i = 0; i < count; i++)
    {
      if (records[i] != null)
      {
        printForm(g, record, records[i], page,
          x, y + header.getSize().height + recordHeight * i);
        //System.out.println(page + "=" + records[i]);
      }
      else donePage = page;
    }

    // Print footer
    printForm(g, footer, null, page,
      x, y + h - footer.getSize().height);
    
    return PAGE_EXISTS;
  }
 
  private void printForm(Graphics g, GridPanel panel,
    Properties rec, int page, int x, int y)
  {
    Component[] elements = panel.getComponents();
    for (int i = 0; i < elements.length; i++)
    {
      FormElement element = (FormElement)elements[i];
      String value = element.value;
      if (value.equals("DATE"))
        value = dateFormatter.format(now);
      if (value.equals("TIME"))
        value = timeFormatter.format(now);
      if (value.equals("PAGE"))
        value = "Page " + page;
      if (rec != null && !element.editable &&
        rec.containsKey(value))
          value = rec.getProperty(value);
      Rectangle bounds = element.getBounds();
      leftText(g, value, x + bounds.x, y + bounds.y,
        bounds.width, bounds.height);
    }
  }
  
  private int getHeight(GridPanel panel)
  {
    int height = 0;
    Component[] elements = panel.getComponents();
    for (int i = 0; i < elements.length; i++)
    {
      FormElement element = (FormElement)elements[i];
      Rectangle bounds = element.getBounds();
      int bottom = bounds.y + bounds.height;
      if (bottom > height) height = bottom;
    }
    return height + 10;
  }
  
  private void leftText(Graphics g, String text, int x, int y, int w, int h)
  {
    FontMetrics metrics = g.getFontMetrics();
    y += (h / 2) - (metrics.getHeight() / 2) + metrics.getAscent();
    g.drawString(text, x, y);
  }
  
  public boolean print()
  {
    try
    {
      currentPage = -1;
      recordSet.reset();
      donePage = Integer.MAX_VALUE;
      PrinterJob job = PrinterJob.getPrinterJob();
      job.setPrintable(this);
      job.print();
      return true;
    }
    catch (PrinterException e)
    {
      return false;
    }
  }	
}

Listing 12

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.print.*;
import javax.swing.*;
import javax.swing.border.*;

public class JReport extends JPanel
{
  protected GridForm header, record, footer;
  protected ReportPrinter printer;
  
  public JReport(PageFormat pageFormat, RecordSet recordSet)
  {
    JPanel page = new JPanel(new BorderLayout());
    page.add(BorderLayout.NORTH,
      header = createView("Header", pageFormat));
    page.add(BorderLayout.CENTER,
      record = createView("Record", pageFormat));
    page.add(BorderLayout.SOUTH,
      footer = createView("Footer", pageFormat));
    printer = new ReportPrinter(recordSet,
      header.gridPanel, record.gridPanel, footer.gridPanel);
    setLayout(new BorderLayout());
    add(BorderLayout.CENTER, page);
    add(BorderLayout.WEST, new DragPanel(recordSet));
  }
  
  public GridForm createView(String name, PageFormat pageFormat)
  {
    int dpi = Toolkit.getDefaultToolkit().getScreenResolution();
    int width = (int)(pageFormat.getImageableWidth());
    int height = (int)(pageFormat.getImageableHeight());
    if (name.equalsIgnoreCase("record")) height -= dpi;
    else height = dpi / 2;
    
    GridForm form = new GridForm(width, height);
    form.setBorder(new TitledBorder(name));
    form.setPreferredSize(new Dimension(width, 115));
    return form;
  }

  public void setDotsPerInch(int dotsPerInch)
  {
    header.setDotsPerInch(dotsPerInch);
    record.setDotsPerInch(dotsPerInch);
    footer.setDotsPerInch(dotsPerInch);
  }
  
  public void setInchDivisions(int divisions)
  {
    header.setInchDivisions(divisions);
    record.setInchDivisions(divisions);
    footer.setInchDivisions(divisions);
  }
  
  public void setSnapToGridSize(int snapToGrid)
  {
    header.setSnapToGridSize(snapToGrid);
    record.setSnapToGridSize(snapToGrid);
    footer.setSnapToGridSize(snapToGrid);
  }
  
  public void save(String prefix)
    throws IOException
  {
    header.save(prefix + ".header");
    record.save(prefix + ".record");
    footer.save(prefix + ".footer");
  }

  public void load(String prefix)
    throws IOException
  {
    header.load(prefix + ".header");
    record.load(prefix + ".record");
    footer.load(prefix + ".footer");
  }
}

Listing 13

import java.util.*;

public class TestRecordSet implements RecordSet
{
  protected String[] fields =
    {"FIRST_NAME", "LAST_NAME", "EMAIL_ADDRESS"}; 
  protected Properties[] records;
  protected int index = 0;
 
  public TestRecordSet()
  {
    records = new Properties[11];
    records[0] = makeRecord("Claude",
      "Duguay", "claude@atrieva.com");
    records[1] = makeRecord("Susan",
      "Schudie", "susans@atrieva.com");
    records[2] = makeRecord("Alan",
      "Bolton", "alanb@atrieva.com");
    records[3] = makeRecord("Dennis",
      "Doherty", "dennisd@atrieva.com");
    records[4] = makeRecord("Jar",
      "Lyons", "jarl@atrieva.com");
    records[5] = makeRecord("Keith",
      "Gregoire", "keithg@atrieva.com");
    records[6] = makeRecord("Al",
      "Sjogren", "allans@atrieva.com");
    records[7] = makeRecord("Job",
      "Brown", "jonb@atrieva.com");
    records[8] = makeRecord("Naoko",
      "Reynolds", "naokor@atrieva.com");
    records[9] = makeRecord("John",
      "Lam", "johnl@atrieva.com");
    records[10] = makeRecord("Make",
      "Bullock", "markb@atrieva.com");
  }		

  public Properties makeRecord(
    String first, String last, String address)
  {
    Properties record = new Properties();
    record.put("FIRST_NAME", first);
    record.put("LAST_NAME", last);
    record.put("EMAIL_ADDRESS", address);
    return record;
  }
  
  public void reset()
  {
    index = 0;
  }

  public String[] getFieldNames()
  {
    return fields;
  }
  
  public Properties getNextRecord()
  {
    if (index >= records.length)
      return null;
    return records[index++];
  }
}