Suppose we want to a game of Tic Tac Toe in a GUI context, like:
The user will click on a cell to 'go' there. So the cells are like buttons - but with extra, since each cell somehow has to know which row and column it is in. The easiest way to program this is to start with an existing class, in this case a JButton, and extend it by using the idea of inheritance. Similarly the main window of the game will be an extension of the basic JFrame.
The idea of inheritance is to take an existing class, called the base class or super class, and extend or subclass it to produce a modified class. The modified class inherits all members (fields and methods) of the base class. However these can be altered, or we can add extra members. Constructors are not inherited - we have to write our own constructor for the sub-class. However we can call super(); as the first statement in the constructor, and this calls the constructor of the super class, doing most of the work for us.
The idea of inheritance is to re-use code - existing code can easily be re-used and built upon.
Dynamics of GUI apps
The character-based Tic Tac Toe game illustrated how such applications execute. There is some output, then the screen is refreshed. This loop is repeated until somehow it ends.
In a GUI app, the user expects a lot more freedom. In the middle of the game they might resize or minimise it, or switch to a different application for a while.
This means a different approach is needed. The application needs to somehow set up an initial screen (normally a window). It must then 'sit back' and wait for the user to do things, and respond appropriately. In other words it must set up event-handlers to deal with user-initiated events.
Most of those (moving the window, re-sizing it, minimizing and restoring it) are handled by existing methods in the base class (the JFrame). The event we do need to handle is a click on one of the cells.
Class design in a GUI context
The classes in this app can be a modification of those in the character-based version:
Game - to contain main, and start things off.
Board - to represent the board on which the game is played. This will be a sub-classed JFrame
These are all singleton classes except for Cell, which will have 9 instances.
The Game class
package GUIttt;
public class Game { // data fields private Board board;
private Human human;
private Computer computer;
// constructor private Game() {
// create the 3 objects which are part of a Game object human = new Human();
computer = new Computer();
board = new Board(human,computer);
computer.setBoard(board);
}
public static void main(String[] args) { Game game = new Game(); //make a game }
}
This instantiates a single Game object. This just creates the human, computer and board objects. The board needs references to the human and the computer, so it can give them a go. The computer needs a reference to the board so it can check which cells are free.
There is no turn-taking in this. That is effectively handled by the board.
The Board class
A key data field of the board class is a 3 by 3 array of TTTCells. These are sub-classed JButtons. When the user clicks on one of these, the actionPerformed method is triggered. This makes one turn to happen - the user's go is marked, we check if they've won, the computer goes, we check if they've won. Then the turn ends.
package GUIttt;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
class Board extends JFrame implements ActionListener { // data members
Board(Human human, Computer computer) { // construct base class
super("Tic Tac Toe");
setBounds(50, 50, 220, 230);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
public void actionPerformed(ActionEvent e) { if (!gameOver) {
if (cells[0][0].getContents() == c && cells[0][1].getContents() == c &&
cells[0][2].getContents() == c) { result = true;
}
if (cells[1][0].getContents() == c && cells[1][1].getContents() == c &&
cells[1][2].getContents() == c) { result = true;
}
if (cells[2][0].getContents() == c && cells[2][1].getContents() == c &&
cells[2][2].getContents() == c) { result = true;
}
//check columns
if (cells[0][0].getContents() == c && cells[1][0].getContents() == c &&
cells[2][0].getContents() == c) { result = true;
}
if (cells[0][1].getContents() == c && cells[1][1].getContents() == c &&
cells[2][2].getContents() == c) { result = true;
}
if (cells[0][2].getContents() == c && cells[1][2].getContents() == c &&
cells[2][2].getContents() == c) { result = true;
}
// leading diagonal
if (cells[0][0].getContents() == c && cells[1][1].getContents() == c &&
cells[2][2].getContents() == c) { result = true;
}
//other diagonal
if (cells[0][2].getContents() == c && cells[1][1].getContents() == c &&
cells[2][0].getContents() == c) { result = true;
//invoked when human clicks a cell void go(ActionEvent e) {
TTTCell whichCell = (TTTCell) (e.getSource());
whichCell.setContents('X');
} }
The single method in this, go, is called when a Cell is clicked. From that event-handler, an
ActionEvent object is passed. This has a getSource method - we can use it to find out which cell was clicked. We then set that to an X. The board checks if we have won.
The Computer class
This still uses the dumb method of going at random to a blank cell.
The Cell class
package GUIttt;
import javax.swing.JButton;
class TTTCell extends JButton { private char contents;
This is a sub-classed JButton, with an extra field, contents, which is just a single character. The setText metod of a JButton sets the text displayed on the button.
There is also a getText method. So we could have done without contents, and remembered the O or X as the text on the button. However it is preferable to store that separately as the actual data, and have the button text reflect that data, rather than being it.
Java 2D
This section is about the Java 2D classes providing support for basic graphics in 2 dimensions in a Swing context. It also intriduces some basic graphics concepts.
Co-ordinates
Positions on a graph, or on a map, are located by co-ordinates - how far across (x or East) and how far up (y or North). Computer graphics usually measure x across and y down, and this is the case for Java 2d graphics.
Care needs to be taken over where the origin (0,0) is. This might be the top left of the screen (or printer, or in general, device co-ordinates), or the top left of the container you are drawing in. The units are usually pixels - the size of which depends on the resolution.
Graphics Context
Graphics can be displayed in different colours, fonts, fill patterns, line styles and so on. One
approach would be to use parameters to specify each of these for each thing drawn. Alternatively a data structure called a graphics context can hold the current values for these, and drawing
operations use these values. To draw a new object in a different colour, the colour of the graphics context is used, which is applicable until it is changed again.
Because this approach is used by the underlying OS, Java also uses it. There is an abstract class called Graphics, instances of which represent a graphics context. We will use the Graphics2D class, which is a concrete class which extends Graphics to provide more facilities.
A First Graphics Program
public class Main extends JFrame { Main() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(50, 50, 200, 200);
main constructs an instance of Main, which is a sub-classed JFrame. The constructor sets the close operation, the frame size and position (in screen co-ordinates), and makes it visible. The paint method (described next) gets a Graphics2D instnce, makes a line object, and draws it. The result is shown here. Note the top left (0,0) of the line is under the JFrame title bar.
Paint
Graphics on a window is usually divided into two parts - a static background and a changing foreground. The static background has to be drawn when the window first appears, when it is restored from minimisation, and when it is revealed after being obscured by an over-lapping window. The changing foreground needs to be re-drawn (if ever) when the user does something, or at short time intervals for animation.
Drawing the static background is done by the paint method, which the system invokes whenever it needs to. Usually our program does not call paint - the system calls it when needed. This means it is a call-back method.In the first program we have coded the paint method, so the line is drawn when, for example, the window is restored.
Exercise
Run this program. See the line is re-drawn after a window restore.
Change it as follows
public class Main extends JFrame { Main() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(50, 50, 200, 200);
Now what happens when the window is restored?
Why?
Simple animation
The other end of the spectrum of the paint idea, which is a fixed drawing, is animation. To do this, we need to re-draw something at frequent intervals. This requires some kind of timer which will repeatedly trigger the drawing - like the following. This has a draw method, which just draws a line, then alters a data member called lineEnd which changes the end of the line. The Animator object has the timer which invokes the draw method.
class Main extends JFrame { Animator animator;
int lineEnd=50;
Main() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(50, 50, 200, 200);
The Animator class is:
class Animator implements ActionListener { Main main;
public void actionPerformed(ActionEvent evt) { main.draw();
} }
This centers around the Timer class (part of Swing - there are other Timer classes). The constructor of a Timer object takes two parameters. The first is a time delay in milliseconds, which is how often the timer triggers. The second is a reference to an object which implements the ActionListener interface - often used as a button event-listener. In this situation, the actionPerformed method is called every time the timer triggers. Here that method just calls the draw method of the window object.
Exercise
Run this program.
Experiment with the animation, including changing the time delay.
Can you add buttons to the window to start, stop and reset the animation? Main would need to implement ActionListener as well.
The Java 2D API The Java 2D API covers
Drawing graphics primitives such as points, lines, rectangles and ellipses, controlling the outline and fill.
Displaying text
Displaying images
We will just look at some of the first set.
Firstly - the content pane of the JFrame. This is the part of a JFrame that is normally used for content, and so excludes the border and title bar. A JFrame has a method for obtaining a reference to the content pane. If we get a graphics context on that, we can draw with the origin (0,0) at the top left, and we do not have to worry about adjusting for the title bar. For example:
void draw() {
Container pane = getContentPane();
Graphics2D g2 = (Graphics2D) pane.getGraphics();
Line2D.Double myLine = new Line2D.Double(0, 0, 100, 100);
g2.draw(myLine);
}
Next, one primitive is a rectangle:
void draw() {
Container pane = getContentPane();
Graphics2D g2 = (Graphics2D) pane.getGraphics();
Rectangle2D.Double myRect = new Rectangle2D.Double(10, 20, 200, 100);
g2.setColor(Color.red);
g2.draw(myRect);
}
This also introduces the Color class. Instances of the Color class can be constructed by for example new Color(50,100,255); where the three parameters are values for red, green and blue components in the range 0 to 255. The class also has some standard colours like Color.red.
Next, the idea of stroke - how the line of the shape is drawn:
void draw() {
Container pane = getContentPane();
Graphics2D g2 = (Graphics2D) pane.getGraphics();
Rectangle2D.Double myRect = new Rectangle2D.Double(10, 20, 50, 100);
g2.setColor(new Color(50,100,200));
BasicStroke stroke=new BasicStroke(6);
g2.setStroke(stroke);
g2.draw(myRect);
}
The parameter in the constructor of the stroke object is the line width in pixels.
Finally we can fill in the insides:
void draw() {
Container pane = getContentPane();
Graphics2D g2 = (Graphics2D) pane.getGraphics();
Rectangle2D.Double myRect = new Rectangle2D.Double(10, 20, 50, 100);
// draw outline
BasicStroke stroke=new BasicStroke(6);
g2.setStroke(stroke);
g2.setColor(Color.green);
void draw() {
Container pane = getContentPane();
Graphics2D g2 = (Graphics2D) pane.getGraphics();
Rectangle2D.Double myRect = new Rectangle2D.Double(10, 20, 50, 100);
GradientPaint gradient = new GradientPaint(10,20, Color.red, 50,100,Color.blue);
g2.setPaint(gradient);
g2.fill(myRect);
}
Exercise
Try this stuff out.
Try using the Ellipse2D.Double to draw ovals.
Overloading
Overloading means having more than one version of a constructor or method. The different versions have the same name, but different numbers or types of parameters.
For example, look at the BasicStroke class. We have seen one constructor:
void draw() {
Container pane = getContentPane();
Graphics2D g2 = (Graphics2D) pane.getGraphics();
Rectangle2D.Double myRect = new Rectangle2D.Double(10, 20, 50, 100);
BasicStroke stroke = new BasicStroke(6);
g2.setStroke(stroke);
g2.draw(myRect);
}
The constructor with one argument takes that as the width of the stroke. But we could also use the constructor with no arguments, which uses default values:
BasicStroke stroke = new BasicStroke();
Or one with three arguments which specify how lines end and join:
BasicStroke stroke = new BasicStroke(10, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL);
Or one with six arguments for dashed lines:
float dash1[] = {10, 15, 5};
BasicStroke stroke = new BasicStroke(3.0f, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER, 10.0f, dash1, 0f);
This is a matter of convenience - we can use the constructor which gives us the
degree of control we want. They must differ in argument number or type, or otherwise the compiler cannot determine which one we want.
If you look through the Java API, you will see countless examples of oveloaded constructors and methods.