Swing Large Examples

From Wikibooks, open books for an open world
Jump to: navigation, search

Content  v  d  e 

Appendix


Calculator[edit]

The example below builds a user interface using multiple nested layout managers, applies listeners and client properties to components, and demonstrates how to create components from the Event Dispatch Thread (EDT).

Main Method[edit]

The main method is pretty straight forward.

Example Main method
public static void main(String[] args) {
    // Remember, all swing components must be accessed from
    // the event dispatch thread.
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            Calculator calc = new Calculator();
            calc.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            calc.setVisible(true);
        }
    });
}

It creates a new calculator, sets it to exit the program when its closed and then makes it visible. The interesting bit about this code is that it doesn't happen in the main thread. It happens in the event dispatch thread. While in a simple application like this it's enough to simply wrap the whole component creation in an invokeLater() to push it into the EDT, larger applications need to be careful that they only access UI components from the EDT. Not doing so can cause mysterious deadlocks in applications.

Constructor[edit]

The constructor is where the calculator is laid out.

Example The constructor
public Calculator() {
    super("Calculator");
 
    JPanel mainPanel = new JPanel(new BorderLayout());
    JPanel numberPanel = buildNumberPanel();
    JPanel operatorPanel = buildOperatorPanel();
    JPanel clearPanel = buildClearPanel();
    lcdDisplay = new JTextArea();
    mainPanel.add(clearPanel, BorderLayout.SOUTH);
    mainPanel.add(numberPanel, BorderLayout.CENTER);
    mainPanel.add(operatorPanel, BorderLayout.EAST);
    mainPanel.add(lcdDisplay, BorderLayout.NORTH);
 
    errorDisplay = new JLabel(" ");
    errorDisplay.setFont(new Font("Dialog", Font.BOLD, 12));
 
    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(mainPanel, BorderLayout.CENTER);
    getContentPane().add(errorDisplay, BorderLayout.SOUTH);
 
    pack();
    resetState();
}

This approach uses inheritance for defining the calculator. Another way of doing it would have been to have a getComponent() method which would return a panel laid out as desired. The calculator would then be a controller dictating the behavior of the panels and own the panel all the components were on. While more complex, that method also scales more easily to larger, more complicated UIs. Inheritance has the problem of child classes having the ability to override methods that are currently being called in the constructor of the parent class. If the overridden methods required a field in the child class to be already initialized, you're in trouble. At the time that the method would be called in the parent, the child hasn't been fully initialized yet.

The constructor uses two BorderLayouts to do the layout. The first is for the main panel, the second is to compose the main panel with the error message panel. Also, each of the subpanels in the main panel are built with their own layout managers. This is a good example of building a more complicated UI as a composite of multiple simpler sub-panels.

Listeners[edit]

Here we start getting into the fun bits. For each of the buttons we want something to listen for the pushed button. The ActionListener class does just that.

Example The listeners
private final ActionListener numberListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        JComponent source = (JComponent)e.getSource();
        Integer number = (Integer) source.getClientProperty(NUMBER_PROPERTY);
        if (number == null){
            throw new IllegalStateException("No NUMBER_PROPERTY on component");
        }
 
        numberButtonPressed(number.intValue());
    }
};
 
private final ActionListener decimalListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        decimalButtonPressed();
    }
};
 
private final ActionListener operatorListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        JComponent source = (JComponent) e.getSource();
        Integer opCode = (Integer) source.getClientProperty(OPERATOR_PROPERTY);
        if (opCode == null) {
            throw new IllegalStateException("No OPERATOR_PROPERTY on component");
        }
 
        operatorButtonPressed(opCode);
    }
};
 
private final ActionListener clearListener = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        resetState();
    }
};

We setup a separate listener for each type of button. Numbers, Decimals, Operators and Clear buttons. Each one processes the message associated with that class of button and only that class of button. While that's fine, how do you determine which button in that class was pressed? Well, Swing provides a way of setting arbitrary properties on objects. This is accomplished by the getClientProperty putClientProperty pair. Below, we tell the difference between the number buttons by which number is associated with the clientProperty of the button. Similarly the operatorListener does the same, but pulling out its own property.

Building the buttons[edit]

Here we can see how the listeners get assigned to the buttons.

Example The buttons
private JButton buildNumberButton(int number) {
    JButton button = new JButton(Integer.toString(number));
    button.putClientProperty(NUMBER_PROPERTY, Integer.valueOf(number));
    button.addActionListener(numberListener);
    return button;
}
 
private JButton buildOperatorButton(String symbol, int opType) {
    JButton plus = new JButton(symbol);
    plus.putClientProperty(OPERATOR_PROPERTY, Integer.valueOf(opType));
    plus.addActionListener(operatorListener);
    return plus;
}

First we set the label for the buttons by passing a String to the constructor of the JButton, then we set the property value that we want using putClientProperty then we add the appropriate action listener to the button. Action listeners are activated when the component takes its intended action. For a JButton that means being pressed.

Building the panels[edit]

Here we show building the number panel.

Example The panels
public JPanel buildNumberPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new GridLayout(4, 3));
 
    panel.add(buildNumberButton(1));
    panel.add(buildNumberButton(2));
    panel.add(buildNumberButton(3));
    panel.add(buildNumberButton(4));
    panel.add(buildNumberButton(5));
    panel.add(buildNumberButton(6));
    panel.add(buildNumberButton(7));
    panel.add(buildNumberButton(8));
    panel.add(buildNumberButton(9));
 
    JButton buttonDec = new JButton(".");
    buttonDec.addActionListener(decimalListener);
    panel.add(buttonDec);
 
    panel.add(buildNumberButton(0));
 
    // Exit button is to close the calculator and terminate the program.
    JButton buttonExit = new JButton("EXIT");
    buttonExit.setMnemonic(KeyEvent.VK_C);
    buttonExit.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    });
    panel.add(buttonExit);
    return panel;
 
}

We want an even grid for the number keys and want them all to be sized the same so we use a GridLayout as our LayoutManager. This grid is 4 rows deep and 3 columns wide. Building the operator panel and the clear panel are both done similarly.

Full code for the Calculator[edit]

Computer code The Calculator
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
 
public class Calculator extends JFrame {
    private static final String NUMBER_PROPERTY = "NUMBER_PROPERTY";
    private static final String OPERATOR_PROPERTY = "OPERATOR_PROPERTY";
    private static final String FIRST = "FIRST";
    private static final String VALID = "VALID";
 
    // These would be much better if placed in an enum,
    // but enums are only available starting in Java 5.
    // Code using them isn't back portable.
    private static interface Operator{
        static final int EQUALS = 0;
        static final int PLUS = 1;
        static final int MINUS = 2;
        static final int MULTIPLY = 3;
        static final int DIVIDE = 4;
    }
 
    private String status;
    private int previousOperation;
    private double lastValue;
    private JTextArea lcdDisplay;
    private JLabel errorDisplay;
 
    public static void main(String[] args) {
        // Remember, all swing components must be accessed from
        // the event dispatch thread.
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                Calculator calc = new Calculator();
                calc.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                calc.setVisible(true);
            }
        });
    }
 
    public Calculator() {
        super("Calculator");
 
        JPanel mainPanel = new JPanel(new BorderLayout());
        JPanel numberPanel = buildNumberPanel();
        JPanel operatorPanel = buildOperatorPanel();
        JPanel clearPanel = buildClearPanel();
        lcdDisplay = new JTextArea();
        lcdDisplay.setFont(new Font("Dialog", Font.BOLD, 18));
        mainPanel.add(clearPanel, BorderLayout.SOUTH);
        mainPanel.add(numberPanel, BorderLayout.CENTER);
        mainPanel.add(operatorPanel, BorderLayout.EAST);
        mainPanel.add(lcdDisplay, BorderLayout.NORTH);
 
        errorDisplay = new JLabel(" ");
        errorDisplay.setFont(new Font("Dialog", Font.BOLD, 12));
 
        getContentPane().setLayout(new BorderLayout());
        getContentPane().add(mainPanel, BorderLayout.CENTER);
        getContentPane().add(errorDisplay, BorderLayout.SOUTH);
 
        pack();
        resetState();
    }
 
    private final ActionListener numberListener = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            JComponent source = (JComponent)e.getSource();
            Integer number = (Integer) source.getClientProperty(NUMBER_PROPERTY);
            if(number == null){
                throw new IllegalStateException("No NUMBER_PROPERTY on component");
            }
 
            numberButtonPressed(number.intValue());
        }
    };
 
    private final ActionListener decimalListener = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            decimalButtonPressed();
        }
    };
 
    private final ActionListener operatorListener = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            JComponent source = (JComponent) e.getSource();
            Integer opCode = (Integer) source.getClientProperty(OPERATOR_PROPERTY);
            if (opCode == null) {
                throw new IllegalStateException("No OPERATOR_PROPERTY on component");
            }
 
            operatorButtonPressed(opCode);
        }
    };
 
    private final ActionListener clearListener = new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            resetState();
        }
    };
 
    private JButton buildNumberButton(int number) {
        JButton button = new JButton(Integer.toString(number));
        button.putClientProperty(NUMBER_PROPERTY, Integer.valueOf(number));
        button.addActionListener(numberListener);
        return button;
    }
 
    private JButton buildOperatorButton(String symbol, int opType) {
        JButton plus = new JButton(symbol);
        plus.putClientProperty(OPERATOR_PROPERTY, Integer.valueOf(opType));
        plus.addActionListener(operatorListener);
        return plus;
    }
 
    public JPanel buildNumberPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(4, 3));
 
        panel.add(buildNumberButton(7));
        panel.add(buildNumberButton(8));
        panel.add(buildNumberButton(9));
        panel.add(buildNumberButton(4));
        panel.add(buildNumberButton(5));
        panel.add(buildNumberButton(6));
        panel.add(buildNumberButton(1));
        panel.add(buildNumberButton(2));
        panel.add(buildNumberButton(3));
 
        JButton buttonDec = new JButton(".");
        buttonDec.addActionListener(decimalListener);
        panel.add(buttonDec);
 
        panel.add(buildNumberButton(0));
 
        // Exit button is to close the calculator and terminate the program.
        JButton buttonExit = new JButton("EXIT");
        buttonExit.setMnemonic(KeyEvent.VK_C);
        buttonExit.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.exit(0);
            }
        });
        panel.add(buttonExit);
        return panel;
 
    }
 
    public JPanel buildOperatorPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(4, 1));
 
        panel.add(buildOperatorButton("+", Operator.PLUS));
        panel.add(buildOperatorButton("-", Operator.MINUS));
        panel.add(buildOperatorButton("*", Operator.MULTIPLY));
        panel.add(buildOperatorButton("/", Operator.DIVIDE));
        return panel;
    }
 
    public JPanel buildClearPanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(1, 3));
 
        JButton clear = new JButton("C");
        clear.addActionListener(clearListener);
        panel.add(clear);
 
        JButton CEntry = new JButton("CE");
        CEntry.addActionListener(clearListener);
        panel.add(CEntry);
 
        panel.add(buildOperatorButton("=", Operator.EQUALS));
 
        return panel;
    }
 
    public void numberButtonPressed(int i) {
        String displayText = lcdDisplay.getText();
        String valueString = Integer.toString(i);
 
        if (("0".equals(displayText)) || (FIRST.equals(status))) {
            displayText = "";
        }
 
        int maxLength = (displayText.indexOf(".") >= 0) ? 21 : 20;
        if(displayText.length() + valueString.length() <= maxLength){
            displayText += valueString;
            clearError();
        } else {
            setError("Reached the 20 digit max");
        }
 
        lcdDisplay.setText(displayText);
        status = VALID;
    }
 
    public void operatorButtonPressed(int newOperation) {
        Double displayValue = Double.valueOf(lcdDisplay.getText());
 
//        if(newOperation == Operator.EQUALS && previousOperation != //Operator.EQUALS){
//            operatorButtonPressed(previousOperation);
//        } else {
            switch (previousOperation) {
                case Operator.PLUS:
                    displayValue = lastValue + displayValue;
                    commitOperation(newOperation, displayValue);
                    break;
                case Operator.MINUS:
                    displayValue = lastValue - displayValue;
                    commitOperation(newOperation, displayValue);
                    break;
                case Operator.MULTIPLY:
                    displayValue = lastValue * displayValue;
                    commitOperation(newOperation, displayValue);
                    break;
                case Operator.DIVIDE:
                    if (displayValue == 0) {
                        setError("ERROR: Division by Zero");
                    } else {
                        displayValue = lastValue / displayValue;
                        commitOperation(newOperation, displayValue);
                    }
                    break;
                case Operator.EQUALS:
                    commitOperation(newOperation, displayValue);
//            }
        }
    }
 
    public void decimalButtonPressed() {
        String displayText = lcdDisplay.getText();
        if (FIRST.equals(status)) {
            displayText = "0";
        }
 
        if(!displayText.contains(".")){
            displayText = displayText + ".";
        }
        lcdDisplay.setText(displayText);
        status = VALID;
    }
 
    private void setError(String errorMessage) {
        if(errorMessage.trim().equals("")){
            errorMessage = " ";
        }
        errorDisplay.setText(errorMessage);
    }
 
    private void clearError(){
        status = FIRST;
        errorDisplay.setText(" ");
    }
 
    private void commitOperation(int operation, double result) {
        status = FIRST;
        lastValue = result;
        previousOperation = operation;
        lcdDisplay.setText(String.valueOf(result));
    }
 
    /**
     * Resets the program state.
     */
    void resetState() {
        clearError();
        lastValue = 0;
        previousOperation = Operator.EQUALS;
 
        lcdDisplay.setText("0");
    }
}

In this "calculator": 4 * 6 = 36, 4 * 9 = 81, 4 * 3 = 9. Error1 in strings:

                case Operator.MULTIPLY:
                    displayValue *= displayValue;
                // This is Error1, must be: displayValue *= lastValue;

After correcting Error1 in this "calculator": 1 / 3 = 3, 5 / 2 = 0.4, 8 / 4 = 0.5. Error2 in strings:

                case Operator.DIVIDE:
                if (displayValue == 0) {
                        setError("ERROR: Division by Zero");
                    } else {
                        displayValue /= lastValue;
                // This is Error2, must be: displayValue = lastValue / displayValue;

After correcting Error2 in this "calculator": 9 - 6 = -3, 6 - 3 = -3, 8 - 4 = -4. Error3 in strings:

                case Operator.MINUS:
                    displayValue -= lastValue;
                // This is Error3, must be: displayValue = lastValue - displayValue;

Error4 in strings:

        if(newOperation == Operator.EQUALS && previousOperation !=
            Operator.EQUALS){
            operatorButtonPressed(previousOperation);
        } else {

After removing this strings calculator work correctly.
Here you can find the similar calculator running on a web like Java applet, with source code and code review available.


Clipboard

To do:
Add screenshots.