Lecture 7

Introduction to Graphics

So far we have done a bit of programming in the terminal but many users find it inconvenient to work with text only interfaces. Fortunately, Java comes with an extensive library for drawing graphics and for making graphical user interfaces built from menus, dialogs, and buttons. In this lecture, we will do some basic graphical programming.

All graphics related classes are defined either in the package java.awt or in java.swing. Some more specialized classes might be defined in a subpackage of one of this two packages. This means that you have to add:

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

in the beginning of every Java file where you have something to do with graphics.

Frames and Components

The first thing to do before we can draw something is to open a window where the drawing can be hosted. The windows are objects of class JFrame and this is how you create them:

import javax.swing.*;

public class EmptyFrame {

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setSize(300,300);
        frame.setTitle("My first frame");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

}

The actual creation is only the first line when we use new to create the object. However, there are a number of options that have to be set before the object is useful. By default the window has size zero by zero, which is hardly useful and we need to resize it by calling the method setSize(with,height). Furthermore, all windows are by default in an invisible state, i.e. they exist as objects but they are not visualized on the screen. We must explicitly ask for the window to be shown, and this happens with the call to the method setVisible(true). By default the title of the window is empty, so we also call the method setTitle(...) which changes the title. Finally the most misterious is the call setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE). This is needed because every application can open more than one window. Clicking on the close button for each window will by default close it, but this will not terminate the application even if all open windows are closed. Usually every application has one main window and closing the main window must terminate the whole application. Calling setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) for some window tells the graphical library that this is the main window for the application.

Did you notice that the main method in the example just creates an objects and then it terminates, but this still does not terminate the application? It is important to note that there is a fundamental difference between how the text based (the terminal) applications and the graphical applications are organized. In the text based interfaces, the application's code is in control of what and when something happens. With graphical applications it is the opposite, i.e. the application just provides some hooks and then the graphical library takes the control. In our case the main method is such a hook, it just creates the graphical window and after that the library and not the application is in control. When the user do something with the window, i.e. it clicks a button for example, then the library notifies the application which handles the event.

The JFrame object is only a frame in which other graphical gadgets can be placed in order to display some content. In Java each gadget must be an object of a class which extends the class JComponent. If we want to show something in this component, then we cannot simply draw it since only the graphical library and not the application knows when something must be drawn. Instead we must provide a hook, i.e. a method, which does the drawing. The library will call the method in the right time. This is the skeleton of a component:

import java.swing.*;

public class RectangleComponent extends JComponent {
    public void paintComponent(Graphics g) {
        ...
    }
}

The drawing must happen in the method 'paintComponent' but right now it is empty. As an example we can make it draw a simple box:

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import javax.swing.JComponent;

public class RectangleComponent extends JComponent {

    public void paintComponent(Graphics g) {
        //Cast to a Graphics2D object
        Graphics2D g2 = (Graphics2D) g;

        //Create a new Rectangle object.
        Rectangle box = new Rectangle(20,40,50, 100);

        //Call the draw method of g2 to draw the rectangle.
        g2.draw(box);
    }
}

The parameter g that paintComponent takes is an object of class Graphics which has methods for drawing different shapes. However, the first line in the body of paintComponent is a sign of the Java legacy. There are two classes that can be used for drawing: Graphics and Graphics2D. The first one was developed earlier and this is what you get as a parameter, the second one is newer and it has extended functionality but in order to use it you have to cast the parameter g from Graphics to Graphics2D. After we have done it we can use the new API through the g2 variable.

The next thing that we do is the actual drawing. First we create an object of class 'Rectangle' with the specified dimensions, and after that we call the method draw of Graphics2D with the rectangle as a parameter.

Now showing the rectangle is trivial. We implement a class which is the same as EmptyFrame, except that it also adds the component RectangleComponent as a content in the frame:

import javax.swing.JFrame;

public class RectangleViewer {

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setSize(300,300);
        frame.setTitle("A Rectangle");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        RectangleComponent comp = new RectangleComponent();
        frame.add(comp);
        frame.setVisible(true);
    }
}
This happens by calling the method add of JFrame.

Containers

The strength of object oriented programming really shines in developing graphical applications. In the above example, we have a frame with only one component, but it real applications we have frames with several small gadgets doing different things. In Java each such gadget is an object which works independently from the other objects but they all communicate with each other to provide the functionality of the whole application.

When you have several components, then you also need to specify how they are arranged in the frame. In Java this is done by using special objects called panels and layouts. If you add more than one component in the same frame then by default they are put on top of each other. This is not very useful of course. The right way is to add to the frame a single panel and after that to add the components into the panel. When we create the panel we also specify a layout, i.e. another object which is responsible to arrange the components in different positions on the screen. By choosing different layout classes we get different arrangements.

A typical example looks like this:

JPanel panel = new JPanel(new BorderLayout());
panel.add(leftButton,BorderLayout.WEST);
panel.add(rightButton,BorderLayout.EAST);
panel.add(startButton,BorderLayout.SOUTH);
panel.add(component,BorderLayout.CENTER);

frame.add(panel);

Here JPanel is the class for panels. The constructor of the class takes as parameter the layout. In this case we use BorderLayout which arranges components by putting them either in the center of the frame or to west, south, east or north. The exact location for each components is specified by the constant that is passed as the second argument in the method add for the panel. After the panel is populated we can add it to the frame.

More about panels and layouts can be read here.

Events and Listeners

The graphical library continuously notifies the application for the user's actions. Each notification in the graphical programming terminology is called event, and the object that listens for events happening is called listener. A listener for a given event must implement a specific interface, and then the library will call the appropriate method when the even happens.

For instance, if you want to place a button in your graphical user interface, then you create an object of class JButton and you place it in a JFrame. When the user has pressed the button, your application needs to be notified. For this purpose your application provides an implementation of the interface ActionListener to the button. The definition of the interface is simple:

public interface ActionListener {
    void actionPerformed(ActionEvent event);
}

The simplest implementation is to create a class implementing the interface ActionListener. The method actionPerformed simply prints something in the terminal:

public class ClickListener implements ActionListener {
    public void actionPerformed(ActionEvent event) {
        System.out.println("Button clicked!");
    }
}

When you create the button you also create an instance of the listener class and you pass it to the method addActionListener of the class JButton:

JButton button = new JButton("Click here!");
ActionListener listener = new ClickListener();
button.addActionListener(listener);

What happens here is something very similar to the situation with the visual objects from the previous lecture. The method addActionListener collects a list of all listeners that were passed to it. It doesn't matter what are the types of the objects; it only matters that they all implement the interface ActionListener. When the button is pressed, somewhere in the implementation of the graphical library there is a loop which iterates over all listeners, and for each of them the 'actionPerformed' method is called. The example application that is attached as code sample for this lecture contains examples about handling events from the keyboard, the mouse, from buttons and from a timer.

Inner Classes

It is very common to have one object which represents the whole graphical application, i.e. the frame, and several objects which are attached as listeners to different events. In this case it is convenient to use inner classes.

The inner class is a class defined inside another class. The first advantage is that you keep related pieces of code in the same file. Normally, you can create instances of an inner class only from the methods of the outer class. In this case the new instance from the inner class remains connected to the instance that created it. This means that the methods of the inner class can access the methods and the variables of the outer class directly without the need to keep explicit references. This is particularly handy for listeners which usually contain only short methods which call methods from the outer class.

Handling events is something that you have to do a lot even for simple graphical applications. Since it is a bit verbose and cumbersome to create a new class for every listener that you want to attach, there is a convenient way to implement interfaces inline:

JButton button = new JButton("Click here!");
button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("Button clicked!");
    }
});

This is an alternative implementation of the previous code where you don't need a new class. Instead you just create an instance of the interface 'ActionListener' and immediately after the name of the interface you supply the implementation of the required methods. Semantically this code is equivalent to the previous. The compiler just creates an annonymous class implementing the interface.