Module 9B: Swing, Part I


Introduction

 

Java provides two quite different packages for developing GUI applications: Swing and JavaFX

 

Typical classes you can expect to use include:

 
Components and Containers:
 


Bringing up a window

 

Let us create a simple blank window that does nothing:

 

Next, let's create a slightly improved window: (source file)

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

public class TestSwing2 {

    public static void main (String[] argv)
    {
        // Create a Frame instance. 
        JFrame f = new JFrame ();

        // Set the size. 
        f.setSize (200, 100);
 
        // Install a title for the titlebar. 
        f.setTitle ("A Test Window");

        // A background color: 
        //   First, extract the contentPane 
        Container cPane = f.getContentPane();
        //   Then, set its color. 
        cPane.setBackground (Color.cyan);

        // Display. 
        f.setVisible (true);
    }

}
  
The result:

Note:

 
Exercise 9.1: Use the above code in the following template to print out the screen size in inches. Next, bring up a window that is 3 inches wide and 2 inches in height and place the window approximately in the center of the screen. (Use the setLocation method of Component).
 


Simple drawing and writing

 

We will now look some basic graphics (drawing and lettering):

Consider the appropriate widget for drawing:

 

All drawing occurs on a graphics context:

  • A graphics context is an instance of class Graphics.

  • However, you cannot instantiate this class yourself.

  • A component (like JPanel) does that for you.

  • Thus, to draw on a JPanel, you need to get a Graphics instance from the panel.

  • The way this works is, you get the Graphics instance as a parameter of paintComponent (Graphics g).


The all-important technique of overriding paintComponent() (and, sometimes, repaint()):

  • Swing does not maintain "bit-mapped memory".

  • That is, if a window is covered and later uncovered, Swing does not re-draw the contents.

  • Thus, when re-drawing is needed, you have to do it.

  • Swing tells you when drawing is needed by calling the paintComponent() method.

  • Thus, you need to override the paintComponent() method to "do your thing".

  • Important: Therefore, you need to extend the appropriate class (usually JPanel to override the paintComponent() method.
  • This method is given a Graphics instance as parameter.
 

Let's look at a simple example: (source file)

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

// Extend JPanel to override its paintComponent() method: 

class NewPanel extends JPanel {

    // Set background in constructor. 

    public NewPanel () 
    {
        this.setBackground (Color.cyan);
    }
  

    // Override paintComponent(): 

    public void paintComponent (Graphics g)
    {
        // Always call super.paintComponent (g): 
        super.paintComponent(g);

        // drawString() is a Graphics method. 
        // Draw the string "Hello World" at location 100,100 
        g.drawString ("Hello World!", 100, 100);

        // Let's find out when paintComponent() is called. 
        System.out.println ("Inside paintComponent");
    }

}

public class TestSwing4 {

    public static void main (String[] argv)
    {
        // Create a frame 
        JFrame f = new JFrame ();

        // Set the title and other parameters. 
        f.setTitle ("Hello World Test");
        f.setResizable (true);
        f.setSize (500, 300);

        // Add the panel using the default BorderLayout 
        NewPanel panel = new NewPanel ();
        Container cPane = f.getContentPane ();
        cPane.add (panel);

        // Show the frame. 
        f.setVisible (true);
    }

}
  
Here is the result:

An explanation:

  • To write or draw in a frame, we need to override the paintComponent() method in JPanel.

  • To override means we have to inherit from JPanel.

  • Above, the class NewPanel extends JPanel:
    class NewPanel extends JPanel {
    
      // ... 
    
    }
        

  • Since the NewPanel component will fill up the contentPane, it's background will be visible.
    Therefore, we set the Panel's background color to what we want.

  • Note that paintComponent() is passed a Graphics context.

  • Only a graphics context has the means for drawing and writing.

  • Here are some methods in Graphics, for example:
    • For writing: drawString(), setFont().
    • For drawing empty shapes: drawRect(), drawOval, drawPolygon(), drawPolyline(), drawArc().
    • For drawing filled shapes: fillRect(), fillOval(), fillPolygon(), fillArc().
    • For drawing a line: drawLine()
    • For clipping and translating: setClip(), translate().

  • All measurements are in pixels:
    • The frame size is 500 x 300.
    • The drawString method draws the specified string at a location specified from the top-left corner.
      (The origin is at the topleft corner).
 

Next, let us draw some geometric figures:

  • We will draw un-filled and filled versions of a square, a circle and a rectangle.

  • All the drawing methods are in Graphics.

  • We will use methods drawRect(), drawOval() and drawRoundRect().

  • drawRect (int topleftx, int toplefty, int width, int height):
    • The first two integers specify the topleft corner.
    • The next two are the desired width and height of the rectangle.


  • drawOval (int topleftx, int toplefty, int width, int height):
    • The first two integers specify the topleft corner.
    • The next two are the desired width and height of the enclosing rectangle.


  • drawRect (int topleftx, int toplefty, int width, int height, int arc_width, int arc_height):
    • The first four numbers specify the rectangle.
    • The last two specify "roundedness".
    • The larger the values, the more rounded the result.
    • How the corners are constructed:
      • An imaginary oval is constructed in a rectangle of dimensions arc_width and arc_height.
      • The oval is broken into four quadrants.
      • Each arc is used as a corner of the target rectangle.


  • The drawLine() method is used to draw lines:
    • Unfortunately, the line thickness is fixed at one pixel.
    • To draw thicker lines, you have to "pack" one-pixel lines together yourself or use advanced graphics (see below).
 

For example, consider the following code: (source file)

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

// Extend JPanel to override paintComponent () 
class NewPanel extends JPanel {

    public NewPanel () 
    {
        this.setBackground (Color.cyan);
    }

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);
    
        // Draw a Square: 
        g.drawRect (50,50,50,50);
        g.drawString ("Square", 50, 115);

        // Circle: 
        g.drawOval (200,50,50,50);
        g.drawString ("Circle", 200, 115);

        // Rounded rectangle: 
        g.drawRoundRect (350,50,75,50,20,20);
        g.drawString ("Rectangle", 350, 115);

        // Draw a line across the middle: 
        g.drawLine (0,150,500,150);

        // Now draw some filled shapes: 

        // Square: 
        g.fillRect (50,200,50,50);
        g.drawString ("Square", 50, 265);

        // Circle: 
        g.fillOval (200,200,50,50);
        g.drawString ("Circle", 200, 265);

        // Rounded rectangle: 
        g.fillRoundRect (350,200,75,50,20,20);
        g.drawString ("Rectangle", 350, 265);
    }

}

public class TestSwing5 {

    public static void main (String[] argv)
    {
        // Create an instance of NewFrame 
        JFrame f = new JFrame ();

        // Set the title and other parameters. 
        f.setTitle ("Some Geometric Figures");
        f.setResizable (true);
        f.setSize (500, 300);

        // Create and add the panel. 
        NewPanel panel = new NewPanel ();
        // Note alternative syntax: 
        f.getContentPane().add (panel);

        // Show the frame. 
        f.setVisible (true);
    }

}
  
The result produced is:

 

We can improve the code slightly by placing all the frame-related code inside NewFrame itself: (source file)

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

// Extend JPanel to override paintComponent () 
class NewPanel extends JPanel {

    public NewPanel () 
    {
        this.setBackground (Color.cyan);
    }

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);
    
        // Draw a Square: 
        g.drawRect (50,50,50,50);
        g.drawString ("Square", 50, 115);

        // Circle: 
        g.drawOval (200,50,50,50);
        g.drawString ("Circle", 200, 115);

        // Rounded rectangle: 
        g.drawRoundRect (350,50,75,50,20,20);
        g.drawString ("Rectangle", 350, 115);

        // Draw a line across the middle: 
        g.drawLine (0,150,500,150);

        // Now draw some filled shapes: 

        // Square: 
        g.fillRect (50,200,50,50);
        g.drawString ("Square", 50, 265);

        // Circle: 
        g.fillOval (200,200,50,50);
        g.drawString ("Circle", 200, 265);

        // Rounded rectangle: 
        g.fillRoundRect (350,200,75,50,20,20);
        g.drawString ("Rectangle", 350, 265);
    }

}


// Create a new Frame class. 

class NewFrame extends JFrame {

    public NewFrame ()
    {
        // Set the title and other parameters. 
        this.setTitle ("Some Geometric Figures");
        this.setResizable (true);
        this.setSize (500, 300);

        // Create and add the panel. 
        NewPanel panel = new NewPanel ();
        this.getContentPane().add (panel);

        // Show the frame. 
        this.setVisible (true);
   }
  
}

public class TestSwing6 {

    public static void main (String[] argv)
    {
        // Simply fire up the whole application. 
        NewFrame f = new NewFrame ();
    }

}
  
Note:
  • We used a constructor to set JFrame attributes.

  • Since methods in JFrame need to be called for the particular instance, we use this for clarity.

  • We could use a no-parameter constructor to create a default size.

  • Note the unusual syntax in replacing
        Container cPane = f.getContentPane ();
        cPane.add (panel);
       
    with the variable-free
        f.getContentPane().add (panel);
       
    If you are uncomfortable with this form, use the former syntax.
 

Exercise 9.2: Use the following template to draw a standard 8x8 chessboard. Place the topleft corner of the chessboard at the topleft corner of the panel.
 


Frame insets

 

In the above chessboard exercise you may have noticed that a filled rectangle is slightly smaller than an empty one of the same size (one pixel smaller on two sides).

To fix the problem, you can draw a line around the periphery of the chessboard.

Note:

  • The graphics instance will let you draw well outside the actual visible area.
    However, only the visible area will be shown.

  • For example, the following code for a 500x300 frame draws well outside the visible region: (source file)
      public void paintComponent (Graphics g)
      {
          super.paintComponent (g);
    
          // Draw a wide rectangle: 
          g.fillRect (-100, 100, 800, 50);
      }
       
 

Exercise 9.3: Modify your Ex 9.3 code or use this template to dynamically compute the size of the Panel in paintComponent(). You can do this by getting the width and height by calling the getWidth() and getHeight() methods of the Panel itself. This way, if the window is re-sized, the chessboard will fit the new window.
 


Writing with fonts

 

Swing's support for text is somewhat modest:

  • There is in-built support for some font families.

  • You can load your own font families, but it's complicated.

  • Some text layout help is provided.
 

The following code tells you what fonts are supported:

    // Print out available font names. 
    GraphicsEnvironment gEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
    String[] fNames = gEnv.getAvailableFontFamilyNames();
    System.out.println ("Available font families: ");
    for (int i=0; i < fNames.length; i++)
        System.out.println ("  " + fNames[i]);
  
Note:
  • GraphicsEnvironment is a general-purpose AWT class that bundles together many system-related properties.

  • To get an instance of GraphicsEnvironment, you need to call its static getLocalGraphicsEnvironment() method.

  • The instance method getAvailableFontFamilyNames() provides a list of fonts, returned as a String-array.
 

About fonts:

  • Swing's font functionality is found in the classes Font and FontMetrics.

  • A font is specified by a type (or name), a size and a style.

  • Font name: the name of the Font family, e.g., Zapf Dingbats

  • Font styles:
    • A font style is the particular thickness and slant used for emphasis.
    • The constants Font.PLAIN, Font.ITALIC and Font.BOLD are used to specify styles.
    • Swing supports 4 styles:
      • Plain:
        • This is the regular appearance of the font.
        • To specify, use Font.PLAIN.
      • Italic:
        • This is a slanted version for emphasis.
        • To specify, use Font.ITALIC.
      • Bold-plain:
        • This is a boldface version of the plain style.
        • To specify, use Font.PLAIN | Font.BOLD.
          (Logical OR).
      • Bold-italic:
        • This is a boldface version of the italic style.
        • To specify, use Font.ITALIC | Font.BOLD.


  • Font sizes:
    • A font's size is specified using the standard "printer's point" measure: one "point" is about 1/72 inches.
    • Swing does not guarantee that your size specification will be implemented exactly - only a "best attempt" is made.
    • The FontMetrics class provides more information about a font's sizes.
    • Note: a font size is not specified in pixels.


  • To use a font, you need to:
    • Create an instance of Font, passing the name, style and size to the constructor.
    • To specify a style, use the constants in class Font.
    • Pass the Font instance to the Graphics context using the setFont() method.
 

Consider this example, which prints out Serif and SansSerif strings in two sizes and all the available styles: (source file)

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

// Extend JPanel to override paintComponent () 
class NewPanel extends JPanel {

    public NewPanel () 
    {
        this.setBackground (Color.cyan);
    }

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);

        // Create 10-point Serif fonts in all styles: 
        Font f = new Font ("Serif", Font.PLAIN, 10);
        g.setFont (f);
        g.drawString ("Serif-Plain-10pt", 30, 60);

        f = new Font ("Serif", Font.ITALIC, 10);
        g.setFont (f);
        g.drawString ("Serif-Italic-10pt", 30, 90);

        f = new Font ("Serif", Font.PLAIN | Font.BOLD, 10);    
        g.setFont (f);
        g.drawString ("Serif-Plain-Bold-10pt", 30, 120);

        f = new Font ("Serif", Font.ITALIC | Font.BOLD, 10);    
        g.setFont (f);
        g.drawString ("Serif-Italic-Bold-10pt", 30, 150);

        // Create 20-point SansSerif fonts in all styles: 
        f = new Font ("SansSerif", Font.PLAIN, 20);
        g.setFont (f);
        g.drawString ("SansSerif-Plain-20pt", 160, 60);

        f = new Font ("SansSerif", Font.ITALIC, 20);
        g.setFont (f);
        g.drawString ("SansSerif-Italic-20pt", 160, 90);

        f = new Font ("SansSerif", Font.PLAIN | Font.BOLD, 20);    
        g.setFont (f);
        g.drawString ("SansSerif-Plain-Bold-20pt", 160, 120 );

        f = new Font ("SansSerif", Font.ITALIC | Font.BOLD, 20);    
        g.setFont (f);
        g.drawString ("SansSerif-Italic-Bold-20pt", 160, 150);
    }

} // End of NewPanel 



// Create a new Frame class. 

class NewFrame extends JFrame {

    public NewFrame ()
    {
        // Set the title and other parameters. 
        this.setTitle ("Some Geometric Figures");
        this.setResizable (true);
        this.setSize (500, 250);

        // Create and add the panel. 
        NewPanel panel = new NewPanel ();
        this.getContentPane().add (panel);

        // Show the frame. 
        this.setVisible (true);

        // Print out available font names. 
        GraphicsEnvironment gEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
        String[] fNames = gEnv.getAvailableFontFamilyNames();
        System.out.println ("Available font families: ");
        for (int i=0; i < fNames.length; i++)
            System.out.println ("  " + fNames[i]);
    }
  
}

public class TestSwing11 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame ();
    }

}
  
The result produced is:


We could use some of the more interesting fonts:

Note:

  • The font name is passed as a string:
        Font f1 = new Font ("Serif", Font.PLAIN, 10);
        
  • The font style is an OR'ed combination of constants, e.g.,
        f1 = new Font ("SansSerif", Font.ITALIC | Font.BOLD, 20);    
        
  • To use a font, you first "set" the font using the setFont() method in the graphics context.

  • The drawString method of Graphics will then draw the specified fonts at a given location.
        g.drawString ("Hello World!", 30, 60);
        
  • The lowest point of a non-descending letter like "h" is used for the y-value.

  • You, the programmer, are responsible for laying out the strings (typesetting):
    - you need to compute line lengths, inter-word spacing, etc.
 

Typesetting:

  • A font has several measurements (characteristics) useful in typesetting:
    • Baseline: An imaginary line on which the characters in a line are placed.
      - usually, the lowest point of a non-descending character like "h".
    • Ascent: the height of a non-descending character above the baseline.
    • Descent: the distance a descender like "y" dips below the baseline.
    • Leading (pronounced "ledding"): the distance between the descent of one line and the ascent of the next line.
      (Space between lines, essentially).
    • Height: The sum of the ascent, descent and leading.

  • Note: the font characteristics depend on the letters in the text.
    • An accent is not counted in the ascent.


  • A FontMetrics instance provides the characteristics for a particular font, style and size.

  • A FontMetrics instance is usually obtained as follows:
        // Create a font.  
        Font f = new Font ("Serif", Font.PLAIN | Font.BOLD, fontsize);    
    
        // Set the font in the graphics instance.  
        g.setFont (f);
    
        // Get a FontMetrics instance from the graphics instance.  
        FontMetrics fm = g.getFontMetrics();
        
  • The class FontMetrics in java.awt has several methods:
      public abstract FontMetrics implements Serializable {
    
          //...  
    
          // Some methods that return a font characteristic.  
          public int getAscent ();
          public int getDescent ();
          public int getLeading ();
          public int getHeight ();
          public int maxAscent ();
          public int maxDescent ();
    
          // Get the resulting width of a string.  
          public int stringWidth (String s);
    
          //...  
    
        }
       
  • If accents are being used, it may be advisable to use the sum of maxAscent, maxDescent and leading.
 

We will consider an example in which:

  • We are given a piece of text as an array of String's, and we want to write out the words so that words are not broken up.

  • We will allow the font size to be specified.

  • We will use the stringWidth() method in FontMetrics to get the size of a string in pixels.
Here is code: (source file)
import java.awt.*;
import javax.swing.*;

// Extend JPanel to override paintComponent () 
class NewPanel extends JPanel {

  String[] words; // List of words to display. 
  int fontsize;   // Size of font. 

  public NewPanel (String[] words, int fontsize)
  {
    // Retain data and font size. 
    this.words = words;
    this.fontsize = fontsize;

    this.setBackground (Color.cyan);
  }

  public void paintComponent (Graphics g)
  {
    super.paintComponent (g);
    // Create a Serif font in the given font size. 
    Font f = new Font ("Serif", Font.PLAIN | Font.BOLD, fontsize);    
    g.setFont (f);

    // Get a FontMetrics instance from the graphics context. 
    FontMetrics fm = g.getFontMetrics();

    // Get the height (height of font plus separator). 
    int fontheight = fm.getHeight ();

    // Size of frame plus insets. 
    Dimension D = this.getSize ();

    // Visible drawing area 
    int max_width = D.width;
    int max_height = D.height;

    // Start at topleft. 
    int startx = 0;

    // Must add font height since bottom left corner  
    // of font is used as starting point. 
    int y = 0 + fontheight;
    
    int x = startx;
    for (int i=0; i < words.length; i++) {

      // Obtain pixel-length of string (with blank) 
      int len = fm.stringWidth (" " + words[i]);

      if (x + len < max_width) {
        // If it fits within the current line, draw it 
	g.drawString (" " + words[i], x, y);
	x += len;
      }
      else {
	// Go to next line. 
	x = startx;
	y += fontheight;
	g.drawString (" " + words[i], x, y);
	x += len;
      }
    }

  }
  
} // End of NewPanel 

class NewFrame extends JFrame {

  // Constructor. 
  public NewFrame (int width, int height, String[] words, int fontsize)
  {
    // Set the title and other frame parameters. 
    this.setTitle ("Font metrics");
    this.setResizable (true);
    this.setSize (width, height);

    // Create and add the panel. 
    NewPanel panel = new NewPanel (words, fontsize);
    this.getContentPane().add (panel);

    // Show the frame. 
    this.setVisible (true);
  }

} // End of class "NewFrame" 

public class TestSwing12 {

  public static void main (String[] argv)
  {
    String[] testwords = {
        "What", "inspired", "this", "little", "rhyme?",
        "A", "shot", "of", "gin", "and", "a", "dash", "of", "lime"
    };
    NewFrame nf = new NewFrame (500, 200, testwords, 35);
  }

}
  

This prints out the following:


Note:

  • We have used the method getSize() in Component (and therefore in NewFrame) to get the window size:
    (The window size may be changed.)
        Dimension D = this.getSize ();
        
  • The x-value is the leftmost drawable location:
        int startx = 0;
        
  • However, the first baseline is well-below the the top of the drawable area.

  • Here, we add the font's height (adding the ascent is actually enough):
        int y = 0 + fontheight;
        
  • The list of words and desired font size is passed to the constructor:
        NewFrame nf = new NewFrame (750, 200, testwords, 35);
        
 

Exercise 9.4: Use the following template to create a frame in which you write the largest possible "Hello World!" that will fit into the drawable area. Start with a font size of 10 and increase the size in increments of 10 until you find the largest possible font that will fit into the drawable area. Allow for the situation where the window may be resized.
 


Images

 

Java provides some support for loading, manipulating and displaying images in a window:

  • Simple features include:
    • Loading a JPEG, GIF or animated GIF image from a file or URL.
    • Displaying the image in a Frame using the drawImage method in Graphics.
    • Tracking the "wait" during image upload.


  • Advanced features include:
    • Creating a buffer for images.
    • Manipulating images: shrinking, rotating.
    • Defining and using image filters.


  • The classes for most simple features are in java.awt whereas the more complicated stuff is in java.awt.image.

In this module, we will only consider simple image features.
 

About image formats

  • Images consist of pixel values: each pixel is determined by its "RGBA" values, typically 8-bits for each value.

  • The "A" is transparency.

  • Images are typically stored in compressed format.

  • GIF (Graphical Interchange Format) is one such format:
    • GIF (created by CompuServe) uses the lossless Lempel-Ziv-Welch (LZW) algorithm.
    • GIF is probably the most popular format.
    • However, there is a patent controversy surrounding GIF.
      (Unisys holds the LZW patent).

  • JPEG (Joint Photographic Experts Group) is another format:
    • JPEG uses lossy compression based on a number of image-processing techniques such as transforms (DCT) and quantization.
    • JPEG is free.
    • JPEG is really an algorithm spec rather than a file spec.

  • PNG (Portable Network Graphics) format:
    • Lossless format, like GIF.
    • Slightly better compression than GIF.
    • Free of patent issues.
    • Support for transparency and progressive display.

  • Java handles GIF, JPEG and PNG formats and decides based on the extensions .gif, .jpeg or .jpg, and .png.
 

The following classes are useful:

  • java.awt.Image:
    • An Image instance is used to store and maintain pixels from an image.
    • The full set of pixels is not necessary -- any subset will do.
    • Image does not provide access to the raw pixels.
    • To actually get at the pixels, you need to use PixelGrabber.

  • java.awt.Toolkit:
    • Toolkit is a motley collection of useful methods bundled together into a single class, in package java.awt.
    • You cannot create an instance, but must instead use the method Toolkit.getDefaultToolkit().
    • The getImage() method is the most important method of the Toolkit instance.
    • getImage() comes in two flavors:
         public Image getImage (String filename);
         public Image getImage (URL u);
            
    • Both methods return an Image instance.

  • MediaTracker:
    • The getImage() method above is written to return immediately.
    • Thus, as the image loads, it starts filling up the window, typically, top-down.
    • If you want to wait until the entire image has loaded, use a MediaTracker instance.

  • Graphics:
    • As with any rendering, the Graphics instance passed to the paintComponent() method is used.
    • The method drawImage() (and its variants) is is used to display an image.

  • ImageObserver:
    • ImageObserver is really an interface in java.awt.
    • It has just one method.
    • The method is used by a image-rendering process to report on progress, on errors etc.
    • An instance must be passed to drawImage().
    • The class Component (and therefore JPanel) implements this interface.
    • Thus, the JPanel instance itself is usually passed via this.
 

Let's look at a simple example: (source file)

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

// Extend JPanel to override paintComponent () 
class NewPanel extends JPanel {

    public NewPanel () 
    {
        this.setBackground (Color.cyan);
    }

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);

        // Get a Toolkit instance. 
        Toolkit tk = Toolkit.getDefaultToolkit ();

        // Provide a filename to the getImage() method. 
        Image img = tk.getImage ("bugs.gif");

        // Use the drawImage method. 
        g.drawImage (img, 0, 0, this);
        // Note: we pass "this" as a parameter. 
    }
  
} // End of NewPanel 

class NewFrame extends JFrame {

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Bugs");
        this.setResizable (true);
        this.setSize (width, height);

        // Create and add the panel. 
        NewPanel panel = new NewPanel ();
        this.getContentPane().add (panel);

        // Show the frame. 
        this.setVisible (true);
    }

} // End of class "NewFrame" 

public class TestSwing13 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (300, 200);
    }

}
  

The result produced is:


Note:

  • The format is GIF.

  • The file is assumed to be in the same directory.

  • Since the insets were not used, part of the image gets clipped.

  • The definition of drawImage() used is:
      public void boolean drawImage (Image img, int x, int y, ImageObserver I);
        
    Here:
    • You pass an Image instance (img).
    • You pass the point where the topleft corner of the image is to be located.
    • You pass an ImageObserver instance.
    • Note that Component implements the ImageObserver interface:
        public abstract class Component 
          implements ImageObserver, MenuContainer, Serializable {
      
          // ...  
      
        }
            
    • Thus, Frame, which is a subclass of Component, also implements ImageObserver.
    • Therefore, we can pass the current Frame instance (via this) as the fourth parameter:
          g.drawImage (img, 0, 0, this);
            
    • You don't really need to understand the purpose of ImageObserver at this time.

    Finally, we can clean up the code in NewPanel a little:

    class NewPanel extends JPanel {
    
        Image img;
    
        public NewPanel () 
        {
            this.setBackground (Color.cyan);
            // Get a Toolkit instance. 
            Toolkit tk = Toolkit.getDefaultToolkit ();
    
            // Provide a filename to the getImage() method. 
            img = tk.getImage ("bugs.gif");
        }
    
        public void paintComponent (Graphics g)
        {
            super.paintComponent (g);
    
            g.drawImage (img, 0, 0, this);
        }
      
    } // End of NewPanel 
      
 

In the next example, we will fetch an image from a URL: (source file)

import java.awt.*;
import javax.swing.*;
import java.net.*;  // For class URL. 

// Extend JPanel to override paintComponent () 
class NewPanel extends JPanel {

    public NewPanel () 
    {
        this.setBackground (Color.cyan);
    }

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);

        Toolkit tk = Toolkit.getDefaultToolkit ();

        // try-catch needed since URL constructor throws an exception. 
        try {
            // URL is in java.net. 
            URL u = new URL ("http://www.seas.gwu.edu/~simha/canyon.jpg");

            // Load the image using the second form of getImage(). 
            Image img = tk.getImage (u);

            // Display image. 
            g.drawImage (img, 0, 0, this);
        }
        catch (MalformedURLException e) {
            System.out.println (e);
        }
    }
  
} // End of NewPanel 
  

class NewFrame extends JFrame {

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Gif image");
        this.setResizable (true);
        this.setSize (width, height);

        // Create and add the panel. 
        NewPanel panel = new NewPanel ();
        this.getContentPane().add (panel);

        // Show the frame. 
        this.setVisible (true);
    }

} // End of class "NewFrame" 

public class TestSwing14 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (600, 300);
    }

}
  
The result is:

Note:

  • The class URL is in package java.net.

  • An actual URL is passed to the constructor of the class URL:
          URL u = new URL ("http://www.seas.gwu.edu/~simha/canyon.jpg"); 
        
  • The getImage() method of Toolkit has a form that takes in a URL instance.

  • Large images can take time to load.
 

Exercise 9.5: Use the following template to create a frame in which you load the image at URL http://www.seas.gwu.edu/~simha/mystery_person.jpg. and then fit the image so that it fits exactly into the drawable area. To do this:

  • use the method getSize() in JPanel to get a Dimension instance which contains the width and height of the frame.
  • use another version of drawImage() that allows you to specify the desired width and height for the image:
      public boolean drawImage (Image img, int x, int y, int width, 
                                int height, ImageObserver I);
       
Can you identify the person in the picture?
 

Next, we will use a MediaTracker instance to complete loading before display: (source file)

  public void paintComponent (Graphics g)
  {
      super.paintComponent (g);

      Toolkit tk = Toolkit.getDefaultToolkit ();

      try {
          URL u = new URL ("http://www.seas.gwu.edu/~simha/canyon.jpg"); 
          Image img = tk.getImage (u);

          // Create an instance of MediaTracker and  
          // pass a Frame instance as parameter (via "this"). 
          MediaTracker mt = new MediaTracker (this);

          // Add the image instance to the tracker. 
          mt.addImage (img, 1);

          // We will use a "wait" (clock) cursor while the 
          // image is loading. 
          this.setCursor (Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

          // Ask the MediaTracker to wait. 
          try {
  	      mt.waitForID (1);
          }
          catch (InterruptedException e) {
	      System.out.println (e);
          }
          // Now the image has been loaded. 

          // Return to default cursor. 
          this.setCursor (Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));

          // Draw the complete image. 
          g.drawImage (img, 0, 0, this);
      }
      catch (MalformedURLException e) {
          System.out.println (e);
      }
  }
  
 

Exercise 9.6: Use the following template to create a frame in which you load the four images person1.jpg, person2.jpg, person3.jpg and person4.jpg at URL http://www.seas.gwu.edu/~simhaweb/java/. (Thus, the URL for person1.jpg is http://www.seas.gwu.edu/~simhaweb/java/person1.jpg). Your goal is to fit all four images side by side and so that each image is the same size. The following methods will be useful:

  • Use MediaTracker to wait for all images to be loaded.
  • Class Image has methods getWidth() and getHeight() to obtain the width and height of an image (after its loaded).
  • Both the above methods need to be given an ImageObserver instance as a parameter.
  • Use setSize(int w, int h) to set the size of the frame.
  • Finally, use drawImage() to draw the images at the right places.
  • Identify the four people.
 


Interaction: buttons

 

Thus far, we have only drawn or painted on a window.

To allow a user to interact, we need to receive user input in the form of mouse-clicks or keyboard characaters.

We will first consider a simple "quit" button as an example: (source file)

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

class NewFrame extends JFrame {

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Button example");
        this.setResizable (true);
        this.setSize (width, height);

        // Extract the contentPane because we'll  
        // need to refer to it a couple of times. 
        Container cPane = this.getContentPane();
        cPane.setBackground (Color.cyan);

        // Create a Button instance and pass the button  
        // label as a parameter to the constructor. 
        JButton b = new JButton ("Quit");

        // Set the layout manager for our Frame. 
        cPane.setLayout (new BorderLayout());

        // Add the button to the frame. 
        cPane.add (b);

        // Show the frame. 
        this.setVisible (true);
    }

} // End of class "NewFrame" 


public class TestSwing16 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (300, 200);
    }

}
  

The result produced is:

Note:

  • The button occupies the entire visible area of the frame.
    (This is a result of the layout method used).

  • The default button color is grey.

  • This button does flash when clicked, but nothing happens.
    (We haven't created a button-pressed event-handler yet).

  • Class javax.swing.JButton is used to create a button instance:
    • Here is (part of) the definition of JButton:
        public class JButton extends AbstractButton implements Accessible {
      
            // Constructors.  
            public JButton ();
            public Button (String label);
            public Button (Icon I);
            public Button (String label, Icon I);
      
            // Some important methods inherited from AbstractButton: 
            public void addActionListener (ActionListener a);
            public String getText ();
            public synchronized void setText (String label);
      
            // ...  
        }
            
    • The text inside a button is passed as a String to the JButton constructor:
           JButton b = new JButton ("Quit");
         
    • Since JButton extends JComponent (and Component), it inherits a whole bunch of methods, e.g.,
      • setBackground (Color c) - set the background color of the button.
      • setForeground (Color c) - set the font color.
      • setFont (Font) - set the font used.
      • setText (String) - set label.
 
When one component (like a button) is placed inside another, there are usually two aspects to pay attention to:
  • A component that can hold other components is called a container.
    • Sometimes, a component like a JFrame is not the right container.
    • In fact, we first need to retrieve JFrame's container to place something in it.
    • Other components like JPanel are containers themselves with their own add() method.

  • The other aspect to keep in mind is the layout:
    • Each container uses a layout manager to arrange components for display:
    • Layout managers don't let you control exact locations, but give you some options in controlling the layout.
    • Layout managers also ignore sizes of components.

  • These are examples of layout managers:
    1. BorderLayout: this manager tries to place components either in one of five locations: North, South, East, West or Center (default).
    2. FlowLayout: this manager places components left to right and row-by-row (in standard scan order).
    3. CardLayout: this manager displays only one component at a time, like a rolodex.
    4. GridLayout: this manager places components in a grid.
    5. GridBagLayout: this manager uses a grid-like approach that allows for different row and column sizes.

  • Each layout manager is a class in java.awt or javax.swing.

  • We will cover layout managers later.

  • The setLayout() method is used to choose the layout manager.

  • The default layout manager for a Frame is BorderLayout, so the setLayout() call was not really necessary, but was used here to highlight the layout manager.

  • The add method is used to add a component:
    • The add method above has this signature:
          public Component add (Component c);
            
    • Since JButton derives from Component, it can be passed as a parameter.
    • Note: we did not use the return value (it has an esoteric purpose).
 
Note: the code for creating and adding a button is inside the constructor of NewFrame:
  • Note that placing the button creation code in paintComponent() won't work: (source file)
      public void paintComponent (Graphics g)
      {
          super.paintComponent (g);
    
          JButton b = new JButton ("QUIT");
    
          // Add the button to the panel 
          this.add (b);
          this.validate ();
      }
          
  • An alternative (less desirable) is to place the code for button creation outside the frame, e.g., (source file)
    public class TestSwing18 {
    
        public static void main (String[] argv)
        {
            NewFrame nf = new NewFrame (300, 200);
            // Extract the contentPane because we'll  
            // need to refer to it a couple of times. 
            Container cPane = nf.getContentPane();
            cPane.setBackground (Color.cyan);
    
            // Create a Button instance and pass the button  
            // label as a parameter to the constructor. 
            JButton b = new JButton ("Quit");
            b.setBackground (Color.cyan);
    
            // Set the layout manager for our Frame. 
            cPane.setLayout (new BorderLayout());
    
            // Add the button to the frame. 
            cPane.add (b);
    
            nf.validate();
        }
    
    }
          
  • Note that we used the validate() method of Container (inherited by NewFrame that is needed to "reset" the interior, so that the button can be displayed.
 

Next, let us try making small changes in the layout:

  1. First, we will explicitly ask BorderLayout to place the button in the "South" area of the window: (source file)
        // Create a Button instance and pass the button  
        // label as a parameter to the constructor. 
        JButton b = new JButton ("Quit");
        b.setBackground (Color.cyan);
    
        // Set the layout manager for our Frame. 
        cPane.setLayout (new BorderLayout());
    
        // Add the button to the frame. 
        cPane.add (b, BorderLayout.SOUTH);
    
        // Show the frame. 
        this.setVisible (true);
        
    This produces:


    Note:

    • This time, the button has a "normal" height, but is stretched to occupy all available width.
    • A different version of add is used: a constant is passed as parameter to indicate "South".
    • To see where BorderLayout places items, let's insert five buttons with this code: (source file)
          // Add five buttons at the different locations: 
          JButton b1 = new JButton ("South Quit");
          b1.setBackground (Color.cyan);
          b1.setForeground (Color.magenta);
          cPane.add (b1, BorderLayout.SOUTH);
      
          JButton b2 = new JButton ();
          b2.setBackground (Color.red);
          b2.setFont (new Font ("Serif", Font.PLAIN, 15));
          b2.setText ("North Quit");
          cPane.add (b2, BorderLayout.NORTH);
      
          JButton b3 = new JButton ("<html> East <p> Quit </html>");
          b3.setBackground (Color.blue);
          cPane.add (b3, BorderLayout.EAST);
      
          JButton b4 = new JButton ("West");
          b4.setBackground (Color.green);
          cPane.add (b4, BorderLayout.WEST);
      
          JButton b5 = new JButton ("CENTER");
          b5.setBackground (Color.yellow);
          cPane.add (b5, BorderLayout.CENTER);
          
    • Here is the (ghastly) result:

    NOTE:

    • We can use limited HTML in the text:
          JButton b3 = new JButton ("<html> East <p> Quit </html>");
             

    • We set the Font in a component:
          b2.setFont (new Font ("Serif", Font.PLAIN, 15));
          b2.setText ("North Quit");
             

    • The default on Mac-OS-X is to use Mac-compatible look-and-feel.

    • To force Java on OS-X to use the cross platform look-and-feel, one needs to add
      	try {
      	    UIManager.setLookAndFeel (UIManager.getCrossPlatformLookAndFeelClassName());
      	} catch (Exception e) {
      	    e.printStackTrace (); 
      	}
             
      to the frame constructor.

  2. Next, let's use FlowLayout for these same buttons.
    • The following code is used: (source file)
          // Set the layout manager for our Frame. 
          cPane.setLayout (new FlowLayout());
      
          // Add five buttons at the different locations: 
          JButton b1 = new JButton ("South Quit");
          b1.setBackground (Color.cyan);
          b1.setForeground (Color.magenta);
          cPane.add (b1);
      
          JButton b2 = new JButton ();
          b2.setBackground (Color.red);
          b2.setFont (new Font ("Serif", Font.PLAIN, 15));
          b2.setText ("North Quit");
          cPane.add (b2);
      
          JButton b3 = new JButton ("<html> East <p> Quit </html>");
          b3.setBackground (Color.blue);
          cPane.add (b3);
      
          JButton b4 = new JButton ("West");
          b4.setBackground (Color.green);
          cPane.add (b4);
      
          JButton b5 = new JButton ("CENTER");
          b5.setBackground (Color.yellow);
          cPane.add (b5);
         
    • Here is the result:

Next, we will add an event-listener that handles the case when the button is pressed.

Here is the code: (source file)

import java.awt.*;
import javax.swing.*;
import java.awt.event.*; // Needed for ActionListener. 

class NewFrame extends JFrame implements ActionListener {

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Button example");
        this.setResizable (true);
        this.setSize (width, height);

        Container cPane = this.getContentPane();

        // Create a new button. 
        JButton b = new JButton ("<html> <b> Quit </b> </html>");
        b.setBackground (Color.red);

        // Add an ActionListener to the button. 
        b.addActionListener (this);

        // BorderLayout is already the manager of the content pane 
        // but leave a comment as a reminder. 
        // cPane.setLayout (new BorderLayout()); 

        // Add the button to the south of the frame. 
        cPane.add (b, BorderLayout.SOUTH);

        // Show the frame. 
        this.setVisible (true);
    }

    // This method is required to implement the  
    // ActionListener interface. 

    public void actionPerformed (ActionEvent a)
    {
        System.out.println ("In actionPerformed");

        // Button must have been pressed - so really quit. 
        System.exit (0);
    }

} // End of class "NewFrame" 


public class TestSwing22 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (300, 200);
    }

}
  
Here is the result:


Note:

  • JButton inherits a method called addActionListener.

  • Exactly what is an ActionListener?
    • The definition can be found in package java.awt.event:
          public interface ActionListener extends EventListener {
              public abstract void actionPerformed (ActionEvent e);
          }
              
    • Note: interface EventListener is empty.
    • Thus, an implementor of ActionListener needs to implement only one method: ActionPerformed().
    • This method will be called whenever an "action" takes place.
    • In the above case, the method is called when the button is pressed.


  • We have made our frame (NewFrame) itself implement the ActionListener interface.

  • Since NewFrame is an ActionListener, we register it with our button:
        // Add an ActionListener to the button.  
        b.addActionListener (this);
       
    This tells the button, that the listener has a method called ActionPerformed that can be called when the button is pressed.

  • Note that ActionPerformed is actually called when the button is pressed:
    • The only thing that needs to be done is to actually quit.
    • The parameter ActionEvent contains some information (that is not needed in this case).
    • This information can be used as follows (an alternative to method ActionPerformed():
        public void actionPerformed (ActionEvent a)
        {
            String event_desc = a.getActionCommand();
            if (event_desc.equalsIgnoreCase("Quit"))
                System.exit (0);
        }
           
    • Here, we make doubly sure that it is the Quit button that generated the event.
    • If multiple buttons are used, we can distinguish between them, e.g.,
        public void actionPerformed (ActionEvent a)
        {
            String event_desc = a.getActionCommand();
            if (event_desc.equalsIgnoreCase("Quit"))
                System.exit (0);
            else if (event_desc.equalsIgnoreCase ("Refresh")) {
                // Whatever... 
            }
        }
            
 

Exercise 9.7: Use the following template to create a frame:

  1. Create a JPanel and install it in the CENTER of the frame's content pane. Write "Hello World!" at location 100,100.
  2. Create a "Quit" button and install it in the SOUTH location - quit when this button is pressed.
  3. Create a "Zoom" button - double the font size of the "Hello World" string in the panel when this button is pressed.
Thus, you need to add lines to ActionPerformed() to change the fontsize in the panel. Then, you need to call repaint() at the end of ActionPerformed. Finally, you need to implement the method paintComponent (as we have done before) to actually draw the string "Hello World!" in the desired size. Explain where polymorphism was used in this example.
 


Interaction: mouse clicks

 

We will now write a simple program to perform some drawing based on mouse-clicks:

  • Whenever the user clicks the mouse inside the frame, a line is drawn from the last click-place to the current click place.
  • The starting point for the first segment will be (0,0).
  • The frame will have a quit button.
To "listen" for mouse-clicks, we need an object that implements the MouseListener interface:
  • Interface MouseListener is defined as:
       public interface MouseListener extends EventListener {
           public abstract void mouseClicked (MouseEvent e);
           public abstract void mouseEntered (MouseEvent e);
           public abstract void mouseExited (MouseEvent e);
           public abstract void mousePressed (MouseEvent e);
           public abstract void mouseReleased (MouseEvent e);
       }
       


  • These five methods have to be overridden to successfully implement the interface.

  • However, we are only interested in mouse-click events.

Here is the code: (source file)

import java.awt.*;
import javax.swing.*;
import java.awt.event.*; // Needed for ActionListener. 

class NewPanel extends JPanel {

    public NewPanel ()
    {
        this.setBackground (Color.cyan);
    }
  
}


class NewFrame extends JFrame 
    implements ActionListener, MouseListener {

    NewPanel panel;

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Button example");
        this.setResizable (true);
        this.setSize (width, height);

        Container cPane = this.getContentPane();
        // cPane.setLayout (new BorderLayout());     

        // The panel for drawing. 
        panel = new NewPanel ();
        cPane.add (panel, BorderLayout.CENTER);

        // Add mouse-listening to panel. 
        panel.addMouseListener (this);

        // Create a quit button. 
        JButton b = new JButton ("Quit");
        b.setBackground (Color.red);
        b.setFont (new Font ("Serif", Font.PLAIN | Font.BOLD, 15));
        b.addActionListener (this);
        cPane.add (b, BorderLayout.SOUTH);

        // Show the frame. 
        this.setVisible (true);
    }

    // This method is required to implement the  
    // ActionListener interface. 
    public void actionPerformed (ActionEvent a)
    {
        // Button must have been pressed - so really quit. 
        System.exit (0);
    }

    // These methods are required to implement  
    // the MouseListener interface. 
    int currentX=0, currentY=0;
    public void mouseClicked (MouseEvent m)
    {
        System.out.println ("mouseClicked event: " + m.paramString());
        int x = m.getX();
        int y = m.getY();
        Graphics g = panel.getGraphics();
        g.drawLine (currentX, currentY, x, y);
        currentX = x;
        currentY = y;
    }

    // We need to implement these methods, but 
    // don't actually have to do anything inside. 
    public void mouseEntered (MouseEvent m) {}
    public void mouseExited (MouseEvent m) {}
    public void mousePressed (MouseEvent m) {}
    public void mouseReleased (MouseEvent m) {}

} // End of class "NewFrame" 


public class TestSwing23 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (300, 200);
    }

}
  
The result (after a few random mouse clicks):


Note:

  • To handle mouse events (clicking, dragging, entering etc), a "mouse listener" needs to be passed to the widget that "sees" the events:
    • The mouse events take place in the frame.
    • Thus, we need to pass a "mouse listener" to the panel:
         panel.add ( /* mouse listener */ );
             
    • In our example, the frame itself implements MouseListener.
    • Therefore, we pass the frame itself:
          // Add mouse-listening to the frame itself.  
          panel.addMouseListener (this);
             
    • The MouseListener interface was implemented by overriding all the abstract methods in the interface.
    • The only one we were really interested in was mouseClicked().
    • For the others, we provided empty bodies:
        public void mouseEntered (MouseEvent m) {}
        public void mouseExited (MouseEvent m) {}
        public void mousePressed (MouseEvent m) {}
        public void mouseReleased (MouseEvent m) {}
             


  • The method MouseClicked is called whenever the mouse is clicked within the frame:
    • The parameter MouseEvent carries information, e.g., the pixel coordinates of the place the mouse was clicked.


  • Observe that it is possible to draw in methods other than paintComponent():
    • We drew in the method MouseClicked():
          Graphics g = this.getGraphics();
          g.drawLine (currentX, currentY, x, y);
          
    • To get the Graphics context, simply call the getGraphics() method of Component (inherited by NewPanel).
 

Now, consider a different version of the program that makes the JPanel the mouselistener: (source file)

import java.awt.*;
import javax.swing.*;
import java.awt.event.*; // Needed for ActionListener. 

class NewPanel extends JPanel implements MouseListener {

    public NewPanel ()
    {
        this.setBackground (Color.cyan);
    }

    // These methods are required to implement  
    // the MouseListener interface. 
    int currentX=0, currentY=0;
    public void mouseClicked (MouseEvent m)
    {
        System.out.println ("mouseClicked event: " + m.paramString());
        int x = m.getX();
        int y = m.getY();
        Graphics g = this.getGraphics();
        g.drawLine (currentX, currentY, x, y);
        currentX = x;
        currentY = y;
    }

    // We need to implement these methods, but 
    // don't actually have to do anything inside. 
    public void mouseEntered (MouseEvent m) {}
    public void mouseExited (MouseEvent m) {}
    public void mousePressed (MouseEvent m) {}
    public void mouseReleased (MouseEvent m) {}
  
}


class NewFrame extends JFrame 
    implements ActionListener {

    NewPanel panel;

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Button example");
        this.setResizable (true);
        this.setSize (width, height);

        Container cPane = this.getContentPane();
        // cPane.setLayout (new BorderLayout());     

        // The panel for drawing. 
        panel = new NewPanel ();
        cPane.add (panel, BorderLayout.CENTER);

        // Add panel's mouse-listening to panel. 
        panel.addMouseListener (panel);

        // Create a quit button. 
        JButton b = new JButton ("Quit");
        b.setBackground (Color.red);
        b.setFont (new Font ("Serif", Font.PLAIN | Font.BOLD, 15));
        b.addActionListener (this);
        cPane.add (b, BorderLayout.SOUTH);

        // Show the frame. 
        this.setVisible (true);
    }

    // This method is required to implement the  
    // ActionListener interface. 
    public void actionPerformed (ActionEvent a)
    {
        // Button must have been pressed - so really quit. 
        System.exit (0);
    }

} // End of class "NewFrame" 


public class TestSwing24 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (300, 200);
    }

}
  
Note:
  • The code can be made a little cleaner by moving the listener identification into the JPanel itself:
      public NewPanel ()
      {
        this.setBackground (Color.cyan);
        // Make the panel the listener for the panel. 
        this.addMouseListener (this);
      }
      
 


Panels as containers

 
Thus far, we have used panels to draw. Panels are also used as containers for layout purposes.

Suppose we wish to build the following feature into our "click drawing" example:

  • A button called "Orange" which when clicked will change the line color to orange.
  • A button called "Green" which when clicked will change the line color to green.
  • We'll change the background to white.
Thus, this is what we want to observe:


Now, let's look at some code to make this happen: (source file):

import java.awt.*;
import javax.swing.*;
import java.awt.event.*; // Needed for ActionListener. 

class NewPanel extends JPanel implements MouseListener {

    Color drawColor = Color.green;  // Default color. 

    public NewPanel ()
    {
        this.setBackground (Color.white);
        // Add panel's mouse-listening to panel. 
        this.addMouseListener (this);
    }

    // These methods are required to implement  
    // the MouseListener interface. 
    int currentX=0, currentY=0;
    public void mouseClicked (MouseEvent m)
    {
        System.out.println ("mouseClicked event: " + m.paramString());
        int x = m.getX();
        int y = m.getY();
        Graphics g = this.getGraphics();
        g.setColor (drawColor);
        g.drawLine (currentX, currentY, x, y);
        currentX = x;
        currentY = y;
    }

    // We need to implement these methods, but 
    // don't actually have to do anything inside. 
    public void mouseEntered (MouseEvent m) {}
    public void mouseExited (MouseEvent m) {}
    public void mousePressed (MouseEvent m) {}
    public void mouseReleased (MouseEvent m) {}
  
    public void setDrawingColor (Color c)
    {
        this.drawColor = c;
    }

}


class NewFrame extends JFrame 
    implements ActionListener {

    NewPanel drawPanel;

    // Constructor. 
    public NewFrame (int width, int height)
    {
        // Set the title and other frame parameters. 
        this.setTitle ("Button example");
        this.setResizable (true);
        this.setSize (width, height);

        Container cPane = this.getContentPane();
        // cPane.setLayout (new BorderLayout());     

        // The panel for drawing. 
        drawPanel = new NewPanel ();
        cPane.add (drawPanel, BorderLayout.CENTER);

        // Create a quit button. 
        JButton b = new JButton ("Quit");
        b.setBackground (Color.red);
        b.setFont (new Font ("Serif", Font.PLAIN | Font.BOLD, 15));
        b.addActionListener (this);
        cPane.add (b, BorderLayout.SOUTH);

        // Use a panel for clear and start buttons. 
        JPanel topPanel = new JPanel ();
        topPanel.setLayout (new FlowLayout());
        cPane.add (topPanel, BorderLayout.NORTH);

        // Now insert the color buttons. 
        JButton orangeB = new JButton ("Orange");
        orangeB.setBackground (Color.orange);
        orangeB.addActionListener (this);
        topPanel.add (orangeB);
    
        JButton greenB = new JButton ("Green");
        greenB.setBackground (Color.green);
        greenB.addActionListener (this);
        topPanel.add (greenB);

        // Show the frame. 
        this.setVisible (true);
    }

    // This method is required to implement the  
    // ActionListener interface. 
    public void actionPerformed (ActionEvent a)
    {
        String label = a.getActionCommand();
        if (label.equalsIgnoreCase ("Quit"))
            System.exit (0);
        else if (label.equalsIgnoreCase ("Orange"))
            drawPanel.setDrawingColor (Color.orange);
        else if (label.equalsIgnoreCase ("Green"))
            drawPanel.setDrawingColor (Color.green);
    }  

} // End of class "NewFrame" 


public class TestSwing25 {

    public static void main (String[] argv)
    {
        NewFrame nf = new NewFrame (300, 200);
    }

}

Note:

  • The panel keeps track of the current color:
    class NewPanel extends JPanel implements MouseListener {
    
        Color drawColor = Color.green;  // Default color. 
     
        // ... 
    
    }
       
  • The setColor() method of Graphics is used to set the drawing color:
        Graphics g = this.getGraphics();
        g.setColor (drawColor);
        g.drawLine (currentX, currentY, x, y);
       
  • We defined a mutator for the panel:
    class NewPanel extends JPanel implements MouseListener {
    
        // ... 
    
        public void setDrawingColor (Color c)
        {
            this.drawColor = c;
        }
    
    }
      
  • The mutator is used when responded to button clicks:
      public void actionPerformed (ActionEvent a)
      {
          String label = a.getActionCommand();
          if (label.equalsIgnoreCase ("Quit"))
              System.exit (0);
          else if (label.equalsIgnoreCase ("Orange"))
              drawPanel.setDrawingColor (Color.orange);
          else if (label.equalsIgnoreCase ("Green"))
              drawPanel.setDrawingColor (Color.green);
      }
      
  • We one panel for drawing:
        drawPanel = new NewPanel ();
        cPane.add (drawPanel, BorderLayout.CENTER);
      
    and one to serve as a container:
        JPanel topPanel = new JPanel ();
        topPanel.setLayout (new FlowLayout());
        cPane.add (topPanel, BorderLayout.NORTH);
    
        // ... 
    
        topPanel.add (orangeB);
        
        // ... 
    
        topPanel.add (greenB);
      
  • The default layout manager for panel's is a FlowLayout so we need not have explicitly set the layout:
        JPanel topPanel = new JPanel ();
        // topPanel.setLayout (new FlowLayout()); 
        cPane.add (topPanel, BorderLayout.NORTH);
      
 
Exercise 9.8: Use a BorderLayout for the top-panel instead of its FlowLayout and report what you observe.
 


Animation - a simple example

 

We will now put together a simple animation to see how that works in Swing.
 
Exercise 9.9: Download, compile and execute Missile.java, a very elementary game.
 
What constitutes an animation?

  • First, there are items in the scene.
        ⇒ In our case, there are two geometric items, a circle and a square.

  • Second, there needs to be "physics laws" that govern motion.
        ⇒ For realism, these are of course, based on real physics.

  • Third, there is perspective, or where is the scene viewed from?
        ⇒ This matters in 3D, not in our 2D example.
 
The main idea:
  • Seeing something move is merely an illusion:
    • The eye is presented with a series of still images.
    • A particular item is in a different position in each image.
    • The brain then believes the item is moving.

  • Thus, our goal is to create the series of images, with the items in different positions.

  • To calculate the new position in the next image, one uses the rules of physics.

  • Note: we can't present the different images too fast, or it would "go by" too fast for the eye/brain.

  • Thus, we also need to "pause" sufficiently long (but not too long).

Next, let's build this in steps.
 
First, the code for drawing:

    public void paintComponent (Graphics g)
    {
        super.paintComponent (g);

        // Clear the screen by drawing a white rectangle
        that fills the screen.
        Dimension D = this.getSize();
        g.setColor (Color.white);
        g.fillRect (0,0, D.width,D.height);
        
        // Draw target (the square).
        // Assume the coordinates are calculated else where.
        g.setColor (Color.blue);
        int x = (int) targetX;
        g.fillRect (x,D.height-20, 20, 20);

        // Draw missile (circle).
        g.setColor (Color.red);
        x = (int) missileX;
        int y = D.height - (int) missileY;
        g.fillOval (x,y, 10, 10);
    }
  
Here:
  • The drawing in paintComponent() is similar to the drawing we've seen before.

  • It's the coordinates of the rectangle and circle that change according to the physics of motion.

  • We'll assume they are calculated elsewhere.
 
Next, let's build the GUI itself:
public class Missile extends JFrame implements ActionListener {

    // This is the JPanel on which we draw, and the one that
    // will override paintComponent()
    DrawPanel drawPanel;

    public Missile () 
    {
        this.setSize (500,500);
        Container cPane = this.getContentPane();

	// Create the center panel on which to draw and place in the center.
        drawPanel = new DrawPanel ();
        cPane.add (drawPanel, BorderLayout.CENTER);

	// This panel serves as the container for the two buttons:
        JPanel panel = new JPanel ();

        JButton resetB = new JButton ("Reset");
        resetB.addActionListener (this);
        panel.add (resetB);

        panel.add (new JLabel("    "));
        JButton fireB = new JButton ("Fire");
        fireB.addActionListener (this);
        panel.add (fireB);

        cPane.add (panel, BorderLayout.SOUTH);

        this.setVisible (true);
    }


    public void actionPerformed (ActionEvent a)
    {
	if (a.getActionCommand().equals("Reset")) {
	    // We'll put the animation and calculation code in DrawPanel
	    drawPanel.reset ();
	}
	else if (a.getActionCommand().equals("Fire")) {
	    drawPanel.fire ();
	}
	else {
	    System.out.println ("ERROR: no such action");
	}
    }


    public static void main (String[] argv)
    {
        new Missile ();
    }

}
  
The code above
  • Builds the GUI.

  • Uses the frame itself as the event "listener" for the buttons.

  • Merely calls the appropriate method in the DrawPanel to handle the events.
 
Next, let's look at the main animation loop:
    void animate ()
    {
        while (true) {

            // Do the "physics" for each step
            targetX += targetVX*delT;
            if (missileFired) {
                missileX += missileVX*delT;
                missileVY += missileAY*delT;
                missileY += missileVY*delT;
            }

	    // Redraw the screen: this will result in paintComponent() being called.
            this.repaint ();

	    // Pause between successive re-drawings to create animation effect.
            try {
                Thread.sleep (50);
            }
            catch (InterruptedException e){
                System.out.println (e);
            }

        } //endwhile
    }
  
Note:
  • The animation loop is simply
    1. Use physics rules to compute new locations.
    2. Draw the next scene on the panel (with the new locations).
    3. Pause.

  • The static sleep() method of the library class Thread handles the pausing.

  • Notice the call to repaint()
    • This forces a call to paintComponent() to force the drawing to occur right away.
    • We can't call paintComponent() directly without having the Graphics instance to pass as parameter.
    • repaint() is merely the way to make this happen.

The rest of the code is the physics of those two moving items.
 


Advanced graphics

 

Recall: the Graphics class has methods to draw lines and shapes:

  • It has very basic functionality.

  • For example, you can't set the line thickness.

  • You have to do your own computations for common graphical transformations like rotations.

Java also provides a Graphics2D class for more sophisticated graphical rendering and transformations.

To see how this work, let's write code to produce this:

 
The relevant part of the program: (source file)

    public void paintComponent (Graphics g)
    {
	super.paintComponent (g);
	setBackground (Color.CYAN);

	// g actually points to a Graphics2D instance:
	Graphics2D g2 = (Graphics2D) g;
	
	// Change the line thickness:
	BasicStroke drawStroke = new BasicStroke (2.0f);
	g2.setStroke (drawStroke);

	// Draw a Square:
	g2.setColor (Color.BLUE);
	g2.drawRect (100,50,50,50);
	g2.drawString ("Square", 100, 115);
	
	// Perform a rotation of axes:
	g2.rotate ( Math.toRadians(30.0) );

	// Then draw again in the rotated coordinate system:
	g2.setColor (Color.RED);
	g2.drawRect (100,10,50,50);
	g2.drawString ("Another Square", 100, 75);
    }
  
Note:
  • As it turns out, Graphics2D extends Graphics.

  • The actual class that is passed into paintComponent() is an instance of Graphics2D.

  • Thus, to use the methods of Graphics2D one has to cast the variable into that type:
    	Graphics2D g2 = (Graphics2D) g;
      
 
Exercise 9.10: What is the purpose of this seemingly roundabout way of getting to use methods in Graphics2D? That is, why not define a newer version of paintComponent() that has a Graphics2D parameter? As in:
    public void paintComponent (Graphics2D g2);
  



© 1998, Rahul Simha (revised 2017)