Articles
Java Platform, Standard Edition
|
| 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.
| |
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.
| |
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.
| |
Two classes help you manage your application:
ApplicationContext and
Application. The
Application and
ApplicationContext objects have a one-to-one relationship.
The
ApplicationContext provides services to the application as shown in Figure 1.
Figure 1.
An
ApplicationContext provides services to your
Application instance.
|
Those services include the following:
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.
| |
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.
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.
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.
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.
Figure 2.
An
ExitListener has the opportunity to respond to exit requests.
|
| |
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:
ResourceMap objects and their resources.
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.
Figure 3.
You must write all code to retrieve and use resources if you manually use a
ResourceMap.
|
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.
Figure 4.
The button's icon and text are injected resources.
|
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! |
Figure 5.
You can use both component and field injection in the same application.
|
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 values
R, 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.
Figure 6.
ResourceConverter object converts text resources to their final object form.
|
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);
}
|
| |
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:
@Action annotation to mark and name methods that will be used by
Action implementations.
ActionManager class creates
javax.swing.ActionMap instances for classes that contain
@Action methods.
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.
Figure 7.
Reuse
Action objects in multiple components.
|
@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.
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
.
| |
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.
Figure 8.
You can use
Task instances to run long-running background event handlers.
|
| |
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(); |
| |
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.
Figure 9.
Applications can use the
LocalStorage class to store data.
|
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.
| |
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.
| |