Graphic Basics

Sample Application

The examples in this document all part of the Java Application GraphicBasics. Download the source archive
GraphicBasics.zip
Extract and install it as a Java Application with Existing Sources. See the Using NetBeans document for details.

The progression of examples is as follows:
  1. Driver1, Frame1: illustrate basic graphical printing onto a JTextArea
  2. Driver2, Canvas2, Frame2: adds a view class, Canvas2, an extension of JTextArea to support repaint.
  3. Driver3, figure.*, Canvas3, Frame3: introduces the figure classes and makes Canvas3 to allow of figures
  4. Driver4, figure.*, Canvas3, Frame3: this adds figure movement by mouse-dragging.
  5. Driver5, figure.*, Canvas5, Frame5: this add figure scaling

Description

Every visual class in Java Swing supports being "drawn on" in the sense that the top of the Java visual hierarchy, java.awt.Component, supports a graphics object from the class Graphics which can be obtained from this function:
Graphics getGraphics()
Naïvely, we instantiate any Java Component, obtain an instance of the Graphics object and start "painting" with it like this:
JLabel canvas = new JLabel();
Graphics g = canvas.getGraphics();
g.drawRect(10, 10, 200, 200);
We'll soon see that this approach is indeed naïve, but we can take the idea a step further.

2D Graphics

The Graphics package is, of course, 2-dimensional, but Java supports an enhanced graphics class Graphics2D which is an extension of Graphics, i.e.,:
Graphics2D extends Graphics
Graphics2D provides significant enhancements over the older Graphics class. Simple casting makes the graphics object usable as the enhanced version:
Graphics2D g2 = (Graphics2D) g;
With Graphics2D we can separate the shape we're drawing from the actual drawing with code like this:
Graphics g = canvas.getGraphics();
Graphics2D g2 = (Graphics2D) g;
Shape myrect = new Rectangle2D.Double(10, 10, 200, 100);
g2.draw(myrect);
We've introduced the java.awt.Shape interface and the java.awt.geom.Rectangle2D.Double class. These two by themselves are part of an interesting hierarchy:
The classes RectangularShape and Rectangle2D are abstract. Observe that the class Rectangle2D.Double is both an inner and derived class of Rectangle2D. How is this possible? The answer is that there's nothing stopping it. The actual Java code looks like this:
public abstract class Rectangle2D extends RectangularShape {
  ...
  public static class Double extends Rectangle2D implements Serializable {
    ...
  }
  ...
}

Version 1

Start by creating a new Java project GraphicBasics. Delete the auto-generated main class.

Create TheFrame1

We're going to follow a procedure similar to that which we've used all along, creating a frame with a JTextArea. We want this textarea to never be editable, since we're going to draw onto it. The first steps are in Design mode:
  1. Create a new JFrame Form in the package views with class name TheFrame1.
  2. Right-click on the frame and select Set Layout ⇾ Border Layout.
  3. From Swing Controls in the Palette, drag a TextArea into the middle of the frame. It should expand and take up the entire frame.
  4. From Swing Containers in the Palette, drag a Panel into the frame at the top of the frame. The layout should "open up" and allow you to drop the Panel into its own area at the top.
  5. Drag and drop a Button onto the Panel. Change the variable name to draw and the text to Draw.
  6. Right-click on the Panel and change the layout to FlowLayout.
  7. Right-click on the TextArea, changing its variable name to canvas.
  8. Right-click on the TextArea, select Properties. Uncheck the editable checkbox.
Then Go into source mode. Add these imports and interface functions as indicated:

views.TheFrame1
package views;
 
import javax.swing.JButton;
import javax.swing.JComponent;
 
public class TheFrame1 extends javax.swing.JFrame {
  public JComponent getCanvas() {
    return canvas;
  }
 
  public JButton getDrawButton() {
    return draw;
  }
  ...
}

Driver1

Create a new Java main class graphicbasics.Driver1 in the graphicbasics package with this content:

graphicbasics.Driver1
package graphicbasics;
 
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
 
import views.TheFrame1;
 
public class Driver1 {
 
  private final TheFrame1 frame = new TheFrame1();
  private final JComponent canvas = frame.getCanvas();
 
  public Driver1() {
    frame.setTitle( getClass().getSimpleName() );
 
    frame.setSize(700,600);
 
    frame.getDrawButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        Graphics g = canvas.getGraphics();
        Graphics2D g2 = (Graphics2D) g;
 
        Shape s = new Rectangle2D.Double(10, 10, 200, 100);
 
        g2.setStroke(new BasicStroke(4.2f));
 
        g2.setColor(Color.red);  // set color
        g2.fill(s);              // and fill the shape
 
        g2.setColor(Color.blue); // set color
        g2.draw(s);              // draw the shape (the outline)
      }
    });
  }
 
  public static void main(String[] args) {
    Driver1 app = new Driver1();
    app.frame.setVisible(true);
  }
}
Try it out. When you click the button the shape is generated! Now the bad news: resize the application and the shape disappears. Our approach is naïve. All GUI applications must "redraw themselves" based on a variety of external events such as window resizing. Java recognizes the redraw operation used as repaint().

Version 2: Repainting

With the flaw of the first version in mind, we are now going to set up to automatically deal with the repaint operation. What we need to know is that the following function, defined in JComponent, is the one which is automatically called by repaint:
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
 
    /* do the draw operations on g2 */
  }

The Canvas2 class

We cannot redefine this function in our JTextArea directly. It must be overridden in an extension of JTextArea. Start by creating the Java Class (not Frame Form) Canvas2 (named to correspond to version #2) in the views package setting this to the content: the graphicbasics package with this content:

views.Canvas2
package views;
 
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.RenderingHints;
import javax.swing.JTextArea;
 
public class Canvas2 extends JTextArea {
 
  private Shape shape = null;
 
  public void setShape(Shape shape) {
    this.shape = shape;
  }
 
  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
 
    if (shape == null) {
      return;
    }
 
    Graphics2D g2 = (Graphics2D) g;
 
    g2.setRenderingHint(
      RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON
    );
 
    g2.draw(shape);
  }
}
Keep in mind that classes in the views package are supposed to be a reusable components. In this case our Canvas2 class can render an arbitrary shape, and want to avoid pinning it down to rendering only the shape used in the Driver1 application.

A new graphics feature has been added:
g2.setRenderingHint(
  RenderingHints.KEY_ANTIALIASING,
  RenderingHints.VALUE_ANTIALIAS_ON
);
We will explore the significance of this when we test the application.

Create TheFrame2

Start by duplicating TheFrame1. Make a copy of it and paste the copy back into the views package, changing the name to TheFrame2. Make these alterations on TheFrame2:
  1. Edit TheFrame2 in Design mode. Right-click on the textarea and select Customize Code.
  2. In the Code Customizer dialog, select custom creation from the top selection list corresponding to
    canvas = new javax.swing.JTextArea();
    
    Change this code to:
    canvas = new Canvas2();
    
    Click OK.
  3. Edit Frame2 in Source mode. Change the getCanvas() function to this:
      public Canvas2 getCanvas() {
        return (Canvas2) canvas;
      }
In the Code Customizer dialog, at the bottom, you see the type declaration:
private javax.swing.JTextArea canvas;
It is not possible to change this code which declares canvas as a JTextArea. It is, however, possible to add Canvas2 or any custom component class to NetBeans Designer Palette, but the modified system is not very portable via New Java Project from Existing Source. Instead we rely on NetBeans Designer's capabilities of managing JTextArea objects and rely on casting such an object to an extended class.

Driver2


Driver2
package graphicbasics;
 
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Line2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
 
import views.TheFrame2;
import views.Canvas2;
 
public class Driver2 {
 
  private final TheFrame2 frame = new TheFrame2();
  private final Canvas2 canvas = frame.getCanvas();
 
  public Driver2() {
    frame.setTitle( getClass().getSimpleName() );
    frame.setSize(700,600);
    frame.setLocationRelativeTo(null);
 
    frame.getDrawButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {        
        Shape s = new Rectangle2D.Double(10, 10, 200, 100);
        //Shape s = new Line2D.Double(10, 10, 155, 90);
        //Shape s = new Ellipse2D.Double(10, 10, 200, 100);
        //Shape s = new Ellipse2D.Double(10, 10, 200, 200);
        //Shape s = new RoundRectangle2D.Double(10, 10, 200, 100, 20, 20);
 
        canvas.setShape(s);
        canvas.repaint();
      }
    });
  }
 
  public static void main(String[] args) {
    Driver2 app = new Driver2();
    app.frame.setVisible(true);
  }
}
Run Driver2. We've lost the pretty colors, but now the simple rectangle is maintained when we resize the application. Take notice of how the drawing display is put into effect by the call:
canvas.repaint();
The code in actionPerformed is meant to give you the opportunity to see how various common shapes appear. Try each one by uncommenting it while commenting out the others.

Note that a circle is nothing but an ellipse with equal sides. A rounded rectangle requires additional parameters specifying the "corner rounding" characteristics.

The need for super.paintComponent

This statement within paintComponent is crucial:
super.paintComponent(g);
Prove to yourself that it is is so by commenting it out to see what happens. You'll see that the underlying JTextArea no longer appears!

Antialiasing effect

Observe the difference in the Line2D shape with and without the graphical settings — try commenting them out — in Canvas2:
g2.setRenderingHint(
  RenderingHints.KEY_ANTIALIASING,
  RenderingHints.VALUE_ANTIALIAS_ON
);
You'll see that the antialiasing makes a diagonal line appear smooth.

Figures

Now we are going to get the additional shape features back by generalizing what happens in the Canvas2 class. We do so by creating a dedicated Figure class hierarchy in which the classes maintain the features necessary to paint the shape. We're going to use a new dedicated figure package to hold these classes:
We want them to be separate from both the Driver and the Frame classes because they contain no visual components, but are used both by the Driver and Frame.

figure.Figure
package figure;
 
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.io.Serializable;
 
public abstract class Figure implements Serializable {  
  // all figures have a "shape" and an "outline stroke"
 
  protected float strokeWidth = 1.0f;
  protected Shape shape;
 
  protected Color lineColor = Color.BLACK;
  protected Color fillColor = null;
 
  protected double xLoc = 0, yLoc = 0;
 
  protected double scale = 1.0;
 
  protected String title = "-- untitled --";
 
  // the stroke is created from strokeWidth + BasicStroke
  // but BasicStroke is not serializable, so the actual
  // stroke used is created "on the fly"
  protected transient Stroke stroke = null;
 
  // the actual drawing is done by the subclass
  public abstract void draw(Graphics2D g2);
  public abstract Shape getPositionShape();
 
  public void setLocation(double xLoc, double yLoc) {
    this.xLoc = xLoc;
    this.yLoc = yLoc;
  }
 
  public void incLocation(double xInc, double yInc) {
    this.xLoc += xInc;
    this.yLoc += yInc;
  }
 
  public Point2D.Double getLocation() {
    return new Point2D.Double(xLoc, yLoc);
  }
 
  public double getScale() {
    return scale;
  }
 
  public void setScale(double scale) {
    this.scale = scale;
  }
 
  public final void setStrokeWidth(float strokeWidth) {
    this.strokeWidth = strokeWidth;
    stroke = null;
  }
 
  public void setLineColor(Color color) {
    this.lineColor = color;
  }
 
  public void setFillColor(Color color) {
    this.fillColor = color;
  }
 
  public final void setTitle(String title) {
    this.title = title;
  }
 
  @Override
  public String toString() {
    return title;
  }
}

figure.RectangleFigure
package figure;
 
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.Shape;
 
public class RectangleFigure extends Figure {
  private final double width, height; 
 
  public RectangleFigure(double width, double height) {
    this.width = width;
    this.height = height;
    shape = new Rectangle2D.Double(0, 0, width, height);
  }
 
  @Override
  public Shape getPositionShape() {
    return new Rectangle2D.Double(xLoc, yLoc, width*scale, height*scale);
  }
 
  @Override
  public void draw(Graphics2D g2) {
    if (stroke == null) {
      stroke = new BasicStroke(strokeWidth);
    }
    g2.setStroke(stroke);
 
    if (fillColor != null) {
      g2.setColor(fillColor);  // set color
      g2.fill(shape);          // and fill the shape
    }
 
    g2.setColor(lineColor); // set color
    g2.draw(shape);         // draw the shape (the outline)
  }
}
Points to note about the Figure classes:
  1. The Figure class is abstract because there is no such thing as a Figure, per se.
  2. The draw operation is abstract, because the actual drawing depends on the specifics of the derived classes.
  3. The RectFigure is based at (0,0) and the location members are used to translate a figure to be drawn at an arbitrary position. The actual translation takes place in the Canvas prior to calling draw.
  4. The incLocation member function, usused at the momemt, is useful when we want to move the figure based on some event.
  5. The polymorphic getPositionShape function, abstract in the Figure class provides access to the "real translated shape" of the figure which we will use to test containment of a point.
  6. We have employed a new data member modifier, transient, which is explained below.

Serializability

It's useful to be able to save your work for later reuse. The declaration is easy enough. Declaring
public abstract class Figure implements Serializable
implies that all subclasses are serializable. The problem is that some of the key features are not serializable, in particular the class
BasicStroke
To me, it make no sense that BasicStroke is not serializable, but we have to deal with it. We have to tell Java not to attempt to store it in the serialized output. Towards this end we use the transient keyword:
protected transient Stroke stroke; 
The problem to solve is then how to generate them prior to drawing the figure. We do not want to create a new BasicStroke object on each draw operation, because these operations occur on each mouse drag event.

The idea is that it is initially null within a newly created object, and so test for that fact within the draw member function. For example, in RectFigure:
public void draw(Graphics2D g2) {
  if (stroke == null) {
    stroke = new BasicStroke(strokeWidth);
  }
  g2.setStroke(stroke);
  ...
}

Version 3: Drawing a Figure

The Canvas3 class


views.Canvas3
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;
 
public class Canvas3 extends JTextArea {
 
  private Figure figure = null;
 
  public void setFigure(Figure figure) {
    this.figure = figure;
  }
 
  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
 
    g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON
    );
 
    if (figure == null) {
      return;
    }
 
    AffineTransform t = g2.getTransform(); // save the transform settings
 
    double x = figure.getLocation().x, y = figure.getLocation().y;
 
    g2.translate(x, y);
    figure.draw(g2);
    g2.setTransform(t);
 
//    g2.translate(x, y);
//    figure.draw(g2);
//    g2.translate(-x, -y);
  }
}
A significant change is that the drawing action is passed off to the figure. In Canvas2 we had paintComponent calling:
g2.draw(shape);
whereas in Canvas, we have this:
figure.draw(g2);
The figure has access to all the features necessary to create the desired appearance

Translating the coordinate system

The rectangular shape behind a RectFigure object has its top/left position set to (0,0) via the constructor. In order to move or this object to an alternative position, (x,y), we
  1. move the entire coordinate system, translating it by the amount (x,y)
  2. draw the rectangle based at (0,0), which is (x,y) based on the un-translated system
  3. reset the coordinate system, translating it by the amount (-x,-y)
The last step is not technically necessary if you are only drawing one figure as we are here; however, if you draw multiple figures, the translation effects would accumulate and generate the wrong display.

Our code comments out the most obvious way to achieve this effect:
g2.translate(x, y);
figure.draw(g2);
g2.translate(-x, -y);
However, we have used a more sophisticated replacement:
AffineTransform t = g2.getTransform(); // save the transform settings
g2.translate(x, y);
figure.draw(g2);
g2.setTransform(t); // restore them after drawing
Why is this latter approach "more sophisticated"? The reason is that, in addition to translation, other so-called "affine transformations" can be applied such as scaling. Instead of trying to undo all the transformations, we simply remember the orginal state:
AffineTransform t = g2.getTransform(); // save the transform settings
and then reset to the original state after applying our translation:
g2.setTransform(t); // restore them after drawing

Create TheFrame

This procedure is analogous to what we did in the previous section. Start by duplicating TheFrame2. Make a copy of it and paste the copy back into the views package, changing the name to TheFrame. Make these alterations on TheFrame:
  1. Edit TheFrame in Design mode. Right-click on the textarea and select Customize Code.
  2. In the Code Customizer dialog, select custom creation from the top selection list corresponding to
    canvas = new Canvas2();
    
    Change this code to:
    canvas = new Canvas();
    
    Click OK.

Driver3


Driver3
package graphicbasics;
 
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.Color;
 
import views.Canvas3;
import views.TheFrame3;
 
import figure.Figure;
import figure.RectangleFigure;
 
public class Driver3 {
 
  private final TheFrame3 frame = new TheFrame3();
  private final Canvas3 canvas = frame.getCanvas();
 
  private Figure currentFigure = null;
 
  private Figure getFigure() {
    Figure fig = new RectangleFigure(200, 100);
    fig.setLocation(20, 20);
    fig.setLineColor(Color.blue);
    fig.setFillColor(Color.red);
    fig.setStrokeWidth(4.2f);
    return fig;
  }
 
  public Driver3() {
    frame.setTitle(getClass().getSimpleName());
    frame.setSize(700, 600);
    frame.setLocationRelativeTo(null);
 
    currentFigure = getFigure();
 
    frame.getDrawButton().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        canvas.setFigure(currentFigure);
        canvas.repaint();
      }
    });
  }
 
  public static void main(String[] args) {
    Driver3 app = new Driver3();
    app.frame.setVisible(true);
  }
}

Version 4: Moving a Figure

We can reuse both Canvas3 and Frame3. We're going to make the figure come up in the constructor, so the Draw button is unnecessary, and not used, but we'll keep it in the Frame anyway.

Driver4


Driver4
package graphicbasics;
 
import java.awt.Color;
 
import views.Canvas3;
import views.TheFrame3;
 
import figure.Figure;
import figure.RectangleFigure;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
 
public class Driver4 {
 
  private final TheFrame3 frame = new TheFrame3();
  private final Canvas3 canvas = frame.getCanvas();
 
  private Figure currentFigure = null;
 
  private int lastX, lastY;
  private Figure selectedFigure = null;
 
  private Figure getFigure() {
    Figure fig = new RectangleFigure(200, 100);
    fig.setLocation(20, 20);
    fig.setLineColor(Color.blue);
    fig.setFillColor(Color.red);
    fig.setStrokeWidth(4.2f);
    return fig;
  }
 
  public Driver4() {
    frame.setTitle(getClass().getSimpleName());
    frame.setSize(700, 600);
    frame.setLocationRelativeTo(null);
 
    currentFigure = getFigure();
    canvas.setFigure(currentFigure);
 
    canvas.addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(MouseEvent e) {
        if (currentFigure == null) { // no figure available
          return;
        }
        int x = e.getX(), y = e.getY();
 
        if (currentFigure.getPositionShape().contains(x, y)) {
          selectedFigure = currentFigure;
        }
 
        lastX = x;
        lastY = y;
      }
 
      @Override
      public void mouseReleased(MouseEvent e) {
        selectedFigure = null;
      }
    });
 
    canvas.addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseDragged(MouseEvent e) {
        if (selectedFigure == null) {
          return;
        }
        int x = e.getX(), y = e.getY();
 
        int incX = x - lastX;
        int incY = y - lastY;
 
        lastX = x;
        lastY = y;
 
        selectedFigure.incLocation(incX, incY);
        canvas.repaint();
      }
    });
 
  }
 
  public static void main(String[] args) {
    Driver4 app = new Driver4();
    app.frame.setVisible(true);
  }
}
When you draw the figure and click on it, you can drag it around the canvas. We have added these mouse-based listeners to the canvas:
void mousePressed(MouseEvent e)   MouseListener event handler
void mouseDragged(MouseEvent e)   MouseMotionListener event handler
void mouseReleased(MouseEvent e)  MouseListener event handler
The key control features are these data members:
private int lastX, lastY;
private Figure selectedFigure = null;

Moving the Figure

It is useful to first understand how the figure is being moved independently of its selection. Toward this end, first comment out the if restriction in setting the selectedFigure so that, if available, currentFigure (if available) is always the selected figure when the mouse is pressed:
      public void mousePressed(MouseEvent e) {
        if (currentFigure == null) { // no figure available
          return;
        }
        int x = e.getX(), y = e.getY();
 
//        if (currentFigure.getPositionShape().contains(x, y)) {
          selectedFigure = currentFigure;
//        }
 
        lastX = x;
        lastY = y;
      }
The mouse is pressed at (x,y) and this position is captured by these control variables:
lastX = x;
lastY = y;
A mouse drag event is sensed at a new position (x,y) and this code is used:
      public void mouseDragged(MouseEvent e) {
        if (selectedFigure == null) {
          return;
        }
        int x = e.getX(), y = e.getY();
 
        int incX = x - lastX;
        int incY = y - lastY;
 
        lastX = x;
        lastY = y;
 
        selectedFigure.incLocation(incX, incY);
        canvas.repaint();
      }
The code computes the difference
incX = x - lastX;
incY = y - lastY; 
The (lastX,lastY) position is reset to current mouse position and the differences are used to reposition the figure which is then repainted] to make the change:
selectedFigure.incLocation(incX, incY);
canvas.repaint();
On completion of dragging the mouse is released and we simply want to register this completion:
      public void mouseReleased(MouseEvent e) {
        selectedFigure = null;
      }
You see that having selectedFigure ≠ null is the signal to ensure that nothing happens on mouse drag if there is no selected figure.

Selecting the figure

Now put the mousePressed code back to its original:
      public void mousePressed(MouseEvent e) {
        if (currentFigure == null) { // no figure available
          return;
        }
        int x = e.getX(), y = e.getY();
 
        if (currentFigure.getPositionShape().contains(x, y)) {
          selectedFigure = currentFigure;
        }
 
        lastX = x;
        lastY = y;
      }
The outcome is that we only get the currentFigure as the selectedFigure if the mouse click is inside this figure. We are using the Shape member function
contains(x,y)
which returns true if and only if the position (x,y) lies "inside" the shape. The getPositionShape() member function correctly sends back the shape positioned at the (xLoc, yLoc) coordinate where it is painted and scaled by the figure's scale amount.

The notion of being "inside" is a bit subtle regarding the shape boundary; half of the boundary is considered to be inside and half outside.

Version 5: Scaling a Figure

The last feature we'll explore in this document is scaling a figure. Our code will rely on the data member from the Figure class
protected double scale = 1.0;
along with getter and setter methods. When the figure is drawn, it is first scaled by the graphics method:
g2.scale(scaleX,scaleY)
In our case, we'll use the figure's scale member for both x and y as follows:
    AffineTransform t = g2.getTransform(); // save transform settings

    x = figure.getLocation().x;
    y = figure.getLocation().y;
    scale = figure.getScale();
 
    g2.translate(x, y);
    g2.scale(scale, scale);
    figure1.draw(g2);
    g2.setTransform(t);  // comment this to see what happens
As suggested, comment out the suggested line to see what happens. In particular, instead of figuring out how to undo the translation and scaling effects, we simply revert to a "snapshot" taken before these two effects were added.

views.Canvas5
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;
 
public class Canvas5 extends JTextArea {
 
  private Figure figure1 = null;
  private Figure figure2 = null;
 
  public void setFigure1(Figure figure) {
    this.figure1 = figure;
  }
 
  public void setFigure2(Figure figure) {
    this.figure2 = figure;
  }
 
  @Override
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
 
    g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON
    );
 
    if (figure1 == null || figure2 == null) {
      return;
    }
 
    AffineTransform t = g2.getTransform(); // save the transform settings
 
    double x, y, scale;
 
    x = figure1.getLocation().x;
    y = figure1.getLocation().y;
    scale = figure1.getScale();
 
    g2.translate(x, y);
    g2.scale(scale, scale);
    figure1.draw(g2);
    g2.setTransform(t);  // comment this to see what happens
 
    x = figure2.getLocation().x;
    y = figure2.getLocation().y;
    scale = figure2.getScale();
 
    g2.translate(x, y);
    g2.scale(scale, scale);
    figure2.draw(g2);
    g2.setTransform(t);
  }
}

Using a Swing JSpinner to control the scale

The views.Frame5 class incorporates a new Swing component called a JSpinner. This component presents the user with a selection of a range of values, both up and down from an initial default value. In the associated Frame class, you'll see it here:

views.TheFrame5
public class TheFrame5 extends javax.swing.JFrame {
 
  public Canvas5 getCanvas() {
    return (Canvas5) canvas;
  }
 
  public JSpinner getScaleSpinner() {
    return scale;
  }
 
  public JButton getSetScale() {
    return setscale;
  }
Like other Swing components, the JSpinner uses a dedicated model object, SpinnerModel, to hold the values displayed by the spinner. Our driver code creates and puts this into place in one step:
frame.getScaleSpinner().setModel(
  new SpinnerNumberModel(1.0, 0.1, 5.0, 0.05)
);
These 4 values in the constructor give the initial value (1.0), the lowest (0.1) and highest (5.0) values, plus the increment(0.05). We capture a value change event with a ChangeListener object, and when one is sensed, apply the new value to the current figure:
frame.getScaleSpinner().addChangeListener(new ChangeListener() {
  @Override
  public void stateChanged(ChangeEvent e) {
    double d = (Double) frame.getScaleSpinner().getValue();
    currentFigure.setScale(d);
    canvas.repaint();
  }
});

Driver5


Driver5
package graphicbasics;
 
import java.awt.Color;
 
import views.Canvas5;
import views.TheFrame5;
 
import figure.Figure;
import figure.RectangleFigure;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
 
public class Driver5 {
 
  private final TheFrame5 frame = new TheFrame5();
  private final Canvas5 canvas = frame.getCanvas();
 
  private Figure currentFigure = null;
 
  private int lastX, lastY;
  private Figure selectedFigure = null;
 
  private Figure getFigure1() {
    Figure fig = new RectangleFigure(200, 100);
    fig.setLocation(20, 20);
    fig.setLineColor(Color.blue);
    fig.setFillColor(Color.red);
    fig.setStrokeWidth(4.2f);
    return fig;
  }
 
  private Figure getFigure2() {
    Figure fig = new RectangleFigure(200, 100);
    fig.setLocation(80, 80);
    fig.setLineColor(Color.green);
    fig.setFillColor(Color.yellow);
    fig.setStrokeWidth(7f);
    return fig;
  }
 
  public Driver5() {
    frame.setTitle(getClass().getSimpleName());
    frame.setSize(700, 600);
    frame.setLocationRelativeTo(null);
 
    frame.getScaleSpinner().setModel(new SpinnerNumberModel(1.0, 0.1, 5.0, 0.05));
 
    Figure figure1 = getFigure1();
    Figure figure2 = getFigure2();
 
    currentFigure = figure1;
    canvas.setFigure1(figure1);
    canvas.setFigure2(figure2);
 
    canvas.repaint();
 
    frame.getScaleSpinner().addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent e) {
        double d = (Double) frame.getScaleSpinner().getValue();
        currentFigure.setScale(d);
        canvas.repaint();
      }
    });
 
    frame.getSetScale().addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        frame.getScaleSpinner().setValue(2.0);
        currentFigure.setScale(2.0);
      }
    });
 
    canvas.addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(MouseEvent e) {
        if (currentFigure == null) { // no figure available
          return;
        }
        int x = e.getX(), y = e.getY();
 
        if (currentFigure.getPositionShape().contains(x, y)) {
          selectedFigure = currentFigure;
        }
 
        lastX = x;
        lastY = y;
      }
 
      @Override
      public void mouseReleased(MouseEvent e) {
        selectedFigure = null;
      }
    });
 
    canvas.addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseDragged(MouseEvent e) {
        if (selectedFigure == null) {
          return;
        }
        int x = e.getX(), y = e.getY();
 
        int incX = x - lastX;
        int incY = y - lastY;
 
        lastX = x;
        lastY = y;
 
        selectedFigure.incLocation(incX, incY);
        canvas.repaint();
      }
    });
  }
 
  public static void main(String[] args) {
    Driver5 app = new Driver5();
    app.frame.setVisible(true);
  }
}
One final feature added is to set the Spinner to some value and change the figure simultaneously through a dedicated "setScale" button in the Frame:
frame.getSetScale().addActionListener(new ActionListener() {
  @Override
  public void actionPerformed(ActionEvent e) {
    frame.getScaleSpinner().setValue(2.0);
  }
});
The member function used on the spinner is setValue. Notice that the spinner's event handler will sense a change (if there is one) and change the currentFigure accordingly.


© Robert M. Kline