Now that you know how to draw beautiful shapes, let’s turn our attention to the JLabel and JButton Swing classes.
JLabel Widgets
A JLabel can be text or an image in a GUI. They are most often used to display information or instructions to the user. As our GUIs become more complex it makes sense to organize code such that our main() test program is as focused as possible and we move most of our logic into a custom class that inherits from JFrame.
Let’s start with a “Hello Java!” example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import javax.swing.JFrame; import java.awt.FlowLayout; import javax.swing.JLabel; import javax.swing.ImageIcon; public class JLabelFrame extends JFrame { public JLabelFrame() { // First call the parent constructor, then we'll add our custom code next. super( "JLabel Demo" ); // Setting the window layout to FlowLayout, will arrange widgets in the // order they are added from left to right and top to bottom. Resizing // the window will automatically re-flow the widgets. setLayout( new FlowLayout() ); // Create a simple text JLabel JLabel textLabel = new JLabel( "Hello Java!" ); add( textLabel ); // Followed by an image JLabel ImageIcon javaIcon = new ImageIcon( "JavaIcon.png" ); JLabel imageLabel = new JLabel( javaIcon ); add( imageLabel ); } } |
Since we are extending the JFrame class, the first thing we do in our JLabelFrame constructor (line 9) is to call the parent (JFrame) constructor to set the window title.
There are different ways that widgets can be arranged in a GUI. The default layout for a JFrame is called BorderLayout. BorderLayout has some restrictions that we’ll get into later so, on line 14, I’m switching the layout to FlowLayout instead. FlowLayout is very flexible and allows us to add as many widgets as we like to the JFrame. It automatically arranges widgets from left to right, top to bottom in the order we add() them. If we resize the window, the FlowLayout manager will automatically “re-flow” the widgets so that as many as possible are visible at one time.
Line 17 is an example of creating a text JLabel. Lines 21 and 22 show creating an ImageIcon object to load an image from disk, and then creating a graphical JLabel.
Here is a test program to instantiate and configure our JLabelFrame. As mentioned earlier, our goal is to keep this test class as lean as possible.
1 2 3 4 5 6 7 8 9 10 11 |
import javax.swing.JFrame; public class JLabelTest { public static void main( String[] args ) { JLabelFrame appWindow = new JLabelFrame(); appWindow.setSize( 320, 240 ); appWindow.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); appWindow.setVisible( true ); } } |
JButton Widgets and Inner Classes
JButton widgets are instantiated and added to a window in much the same way as JLabels. The biggest difference is that buttons are interactive! When a user interacts with a widget, like clicking a button, this is called an event. To respond to user events we need to write an event listener class that contains code that is executed when an event occurs.
In Java, an event listener is written as an inner class. An inner class is a class that’s coded within the body of another class. Consider this:
1 2 3 4 5 6 7 8 9 10 |
public class OuterClass { // may define instance varibles and methods // may also define static variables and static methods. class InnerClass { // may access all of the instance variables and methods of the OuterClass // may define instance variables and methods // may NOT define static variables or static methods. } } |
Here the OuterClass follows all of the Java rules we’ve learned up to this point, but the InnerClass follows slightly different rules. First, an inner class may not be declared as public and therefore cannot be accessed from outside the OuterClass. This scope makes sense because the event listener code will be related to a specific widget created in the OuterClass only. Second, the InnerClass has direct access to all public and private instance variables and methods defined in the OuterClass, and can define it’s own instance variables and methods. And finally, the InnerClass is not allowed to contain any static variables or methods.
Now let’s convert our previous JLabel example into a JButton example with event handling:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
import javax.swing.JFrame; import java.awt.FlowLayout; import javax.swing.JButton; import javax.swing.ImageIcon; import javax.swing.JOptionPane; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; public class JButtonFrame extends JFrame { // These JButton references are declared as instance variables (rather than // local variables) so that they can be directly accessed by the inner class. private JButton textButton; private JButton imageButton; public JButtonFrame() { // Call the parent JFrame constructor to set the title, and switch to FlowLayout super( "JButton Demo" ); setLayout( new FlowLayout() ); // Create a JButton with text on it textButton = new JButton( "Click Me!" ); // Create a ButtonEventListener object ActionListener eventListener = new ButtonEventListener(); // And associate it with the textButton textButton.addActionListener( eventListener ); add( textButton ); // Followed by a JButton with an image on it ImageIcon javaIcon = new ImageIcon( "JavaIcon.png" ); imageButton = new JButton( javaIcon ); // Use the same ButtonEventListener object for this button too imageButton.addActionListener( eventListener ); add( imageButton ); } // Here is the inner class for event handling class ButtonEventListener implements ActionListener { // The ActionListener interface requires that we override the actionPerformed() method. // This method will be called automatically whenever a button event occurs. @Override public void actionPerformed( ActionEvent e ) { // The ActionEvent getSource() method returns a reference to the button widget that was clicked // This allows us to use one event listener for more than one JButton, if desired. if ( e.getSource() == textButton ) { JOptionPane.showMessageDialog( null, "You clicked the TEXT JButton.", "Event!", JOptionPane.INFORMATION_MESSAGE ); } else { JOptionPane.showMessageDialog( null, "You clicked the IMAGE JButton.", "Event!", JOptionPane.INFORMATION_MESSAGE ); } } } } |
On line 6 we import the ActionListener interface which defines a method called actionPerformed() that is called automatically when a button event occurs. Our overridden actionPerformed() method (lines 40 to 50 receives an ActionEvent parameter from the JVM which contains useful information about the event. For example, on line 44 I use its getSource() method to figure out which of the two buttons triggered the event.
On lines 23 and 25, I instantiate our ButtonEventListener and associate it with our textButton. On line 32, the same ButtonEventListener object is associated with our imageButton. There are different ways to do this, you might choose to create two independent (inner) event listener classes, one for each button that do completely different things. In this case, I’m handling events for both buttons using the same event listener.
Here is the test class:
1 2 3 4 5 6 7 8 9 10 11 |
import javax.swing.JFrame; public class JButtonTest { public static void main( String[] args ) { JButtonFrame appWindow = new JButtonFrame(); appWindow.setSize( 320, 240 ); appWindow.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); appWindow.setVisible( true ); } } |
Anonymous Inner Classes
In the previous example, the name of the event listener class that implemented the ActionListener interface (line 37) was called ButtonEventListener. It’s also possible (and common in Java) to create an inner class that implements the ActionListener interface without naming the event listener class. This kind of class definition is called an anonymous inner class.
Consider the JButtonFrame constructor again, this time using an anonymous inner class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public JButtonFrame() { // Call the parent JFrame constructor to set the title, and switch to FlowLayout super( "JButton Demo" ); setLayout( new FlowLayout() ); // Create a JButton with text on it textButton = new JButton( "Click Me!" ); // Create an ActionListener object that implements the actionPerformed() method. ActionListener eventListener = new ActionListener() { @Override public void actionPerformed( ActionEvent e ) { // The ActionEvent getSource() method returns a reference to the button widget that was clicked // This allows us to use one event listener for more than one JButton, if desired. if ( e.getSource() == textButton ) { JOptionPane.showMessageDialog( null, "You clicked the TEXT JButton.", "Event!", JOptionPane.INFORMATION_MESSAGE ); } else { JOptionPane.showMessageDialog( null, "You clicked the IMAGE JButton.", "Event!", JOptionPane.INFORMATION_MESSAGE ); } } }; // And associate it with the textButton textButton.addActionListener( eventListener ); add( textButton ); // Followed by a JButton with an image on it ImageIcon javaIcon = new ImageIcon( "JavaIcon.png" ); imageButton = new JButton( javaIcon ); // Use the same event listener object for this button too imageButton.addActionListener( eventListener ); add( imageButton ); } |
Lines 9 to 21 show how to define and use an anonymous inner class. On line 9 we still have the eventListener reference variable to refer to the ActionListener object that we are instantiating, this allows the second button to share the same event handler (line 29) as before. What’s different is that the inner class is defined within the JButtonFrame() constructor, and we no longer have to name a class to implement the ActionListener interface.
If an event listener object will only be used by one widget, and never shared, it’s possible to go a step further. Let’s change the example code one more time to demonstrate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public JButtonFrame() { // Call the parent JFrame constructor to set the title, and switch to FlowLayout super( "JButton Demo" ); setLayout( new FlowLayout() ); // Create a JButton with text on it textButton = new JButton( "Click Me!" ); // Add an event listener just for this textButton, all in one step textButton.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent e ) { JOptionPane.showMessageDialog( null, "You clicked the TEXT JButton.", "Event!", JOptionPane.INFORMATION_MESSAGE ); } } ); add( textButton ); // Followed by a JButton with an image on it ImageIcon javaIcon = new ImageIcon( "JavaIcon.png" ); imageButton = new JButton( javaIcon ); // Add an event listener just for this imageButton, all in one step imageButton.addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent e ) { JOptionPane.showMessageDialog( null, "You clicked the IMAGE JButton.", "Event!", JOptionPane.INFORMATION_MESSAGE ); } } ); add( imageButton ); } |
Above we no longer need to define a reference variable to keep track of (and share) one ActionListener object. We are actually instantiating two independent ActionListener objects within the addActionListener() parameter list for each button. At first it may feel strange defining a class within a parameter list, but this is a common short cut in Java. Since each ActionListener object is associated with only one button, we no longer need to figure out which button might have triggered the event and this results in more concise actionPerformed() code.
For simple graphical user interfaces, the two forms of anonymous inner classes shown above are widely used in Java. However, as interfaces become more complex using regular (named) inner classes, as in our first JButton example, will result in code that is better designed and easier to read and maintain. Imagine how long your constructor method code would become if you have dozens of interactive widgets all with their own anonymous inner classes for event handling!
You Try!
- Write a GUI application to help track student success. Your solution should look something like this. Include two buttons to keep track of the number of passes and fails, with a label to give an ongoing status update. Use a regular (not anonymous) inner class to take care of the event handling for both buttons. Hint: the JLabel class has a method that you can use to update the text on a JLabel object any time after it has been instantiated.
- Modify your solution to use an anonymous inner class for event handling instead. Which method do you prefer more? Why?
- It’s easy to add mouse-over text to JLabel and JButton widgets. Research the setToolTipText() method and add this feature to both buttons in your student success tracker.