Figures

Install the App

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

This 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 class hierarchy used here is the same as that used in Graphic Basics.

JList

We make the figures accessible within a Swing JList. You have to adjust to the idea that there are two representations of the same list. This one, defined in the controller:
List<Figure> figureList
and this one, defined in the Frame:
frame.getFigureList()
The JList has been used in previous examples and is discussed here:
File I/O - Baseball Stats
In our case, we simplify it by only allowing single selection via the statement in the controller:
frame.getFigureList().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
The contents of the JList are managed by a separate object:
private final DefaultListModel listModel = new DefaultListModel();
This list is associated to the model by:
frame.getFigureList().setModel(listModel);
We will need to respond to a selection made in the JList, and this is done via a ListSelectionListener defined in the controller:
frame.getFigureList().addListSelectionListener(new ListSelectionListener() {
  @Override
  public void valueChanged(ListSelectionEvent evt) {
    if (evt.getValueIsAdjusting()) {
      return;
    }
    Figure selected = (Figure) frame.getFigureList().getSelectedValue();
    // ...
});
This listener will respond to two events when a new selection is made: the deselection of the current item and the selection of the new item. We have done step of filtering out the initial deselection event using the getValueIsAdjusting method.

As before we want to access the selected element, most commonly done by:
Figure selected = (Figure) frame.getFigureList().getSelectedValue();
Be careful in you code! It's easier to get selected == null when it's used in the list handler than you may first think.

The other thing we need to do in this application is to programmatically make a list selection. To do so for the first element we can use:
frame.getFigureList().setSelectedIndex(0);
We can also use
frame.getFigureList().setSelectedValue(some_figure,true);

The Canvas class

The novelty of this version is the ability to display 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.getXloc();
      double y = figure.getYloc();
      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.

Frame, Controller and Helpers

The GUI basis is the JFrame Form FigureFrame which presents the usual textarea canvas plus several control features. The Frame supports a JList 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 the figure "covers up" the others and therefore must be actually painted last. Consequently, the order of painting is opposite the order of listing.

These menu control features are these: The public interface is this:

views.FigureFrame
import javax.swing.JButton;
import javax.swing.JList;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JSpinner;
 
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;
  }
  public JMenuItem getAddCircleDialog() {
    return this.addCircleDialog;
  }
  public JMenuItem getSaveToFile() {
    return saveToFile;
  }
  public JMenuItem getLoadFromFile() {
    return loadFromFile;
  }
  public JButton getMoveToTopButton() {
    return moveToTop;
  }
  public JButton getRemoveButton() {
    return remove;
  }
  public JMenu getFileMenu() {
    return fileMenu;
  }
  ...
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.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JFileChooser;
import javax.swing.ListSelectionModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import views.AddRectangleDialog;
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 AddRectangleDialog addRectDialog = new AddRectangleDialog(frame, true);
 
  // to support Figure click/drag
  private int lastX, lastY;
  private Figure selectedFigure = null;
 
  public Controller() {
    frame.setTitle("Figures");
    frame.setLocationRelativeTo(null);
    frame.setSize(800, 500);
 
    canvas.setFigures(figureList);
 
    frame.getFigureList().setModel(listModel);
 
    // set the spinner model
    frame.getScaleSpinner().setModel(new SpinnerNumberModel(1.0, 0.1, 5.0, 0.05));
 
    // keep figureList from taking up too much horizontal space
    frame.getFigureList().getParent().setPreferredSize(new Dimension(0, 0));
 
    // allow figureList to only to only select one figure
    frame.getFigureList().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
 
    frame.getFigureList().addListSelectionListener(new ListSelectionListener() {
      @Override
      public void valueChanged(ListSelectionEvent evt) {
        if (evt.getValueIsAdjusting()) {
          return;
        }
        Figure selected = (Figure) frame.getFigureList().getSelectedValue();
        System.out.println("list selection: " + selected);
      }
    });
 
    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);
        }
 
        frame.getFigureList().setSelectedIndex(0);
 
        canvas.repaint();
      }
    });
 
    frame.getScaleSpinner().addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent evt) {
        double d = (Double) frame.getScaleSpinner().getValue();
        System.out.println("scale spinner: " + d);
      }
    });
 
    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);
      }
    });
 
    // addRectDialog needs the remaining arguments to do its work in events
    Helpers.addEventHandlers(addRectDialog, figureList, listModel, frame);
  }
 
  private static JFileChooser getFileChooser() {
    JFileChooser chooser = new JFileChooser();
 
    // specify where chooser should open up
    chooser.setCurrentDirectory(new File(System.getProperty("user.dir")));
 
    // define a set of "Editable Files" files by extension
    chooser.addChoosableFileFilter(
        new FileNameExtensionFilter("Figure Files (.fig)", "fig")
    );
 
    // do not accept "All Files"
    chooser.setAcceptAllFileFilterUsed(false);
 
    return chooser;
  }
 
  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.AddRectangleDialog;
import views.Canvas;
import views.FigureFrame;
 
class Helpers {
  static List<Figure> getSampleFigureList() {
    List<Figure> figures = new ArrayList<>();
    Figure figure;
 
    figure = new RectangleFigure(250, 180);
    figure.setLoc(40, 40);
    figure.setLineColor(Color.blue);
    figure.setFillColor(Color.red);
    figure.setStrokeWidth(4.2f);
    figure.setTitle("red square");
    figures.add(0, figure);
 
    figure = new RectangleFigure(300, 300);
    figure.setLoc(100, 100);
    figure.setStrokeWidth(3f);
    figure.setTitle("austere");
    figures.add(0, figure);
 
    figure = new RectangleFigure(300, 100);
    figure.setLoc(220, 140);
    figure.setLineColor(Color.magenta);
    figure.setFillColor(Color.yellow);
    figure.setStrokeWidth(12f);
    figure.setTitle("aura glow");
    figures.add(0, figure);
 
    return figures;
  }
 
  static void addEventHandlers(
      AddRectangleDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame
  ) {
    Canvas canvas = frame.getCanvas();
 
    // 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);
          }
          System.out.println(listModel);
          frame.getFigureList().setSelectedIndex(0);
          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(addRectDialog, "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(addRectDialog, "Choose color", Color.white);
        if (color != null) {
          addRectDialog.getFillColorField().setBackground(color);
        }
      }
    });
  }
 
  static Figure makeFigureFromDialog(AddRectangleDialog 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();
    if (title.isEmpty()) {
      title = "untitled";
    }
 
    Color lineColor = dialog.getLineColorField().getBackground();
    Color fillColor = dialog.getFillColorField().getBackground();
 
    Figure fig = new RectangleFigure(width, height);
    fig.setStrokeWidth(strokeWidth);
 
    fig.setTitle(title);
    fig.setLineColor(lineColor);
    fig.setFillColor(fillColor);
 
    return fig;
  }  
}

JDialog: AddRectangleDialog

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 difference is that it must be "associated with" a JFrame through the constructor:
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 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 something like this:
class Controller {
  private final MyFrame frame = new MyFrame();
  private final MyDialog dialog = new MyDialog(frame,true);
In our application, the Figures ⇾ Add Rectangle menu item invokes the views.AddRectangleDialog. This was created in NetBeans using
New (⇾ Other ⇾ Swing GUI Forms) ⇾ JDialog Form
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 AddRectangleDialog 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 AddRectangleDialog rectDialog = new AddRectangleDialog(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 Helpers

We could put the dialog event handlers directly into Controller, but it makes the Controller code simpler if we manage them elsewhere, in a separate Helpers class. To do so we make the following call:
public class Controller {
  ...
  public Controller() {
    ...
    Helpers.addEventHandlers(addRectDialog, figureList, listModel, frame);
The idea is that addRectDialog needs the other arguments for the behavior of 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(
      AddRectangleDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame) 
  {
    ...
    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, cancel, setLineColor, setFillColor
The lineColor and fillColor textfields are used only to present a chosen color as their background color.

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 getChooseLineColor() {
    return chooseLineColor;
  }
 
  public JButton getChooseFillColor() {
    return chooseFillColor;
  }
 
  public JButton getAddButton() {
    return add;
  }
 
  public JButton getCancelButton() {
    return cancel;
  }
  ...

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("" + 1F);
        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 JList.

controller.Helpers
  static void addEventHandlers(
      AddRectangleDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame
  ) {
    Canvas canvas = frame.getCanvas();
 
    // 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);
          }
          System.out.println(listModel);
          frame.getFigureList().setSelectedIndex(0);
          canvas.repaint();
          addRectDialog.setVisible(false);
        }
        catch (Exception ex) {
          JOptionPane.showMessageDialog(frame, ex.toString());
        }
      }
    });
Much of the work is done in the makeFigureFromDialog function from the Helpers class:

controller.Helpers
  static Figure makeFigureFromDialog(AddRectangleDialog 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();
    if (title.isEmpty()) {
      title = "untitled";
    }
 
    Color lineColor = dialog.getLineColorField().getBackground();
    Color fillColor = dialog.getFillColorField().getBackground();
 
    Figure fig = new RectangleFigure(width, height);
 
    fig.setStrokeWidth(strokeWidth);
    fig.setTitle(title);
    fig.setLineColor(lineColor);
    fig.setFillColor(fillColor);
 
    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)
  {
    ...
    // the ChooseLineColor button
    addRectDialog.getChooseLineColor().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Color color = JColorChooser.showDialog(addRectDialog, "Choose color", 
            addRectDialog.getLineColorField().getBackground());
 
        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(addRectDialog, "Choose color", 
            addRectDialog.getFillColorField().getBackground());
 
        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