Using the Swing Application Framework (JSR 296)
By John O'Conner, July 2007
If you've developed many applications using a Swing-based graphical user interface (GUI), you've probably solved some common problems over and over again. Those problems include managing the application life cycle, event handling, threading, localizable resources, and maybe even persistence.
To save time and effort, you can develop reusable libraries and small application frameworks to use on each new project. An application framework can provide much of the common infrastructure that most applications share. A framework is a reusable library of classes and functionality that help you design and implement applications using consistent designs and patterns.
If you develop Swing applications, you can benefit from the Swing Application Framework, which is currently being developed as part of Java Specification Request (JSR) 296.
Contents
Framework Scope
The term framework can cause apprehension because frameworks can be large, complicated, and overbearing. For small applications, a framework can introduce more complexity than the original system it supposedly helps. However, the Swing Application Framework has goals that minimize any burdensome effects that a larger framework might cause.
The framework's primary goal is to provide the kernel of a typical Swing application, helping programmers to get started quickly and to adopt best practices for just a few elements common to every Swing application: architecture, lifecycle, resource management, event handling, threading, session state, and local storage.
Framework Project Files
As you read about the framework, you may be tempted to experiment with it yourself. Don't resist. You can download the Swing Application Framework project files from its online project location at java.net. Navigate to the project's documents and files section. The project is in the early stages at this time. The examples and source code in this article run correctly with version .50, the project's current version.
Note that the JSR 296 reference implementation libraries, documentation, and source files change frequently, so you may have to adjust or modify the source code in this article as a result. Project files generally follow the following naming conventions, where <version>
represents a changing version number:
ApplicationFramework-<version>.jar
ApplicationFramework-<version>-doc.jar
ApplicationFramework-<version>-src.zip
You can use the framework by downloading just the library and documentation. If you're interested in implementation details, consider downloading the source as well, which is easily compiled with an ANT project-based integrated development environment (IDE) such as the NetBeans IDE. Once you have the framework implementation, put its jar
file in your compiler and runtime classpath so that your application can use its application programming interface (API).
The framework's API exists in just one package: application
. Although a few subpackages hold text and icon resources, all the API is in the application
package itself.
Framework Architecture
Two classes help you manage your application: ApplicationContext
and Application
. The Application
and ApplicationContext
objects have a one-to-one relationship.
Those services include the following:
- Localizable resource management
- Task services and monitoring
- Event-action management
- Session-state storage
All Swing Application Framework applications must subclass either the Application
class or its SingleFrameApplication
subclass. The Application
class provides lifecycle methods for launching the application, starting the user interface (UI), and shutting down.
The SingleFrameApplication
adds a default main GUI frame, retrieves and injects default resources, and uses the ApplicationContext
to save and restore simple session state. Session state includes UI component location, size, and configuration.
Both superclasses provide an ApplicationContext
, but the SingleFrameApplication
class provides additional default behaviors that use the context. You probably will not use Application
as your superclass. Most likely, the SingleFrameApplication
class provides the default behavior you need. In the future, other subclasses should be available to handle multiframe applications as well. Regardless of the superclass, your application will use its ApplicationContext
instance to access most services that the framework provides.
Application Life Cycle
All applications have a life cycle of events that are called in a specific order. Your primary Application
or SingleFrameApplication
subclass should start in its static main
method and should launch the application from that point. The supported life cycle includes the following methods, called in this order:
launch
-- You must call this framework method.initialize
-- The framework will invoke this optional overridden method.startup
-- The framework will invoke this overridden method.ready
-- The framework will invoke this optional overridden method.exit
-- You must call this framework method.shutdown
-- The framework will invoke this optional overridden method.
Your application's minimum requirement is to call the launch
method and to override the startup
method. By calling the launch
method, your application begins the life cycle. The launch
method will then call the initialize
, startup
, and ready
methods. You should also handle your application frame's closing by calling the exit
method, which will eventually call the shutdown
method. A few examples should help make the sequence more clear.
Application Launch, Initialization, and Startup
You must create and initialize your GUI on the Swing event dispatch thread (EDT). Many application developers forget this important step. Code Example 1 shows a basic Swing application without the framework, and the code examples that follow add the framework.
Code Example 1
public class BasicApp implements Runnable {
JFrame mainFrame;
JLabel label;
public void run() {
mainFrame = new JFrame("BasicApp");
label = new JLabel("Hello, world!");
label.setFont(new Font("SansSerif", Font.PLAIN, 22));
mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
mainFrame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
mainFrame.setVisible(false);
// Perform any other operations you might need
// before exit.
System.exit(0);
}
});
mainFrame.add(label);
mainFrame.pack();
mainFrame.setVisible(true);
}
public static void main(String[] args) {
Runnable app = new BasicApp();
try {
SwingUtilities.invokeAndWait(app);
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
Adding the basic framework for this example does not really reduce your workload, but you have to start somewhere. Still, even with an example this simple, the framework does several important jobs. Review Code Example 2, which implements the same "Hello, world!" functionality within the framework, and see the description that follows it.
Code Example 2
public class BasicFrameworkApp extends Application {
private JFrame mainFrame;
private JLabel label;
@Override
protected void startup() {
mainFrame = new JFrame("BasicFrameworkApp");
mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
mainFrame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
mainframe.setVisible(false);
exit();
}
});
label = new JLabel("Hello, world!");
mainFrame.add(label);
mainFrame.pack();
mainFrame.setVisible(true);
}
public static void main(String[] args) {
Application.launch(BasicFrameworkApp.class, args);
}
}
Even in this simple example, the framework does several important jobs. First, by using the launch
method, you always ensure that the UI starts on the EDT, something many developers simply forget. For more information about interacting with the EDT, you can read Improve Application Performance With SwingWorker in Java SE 6. Second, this code uses a distinct, well-defined startup
method to create and display the UI components. Finally, by using the Application.launch
method, you begin the application life cycle, which means that the framework will call your overridden lifecycle methods -- such as startup
-- at important points in the application lifetime.
All framework applications must override the startup
method. Use this method to create and display the UI. The framework will call this method on the EDT as a result of your invoking the launch
method. In Code Example 2, the startup
method creates a frame, adds a window listener for closing the frame and exiting, and displays the simple frame.
The launch
method calls the application's optional initialize
method just prior to calling the startup
method. You can use the initialize
method to perform any initial configuration or setup steps. For example, you can process command-line arguments from within the initialize
method. You can also check a database connection or set system properties. In short, the framework provides this method for any non-UI related setup that your application may need before displaying the UI. The Application
and SingleFrameApplication
classes provide an empty method body for the initialize
method. The method does nothing by default.
Code Example 3 subclasses the SingleFrameApplication
class. Subclassing the SingleFrameApplication
class has many benefits over using the Application
class. For example, a SingleFrameApplication
already has a primary JFrame
instance as its main window. The superclass already overrides many of the lifecycle events to provide default behavior. The SingleFrameApplication
class injects resources, implements a simple WindowAdapter
object to handle window closings, implements a shutdown
method, and performs basic operations to save and restore a session. In general, you should probably avoid subclassing the Application
class directly. Using SingleFrameApplication
provides your application with helpful default behaviors.
Code Example 3
public class BasicSingleFrameApp extends SingleFrameApplication {
JLabel label;
@Override
protected void startup() {
getMainFrame().setTitle("BasicSingleFrameApp");
label = new JLabel("Hello, world!");
label.setFont(new Font("SansSerif", Font.PLAIN, 22));
show(label);
}
public static void main(String[] args) {
Application.launch(BasicSingleFrameApp.class, args);
}
}
As you can see, the basic "Hello, world!" application gets noticeably shorter in Code Example 3. The only new APIs here are the show
and getMainFrame
method calls. A SingleFrameApplication
subclass can use the show
method to provide its main, default UI component. The superclass adds the component -- a label in the case of Code Example 3 -- to the main frame and displays the main frame with that component.
The Ready State
After initialization and startup, the framework calls another lifecycle method that you can optionally override. The framework calls your application's ready
method after all initial GUI events related to your UI startup have been processed. Overriding the ready
method is your opportunity to perform tasks that will not delay your initial UI. Place here any work that depends on your UI being ready and visible.
Application Exit and Shutdown
The Application
class implements an exit
method to gracefully shut down the application. According to the Application
implementation, a graceful shutdown involves asking any ExitListener
objects whether exiting is possible, then alerting those same listeners that the Application
will actually shut down, calling the shutdown
method, and finally calling the System.exit
method.
But the Application
class does not call the exit
method directly. Your application should do this if it subclasses the Application
class. However, the SingleFrameApplication
class does this for you when you close the main window frame. It implements a WindowListener
that calls the exit
method when you close the application's main window frame. Regardless of how you finally call the exit
method, you should override the shutdown
method to perform application-specific cleanup before the application terminates completely. The shutdown
method is your opportunity to close database connections, save files, or perform any other final tasks before your application finally quits.
The SingleFrameApplication
superclass implements a simple shutdown
method. It saves its window-frame session state and includes all secondary frame state as well. For this reason, you should remember to call super.shutdown()
if you override this method. Code Example 4 shows you what to do.
Code Example 4
@Override
protected void shutdown() {
// The default shutdown saves session window state.
super.shutdown();
// Now perform any other shutdown tasks you need.
}
Implement the Application.ExitListener
interface to allow your application the chance to veto or approve requests to exit the application. The default exit algorithm includes calls to all listeners before calling the shutdown
method. By implementing the ExitListener
interface, you can alert your customers or users of the impending shutdown operation and even allow them to stop the shutdown.
The ExitListener
interface has two methods:
public boolean canExit(EventObject e)
public void willExit(EventObject e)
Use the canExit
method to respond to the exit request. Return a true
value to allow the exit, false
otherwise. The willExit
method is simply an alert notification, but you can also use it for any preparations you need for the ensuing shutdown.
Code Example 5 shows how you might implement an ExitListener
object. Notice that the example calls the exit
method, which is implemented by the Application
superclass. The exit
method notifies all ExitListener
objects and calls the shutdown
method only if all listeners approve the request to exit.
Code Example 5
public class ConfirmExit extends SingleFrameApplication {
private JButton exitButton;
@Override
protected void startup() {
getMainFrame().setTitle("ConfirmExit");
exitButton = new JButton("Exit Application");
exitButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
exit(e);
}
});
addExitListener(new ExitListener() {
public boolean canExit(EventObject e) {
boolean bOkToExit = false;
Component source = (Component) e.getSource();
bOkToExit = JOptionPane.showConfirmDialog(source,
"Do you really want to exit?") ==
JOptionPane.YES_OPTION;
return bOkToExit;
}
public void willExit(EventObject event) {
}
});
show(exitButton);
}
@Override
protected void shutdown() {
// The default shutdown saves session window state.
super.shutdown();
// Now perform any other shutdown tasks you need.
// ...
}
/**
* @param args the command-line arguments
*/
public static void main(String[] args) {
Application.launch(ConfirmExit.class, args);
}
}
Code Example 5 defines a SingleFrameApplication
that contains a single JButton
in its main frame. When you click that button or attempt to close the main window, the application's ExitListener
confirms the user request. Figure 2 shows the application responding through its listener interface.
Resource Management
Most applications use common resources such as text, icons, colors, and font definitions. If you ever want to localize your application to other locales or platforms, you should externalize resources to facilitate easy modifications or translations to other languages. Resources are defined in ResourceBundle
implementations, which are usually ListResourceBundle
subclasses or properties files.
The framework helps you define and organize resources for individual classes and for the entire application. Resources that are shared throughout the application should exist in a ResourceBundle
file named after your Application
subclass name. Resources for a specific form or class should exist in a ResourceBundle
file named after that specific class. In both cases, the ResourceBundle
implementations must exist in a resources
subpackage immediately below that of the class for which they provide resources.
Table 1 shows the relationship among an application class or form, its ResourceBundle
name, and the ResourceBundle
file name.
Table 1. Relationships Among Application
Class, ResourceBundle
Name, and ResourceBundle
File Name
Class | ResourceBundle Name | ResourceBundle File Name |
---|---|---|
demo.MyApp |
demo.resources.MyApp |
demo/resources/MyApp.properties |
demo.hello.HelloPanel |
demo.hello.resources.HelloPanel |
demo/hello/resources/HelloPanel.properties |
demo.hello.ExitPanel |
demo.hello.resources.ExitPanel |
demo/hello/resources/ExitPanel.properties |
Instead of loading and working with ResourceBundle
files directly, you will use the ResourceManager
and ResourceMap
framework classes to manage resources. A ResourceMap
contains the resources defined in a specific ResourceBundle
implementation. A map also contains links to its parent chain of ResourceMap
objects. The parent chain for any class includes the ResourceMap
for that specific class, the application subclass to which the class belongs, and all superclasses of your application up to the base Application
class.
The ResourceManager
is responsible for creating maps and their parent chains when you request resources. You will use the ApplicationContext
to retrieve ResourceManager
and ResourceMap
objects.
You have three options for working with resources:
- Manually load and use
ResourceMap
objects and their resources. - Use the framework to automatically inject resources into the UI components that need them.
- Use the framework to automatically inject resources into the object fields that need need them.
Manual Resource Management
The ApplicationContext
class provides access to the ResourceManager
and its ResourceMap
instances. Retrieve the context using the getContext
method of your Application
instance. Use the context to retrieve a resource manager and map. Use the map to retrieve resources for your application or specific class. Code Example 6 shows how to retrieve resources and apply them to UI components.
Code Example 6
public class HelloWorld extends SingleFrameApplication {
JLabel label;
ResourceMap resource;
@Override
protected void initialize(String[] args) {
ApplicationContext ctxt = getContext();
ResourceManager mgr = ctxt.getResourceManager();
resource = mgr.getResourceMap(HelloWorld.class);
}
@Override
protected void startup() {
label = new JLabel();
String helloText = (String) resource.getObject("helloLabel", String.class);
// Or you can use the convenience methods that cast resources
// to the type indicated by the method names:
// resource.getString("helloLabel.text");
// resource.getColor("backgroundcolor");
// and so on.
Color backgroundColor = resource.getColor("color");
String title = resource.getString("title");
label.setBackground(backgroundColor);
label.setOpaque(true);
getMainFrame().setTitle(title);
label.setText(helloText);
show(label);
}
// ...
}
You can also retrieve a resource map using the convenience method in the ApplicationContext
instance:
resource = ctxt.getResourceMap(HelloWorld.class);
In Code Example 6, the HelloWorld
class uses three resources: a label's text, a color for the label background, and text for the frame's window title. It gets those resources from a resource map for the HelloWorld
class:
resource = mgr.getResourceMap(HelloWorld.class);
Provide the Class
instance that represents either your Application
class or another specific class in your application. In this example, the resource manager will search and load a ResourceMap
that contains resources from the resources/HelloWorld
resource bundle. In this case, the bundle implementation is a file named resources/HelloWorld.properties
. Code Example 7 shows part of the file.
Code Example 7
helloLabel = Hello, world!
color = #AABBCC
title = HelloWorld with Resources
Figure 3 shows the HelloWorld application. Each of the resources -- the window title, the label text, and the label background color -- were manually retrieved and applied to each specific component. If you use manual resource management, you must provide code to perform all steps of retrieving and using the resources in the appropriate component.
Component Resource Injection
The framework can automatically inject resources into your components. Using the ResourceMap
object's injectComponents
method, you tell the framework to retrieve resources from its resource map chain and to apply them to components. This works very well when you provide your application's root window because the method recursively travels down containers, injecting UI components as it traverses the container hierarchy. To use the injectComponents
method, you must use a naming convention that helps the framework match component names and their resources.
The naming convention is simple. Just use the setName
method on your UI components to give them a name. In your ResourceBundle
files, use that name to define resources for that component. In the resource file, append a period ( .
) and the property name to the component name to define a resource for a specific property.
For example, if you want to define the text property of a button, you should name the button and use the button's name in the resource file. If the button's name is btnShowTime
, you can define its resource text in the resource file using the same btnShowTime
name. Because you want to set the text of the button, the resource name should be btnShowTime.text
.
Code Example 8 shows several resource definitions in a resources/ShowTimeApp.properties
file. Use UI component names and their property names to define injectable resources.
Code Example 8
btnShowTime.text = Show current time!
btnShowTime.icon = refresh.png
txtShowTime.text = Press the button to retrieve time.
txtShowTime.editable = false
Code Example 8 shows resources for two components: btnShowTime
and txtShowTime
. The btnShowTime.text
resource defines the text that should be injected into a button component. The btnShowTime.icon
resource defines the icon that the same button will display. The second component name is txtShowTime
, which is a text component in the application.
You can call injectComponents
directly, but you can save yourself some effort just by subclassing the SingleFrameApplication
framework class. This class defines a show
method that automatically injects component resources. Code Example 9 shows how to use component naming conventions, the SingleFrameApplication
superclass, and the properties file shown in Code Example 8 to inject resources.
Code Example 9
public class ShowTimeApp extends SingleFrameApplication {
JPanel timePanel;
JButton btnShowTime;
JTextField txtShowTime;
@Override
protected void startup() {
timePanel = new JPanel();
btnShowTime = new JButton();
txtShowTime = new JTextField();
// Set UI component names so that the
// framework can inject resources automatically into
// these components. Resources come from similarly
// named keys in resources/ShowTimeApp.properties.
btnShowTime.setName("btnShowTime");
txtShowTime.setName("txtShowTime");
btnShowTime.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Date now = new Date();
txtShowTime.setText(now.toString());
}
});
timePanel.add(btnShowTime);
timePanel.add(txtShowTime);
show(timePanel);
}
// ...
}
Code Example 9 does not explicitly load a ResourceMap
. The framework does that for you. All you have to do is follow the naming conventions for components, create a ResourceBundle
file using the application class name, and use the component names appropriately in the resource file. The framework will find and inject the resources for you. Figure 4 shows the ShowTimeApp
running. Notice that the button label and icon are set correctly using resources from the resources/ShowTimeApp.properties
file.
Field Resource Injection
The framework supports field resource injection too. You can inject resources into fields with types such as String
, Color
, and Font
by marking those fields with the @Resource
annotation. When you use the ResourceMap
object's injectFields
method, the framework will automatically inject resources into fields that you mark with this annotation.
Field resource injection is similar to component resource injection because the framework handles the retrieval for you. However, important differences do exist. For example, field resource injection does not work on container hierarchies. When you inject field resources, the injectFields
method works on a single target object's fields and does not traverse containers.
Again, you should put the resources in your application-level or class-level resource file. Remember to load those resources using the getResourceMap
method, passing in the class object if you want class-level resources instead of application-level resources.
By default, field resources should have a name that contains their class name as a prefix. For example, if the class MyApp
has a field named myIcon
, the resource will have the name MyApp.myIcon
in the ResourceBundle
file and in the corresponding ResourceMap
object.
Code Example 10 shows how you can inject field resources into your code. Not only does this example inject the field, but it also uses class-specific resources defined in a resources/NameEntryPanel
resource bundle. Mark fields with the @Resource
annotation to use field injection.
Code Example 10
public class NameEntryPanel extends javax.swing.JPanel {
@Resource
String greetingMsg;
ApplicationContext ctx;
ResourceMap resource;
/** Creates new form NameEntryPanel. */
public NameEntryPanel(ApplicationContext ctxt) {
initComponents();
ResourceMap resource = ctxt.getResourceMap(NameEntryPanel.class);
resource.injectFields(this);
}
private void initComponents() {
lblNamePrompt = new javax.swing.JLabel();
txtName = new javax.swing.JTextField();
btnGreet = new javax.swing.JButton();
lblNamePrompt.setName("lblNamePrompt");
btnGreet.setName("btnGreet");
// ...
}
private void btnGreetActionPerformed(java.awt.event.ActionEvent evt) {
String personalMsg = String.format(greetingMsg, txtName.getText());
JOptionPane.showMessageDialog(this, personalMsg);
}
private javax.swing.JButton btnGreet;
private javax.swing.JLabel lblNamePrompt;
private javax.swing.JTextField txtName;
}
In order to inject the resources for the NameEntryPanel
class, the resources must be defined in a class resource file. Code Example 11 shows the sample file, which defines a parameterized string with key NameEntryPanel.greetingMsg
. You should name field resources in the resource file using the class name followed by a period ( .
) and the field name: <classname>.<fieldname>
. That naming pattern is the default, but you can override this by providing a different name using the @Resource
annotation's key
element.
Code Example 11
# resources/NameEntryPanel.properties
NameEntryPanel.greetingMsg = Hello, %s, this string was injected!
Conversions and Conveniences
The ResourceMap
class provides automatic conversion for common resource types represented as text strings. This is useful because resource values in a .properties
file are always simple text values. Resource converters, however, can convert the text representation of fonts, color, and icon resources into actual Font
, Color
, and Icon
objects. Other ResourceConverter
classes are available, but the following examples show how to represent these resource types. Of course, conversion of any .properties
file value into a String
type is always supported. Consult the framework documentation for information about other converters.
Code Example 12 shows the resource file and the ConverterApp
source code that demonstrates how to create and use resources with ResourceConverter
objects.
Code Example 12
# resources/ConverterApp.properties
Application.id = ConverterApp
Application.title = ResourceConverter Demo
msg = This app demos ResourceConverter.
font = Arial-BOLD-22
color = #BB0000
icon = next.png
**
public class ConverterApp extends SingleFrameApplication {
protected void startup() {
ApplicationContext ctx = getContext();
ResourceMap resource = ctx.getResourceMap();
String msg;
Color color;
Font font;
Icon icon;
JLabel label;
// Use resource converters to convert text representations
// of resources into Color and Font objects.
msg = resource.getString("msg");
color = resource.getColor("color");
font = resource.getFont("font");
icon = resource.getIcon("icon");
label = new JLabel(msg);
label.setOpaque(false);
label.setForeground(color);
label.setFont(font);
label.setIcon(icon);
show(label);
}
// ...
}
In Code Example 12, notice the format of the resource values. To use the resource converters, you must format the text representation of your resources in specific ways. Each resource type -- whether it be a font, icon, color, or other resource -- has a specific format.
For example, you should use the red (R), green (G), and blue (B) values -- commonly referred to as RGB values -- between 0 and 255 for colors. The accepted formats use RGB color values in any of these forms:
#RRGGBB
for the hexadecimal representation of RGB values#AARRGGBB
for hexadecimal RGB values with an alpha channel (A)R, G, B
for decimal RGB valuesR, G, B, A
for decimal RGB values with an alpha channel
Represent fonts using the form <name>-<style>-<size>
. The name element is simply the font face name. Common names are Arial and Helvetica, for example. Styles can include values such as PLAIN
, BOLD
, or ITALIC
. Finally, the size element is the font size. Examples of valid font resource values are Arial-PLAIN-12
or Helvetica-BOLD-22
.
Icon resources are represented by their file names in your classpath. For example, if you want to create an icon for an image file in your resources
package, you just use the image's file name.
Figure 6 shows the result of running the ConverterApp
code in Code Example 12. The color, font, and icon resources in the panel were converted by a ResourceConverter
object.
The ResourceMap
class provides support for parameterized strings. Code Example 10 uses a parameterized string in its resources but did not use ResourceMap
to insert the parameter. The parameterized string is this:
NameEntryPanel.greetingMsg = Hello, %s, this string was injected!
To use this same resource and insert the parameter as you retrieve the NameEntryPanel.greetingMsg
text, you must provide an additional argument list to the getString
method. In addition to the resource key, you should provide an argument list that contains the data to insert into the parameterized string. Code Example 13 shows code that could replace the original action handler code in Code Example 10. This new code uses the additional convenience functionality of getString
to insert a name into the parameterized resource string.
Code Example 13
private void btnGreetActionPerformed(java.awt.event.ActionEvent evt) {
String personalMsg = resource.getString("NameEntryPanel.greetingMsg", txtName.getText());
JOptionPane.showMessageDialog(this, personalMsg);
}
Actions
To handle UI events, you will implement an ActionListener
object for a specific component. A more powerful form of an ActionListener
is an AbstractAction
object, which implements the javax.swing.Action
interface. The Action
interface allows you to define an event handler. In addition, it lets you associate an icon, text, mnemonic, and other visual elements with the event. Using the Action
interface, often with an AbstractAction
class, you can conveniently put all the relevant visual cues and the event-processing logic for a component in one place.
Another convenient benefit of using Action
interfaces is that you can reuse the same action across multiple UI components. GUIs often provide multiple ways to accomplish a task. For example, to increase the font size of a piece of text, an application might provide both a menu and a button interface to perform the same task. The multiple different ways of accomplishing the same task should behave the same way, and you should enable or disable those actions simultaneously across all associated components -- something that Action
interfaces will help you do.
Despite the convenience of using the Action
interface, action objects have a few problems. First, visual properties should be localized. All too often, developers hardcode the action's visual elements, making localization difficult or impossible. Additionally, manually creating Action
objects can be a chore, especially if you use all the visual components that are available.
The Swing Application Framework helps you manage actions in three ways:
- The framework provides the
@Action
annotation to mark and name methods that will be used byAction
implementations. - An
ActionManager
class createsjavax.swing.ActionMap
instances for classes that contain@Action
methods. - The framework supports localization of
Action
elements such as icons, text, and mnemonics.
The following discussion about Action
objects uses a demo application called ActionApp and a panel named ResizeFontPanel
. Figure 7 shows the demo UI, which provides the context for the Action
class and annotation descriptions.
The @Action
Annotation
Define and name your Action
objects using the @Action
annotation. Mark Action
event-handling methods with this annotation. The annotation has several optional elements, including a way to override the default name of the Action
object. The default action name is the method name. Code Example 14 shows how to use this annotation with code that increases and decreases the font size for a text component.
Code Example 14
public class ResizeFontPanel extends javax.swing.JPanel {
...
@Action
public void makeLarger() {
Font f = txtArea.getFont();
int size = f.getSize();
if (size < MAX_FONT_SIZE) {
size++;
f = new Font(f.getFontName(), f.getStyle(), size);
txtArea.setFont(f);
}
}
@Action
public void makeSmaller() {
Font f = txtArea.getFont();
int size = f.getSize();
if (size > MIN_FONT_SIZE) {
size--;
f = new Font(f.getFontName(), f.getStyle(), size);
txtArea.setFont(f);
}
}
...
}
Code Example 14 defines two Action
methods with default names: makeLarger
and makeSmaller
. When invoked, the actions reset the font for a text area, which is named txtArea
.
The ActionMap
class associates actions with UI components by using the component's setAction
method. Use the Action
object's name. In Code Examples 14 and 15, the names are makeLarger
and makeSmaller
. The ResizeFontPanel
object has a button for both actions. Code Example 15 shows how to bind the Action
objects with the components.
Code Example 15
// ctx is the ApplicationContext instance.
ResourceMap resource = ctxt.getResourceMap(ResizeFontPanel.class);
resource.injectComponents(this);
ActionMap map = ctxt.getActionMap(this);
btnMakeLarger.setAction(map.get("makeLarger"));
btnMakeSmaller.setAction(map.get("makeSmaller"));
Notice that the ApplicationContext
provides the ActionMap
for this class. As a side note, the code also uses the context to inject component resources. An ActionMap
instance stores all the Actions that are associated with this class. In this case, only two Action
objects exist: makeLarger
and makeSmaller
. Retrieve the Action
objects using the ActionMap
instance's get
method, providing the Action
instance name. Bind the UI component and Action
using the component's setAction
method.
Action Resources
Code Example 15 shows how to get the ActionMap
for the ResizeFontPanel
. It also shows how to call setAction
on specific components using the Action
objects defined by the @Action
annotation. But where did the action's icon and text come from in Figure 7? That's also shown in Code Example 15. Take a closer look at the line that retrieves the ActionMap
instance for the ResizeFontPanel
object:
ActionMap map = ctxt.getActionMap(this);
This single line of code does a lot of work behind the scenes. First, as described earlier, it creates Action
instances for each method marked with an @Action
annotation in the target object. Second, it retrieves resources for the Action
from the target's ResourceMap
. The ResourceMap
for this ResizeFontPanel
class includes properties from the resources/ResizeFontPanel.properties
file. This file defines a few resource key-value pairs and includes the data as shown in Code Example 16. The makeLarger
and makeSmaller
actions have text and icons defined by this resource file. The getActionMap
method includes these resources when it creates the ActionMap
for the ResourceFontPanel
object.
# resources/ResizeFontPanel.properties
makeLarger.Action.text = Increase font size
makeLarger.Action.icon = increase.png
makeSmaller.Action.text = Decrease font size
makeSmaller.Action.icon = decrease.png
Because an Action
object's resources exist in a ResourceMap
, you can localize the resources as appropriate for a specific operating system (OS) platform or locale. Follow the usual naming standards for creating alternate ResourceBundle
files. You can find out more about localization and ResourceBundle
naming by consulting the ResourceBundle
documentation .
Tasks
Always use a background thread for input/output (I/O) bound or computationally intensive tasks. Long-running tasks can block the event dispatch thread (EDT). The new Swing Application Framework provides support for starting, stopping, and monitoring the progress of background tasks. Although the SwingWorker
class in the Java SE 6 platform has most of what's needed, the framework provides a Task
class to support more functionality.
The Task
class extends a SwingWorker
implementation, which is similar to the SwingWorker
available in Java SE 6. A TaskService
class helps you execute tasks, and a TaskMonitor
helps you monitor their progress.
The easiest way to use the Task
class is to extend it, override one or more methods, and use the Task
with an Action
handler. You can can monitor the task and get intermediate result information as the task runs. This article will describe only the basic usage associated with event handling.
Code Example 17 creates a Task
class. The NetworkTimeRetriever
class overrides the doInBackground
method, which is the method that performs your long-running task. The Task
class defines several methods that communicate the success -- or failure -- of the completed task. One of those methods is the succeeded
method. The Task
superclass calls succeeded
when the doInBackground
method finishes its work. The succeeded
method runs on the EDT, so you can use it to update GUI components when your task completes successfully. The NetWorkTimeRetriever
is an inner class, so it has access to fields in the NetworkTimeApp
class. The important field that it needs is the txtShowTime
variable, which is a JTextField
instance. The Task
completes its duties when it finally displays the current time provided by the National Institute of Standards and Technology (NIST) time servers.
Code Example 17
public class NetworkTimeApp extends SingleFrameApplication {
JPanel timePanel;
JButton btnShowTime;
JTextField txtShowTime;
@Override
protected void startup() {
// Create components and so on.
// ...
// Retrieve and set Actions.
ActionMap map = getContext().getActionMap(this);
javax.swing.Action action = map.get("retrieveTime");
btnShowTime.setAction(action);
timePanel.add(btnShowTime);
timePanel.add(txtShowTime);
show(timePanel);
}
@Action
public Task retrieveTime() {
Task task = new NetworkTimeRetriever(this);
return task;
}
// ...
class NetworkTimeRetriever extends Task<Date, Void> {
public NetworkTimeRetriever(Application app) {
super(app);
}
@Override
protected Date doInBackground() throws Exception {
URL nistServer = new URL("http://time.nist.gov:13");
InputStream is = nistServer.openStream();
int ch = is.read();
StringBuffer dateInput = new StringBuffer();;
while(ch != -1) {
dateInput.append((char)ch);
ch = is.read();
}
String strDate = dateInput.substring(7, 24);
DateFormat dateFormat = DateFormat.getDateTimeInstance();
SimpleDateFormat sdf = (SimpleDateFormat)dateFormat;
sdf.applyPattern("yy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("GMT-00:00"));
Date now = dateFormat.parse(strDate);
return now;
}
@Override
protected void succeeded(Date time) {
txtShowTime.setText(time.toString());
}
}
}
In Code Example 17, a button prompts the user to retrieve the time from a network server. The button event handler has the @Action
annotation, marking it as an Action
handler in its class. The action has the name retrieveTime
, and the application's ActionMap
stores it. After you retrieve the Action
and set the button's Action
property, the button will contain the resources associated with it too. For example, the btnShowTime
component has an icon and some text that are referenced in a ResourceBundle
for the application. When you click on the button, its action handler method retrievetime
will execute.
Notice that the retrieveTime
method returns a Task
. The method creates a NetworkTimeRetriever
task and returns that object. The framework automatically runs it as a background thread. When the task completes successfully, it updates the UI with the network time. The combination of tasks, actions, and resource injection is a powerful and simple way to handle component event handling.
Figure 8 shows the running NetworkTimeApp
application. This application is similar to the previous ShowTimeApp
application. The primary difference is that the NetworkTimeApp
class retrieves its time from a network time server. As a result, the "Retrieve time!" button must execute a long-running task as a background thread.
Session State
The Swing Application Framework provides a way to save session state when your application exits and restore the state when you restart. Session state is the graphical window configuration of your application. This state includes window size, internal frame locations, selected tabs, column widths, and other graphical properties. Saving the GUI session state allows you to restart the application and return to the same window or form at a later time.
The SingleFrameApplication
class implements session storage for you. When you restart any application subclass of SingleFrameApplication
, all GUI geometry should be restored exactly as it was when you exited the application.
If you use the Application
class and want to store the session state, you must save and restore the state yourself. Fortunately, this is not difficult. Like most functionality in the framework, session storage management starts with the ApplicationContext
class. Use the ApplicationContext
and SessionStorage
classes in the application startup
method to restore state. Use these classes in the shutdown
method to save state. Code Example 18 shows how to save and restore session state using the SessionStorage
class:
Code Example 18
// ...
String sessionFile = "sessionState.xml";
ApplicationContext ctx = getContext();
JFrame mainFrame = getMainFrame();
@Override protected void startup() {
//...
/* Restore the session state for the main frame's component tree.
*/
try {
ctxt.getSessionStorage().restore(mainFrame, sessionFile);
}
catch (IOException e) {
logger.log(Level.WARNING, "couldn't restore session", e);
}
// ...
}
@Override protected void shutdown() {
/* Save the session state for the main frame's component tree.
*/
try {
ctxt.getSessionStorage().save(mainFrame, sessionFile);
}
catch (IOException e) {
logger.log(Level.WARNING, "couldn't save session", e);
}
// ...
Note that applications will not usually need to create their own SessionStorage
instance. Instead, you should use the ApplicationContext
object's shared storage:
SessionStorage ss = ctxt.getSessionStorage();
Local Storage
Session storage depends on the framework class LocalStorage
. The LocalStorage
class helps the SessionStorage
class do its work, but it can also help you to store simple XML-encoded representations of any JavaBean component. The LocalStorage
class uses the XMLEncoder
and XMLDecoder
classes to encode and decode XML files using your application's objects.
Again, the ApplicationContext
provides access to a shared LocalStorage
instance. You should retrieve the LocalStorage
instance like this:
LocalStorage ls = ctxt.getLocalStorage();
Now you can use the object's save
and load
methods to encode and decode objects to your local storage. The LocalStorage
class uses your home directory as a base subdirectory for determining the default location of storage files.
Code Example 19 shows how to use the LocalStorage
class to store a list of phone numbers named phonelist.xml
. In this example, a JList
component model is both loaded and saved with an application context's shared LocalStorage
instance. The variable file
contains the file name that will contain the list's contents. You can see the entire program listing for this and all other code examples in the demo source code, which is provided as a downloadable link at the end of this article.
Code Example 19
@Action
public void loadMap() throws IOException {
Object map = ctxt.getLocalStorage().load(file);
listModel.setMap((LinkedHashMap<String, String>)map);
showFileMessage("loadedFile", file);
}
@Action
public void saveMap() throws IOException {
LinkedHashMap<String, String> map = listModel.getMap();
ctxt.getLocalStorage().save(map, file);
showFileMessage("savedFile", file);
}
Figure 9 shows the My Little Black Book application. Notice the tool tip and labels. All these resource items are in an application resource file. All the buttons and text fields have associated actions that provide labels, tips, and event handling.
The text field at the bottom of the application window shows the file name of the phone list. Files created by the LocalStorage
class always exist in platform-specific locations on your host system. The files are, however, always stored under the application user's home directory.
Summary
The Swing Application Framework (JSR 296) provides a basic architecture and a set of commonly used services for Swing applications. Most applications must implement and manage lifecycle events, UI component event handling, threading, localizable resources, and simple persistence. The framework provides services for managing all these common needs, allowing you to concentrate on your application's unique functionality.
Framework architecture includes Application
and ApplicationContext
classes. The ApplicationContext
instance will provide help for session and local storage, task management, resource management, and most other services that the framework provides.
Your framework application has a well-defined life cycle. Each lifecycle stage has a corresponding method that your application can override to provide its unique functionality.
Framework applications have support for application- and class-level resources. Using the ResourceManager
and ResourceMap
classes, you can automatically inject those resources into your application, making them easy to localize.
The @Action
annotation helps you create UI component event handlers. Using this annotation and ResourceMap
objects, you can combine an action's functionality and visual elements in a single place, making the resulting Action
object reusable and localizable.
Some event handlers should run as background threads. Use the Task
class to define background threads. The framework makes it easy to combine Action
and Task
instances to make your GUI run smoothly and reliably with long-running event handlers.
Finally, the framework provides support for session state and local storage. Session state includes component geometry, screen location, selected tabs, column widths, and other UI state. You can save session state before the application exits, and you can restore the same state when the application starts again. Additionally, the framework gives you a simple storage API that lets you store local objects on your host platform.
The Swing Application Framework is a work in progress. Expect some changes as the JSR 296 expert group revises both the specification and reference implementation before its final release. The expert group hopes to include the framework in a future release of the Java platform.
For More Information
- JSR 296: The Swing Application Framework
- Source code, documentation, and binaries for the JSR 296 reference implementation
- Network Time Protocol NIST Service
- Article demo project and source code
- NetBeans IDE
- Online article Improve Application Performance With SwingWorker in Java SE 6