Java Programming/SwingLargeExamples
From Wikibooks, open books for an open world
Navigate User Interface topic: ) |
Navigate Swing topic:
)
|
Contents |
[edit] Calculator
The below example demonstrates using multiple nested layout managers in building a user interface, applying listeners and client properties to components, and how to create components from the Event Dispatch Thread.
[edit] Main Method
- The main method is pretty straight forward. 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 its 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.
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); } }); }
[edit] Constructor
- The constructor is where the calculator is laid out. 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 initialized already you're in trouble. At the time that the method would be called in the parent, the child hasn't been fully initialized yet.
- Note that 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.
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(); }
[edit] Listeners
- Here we start getting into the fun bits. For each of the buttons we want something to listen for the button being pushed. The ActionListener class does just that. 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.
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(); } };
[edit] Building the buttons
- Here we can see how the listeners get assigned to the buttons. 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 usingputClientProperty
then we add the appropriate ActionListener to the button. Action listeners are activated when the component takes its intended action. For aJButton
that means being pressed.
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; }
[edit] Building the panels
- Here we show building the number 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.
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; }
[edit] Full code for 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.