Figures

Install the App

Download the source archive
Figures.zip
Extract and install it into NetBeans as a Java Project with Existing Sources.

Theis sample application extends the ideas developed in the Graphic Basics document. The figure drawing is taken a step further by working with multiple figures instead of a single figure. The figure hierarchy used here is the same as that used in Graphic Basics. We make the figures accessible within a Swing JList. The JList is used and discussed here:
File I/O - Baseball Stats

The Canvas class

The novelty of this version is the ability to print multiple Figure objects stored in an a List.

views.Cavas
package views;
 
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JTextArea;
import java.awt.RenderingHints;
 
import figure.Figure;
import java.awt.geom.AffineTransform;
import java.util.List;
 
public class Canvas extends JTextArea {
 
  private List<Figure> figures = null;
 
  public void setFigures(List<Figure> figures) {
    this.figures = figures;
  }
 
  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
 
    if (figures == null) {
      return;
    }
 
    Graphics2D g2 = (Graphics2D) g;
 
    g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON
    );
 
    AffineTransform t = g2.getTransform(); // save the transform settings
 
    for (int i = figures.size() - 1; i >= 0; --i) {
      Figure figure = figures.get(i);
      double x = figure.getLocation().x;
      double y = figure.getLocation().y;
      double scale = figure.getScale();
      g2.translate(x, y);
      g2.scale(scale, scale);
      figure.draw(g2);
      g2.setTransform(t); // restore each after drawing
    }
  }
}
select
Note that we print the list in reverse so that the first figure appears on top.

We approach drawing at a position in the same way as before: translate/scale the coordinate system, draw a shape based at (0,0) and then undo the effect. If you comment out the g2.setTransform(t) line in the loop and re-run you'll see the mistake in that the effects are accumulated.

Frame, Controller and Helper

The GUI basis is the JFrame Form TheFrame which presents the usual textarea canvas plus several control features. The Frame supports a JComboBox which is meant to display the figure list the way it is seen from top to bottom. Being on top in the display list means that you "cover up" all others and therefore are painted last. Consequently, the orders of the two listings are reversed.

These control features are the ones implemented: The public interface is this:

views.FigureFrame
package views;
 
import javax.swing.JComboBox;
import javax.swing.JMenuItem;
 
public class FigureFrame extends javax.swing.JFrame {
  public Canvas getCanvas() {
    return (Canvas) canvas;
  }
  public JList getFigureList() {
    return figureList;
  }
  public JSpinner getScaleSpinner() {
    return scale;
  }
  public JMenuItem getLoadSamples() {
    return loadSamples;
  }
  public JMenuItem getAddRectDialog() {
    return addRectDialog;
  }
  ...
The controller class contains a number of new GUI features, in particular the JDialog.

controller.Controller
package controller;
 
import figure.Figure;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SpinnerNumberModel;
import views.AddRectDialog;
import views.Canvas;
import views.FigureFrame;
 
public class Controller {
 
  private final FigureFrame frame = new FigureFrame();
  private final Canvas canvas = frame.getCanvas();
 
  // figures which appear in canvas and in list
  private final List<Figure> figureList = new ArrayList<>();
 
  // model for Figure selection list
  private final DefaultListModel listModel = new DefaultListModel();
 
  // dialogs
  private AddRectDialog addRectDialog = new AddRectDialog(frame, true);
 
  public Controller() {
    frame.setTitle("Figures");
    frame.setLocationRelativeTo(null);
    frame.setSize(800, 500);
 
    canvas.setFigures(figureList);
 
    frame.getFigureList().setModel(listModel);
 
    // keep figureList from taking too much vertical space
    frame.getFigureList().getParent().setPreferredSize(new Dimension(0, 0));
 
    // permit only single element selection
    frame.getFigureList().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
 
    // set the spinner model
    frame.getScaleSpinner().setModel(new SpinnerNumberModel(1.0, 0.1, 5.0, 0.05));
 
    frame.getLoadSamples().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent evt) {
        List<Figure> samples = Helpers.getSampleFigureList();
        figureList.clear();
 
        for (Figure sample : samples) {
          figureList.add(sample);
        }
 
        listModel.removeAllElements();
        for (Figure figure : figureList) {
          listModel.addElement(figure);
        }
        canvas.repaint();
      }
    });
 
    // Invoke the addRect dialog
    frame.getAddRectDialog().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent evt) {
        addRectDialog.setLocationRelativeTo(null);
        addRectDialog.setTitle("Add a RectFigure");
 
        addRectDialog.getHeightField().setText("" + 100);
        addRectDialog.getWidthField().setText("" + 200);
        addRectDialog.getStrokeWidthField().setText("" + 1);
        addRectDialog.getTitleField().setText("");
 
        addRectDialog.getLineColorField().setEditable(false);
        addRectDialog.getFillColorField().setEditable(false);
 
        addRectDialog.getLineColorField().setBackground(Color.black);
        addRectDialog.getFillColorField().setBackground(Color.white);
 
        addRectDialog.setVisible(true);
      }
    });
 
    // addRectDialog needs the remaining arguments to do its work in events
    Helpers.addEventHandlers(addRectDialog, figureList, listModel, frame, canvas);
 
  }
 
  public static void main(String[] args) {
    Controller app = new Controller();
    app.frame.setVisible(true);
  }
}
The main controller relies on a non-public class, controller.Helpers to hold support functions so as to reduce the size of the controller class code. Inside, we introduce another utility class, JColorChooser, which will be discussed later.

Note that the Helpers classes uses the default access ("none") which makes it accessible to Controller, but inaccessible to classes outside to the package.

controller.Helpers
package controller;
 
import figure.RectangleFigure;
import figure.Figure;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JColorChooser;
import javax.swing.JOptionPane;
import views.AddRectDialog;
import views.Canvas;
import views.FigureFrame;
 
class Helpers {
 
  static List<Figure> getSampleFigureList() {
    List<Figure> figures = new ArrayList<>();
    Figure figure;
 
    figure = new RectangleFigure(300, 100);
    figure.setLocation(220, 140);
    figure.setLineColor(Color.magenta);
    figure.setFillColor(Color.yellow);
    figure.setStrokeWidth(12f);
    figure.setTitle("aura glow");
    figures.add(figure);
 
    figure = new RectangleFigure(300, 300);
    figure.setLocation(100, 100);
    figure.setStrokeWidth(3f);
    figure.setTitle("austere");
    figures.add(figure);
 
    figure = new RectangleFigure(250, 180);
    figure.setLocation(40, 40);
    figure.setLineColor(Color.blue);
    figure.setFillColor(Color.red);
    figure.setStrokeWidth(4.2f);
    figure.setTitle("red square");
    figures.add(figure);
 
    return figures;
  }
 
  static void addEventHandlers(
          AddRectDialog addRectDialog,
          List<Figure> figureList,
          DefaultListModel listModel,
          FigureFrame frame,
          Canvas canvas
  ) {
 
    // the Add Button
    addRectDialog.getAddButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        try {
          Figure fig = Helpers.makeFigureFromDialog(addRectDialog);
          figureList.add(0, fig);
 
          listModel.removeAllElements();
          for (Figure figure : figureList) {
            listModel.addElement(figure);
          }
          canvas.repaint();
          addRectDialog.setVisible(false);
        }
        catch (Exception ex) {
          JOptionPane.showMessageDialog(frame, ex.toString());
        }
      }
    });
 
    // the Cancel Button
    addRectDialog.getCancelButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        addRectDialog.setVisible(false);
      }
    });
 
    // the ChooseLineColor button
    addRectDialog.getChooseLineColor().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Color color
                = JColorChooser.showDialog(frame, "Choose color", Color.white);
        if (color != null) {
          addRectDialog.getLineColorField().setBackground(color);
        }
      }
    });
 
    // the Choose FillColor button
    addRectDialog.getChooseFillColor().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Color color
                = JColorChooser.showDialog(frame, "Choose color", Color.white);
        if (color != null) {
          addRectDialog.getFillColorField().setBackground(color);
        }
      }
    });
 
  }
 
  static Figure makeFigureFromDialog(AddRectDialog dialog) throws Exception {
    String widthText = dialog.getWidthField().getText().trim();
    String heightText = dialog.getHeightField().getText().trim();
    String strokeWidthText
            = dialog.getStrokeWidthField().getText().trim();
 
    double width = Double.parseDouble(widthText);
    double height = Double.parseDouble(heightText);
    float strokeWidth = Float.parseFloat(strokeWidthText);
 
    if (width <= 0 || height <= 0 || strokeWidth <= 0) {
      throw new Exception("fields must have positive values");
    }
 
    String title = dialog.getTitleField().getText().trim();
 
    Color lineColor = dialog.getLineColorField().getBackground();
    Color fillColor = dialog.getFillColorField().getBackground();
 
    Figure fig = new RectangleFigure(width, height);
    fig.setStrokeWidth(strokeWidth);
    if (!lineColor.equals(Color.WHITE)) {
      fig.setLineColor(lineColor);
    }
    if (!fillColor.equals(Color.WHITE)) {
      fig.setFillColor(fillColor);
    }
    if (!title.isEmpty()) {
      fig.setTitle(title);
    }
 
    return fig;
  }
}

The JList figure list

The first thing to notice is how the figures are listed in the JList. The default presentation is, as you would expect, based on the toString() method.

JDialog: AddRectDialog

The JDialog is a common Swing class used to generate additional windows. Dialogs pop up to perform a function and then go away when completed; in that way the Dialog functioning does not clutter up or interfere with the Frame's functioning.

A JDialog object has all the capabilities of the JFrame; the biggest differences is that it must be "associated with" a JFrame and the association expresses a "modality". In particular, the constructor for a JDialog class must initialize the superclass with the JFrame parent object. A constructor for the dialog minimally requires at least one parameter in order to achieve this effect with code like this:
public class MyDialog extends JDialog {
  public MyDialog(java.awt.Frame parent) { 
    super(parent);
    //...
  }
}
The other key feature of a JDialog is its boolean modal property. This property expresses whether the dialog's presence should "block" actions in the Frame or not. A modality value of true means to block actions, a modality of false (the default) means to let the actions in the Frame continue. A common way to set the modality is calling the superclass constructor with a second parameter like this:
public class MyDialog extends JDialog {
  public MyDialog(java.awt.Frame parent, boolean modal) { 
    super(parent, modal);
    //...
  }
}
For most situations we want the modality to be true to guarantee that the dialog's actions do not interfere with and take effect before the Frame continues. Therefore, the dialog creation is most likely this: (expressing the most common situation):
class Controller {
  private final TheFrame frame = new TheFrame();
  private final MyDialog dialog = new MyDialog(frame,true);
In our application, the Figures ⇾ Add RectFigure menu item invokes the views.AddRectDialog. This was created in NetBeans using
New ⇾ JDialog Form
Like the JFrame Form, this may not be immediately available in the New list, but it can be found Other ⇾ Swing GUI Forms. The manner of creating it is exactly the same as the JFrame Form; all the same features are available. What is created is
public class AddRectDialog extends javax.swing.JDialog
The corresponding code within the Driver program is

controller.Controller
public class Controller {
  private final TheFrame frame = new TheFrame();
  ...
  private final AddRectDialog rectDialog = new AddRectDialog(frame, true);
  ...
As we mentioned above, the true usage implies that the dialog blocks the application until completed.

Pop up/down

The dialog is "popped" up and down using the setVisible call with values true and false, respectively. A common way to invoke a dialog is through a menu activation in the Frame and then close it with a button activation in the dialog. The handler code would be something like this:
frame.get_MENUITEM_().addActionListener(new ActionListener(){
  @Override
  public void actionPerformed(ActionEvent evt) {
    ...
    dialog.setVisible(true);
  }
});

dialog.get_BUTTON_().addActionListener(new ActionListener(){
  @Override
  public void actionPerformed(ActionEvent evt) {
    ...
    dialog.setVisible(false);
  }
});

Managing the dialog event handlers in Helper

We could put the dialog event handlers directly into Controller, but it makes the Controller code simpler if we manage them elsewhere, in the Helpers class. To do so we make the following call:
public class Controller {
  ...
  public Controller() {
    ...
    Helpers.addEventHandlers(addRectDialog, figureList, listModel, frame, canvas);
The idea is that addRectDialog needs the other parameters for the event handlers. In the case of opening and closing the dialog we see this:

controller.Controller
public class Controller {
  ...
  public Controller() {
    ...
    frame.getAddRectFigure().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        ...
        addRectDialog.setVisible(true);
      }
    });

controller.Helpers
class Helpers {
  ...
  static void addEventHandlers(
      AddRectDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame,
      Canvas canvas
  ) {
    ...
    addRectDialog.getAddButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        try {
          ...
          addRectDialog.setVisible(false);
        }
        catch (Exception ex) {
          JOptionPane.showMessageDialog(frame, ex.getMessage());
        }
      }
    });
    addRectDialog.getCancelButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        addRectDialog.setVisible(false);
      }
    });
    ...
  }

AddRectDialog functionality

There are several components in the dialog. These are the textfields:
width, height, lineWidth
lineColor, fillColor
The buttons are:
add, setLineColor, setFillColor
The lineColor and fillColor textfields are used only to present a chosen color as their background color. The color "white" is interpreted as "no chosen color," which is a bit restrictive, but simplifies the figure creation.

The dialog interface functions

Here are the associated getters used:

views.AddRectDialog
public class AddRectDialog extends javax.swing.JDialog {
 
  public JTextField getWidthField() {
    return width;
  }
 
  public JTextField getHeightField() {
    return height;
  }
 
  public JTextField getStrokeWidthField() {
    return strokeWidth;
  }
 
  public JTextField getLineColorField() {
    return lineColor;
  }
 
  public JTextField getFillColorField() {
    return fillColor;
  }
 
  public JTextField getTitleField() {
    return title;
  }
 
  public JButton getAddButton() {
    return add;
  }
 
  public JButton getChooseLineColor() {
    return chooseLineColor;
  }
 
  public JButton getChooseFillColor() {
    return chooseFillColor;
  }
  ...

Invoke and add a Figure

We discussed how the dialog is popped up and down. Prior to being popped up, the fields are initialized. In this case we want to give default values which can be used by themselves, or modified.

controller.Controller
    frame.getAddRectDialog().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent evt) {
        addRectDialog.setLocationRelativeTo(null);
        addRectDialog.setTitle("Add a RectangleFigure");
 
        addRectDialog.getHeightField().setText("" + 100);
        addRectDialog.getWidthField().setText("" + 200);
        addRectDialog.getStrokeWidthField().setText("" + 1);
        addRectDialog.getTitleField().setText("");
 
        addRectDialog.getLineColorField().setEditable(false);
        addRectDialog.getFillColorField().setEditable(false);
 
        addRectDialog.getLineColorField().setBackground(Color.black);
        addRectDialog.getFillColorField().setBackground(Color.white);
 
        addRectDialog.setVisible(true);
      }
    });
When the dialog's Add button is pressed after the field changes have been made, this is the code called to create the figure, add it to the figureList and add it to the combo list.

controller.Helpers
  static void addEventHandlers(
      AddRectDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame,
      Canvas canvas
  ) {
    ...
    addRectDialog.getAddButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        try {
          Figure fig = Helpers.makeFigureFromDialog(addRectDialog);
          figureList.add(0, fig);
 
          listModel.removeAllElements();
          for (Figure figure : figureList) {
            listModel.addElement(figure);
          }
          canvas.repaint();
          addRectDialog.setVisible(false);
        }
        catch (Exception ex) {
          JOptionPane.showMessageDialog(frame, ex.toString());
        }
      }
    });
Much of the work is done in makeFigureFromDialog function from the Helpers class:

controller.Helpers
  static Figure makeFigureFromDialog(AddRectDialog dialog) throws Exception {
    String widthText = dialog.getWidthField().getText().trim();
    String heightText = dialog.getHeightField().getText().trim();
    String strokeWidthText 
            = dialog.getStrokeWidthField().getText().trim();
 
    double width = Double.parseDouble(widthText);
    double height = Double.parseDouble(heightText);
    float strokeWidth = Float.parseFloat(strokeWidthText);
 
    if (width <= 0 || height <= 0 || strokeWidth <= 0) {
      throw new Exception("fields must have positive values");
    }
 
    String title = dialog.getTitleField().getText().trim();
 
    Color lineColor = dialog.getLineColorField().getBackground();
    Color fillColor = dialog.getFillColorField().getBackground();
 
    Figure fig = new RectangleFigure(width, height);
    fig.setStrokeWidth(strokeWidth);
    if (!lineColor.equals(Color.WHITE)) {
      fig.setLineColor(lineColor);
    }
    if (!fillColor.equals(Color.WHITE)) {
      fig.setFillColor(fillColor);
    }
    if (!title.isEmpty()) {
      fig.setTitle(title);
    }
 
    return fig;
  }
Of course, here we have to deal with the field validation exceptions, but we will just do the minimal action. Back in the event handler, a JOptionPane popup simply reports a (field validation) error.

Managing the colors

The button action handlers used to set the two relevant colors are these:

controller.Helpers
  static void addEventHandlers(
      AddRectDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame,
      Canvas canvas
  ) {
    ...
    addRectDialog.getChooseLineColor().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Color color = JColorChooser.showDialog(frame, "Choose color", Color.white);
        if (color != null) {
          addRectDialog.getLineColorField().setBackground(color);
        }
      }
    });
 
    addRectDialog.getChooseFillColor().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Color color = JColorChooser.showDialog(frame, "Choose color", Color.white);
        if (color != null) {
          addRectDialog.getFillColorField().setBackground(color);
        }
      }
    });
The JColorChooser is very easy to use. A call to the showDialog function invokes the modal dialog. If it is OK'd, a color is sent back from the function, otherwise null is sent back.


© Robert M. Kline