Universidad Carlos III de Madrid

Ingeniería de Telecomunicación

Enero-Mayo 2010 / January-May 2010

Graphical User Interfaces

Lab Section1.  Session 4 (lab): Graphical User Interfaces (II)

Exercise Section1.1. Calculator using Swing, now with events

In this exercise, you have to finish the calculator you started to code in the previous lab session. Use your solution to that exercise as starting point. If you do not have the code handy, use the official solution published for that exercise.

In order to simplify the problem, this calculator version has to work just with integers. Therefore, you must disable the "." key in the graphical interface, so that it is not clickable.

The internal operations of the calculator are already coded and available for you to use them. You can find the corresponding code in the class CalculatorLogic, shown below. However, you must link this class with you graphical interface. CalculatorLogic has public methods you need to call when the user presses a button of the calculator. Besides using these methods when pressing a button, you then must invoke the method getDisplay(). This method will tell you what you need to show in your calculator screen.

/**
 * Class that implements the logic of a basic calculator for integer
 * numbers.
 *
 */
public class CalculatorLogic {
    public static final int OPERATOR_NONE = 0;
    public static final int OPERATOR_SUM = 1;
    public static final int OPERATOR_SUB = 2;
    public static final int OPERATOR_MUL = 3;
    public static final int OPERATOR_DIV = 4;

    private int accumulator;
    private int currentOperand;
    private int currentOperator;
    private boolean operandAvailable;

    /**
     * Creates a new instance of the calculator logic. In its initial
     * state the calculator displays just a zero, the value of its
     * internal accumulator is also zero and there is no operator
     * pending to be applied.
     *
     */
    public CalculatorLogic() {
        pressReset();
    }

    /**
     * Returns the information that should be shown at the display of
     * the calculator for its current state.
     *
     * @return The information to be shown at the display.
     *
     */
    public String getDisplay() {
        String display;
        if (operandAvailable) {
            display = Integer.toString(currentOperand);
        } else {
            display = Integer.toString(accumulator);
        }
        return display;
    }

    /**
     * Notifies that the user has pressed a button with a digit.
     *
     * @param digit The digit the user has pressed (0 to 9).
     *
     */
    public void pressDigit(int digit) {
        currentOperand = currentOperand * 10 + digit;
        operandAvailable = true;
    }

    /**
     * Notifies that the user has pressed a button with an operand.
     *
     * @param operator The operand expressed with one of the constants
     *                 OPERATOR_ADD, OPERATOR_SUB, OPERATOR_MUL or
     *                 OPERATOR_DIV.
     *
     */
    public void pressOperator(int operator) {
        if (operandAvailable) {
            performPendingOperation();
        }
        expectNewOperand(operator);
    }

    /**
     * Notifies that the user has pressed the "=" button.
     *
     */
    public void pressEqual() {
        if (operandAvailable) {
            performPendingOperation();
        }
        expectNewOperand(OPERATOR_NONE);
    }

    /**
     * Notifies that the user has pressed the "C" button, which removes
     * the last digit that was typed by the user.
     *
     */
    public void pressDelete() {
        currentOperand /= 10;
        if (currentOperand == 0) {
            operandAvailable = false;
        }
    }

    /**
     * Notifies that the user has pressed the "AC" button, which
     * resets the state of the calculator to its initial state
     * (display 0, accumulator 0, no pending operators.)
     *
     */
    public void pressReset() {
        accumulator = 0;
        expectNewOperand(OPERATOR_NONE);
    }

    private void expectNewOperand(int operator) {
        currentOperator = operator;
        currentOperand = 0;
        operandAvailable = false;
    }

    private void performPendingOperation() {
        switch (currentOperator) {
        case OPERATOR_NONE:
            accumulator = currentOperand;
            break;
        case OPERATOR_SUM:
            accumulator += currentOperand;
            break;
        case OPERATOR_SUB:
            accumulator -= currentOperand;
            break;
        case OPERATOR_MUL:
            accumulator *= currentOperand;
            break;
        case OPERATOR_DIV:
            accumulator /= currentOperand;
            break;
        }
    }
}

Solutions

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

public class Calculator extends JFrame {

    public Calculator() {
        super("Calculator");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        JPanel buttonsPanel = new JPanel(new GridLayout(6, 3));
        CalculatorLogic logic = new CalculatorLogic();
        JLabel display = new JLabel(logic.getDisplay(), SwingConstants.RIGHT);
        ActionListener digitListener = new DigitListener(logic, display);
        ActionListener operatorListener = new OperatorListener(logic, display);
        addButton(buttonsPanel, "AC", operatorListener);
        addButton(buttonsPanel, "+", operatorListener);
        addButton(buttonsPanel, "-", operatorListener);
        addButton(buttonsPanel, "C", operatorListener);
        addButton(buttonsPanel, "*", operatorListener);
        addButton(buttonsPanel, "/", operatorListener);
        addButton(buttonsPanel, "7", digitListener);
        addButton(buttonsPanel, "8", digitListener);
        addButton(buttonsPanel, "9", digitListener);
        addButton(buttonsPanel, "4", digitListener);
        addButton(buttonsPanel, "5", digitListener);
        addButton(buttonsPanel, "6", digitListener);
        addButton(buttonsPanel, "1", digitListener);
        addButton(buttonsPanel, "2", digitListener);
        addButton(buttonsPanel, "3", digitListener);
        addButton(buttonsPanel, ".", null);
        addButton(buttonsPanel, "0", digitListener);
        addButton(buttonsPanel, "=", operatorListener);
        getContentPane().add(display, BorderLayout.NORTH);
        getContentPane().add(buttonsPanel, BorderLayout.CENTER);
        pack();
        setVisible(true);
    }

    private void addButton(Container container, String command,
                           ActionListener listener) {
        JButton button = new JButton(command);
        if (listener != null) {
            button.addActionListener(listener);
        } else {
            button.setEnabled(false);
        }
        container.add(button);
    }

    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    Calculator gui = new Calculator();
                }
            });
    }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JLabel;


public class DigitListener implements ActionListener {

    private CalculatorLogic logic;
    private JLabel display;

    public DigitListener(CalculatorLogic logic, JLabel display) {
        this.logic = logic;
        this.display = display;
    }

    public void actionPerformed(ActionEvent e) {
        logic.pressDigit(Integer.parseInt(e.getActionCommand()));
        display.setText(logic.getDisplay());
    }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JLabel;


public class OperatorListener implements ActionListener {

    private CalculatorLogic logic;
    private JLabel display;

    public OperatorListener(CalculatorLogic logic, JLabel display) {
        this.logic = logic;
        this.display = display;
    }

    public void actionPerformed(ActionEvent e) {
        String command = e.getActionCommand();
        if (command.equals("+")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_SUM);
        } else if (command.equals("-")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_SUB);
        } else if (command.equals("*")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_MUL);
        } else if (command.equals("/")) {
            logic.pressOperator(CalculatorLogic.OPERATOR_DIV);
        } else if (command.equals("AC")) {
            logic.pressReset();
        } else if (command.equals("=")) {
            logic.pressEqual();
        } else if (command.equals("C")) {
            logic.pressDelete();
        }
        display.setText(logic.getDisplay());
    }
}

Exercise Section1.2. Cursor tracking

In this exercise you will code a graphical interface where the eyes of a face will follow your mouse pointer when it is inside the window. The application must have a similar appearance than the one shown in the picture below:

The outside border of the eye will be drawn as a circle of radius value of 60. The pupil of the eye has a radius value of 10. Its position depends on the mouse cursor position inside the window: it must always be in the straight line joining the cursor with the center of the eye, 30 pixels away from this center. You can see an example below:

As you can see in the following scheme, having the coordinates of the center of the eye (o), the coordinates of the cursor (C) and the distance d = 30 pixels, you can calculate the position of the pupil (X) by solving a problem on rectangle triangles similarity:

Section 1

First, write the code needed for your application to work with respect to a fixed point. This is, the pupils will align with respect to a testing fixed point you will define, instead of following the mouse cursor. This way you will not need to manage any event by now.

You can use the code from the canvas exercise of the previous lab session. In order to ease the drawing of circles, you can add the following method to your code. This method receives the coordinates of the circle center, radius, color and a boolean indicating if you want to paint the filling or the border. In order to draw a circle with white filling and black border, first draw a circle with white filling, and then a circle with no filling and black border.

    private void drawCircle(Graphics g, int x, int y, int radius, Color color,
                            boolean filled) {
        int posX = x - radius;
        int posY = y - radius;
        g.setColor(color);
        if (filled) {
            g.fillOval(posX, posY, 2 * radius, 2 * radius);
        } else {
            g.drawOval(posX, posY, 2 * radius, 2 * radius);
        }
    }

Test your application with different fixed points.

Section 2

The next step is to make the eyes to follow the mouse cursor. In order to do that, you need to make your canvas to be the listener MouseMotionListener of itself. This is, from the panel itself, you need to manage the events happening when the cursor moves inside the panel.

Every time you detect the movement of the mouse cursor, you need to collect its new coordinates and force the drawing to be updated by invoking the method repaint() from the panel. When you invoke this method, you are asking the panel to redraw the canvas again. As part of this operation, the panel will invoke the method paintComponent() (the one you just coded). This method must redraw the figure, taking into account the new cursor position.

Section 3

Next, we want the application to switch between two modes every time the user makes a left-click with the mouse. In the first mode, the pupils will follow the mouse as in the previous section. In the second mode, the pupils will not follow the mouse. They will be in the default position (the center of the eye).

In order to detect when the user makes a click with the mouse, you need your panel to be the MouseListener of itself.

Section 4

Finally, we want the face to be angry, as shown in the following picture, when the mouse cursor is not inside the window. We want also the face to recover the smile when the cursor comes back inside the window. Take a look at the MouseListener API in order to know how to detect when the mouse cursor moves in or out the window.

Solutions

import java.awt.Dimension;

import javax.swing.JFrame;

public class EyesApplication extends JFrame {

    public EyesApplication() {
        super("Eyes");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        getContentPane().add(new EyesPanel(new Dimension(640, 480)));
        pack();
        setVisible(true);
    }

    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    EyesApplication gui = new EyesApplication();
                }
            });
    }
}
import java.awt.Dimension;
import java.awt.Graphics;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

import javax.swing.JPanel;


public class EyesPanel extends JPanel
    implements MouseListener, MouseMotionListener {
    private Dimension preferredSize;
    private Eye leftEye;
    private Eye rightEye;
    private Mouth mouth;
    private int cursorX;
    private int cursorY;
    private boolean track;
    private boolean happy;

    public EyesPanel(Dimension preferredSize) {
        this.preferredSize = preferredSize;
        int centerX = (int) preferredSize.getWidth() / 2;
        int centerY = (int) preferredSize.getHeight() / 2;
        leftEye = new Eye(centerX - 80, centerY, 60, 30, 10);
        rightEye = new Eye(centerX + 80, centerY, 60, 30, 10);
        mouth = new Mouth(centerX, centerY + 100, 260, 30);
        cursorX = centerX;
        cursorY = centerY;
        track = true;
        happy = false;
        addMouseListener(this);
        addMouseMotionListener(this);
    }

    public Dimension getPreferredSize() {
        return preferredSize;
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        leftEye.draw(g, cursorX, cursorY, track);
        rightEye.draw(g, cursorX, cursorY, track);
        mouth.draw(g, happy);
    }

    public void mouseClicked(MouseEvent e) {
        if (e.getButton() == MouseEvent.BUTTON1) {
            track = !track;
            repaint();
        }
    }

    public void mouseEntered(MouseEvent e) {
        happy = true;
        repaint();
    }

    public void mouseExited(MouseEvent e) {
        happy = false;
        repaint();
    }

    public void mousePressed(MouseEvent e) {
        // Nothing to do
    }

    public void mouseReleased(MouseEvent e) {
        // Nothing to do
    }

    public void mouseMoved(MouseEvent e) {
        if (track) {
            cursorX = e.getX();
            cursorY = e.getY();
            repaint();
        }
    }

    public void mouseDragged(MouseEvent e) {
        mouseMoved(e);
    }


}
import java.awt.Color;
import java.awt.Graphics;


public class Eye {
    private int centerX;
    private int centerY;
    private int outerRadius;
    private int innerRadius;
    private int pupilRadius;

    public Eye(int centerX, int centerY, int outerRadius, int innerRadius,
               int pupilRadius) {
        this.centerX = centerX;
        this.centerY = centerY;
        this.outerRadius = outerRadius;
        this.innerRadius = innerRadius;
        this.pupilRadius = pupilRadius;
    }

    public void draw(Graphics g, int cursorX, int cursorY, boolean track) {
        // Draw the outer circle of the eye
        drawCircle(g, centerX, centerY, outerRadius, Color.WHITE, true);
        drawCircle(g, centerX, centerY, outerRadius, Color.BLACK, false);

        // Draw the pupil of the eye
        if (track) {
            int cursorRelX = cursorX - centerX;
            int cursorRelY = cursorY - centerY;
            double hypotenuse1 = Math.sqrt(cursorRelX * cursorRelX
                                           + cursorRelY * cursorRelY);
            double ratio = hypotenuse1 / innerRadius;
            int eyeRelX = (int) (cursorRelX / ratio);
            int eyeRelY = (int) (cursorRelY / ratio);
            drawCircle(g, centerX + eyeRelX, centerY + eyeRelY,
                       pupilRadius, Color.BLACK, true);
        } else {
            drawCircle(g, centerX, centerY, pupilRadius, Color.BLACK, true);
        }
    }

    private void drawCircle(Graphics g, int x, int y, int radius, Color color,
                            boolean filled) {
        int posX = x - radius;
        int posY = y - radius;
        g.setColor(color);
        if (filled) {
            g.fillOval(posX, posY, 2 * radius, 2 * radius);
        } else {
            g.drawOval(posX, posY, 2 * radius, 2 * radius);
        }
    }
}
import java.awt.Color;
import java.awt.Graphics;


public class Mouth {
    private int posX;
    private int posY;
    private int width;
    private int height;

    public Mouth(int posX, int posY, int width, int height) {
        this.posX = posX;
        this.posY = posY;
        this.width = width;
        this.height = height;
    }

    public void draw(Graphics g, boolean happy) {
        if (happy) {
            g.setColor(Color.WHITE);
            g.fillPolygon(new int[] {posX - width / 2, posX + width / 2, posX},
                          new int[] {posY, posY, posY + height}, 3);
            g.setColor(Color.BLACK);
            g.drawPolygon(new int[] {posX - width / 2, posX + width / 2, posX},
                          new int[] {posY, posY, posY + height}, 3);
        } else {
            g.setColor(Color.BLACK);
            g.drawLine(posX - width / 2, posY, posX + width / 2, posY);
        }
    }
}

Homework Section2.  Homework

Exercise Section2.1.  Tic Tac Toe

In this exercise you will code a well-known game, the Tic Tac Toe. This game serves as example of a Graphical User Interface (GUI) that integrates the corresponding Events Manager.

Section 1

Code in Java the Tic Tac Toe board, as shown in the picture below. In order to do that, create first a class called TicTacToe. Use the JFrame, JPanel, JLabel and JButton classes from javax.swing package, and BorderLayout and GridLayout classes from java.awt package to create your board. Include an attribute called player in your TicTacToe class. This attribute will store who is the current player. Implement also the corresponding get and set methods. Finally, code a main method that let you test the TicTacToe class.

Section 2

Implement a class TicTacToeListener, which will manage the events happening when the users press the buttons in the board. This class must implement the ActionListener interface, and have two attributes: an instance of the TicTacToe class (since the listener will need to use the TicTacToe class methods); and an instance of the label that shows who is the current player. The constructor of this class will be in charge of initializing these two attributes.

The method actionPerformed you will implement in this class must include the following functionality:

  • Disable the button that was just pressed.

  • Show (in the button) an "X" if the player 1 presses the button, or an "O" if it is player 2 who presses the button.

  • Update the value of the attribute player in order to change the turn.

  • Update the label in the board so that it shows the player who is currently in turn and its associated symbol ("X" or "O").

Section 3

Implement in the TicTacToe class a new method called boolean isWinner(String value). This method will check if a player wins by inspecting the associated symbol. You have to take into account every possible combination of three consecutive marked positions (with the same symbol) to decide if there is a winner.

Invoke this method from actionPerformed. When a player wins, show a message in the screen using the JOptionPane class.

Solutions

import java.awt.BorderLayout;
import java.awt.GridLayout;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class TicTacToe extends JFrame {

    private boolean player = false;
    JButton[] buttons = new JButton[9];			
	
    public TicTacToe() {
        super("TicTacToe");
        getContentPane().setLayout(new BorderLayout());		
        JLabel display = new JLabel("Player 1: X");
        JPanel buttonsPanel = new JPanel(new GridLayout(3, 3));
        for (int i = 0; i < 9; i++){
            buttons[i] = new JButton();
            buttonsPanel.add(buttons[i]);
            buttons[i].addActionListener(new TicTacToeListener(this, display));
        }
        getContentPane().add(display, BorderLayout.NORTH);
        getContentPane().add(buttonsPanel, BorderLayout.CENTER);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(320,240);
        setVisible(true);
    }
		
    public boolean getPlayer(){
        return player;
    }
	
    public void setPlayer(boolean player){
        this.player = player;
    }	

    /**
     * Calculates if a player is a winner checking the eight possible ways
     * of being a winner in tictactoe
     * @param value. The value representing the player ("X") or ("O")
     * @return If the player represented by the value is a winner
     * 
     */
    
    public boolean isWinner(String value){
        if (buttons[0].getText().equals(value) && buttons[1].getText().equals(value)
            && buttons[2].getText().equals(value)){
            return true;
        } else if (buttons[3].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[5].getText().equals(value)){
           return true;
        } else if (buttons[6].getText().equals(value) && buttons[7].getText().equals(value)
            && buttons[8].getText().equals(value)){
           return true;
        } else if (buttons[0].getText().equals(value) && buttons[3].getText().equals(value)
            && buttons[6].getText().equals(value)){
           return true;
        } else if (buttons[1].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[7].getText().equals(value)){
           return true;
        } else if (buttons[2].getText().equals(value) && buttons[5].getText().equals(value)
            && buttons[8].getText().equals(value)){
           return true;
        } else if (buttons[0].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[8].getText().equals(value)){
           return true;
        } else if (buttons[2].getText().equals(value) && buttons[4].getText().equals(value)
            && buttons[6].getText().equals(value)){
           return true;
        } else {
           return false;
        }	
    }
	
    public static void main(String[] args) {
        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                TicTacToe gui = new TicTacToe();
            }
        });
    }
}
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

public class TicTacToeListener implements ActionListener {

    private TicTacToe t;
    private JLabel display;
	
    public TicTacToeListener(TicTacToe t, JLabel display){
        this.t = t;
        this.display = display;	
    }
	
    public void actionPerformed(ActionEvent e){
        JButton clickedButton = (JButton) e.getSource(); 
        clickedButton.setEnabled(false);
        clickedButton.getParent();
        if (t.getPlayer() == false){
            clickedButton.setText("X");
            t.setPlayer(true);
            display.setText("Player 2: O");
            if (t.isWinner("X")){
            JOptionPane.showMessageDialog(t, "Player 1 wins");
            }
        } else {
            clickedButton.setText("O");
            t.setPlayer(false);
            display.setText("Player 1: X");			
            if (t.isWinner("O")){
                JOptionPane.showMessageDialog(t, "Player 2 wins");
            }			
        }
    }
}