Java Swing Tutorial

Disclaimer: Most of this stuff I have learned from doing project 3 last year and from reading the SDK. I am by no means a Swing expert. If you know of a better way to do something than what is presented here, let us know!

What is Swing?

Swing is a part of the Java Foundation Classes which were made to help develop GUI applications. Swing is built on top of the old windowing system toolkit, AWT (Abstract Window Toolkit). Swing is meant to provide cross-platform GUI features.

Swing offers a lot of features, much more than we can cover here. The SDK and Sun tutorial will prove useful references. You can check out Sun's Swing Tutorial for more information regarding particular features and components.

Tutorial Introduction

I figured the best way to get a nice look at Swing would be to work through an extended example. We will make a simple drawing application that allows the user to place dots on the screen with the mouse.


Step 1: A Window, please

Code
First things first. We need a window. Swing provides us with a handy class called JFrame just for this purpose. Let's see how this works:
 1 public MainWindow{
 2
 3         private static void createGUI(){
 4            JFrame frame = new JFrame("Amazing Swing App");
 5            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 6            frame.setSize(new Dimension(320,260));
 7
 8            frame.setVisible(true);
 9
10          }
11
12        public static void main(String args[]){
13          javax.swing.SwingUtilities.invokeLater(new Runnable(){
14             public void run(){
15                createGUI();
16             }
17            });
18     }

Let's look at what is going on. The createGUI() function makes a new JFrame on line 4. The parameter "Amazing Swing App" is the title of the new frame and will appear on the title bar of the window. Line 5 tells Swing that if someone closes the window, the application should quit. This is important to note; just because you close the window doesn't mean the application exits! Line 6 sets the size of the window. Line 8 sets the frames visibility to true. Yup, that is correct. A newly created frame is not visible. You have to tell it to show itself when you know it is ready.

Now we look at the main function. It creates an anonymous instance of Runnable and defines the run function to call createGUI. As you may know from the concurrency in Java lecture, Runnable is an interface used by any class which wants its own thread. The run method is called when the thread starts execution. The invokeLater call is the way Swing likes to use threads. Since coordinating the GUI already presents some timing issues, it is best to let Swing determine when threads should be running.

Now what? There is nothing left in main and the createGUI function just sets up a frame. If you run the application, a window will pop up with the title in the title bar. You can quit by closing the window. But what code is executing once the window appears? Not any of the code we have just written, that is for sure. This is where GUI programming departs from what we are used to. Once our code is done executing, Swing takes over. In fact, for most GUI programs, Swing is really the one in control. But then how do WE get to do anything?

Menu Bar

Code
In order to do something, our GUI should be more than an empty canvas. Swing views many window objects, such as JFrame, as Containers. That is, you can put other Swing classes (or classes which implement Swing interfaces/abstract classes) inside of Containers. These other classes may themselves be containers, which allows for nested containment to occur. Each container is in charge of notifying the classes in it where to draw themselves and when to repaint. However, Swing handles all this notification for you. If you tell the top level container to validate, it will automatically validate everything inside it. Let's add a menu to our JFrame.
 1 public class MainWindow implements ActionListener {
 2
 3     private static void createGUI(){
 4         MainWindow mainWindow = new MainWindow();
 5         JFrame frame = new JFrame("Amazing Swing App");
 6         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 7         frame.setSize(new Dimension(320,260));
 8
 9         /*** setup menu ***/
10         JMenuBar menuBar = new JMenuBar(); /* the menu bar itself */
11         JMenu menu = new JMenu("File");    /* create a new menu for the bar */
12         menuBar.add(menu);                 /* add the menu to the menu bar*/
13
14         JMenuItem item = new JMenuItem("Exit"); /* add an exit item to the file menu */
15         item.addActionListener(mainWindow);     /* set this class as the action listener */
16         item.setActionCommand("exit");          /* set's the text in the action notification */
17         menu.add(item);                         /* add the item to the menu */
18         frame.setJMenuBar(menuBar);             /* add the menu bar to this frame */
19
20         frame.setVisible(true);
21         frame.validate();
22     }
23
24     /**
25      * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
26      * @param e the action to handle
27      */
28     public void actionPerformed(ActionEvent e) {
29         //figure out what action we have 
30        if(e.getActionCommand().equals("exit")){
31            System.exit(0); /* quit */
32        }
33
34     }

I will refer you to the comments for a line-by-line summary. What is important to notice is the creation of the JMenuBar. The menu bar itself does nothing else but contain menus, which in turn contain menu items. We see that these are created and added to the menu bar, which is itself added to the JFrame. MenuBars are different than other items because only one of them can exist for a given JFrame. Also note that the "add" itself only tells Swing to keep track of a class. That is, you can later modify the class and the container will see those changes. validate() tells the component it is called on and every subcomponent to update and redraw.

You probably noticed that the class now implements ActionListener. This is how Swing notifies us when something happens. Most of the time, the application is just sitting, waiting for something to do. When someone clicks on the exit menu item, Swing calls the actionPerformed() method of that items associated listener. You can see above that the listener for the exit menu item is the MainWindow class. When the actionPerformed() method is called, it receives an ActionEvent object which contains information about what happened. The standard way of discerning what happenned is the action command. When creating the exit item, we set its command to be "exit". By default, the action command is the text of the menu item ("Exit" in this case). It may be preferable to have the actual display text be different than the action command text. Regardless, once you figure out what the action was, you can handle the event that prompted the action (calling System.exit in this case).

Drawing with the mouse

Code
 1 public class MainWindow extends JComponent implements ActionListener {
 2     private DrawingComponent canvas;
 3     private static void createGUI() {
 4         /*... (lines omitted)*/
 5         /*** setup main window area */
 6         mainWindow.canvas = new DrawingComponent();
 7         frame.getContentPane().add(mainWindow);  /* add the main window component to the frame */
 8         mainWindow.setLayout(new BorderLayout());  /* use the border layout manager */
 9         mainWindow.add(mainWindow.canvas,BorderLayout.CENTER);
10
11
12         frame.setVisible(true);
13         frame.validate();
14     }

This code now appears towards the end of the createGUI() function. First notice that the mainWindow class now extends JComponent. JComponent is the most generic of Swing classes and can contain other components. However, it cannot be a top-level component. Only JFrame, JDialog and JApplet can do that. Also note that to add to the JFrame, we add to its mainContentPane, not directly to itself. This lets the JFrame distinguish the menu and main content areas.

More interesting is the setLayout() call. Up until now, I have been mentioning how you can add to containers and nest components, but I still haven't discussed how swing decides where to put things. That is where the Layout managers come in. This classes are in charge of placing components inside the container. The Border Layout manager here is very simple. You divide the container into 5 main parts, the center, and four surrounding points, labeled by cardinal directions. There is a really ugly picture to help you visualize the container. There are also other layout managers, such as a GirdLayout and my personal favorite, BoxLayout. After setting up the manager, you can specify settings when adding components. When we add the DrawingComponent, we specify where the particular layout manager we are using should put it.

So what is this DrawingComponent? It is an inner class which we are using to represent the drawing area. It extends JComponent and encapsulates the "drawing" features in our program.

 1 private static class DrawingComponent extends JComponent implements MouseListener{
 2         private BufferedImage _buff;
 3         private int _w = 320, _h = 240;
 4         public DrawingComponent() {
 5             /* set our size*/
 6             this.setPreferredSize(new Dimension(_w, _h));
 7             this.setMaximumSize(new Dimension(_w, _h));
 8             this.setMinimumSize(new Dimension(_w, _h));
 9             this.setDoubleBuffered(true);
10             /* create the buffer we will draw on */
11             _buff = new BufferedImage(_w, _h, BufferedImage.TYPE_INT_RGB);
12             this.addMouseListener(this);
13             revalidate();
14         }
15         /**
16         * Overridden paint method for JComponent. Mostly just draws the BufferedImage.
17         */
18         protected void paintComponent(Graphics g) {
23             Graphics2D g2d = (Graphics2D)g.create();
24             g2d.drawImage(_buff, 0, 0, this);
25             g2d.dispose(); //clean up
26         }
27         private int max(int a,int b){
28             return (a > b) ? a : b;
29         }
30         private int min(int a,int b){
31             return (a < b) ? a : b;
32         }
33         public void mouseClicked(MouseEvent e) {
34              for(int row = e.getX()-2;row<e.getX()+2;row++)
35               for(int col = e.getY()-2;col<e.getY()+2;col++)
36             _buff.setRGB(min(max(row,1),_w)-1,min(max(col,1),_h)-1,new Color(255,0,0).getRGB());
37             repaint();
38         }
39        /* omitted other mouseListener methods */
40
41     }

Whoa. That is a LOT of stuff. Let's break it down. In the constructor we setup the size of the component and create the internal structures. The BufferedImage is a useful java class for representing bitmapped images. A bitmapped image is defined by the color values of its pixels. We also see that this class implements the MouseListener interface, which will let it respond to mouse events.

Great, we have our buffer which reperesents the image, but how do we get the component to paint it? We just override the paintComponent() method. Not surprisingly, this method is called whenever the component needs to draw itself to the screen. There are actually a variety of ways to do this, but this is the one that I'm used to. We grab the "context" for the area we are drawing to and copy our buffer there.

The MouseListener interface actual has 5 methods, but we only use the one seen above. This method is called whenever a mouse click (press & release) has occurred. The MouseEvent structure tells us where the click occurred, relative to the upper-left corner of the component it occurs in. In the handler, we add a 4x4 red dot in the area that was clicked, making sure we don't index out of bounds into the buffer. We then tell this component to repaint, since something has changed.

It is also interesting to note on line 12 that we are adding a mouse listener, not setting one. This implies that multiple classes which implement the appropriate interface can listen to the same events. This is true for all the listener interfaces.

The Final Application

Code

In this last secion, we add another component and the ability to clear our canvas.

 1 public setupGUI(){
 2         ...
 3         /*** setup log */
 4         mainWindow.log = new JTextArea(5,30);
 5         mainWindow.log.setEditable(false);
 6         JScrollPane scrollPane = new JScrollPane(mainWindow.log);
 7         mainWindow.add(scrollPane,BorderLayout.SOUTH);
 8         mainWindow.logEvent("Initialization complete");
 9         ...
10 }

A common feature in GUI applications is a log area, where users can see a history of events. We implement this as a JTextArea inside a JScrollPane. The JTextArea allows us to append text and handles displaying all of its text as a component. However, we don't want the component to keep growing as we add text. We prefer it to stay the same size and instead let the user scroll through the text. The JScrollPane container automates scrolling other componenets. Just plop the text area inside the scroll pane and it will handle displaying the scrollbars when necessary, as well as actually scrolling the component inside when this users clicks on the scrollbar.

The log event function merely calls the append method of JTextArea, appending the text given as a parameter

We have also added a clear option to the file menu. Accordingly, we updated the action performed method and handle the event when a user clicks the clear option. We call the reset method of the DrawingComponent, which simply replaces the current buffer with a blank one and repaints.

I have omitted discussing these additions in detail, because they follow the same general code that we have discussed already. To be honest, the SDK and Sun's site are the best references in developing your application. Just try to plan ahead and familiarize yourself with the tools Swing offers you.


© Brad Chase 2005
syntax highlighted by Code2HTML, v. 0.9.1