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 class hierarchy used here is the same as that used in Graphic Basics. We make the figures accessible within a Swing JComboBox which is a drop-down list.

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 Helper

The GUI basis is the JFrame Form FigureFrame 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 the figure "cover up" the others and therefore must be 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
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
import javax.swing.SpinnerNumberModel;
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 DefaultComboBoxModel comboModel = new DefaultComboBoxModel();
 
  // dialogs
  private AddRectangleDialog addRectDialog = new AddRectangleDialog(frame, true);
 
  public Controller() {
    frame.setTitle("Figures");
    frame.setLocationRelativeTo(null);
    frame.setSize(800, 500);
 
    canvas.setFigures(figureList);
 
    frame.getFigureList().setModel(comboModel);
 
    // set the spinner model
    frame.getScaleSpinner().setModel(new SpinnerNumberModel(1.0, 0.1, 5.0, 0.05));
 
    frame.getFigureList().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Figure selected = (Figure) comboModel.getSelectedItem();
//        Figure selected = (Figure) frame.getFigureList().getSelectedItem();
        System.out.println("selected = " + 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);
        }
 
        comboModel.removeAllElements();
        for (Figure figure : figureList) {
          comboModel.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 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);
      }
    });
 
    // addRectDialog needs the remaining arguments to do its work in events
    Helpers.addEventHandlers(addRectDialog, figureList, comboModel, frame);
 
  }
 
  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 figure.PolyFigure;
import java.awt.Color;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
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(300, 100);
    figure.setStrokeWidth(12f);
    figure.setLoc(220, 140);
    figure.setLineColor(Color.magenta);
    figure.setFillColor(Color.yellow);
    figure.setTitle("aura glow");
    figures.add(figure);
 
    figure = new RectangleFigure(300, 200);
    figure.setStrokeWidth(3f);
    figure.setLoc(85, 100);
    figure.setTitle("austere");
    figures.add(figure);
 
    List<Point> points = new ArrayList<>();
    points.add(new Point(180, 20));
    points.add(new Point(20, 200));
    points.add(new Point(20, 75));
    figure = new PolyFigure(points);
    figure.setLoc(0, 0);
    figure.setFillColor(Color.DARK_GRAY);
    figure.setLineColor(Color.magenta);
    figure.setStrokeWidth(4.2f);
    figure.setTitle("zorro");
    figures.add(figure);
 
    figure = new RectangleFigure(250, 180);
    figure.setStrokeWidth(5.1f);
    figure.setLoc(40, 40);
    figure.setLineColor(Color.blue);
    figure.setFillColor(Color.red);
    figure.setTitle("red square");
    figures.add(figure);
 
    return figures;
  }
 
  static void addEventHandlers(
      AddRectangleDialog addRectDialog,
      List<Figure> figureList,
      DefaultComboBoxModel comboModel,
      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);
 
          comboModel.removeAllElements();
          for (Figure figure : figureList) {
            comboModel.addElement(figure);
          }
          canvas.repaint();
          addRectDialog.setVisible(false);
        }
        catch (Exception ex) {
          JOptionPane.showMessageDialog(addRectDialog, ex);
        }
      }
    });
 
    // 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", 
            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);
        }
      }
    });
 
  }
 
  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;
  }
}

The JComboBox figure list

After executing Load Samples, the figures are listed in a JComboBox which is simply a drop-down selection list. The JComboBox object is created in the Frame as:
private javax.swing.JComboBox<String> figureList;
and it is made available to the controller through the public member call:
frame.getFigureList()
If you look at the properties of the JComboBox in the Design mode, you'll see the line:
model     Item 1, Item 2, Item 3, Item 4
This indicates that the list's default contents consists of these 4 Strings. Our code will ignore this initial content and create a list of Figure objects.

The JComboBox contents are controlled through a separate model object which implements the interface ComboBoxModel. The most common implementation class is DefaultComboBoxModel. In our case, the variable comboModel is initialized as a data member:
DefaultComboBoxModel comboModel = new DefaultComboBoxModel();
This model is then associated with the JComboBox via this call in the Controller constructor:
frame.getFigureList().setModel(comboModel);
This statement clears the initial JComboBox contents. Maintaining the contents of the list is done from the comboModel with a variety of member functions. You can see it in action when the sample figures are loaded. They are first loaded into the figureList and then loaded from the figureList into the model via:
comboModel.removeAllElements();
for (Figure figure : figureList) {
  comboModel.addElement(figure);
}
Note that the title is what is displayed for each Figure in the list. This is because the combo list display, by default, uses the toString function which is precisely the Figure's title.

Figure selection event

The combo box supports two kinds of listeners: an ActionListener, and an ItemListener:
  frame.getFigureList().addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      // ...
    }
  });
 
  frame.getFigureList().addItemListener(new ItemListener() {
    @Override
    public void itemStateChanged(ItemEvent e) {
      // ...
    }
  });
For our purposes, the ActionListener is sufficient.

Getting the selected figure

We often need to work with the currently selected member of the combo list. You can retrieve this in one of two ways, through the list, or through the model. In our code, both of these work: Toward this end, the code which loads the sample figures has these (inessential) statements at the end:
Figure selected = (Figure) comboModel.getSelectedItem();
// or
Figure selected = (Figure) frame.getFigureList().getSelectedItem();

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 Helper

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 argument 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 combo list.

controller.Helpers
  static void addEventHandlers(
      AddRectDialog addRectDialog,
      List<Figure> figureList,
      DefaultListModel listModel,
      FigureFrame frame)
  {
    Canvas canvas = frame.getCanvas();
 
    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(addRectDialog, ex);
        }
      }
    });
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