by Steven J. Owens (unless otherwise attributed)
There's a lot to Java Swing programming; unfortunately, a lot of Swing tutorials kind of throw you into the deep end. This is just a quick brain dump of some general points to help you "get" swing. I wrote this because I found it too painful to get started with Swing, and then when I came back to Swing after a long absence, I found it almost as painful to get re-started.
This is not going to be a thorough Swing tutorial, or even what I would call a "real" Swing tutorial. For one thing, I don't have enough experience with Swing to write that. What I'm trying to do here is get you oriented, give you the ten-thousand-foot view, and point out the gotchas you're going to have to figure out.
Another thing that a lot of swing tutorials do is drown you in details without ever giving you a chance to get to practical application. So at the end you'll find a simple, minimalist example Swing program. It simply pops up a JFrame that contains a JPanel that contains a JScrollPane that contains a JTable (see my comment about deep nesting, below).
To make really nice-looking interfaces takes some work, but it's doable. There's a Sun Java swing guy who has a blog, Romain Guy:
http://weblogs.java.net/blog/gfx/
Romain has done some some really, really cool looking stuff. Functional comes first, cool comes later, but it's important to know that it's possible.
To "get" Swing you need to get three things:
Various Java IDEs have WYSIWYG GUI creation tools. Some people swear by them, other people swear at them. These tools generally suffer from the same problem that WYSIWYG web design tools suffer from - the generated source is unmaintainable.
Also, they're generally one-way - once you start customizing the code, forget about using the tool anymore. Only, of course, you got here by using the GUI creation tool, so by definition you're in over your head at this point...
A Swing GUI basically consists of three interlocking subsets of elements, in addition to the actual application code.
The visual components are the discrete elements that appear on the screen - the buttons, etc. The layout managers are the part that controls how all the visual components get arranged and displayed. The listeners are object instances you use as connectors, to hook together the visual components to the java classes that implement your functionality.
Every Swing tutorial starts off talking about MVC (Model/View/Controller) so I'll just hit it with a paragraph and get it out of the way:
MVC a pretty common and generally useful approach for carving up the code and making it easier to keep organized. The Model is your normal application stuff - the part that loads, munges and alters data and keeps track of it. Typically the View is the visual elements -- they load data from the Model and display it. When you click on a display button in the View, it calls a method to do something. That method lives over in the Controller area, which is what we call the code responsible for acting as a go-between between the View and the Model. The Controller code figures out how to convey whatever you did in the View back into the Model. And then the View notices the Model has changed and refreshes its rendering of data from the Model.
In this tutorial I'm lumping the controller and the model together as the business logic, but of course you're going to use good design and keep those cleanly separated, right?
Right. Moving right along...
You have various specific components that provide different visual features. I'm going to call these the visual components.
For example, JTables for displaying things in a table, JScrollPanes for displaying things in a scrolling pane - and you can put a JTable inside a JScrollPane to provide a scrolling table, and so forth.
There're a whole bunch of different visual components; you'll need to learn what they are, and how to use them.
There's a mess lurking here due to the original Java GUI API, which was called AWT, and the "new" API, Swing, and "lightweight" components vs. "heavyweight" components, etc, but I'm just going to sidestep that; for now just focus on using Swing components that start with a "J" and research this heavy/light thing later.
However, you have to figure out how those visual components are going to be arranged on your screen. That's where the layout managers come in. You use a layout manager to define the visual relationships between the visual components.
Layout managers are analogous to a web browser rendering HTML. By definition lay managers are generally supposed to be a little smart about how they lay out the components, enabling the layout manager to adapt when the window is displayed at different sizes.
However, Swing layout managers are generally much more complex and much harder to work with than HTML. Unfortunately, you can't use XML to specify your layout (I have come across a few experimental Java GUI tools that use XML markup to generate the GUI, but AFAIK these aren't in mainstream use).
This is, again, one of those topics that you're going to have to spend some time learning about and figuring out.
[I generally feel like layout managers just shouldn't have to be that hard to use, but then again, I haven't produced the ultimate Swing layout manager, so I guess it's too hard for me to get around to doing it.]
So, now you have windows popping up that contain components arranged in some layout. But you need those buttons to do things when the user clicks on them. The general scheme that Swing uses to do this is called "listeners". This is generally harder to learn than it should be, because it's really pretty straightforward, but the language is sorta twisty.
In general, the listener idea is that two sets of objects - the visual gui component objects on one side and the business logic objects on the other - need to be kept in two cleanly separated groups, but also need to be connected in some fashion. When you click on a button in a visual GUI component you want something to happen in the business logic.
For example, in old-fashioned Visual Basic code, poorly trained programmers would just cram the business logic code into the individual buttons and such. The result was an unholy mess to sort out. So instead, you put the business logic elsewhere, and have the button code simply call the right method on the business logic code, and that's it, no other code in the button.
In Swing, you take it one step further; instead of simply typing your own invoke-the-logic code into the the button's "I've-been-clicked" method when you write the code, you instead put your "I've been clicked" code in a separate method and pass a reference to the object holding that method into the button via the button's addListener() method. The object you pass in is called (big surprise) a listener.
However, this now makes things a bit more complicated, since you have to pass in an object reference, not a method reference. There's no such thig as a method reference in java, after all. So how does the button figure out which method to call on the object? There's a predefined listener/listenee method scheme, which is defined as an interface (in the java class/interface sense of the word, not the graphical-user-interface sense of the word) in the java Swing API.
If life were simple, Swing would just have two interfaces -- the listenee (source of events) and the listener (consumer of events). Unfortunately, life isn't simple.
http://java.sun.com/docs/books/tutorial/uiswing/events/eventsandcomponents.html
It's a bit more complex but in general you have several listener interfaces for different types of listeners. The listener interfaces follow a roughly similar pattern (methods that are called when a given event occurs and are passed as a parameter an event object) and the components - the listenees - follow a similar pattern of having an "addFooListener(FooListener e)" method.
What often makes this even more complicated is the Swing programming custom of using anonymous classes to implement the listeners. Anonymous classes are actually a very powerful and handy technique, but they tend to add an extra layer to an already confusing situation.
In a nutshell, you often have a single business logic class that has to listen for several different Listener events. However, the listener interface only has a single method that can be called. For example, the ActionListener interface has just the method:
actionPerformed(ActionEvent e)
So when yourLogicClassInstance.actionPerformed() is called by one of three different visual components, how do you figure out what to do?
You could use a big if/else clause in the actionPerformed() method to examine the event, figure out the source and call the right method. Kind of ugly.
A cleaner approach is to insert an extra layer of adapters between the view and the controllers; have an actionPerformed() method on each of these adapter classes, and have it invoke the appropriate method on your logic class.
Strictly speaking, there's no real reason you couldn't just code up each of these adapter classes as a separate class. However, for some reason people get twitchy at the idea of having scads of these little adapter classes littering their code. Maybe that's reasonable - after all, a Swing app is complicated enough without adding another zillion little classes that each just convey a button press or something to one of the real classes.
This is where anonymous inner class listeners come in. They give us a way to define these adapters really quick and off the cuff, in the code that instantiates and assembles all of our Swing components. Let's break that phrase down - anonymous, inner class listeners - and tackle it a piece at a time.
An inner class is a class whose source code is defined inside the source code of another class; for example:
public class Foo { public class InnerFoo { // some methods and instance variables } }
http://java.sun.com/docs/books/tutorial/uiswing/events/generalrules.html#innerClasses
In general, inner classes seem to be for times when you really need more internal organization in a class than simply methods and instance variables. Besides where the source code lives, the chief concrete difference of an inner classes is that the inner class has special access to the innards of its enclosing class. (A classic example of such use is the various Iterator classes.)
An anonymous inner class is one that is defined on the fly, basically if you just need a very simple, minimally customized version of an existing class. For example, if you just need somewhere to stick an actionPerformed method. Here's a brute-force example excerpted from a nice little tutorial over at JavaRanch. The chunk that's in bold is the anonymous inner class:
goButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { doImportantStuff(); } } );
Don't be confused by the extra line breaks I added for readability. The anonymous inner class here is being defined literally in the act of calling the addActionListener() method. You could (and most of the time people do) write that code just as easily this way:
goButton.addActionListener(new ActionListener() { public void actionPerformed( ActionEvent e ) { doImportantStuff(); } });
Go check out the tutorial:
http://www.javaranch.com/campfire/StoryInner.jsp
I'll still be here when you get back. Probably.
What's going on here is that the anonymous inner class can get at anything it needs to get at in the enclosing class, so it's just like you had a different actionPerformed() method for each action event the enclosing class needs to handle. For example, to extend the above sample a bit, you could have a goButton and a stopButton:
goButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { doImportantStuff(); } } ); stopButton.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { doOtherImportantStuff(); } } );
Incidentally, there's a parallel here with how MVC-based java web applications are properly written - only, in the case of java web applications, the listener adapters are instead databeans that carry the form parameters into the business logic and out of the busines logic into the presentation layer.
I'm not even going to pretend to get into multithreading. It's too hairy of a topic and people have written entire books about it, not to mention that it's one of the classic gotchas in Swing programming. The main thing you need to take away from this section is that you need to be aware of how threading affects things in Swing.
In Swing there's a separate thread, called the Swing thread or the Event Dispatch Thread (EDT) or even sometimes the AWT thread. The Swing thread is responsible for updating/repainting Swing components on screen and for calling listeners whenever an event happens.
There are two classic Swing threading mistakes:
How does this happen? Well, imagine you have a button. You click on the button, this causes an Event to be fired and the button calls the actionPeformed() method on all of its listeners. The thread that makes this happen is the Swing thread. If one of those listeners does something that takes a long time (for example, downloading a file), everything else in the Swing GUI freezes up until that gets done and the thread gets back out of the actionPerformed() method.
Like I said, this quickly gets hairy, so do your homework. Meanwhile:
http://java.sun.com/developer/technicalArticles/Threads/swing/
http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html
Here's a simple swing example program. All it does is display a jpanel that holds a jtable that lists all of the environment variables. I'll give you the full listing, then break it down further below.
package com.foo ; import java.util.Properties ; import java.util.Enumeration ; import java.util.Vector ; import java.awt.GridLayout ; import java.awt.Dimension ; import javax.swing.JPanel ; import javax.swing.JTable ; import javax.swing.table.TableModel ; import javax.swing.table.DefaultTableModel ; import javax.swing.SwingUtilities ; import javax.swing.JFrame ; import javax.swing.JScrollPane ; public class LaunchTest extends JPanel { 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() { createAndShowGUI(); } }); } /** * Create the GUI and show it. For thread safety, * this method should be invoked from the * event-dispatching thread. */ private static void createAndShowGUI() { //Make sure we have nice window decorations. JFrame.setDefaultLookAndFeelDecorated(true); //Create and set up the window. JFrame frame = new JFrame("Launch Test"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Create and set up the content pane. LaunchTest newContentPane = new LaunchTest(); newContentPane.setOpaque(true); //content panes must be opaque frame.setContentPane(newContentPane); //Display the window. frame.pack(); frame.setVisible(true); } int width = 800 ; int height = 600 ; JTable table; DefaultTableModel model ; public LaunchTest() { super(new GridLayout(1,0)); setSize(width, height) ; this.model = new DefaultTableModel() ; this.table = new JTable(model) ; table.setPreferredScrollableViewportSize(new Dimension(width, height)); // Disable auto resizing to make the table horizontal scrollable // table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); populateModel() ; //Create the scroll pane and add the table to it. JScrollPane scrollPane = new JScrollPane(this.table); //Add the scroll pane to this panel. add(scrollPane); } public void populateModel() { this.model.addColumn("Property") ; this.model.addColumn("Value") ; Properties props = System.getProperties() ; Enumeration en = props.propertyNames() ; while (en.hasMoreElements()) { String name = (String)en.nextElement() ; String value = props.getProperty(name) ; addRow(name, value) ; if (isPath(name)) { addPath(name, value) ; } } } public void addRow(String name, String value) { Vector v = new Vector(2) ; v.add(name) ; v.add(value) ; this.model.addRow(v) ; } public String separator = System.getProperty("path.separator") ; public boolean isPath(String name) { // return (-1 < name.indexOf(this.separator)) ; return name.endsWith(".path") ; } public void addPath(String name, String value) { String[] paths = value.split(this.separator) ; for (int i = 0; i < paths.length; i++) { addRow(name + "[" + i + "]", paths[i]) ; } } }
Okay, so let's go through this and dissect it in detail. First off, this class is named LaunchTest, so it's in a file named LaunchTest.java. This is just a little swing demo that I cobbled together when I was experimenting with some installer and packaging tools for Java.
package com.foo ;
Okay, here we have the package statement. Just about every program in java has a package statement. In theory you don't absolutely need a package, but in practice you almost always need it, and even if you don't, you might as well get used to it.
Just to save time, I'll address another issue related to packages. It sucks, but you basically must create a directory structure that mirrors the package structure. In this case I have a project directory; inside the project directory is a com directory, and inside that is a foo directory, in which we find LaunchTest.java.
Now here's a gotcha: it's simplest and safest if you compile and run the class from the project directory. This is annoying as hell, and I guess most people just depend on their IDE to sort it out, but my IDE is emacs and the command-line tools. So to compile this and run it, I'd enter:
/home/puff$ cd project /home/puff/project$ javac com/foo/LaunchTest.java /home/puff/project$ java com.foo.LaunchTest
Okay, so moving on: next we have the imports:
import java.util.Properties ; import java.util.Enumeration ; import java.util.Vector ; import java.awt.GridLayout ; import java.awt.Dimension ; import javax.swing.JPanel ; import javax.swing.JTable ; import javax.swing.table.TableModel ; import javax.swing.table.DefaultTableModel ; import javax.swing.SwingUtilities ; import javax.swing.JFrame ; import javax.swing.JScrollPane ;
These imports list all of the classes we're going to be using, so the java compiler can sort out whether or not we're making any stupid mistakes. To do this, the java compiler needs to know what methods and arguments are available. To know that, it needs to know what class files to load. Note that I'm explicitly listing every individual class. I don't really have to. You can use simply import all of the classes in a package:
import java.util.* import java.awt.* import javax.swing.*
Which way to go with this is a subject of some watercooler debate. I prefer to be specific when it makes sense :-). In general I define "when it makes sense" as "in such a way that the import statements convey useful information." So, for example, if I'm using stuff that people use all the time (like java.util) I'll be more likely to import it all. If I'm importing so many classes that it starts to look like a package listing, then I'll be more likely to import it all. And so forth.
Next we have the main class declaration. The class is named LaunchTest because, as I said above, I wrote this to use in testing out some different installation and packaging tools for java-on-the-desktop application development.
public class LaunchTest extends JPanel {
In this case, we're extending JPanel, because the LaunchTest is actually going to be a JPanel that we're going to insert into the JFrame that we display.
Strictly speaking, we might want to create a distinct class to start
this all, like LaunchTestStarter
, to contain this next
bit, the main() method that fires it all off. However, since this was
just a simple little example I put together to play with the app
installer stuff, I just put that main() method right in LaunchTest.
If I find time to come back and hit this stuff again, I'll probably
extract it out into a separate class, just to make it a more realistic
example.
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() { createAndShowGUI(); } }); }
In java, everything happens with object instances. If you're not using object instances, you're not really doing java. However, something has to get the ball rolling, something has to set up an interconnected web of objects - a bunch of objects that have references to each other. That's the job of the main method.
These objects exist inside a context of an even larger set of objects - the java environment, and specifically a framework of code, in this case Swing. The framework conveys input/output to the web of objects by invoking predefined methods (that's what makes it a framework - you define methods that fit the prescribed framework, and ba-da-bing, the framework can make things happen for you). Your program reacts to the input/output by invoking other objects, creating more instances, etc.
But something needs to set up the dominos at the start, and that's where the main() method comes in.
The main() method is declared with the keyword static, which means that you can run the method directly, without needing a class instance. When you enter "java com.foo.LaunchTest", you're asking the java interpreter to start up and then to find the class LaunchTest and invoke it's main() method.
In this case, main() calls the SwingUtilities.invokeLater() method, passing in an anonymous inner class that extends Runnable. That anonymous inner class has one job - to invoke LaunchTest's static createAndShowGUI() method.
Let's repeat that, this time from the inside out. Here we define a method run(), that will invoke LaunchTest's static method createAndShowGUI():
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() { createAndShowGUI(); } }); }
This run() method is the majority of this anonymous inner class:
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() { createAndShowGUI(); } }); }
And we're passing that anonymous Runnable to SwingUtilities.invokeLater():
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() { createAndShowGUI(); } }); }
By the way, you'll find that often code formatting is left out of anonymous inner class listeners:
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() { createAndShowGUI(); } }); }
In this simple four lines we've opened up a whole ball of java wax, using both threads (runnables) and anonymous classes. You'll find that there's a lot of this sort of thing in Java.
/** * Create the GUI and show it. For thread safety, * this method should be invoked from the * event-dispatching thread. */ private static void createAndShowGUI() { //Make sure we have nice window decorations. JFrame.setDefaultLookAndFeelDecorated(true); //Create and set up the window. JFrame frame = new JFrame("Launch Test"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Create and set up the content pane. LaunchTest newContentPane = new LaunchTest(); newContentPane.setOpaque(true); //content panes must be opaque frame.setContentPane(newContentPane); //Display the window. frame.pack(); frame.setVisible(true); } int width = 800 ; int height = 600 ; JTable table; DefaultTableModel model ; public LaunchTest() { super(new GridLayout(1,0)); setSize(width, height) ;this.model = new DefaultTableModel() ; this.table = new JTable(model) ; table.setPreferredScrollableViewportSize(new Dimension(width, height)); // Disable auto resizing to make the table horizontal scrollable // table.setAutoResizeMode(JTable.AUTORESIZEOFF); populateModel() ; //Create the scroll pane and add the table to it. JScrollPane scrollPane = new JScrollPane(this.table); //Add the scroll pane to this panel. add(scrollPane); } public void populateModel() { this.model.addColumn("Property") ; this.model.addColumn("Value") ; Properties props = System.getProperties() ; Enumeration en = props.propertyNames() ; while (en.hasMoreElements()) { String name = (String)en.nextElement() ; String value = props.getProperty(name) ; addRow(name, value) ; if (isPath(name)) { addPath(name, value) ; } } } public void addRow(String name, String value) { Vector v = new Vector(2) ; v.add(name) ; v.add(value) ; this.model.addRow(v) ; } public String separator = System.getProperty("path.separator") ; public boolean isPath(String name) { // return (-1 < name.indexOf(this.separator)) ; return name.endsWith(".path") ; } public void addPath(String name, String value) { String[] paths = value.split(this.separator) ; for (int i = 0; i < paths.length; i++) { addRow(name + "[" + i + "]", paths[i]) ; } } }