Skip to main content

Swing Application Framework Hacks Unleashed For Smarty Pantses

Posted by diverson on April 2, 2007 at 1:06 PM PDT

Join me now as we explore the murky depths of the Swing Application Framework.

Ok, there is nothing murky about the app framework. Hans is fond of saying that one of the primary goals of the framework is to be able to explain it to someone in one hour. It's a noble goal. In fact, I think Hans' presentation does it pretty well.

Even better than the one-hour goal, in my opinion, is the one-day goal. With the app framework as it stands today, you can sit down and read through the source code for one day and know everything there is to know about it. How each feature is implemented, the way they work together, where you need to look if something goes wrong. Transparency is one of my favorite features in a framework and the application framework is a veritable cornucopia of transparency.

This is really important for a framework that you intend to use as the foundation for your entire application. Maybe the code monkeys (and I use that term affectionately) can get away with the one hour explanation. But if the buck stops at your keyboard, you really need a deeper understanding. We don't want any surprises down the road.

So let's get to it!

One of the coolest things about the app framework is it's support for resource injection. It can be used to inject resources like text, fonts, and colors into our Swing components and actions. How does it work? Magic? Unfortunately it's not that fantastical. It turns out that all we need is a good map.

Uhm No, Not That Kind Of Map
Wrong Map

No, application frameworks are way more mundane than that. What we need is a ResourceMap. So how do we get one? Well, like most other application frameworks, the Swing app framework features an Application class as its main abstraction. We will be using one of Application's subclasses, the SingleFrameApplication class since it provides a few extra house keeping services for us.

package playground.swing.framework.actions

// import this.and.that

public class ActionGame1 extends SingleFrameApplication
{
  protected void startup( String[] args )
  {
    System.out.println( "Ahhhh! I've been launched!" );
  }
 
  public static void main( String[] args )
  {
    Application.launch( ActionGame1.class, args );
  }
}

When we call Application.launch( ActionGame1.class, args ), the framework performs some initialization steps, creates an instance of our application object (based on the class we passed it in the launch method), and calls that new instance's startup method. The call to the startup method is done on Swing's event dispatch thread (otherwise known as the EDT). Therefore it is safe to construct Swing components within the friendly confines of our overridden startup. So all we've done here is write a program that prints a cutesy message on the console and quits. As Romeo might say: Woo who.

What about the resource map?

One of those initialization steps that the framework performes before creating an instance of our application class is to build the application ResourceMap. A ResourceMap is really just a wrapper around a regular old Java ResourceBundle which is itself just a wrapper around a .properties file. The convention is that the properties file is named ClassName.properties and is located in a "resources" subpackage of its class' package. The application ResourceMaps are arranged hierarchically (each ResourceMap has a "parent" property and a getParent method) and mirrors the Application class hierarchy.

So for this case we end up with the following structure for our application-level ResourceMap (where I use the ResourceBundle filename to represent the ResourceMap):

ResourceMap Hierarchy
AppFrameworkResourceHierarchy.png

Conceptually, our resources are layered on top of the standard Application class resources. Since resources are searched for from the bottom up, this allows a resource in ActionGame1 to override a resource in Application. Which, by the way, is how you would localize the names of the default actions defined by the framework's Application class.

Note that technically our Application class hierarchy includes the SingleFrameApplication class. Since this class does not have a resource bundle associated with it, it does not appear in our ResourceMap hierarchy.

So much for ResourceMap 101

Now that we know where to put our resource files, we just need to know what to put in them. A Java properties file is just a series of key/value pairs.

  # The format of a properties file
  someKey=Some Value

In order to inject a resource into a Swing component, all we need is some way to tie the value of the resource (identified by its key) to a particular component. The Swing app framework uses the component's name to make this connection.

com/acme/coyote/order/form/Anvil.java

  package com.acme.coyote.order.form;
  ...
  JLabel description = new JLabel();
  description.setName( "anvilDescription" );
  ...

 

com/acme/coyote/order/form/resources/Anvil.properties

anvilDescription.text=Anvils are heavy, black, and can be dropped on the head of Tastyus Supersonicus.
anvilDescription.font=Arial-BOLD-24
anvilDescritpion.background=255,255,255

Now your resources are not only localizable but are injected into your component automatically. But wait! How do resources like "Arial-BOLD-24" and "255,255,255" get turned into the component's font and background color? Is there some magic there (we ask hopefully; clinging to our last shred of child-like wonderment)?

Nope, sorry (we answer ourselves; heartlessly dashing all remaining childish hope and, frankly, worrying ourselves a bit for asking and answering our own questions). It's actually very simple. The framework looks at the name of the property we are trying to set (like "text" or "font") and uses reflection to look up the corresponding property of the component (like "setText" or "setFont"). When it finds the property, it looks at what type of object the set method should be called with (like a Color for setBackground). Once it knows the type of object it needs, it just queries its list of ResourceConverters looking for one that will do the job.

The app framework comes with several standard resource converters and you can easily write your own as well. Converters exist for all of the basic types, of course: int, float, double, short, byte, boolean. In the case of the boolean converter, the words "true", "on", and "yes" evaluate to true. Everything else evaluates to false.

There are also a few more complex converters which are listed below along with the string formats they accept.

ResourceConverter Format Example Note
ColorStringConverter #RRGGBB #FF0000 Red
#AARRGGBB #80FF0000 Half transparent red
R, G, B 0, 255, 0 Green
R, G, B, A 0, 0, 255, 128 Half transparent blue
FontStringConverter fontName-style-size Arial-BOLD-12 This converter is just a wrapper around the Font#decode method
fontName style size times italic 18 Dashes are optional, case is insensitive
IconStringConverter filename.[png,jpg,gif] new_file.png Any of the usual image formats Java supports

 

Injection Junction, what's your function?

Let's try a simple example.

playground/swing/framework/actions/ActionGame1.java

package playground.swing.framework.actions;

// import a.bunch.of.stuff

public class ActionGame1 extends SingleFrameApplication
{
  protected void startup(String[] args)
  {
    JButton btn = new JButton();
    btn.setName( "startCountingBtn" );

    JPanel panel = new JPanel();
    panel.add( btn );
    show( panel );
  }

  public static void main( String[] args )
  {
    Application.launch( ActionGame1.class, args );
  }
}

 

playground/swing/framework/actions/resources/ActionGame1.properties

Application.title=Action Game 1
Application.id=ActionGame1

startCountingBtn.text=Start Counting
startCountingBtn.font=times italic 18
startCountingBtn.foreground=0,0,255

Here we've created a button and added it to a panel. We then display the panel in a JFrame by calling the show method of SingleFrameApplication. In addition to constructing and displaying our frame, the show method also causes our resources to be injected by calling the ResourceMap#injectComponents method and passing our new JFrame as the argument. The results are simply stunning.

Simply Stunning!
buttononly.png

Hey! What about actions?

So what happens when you press the button? That's right! Absolutely nothing.

So let's add an action to our button. Now, in the bad old days (circa 2 months ago), that meant having to create an action listener and add it to the button. But these days the sun is shining, the birds are singing, and we have the @Action annotation.

playground/swing/framework/actions/ActionGame1.java

package playground.swing.framework.actions;

// import a.bunch.of.stuff

public class ActionGame1 extends SingleFrameApplication
{
  @Action
  public void startCounting( ActionEvent ev )
  {
    System.out.println("Starting to count...1, 2, 3, 4");
  }
 
  protected void startup(String[] args)
  {
    ApplicationActionMap aMap = ApplicationContext.getInstance().getActionMap( getClass(), this );
   
    JButton btn = new JButton();
    btn.setName( "startCountingBtn" );
    btn.setAction( aMap.get( "startCounting" ) );

    JPanel panel = new JPanel();
    panel.add( btn );
    show( panel );
  }

  public static void main( String[] args )
  {
    Application.launch( ActionGame1.class, args );
  }
}

 

playground/swing/framework/actions/resources/ActionGame1.properties

Application.title=Action Game 1
Application.id=ActionGame1

startCountingBtn.font=times italic 18
startCountingBtn.foreground=0,0,255

startCounting.Action.text = &Start Counting

Now when we push the button we see our message printed on the console. All we had to do was add the @Action annotation to a method. Then we used our ApplicationContext to get the action map for our class. We used that action map to get the action that calls our startCounting method. And finally, we gave that action to the button.

So we saved a few lines of code, big deal.

Wait for it.

When counting is allowed, counting aloud can be rude. But counting aloud while blocking the EDT is downright obnoxious. The problem is that launching a background thread to do a simple task like counting is a huge pain in the neck. We'll do it anyway. Put on a brave face, threads can smell fear.

The first thing we'll need is a background task. The Swing app framework comes with a class made just for this very purpose. It's called Task, oddly enough. Creating a new one is fairly easy.

public class CountingTask extends Task<Void, Void>
{
 
  /** Creates a new instance of CountingTask */
  public CountingTask()
  {
    super(CountingTask.class);
  }
 
  public Void doInBackground()
  {
    System.out.println("In the background task and counting...");
   
    for( int i=0; i<5; ++i )
    {
      System.out.println( String.valueOf( i ) );
     
      try
      {
        Thread.sleep( 250 );
      }
      catch (InterruptedException ex)
      {
        System.out.println("I hate being interrupted while counting!  Now where was I?");
      }
    }
   
    return null;
  }
}

The really slick part is that launching a background task is completely trivial with the app framework. We just need to make two changes to our action method.

  @Action
  public CountingTask startCounting( ActionEvent ev )
  {
    return new CountingTask();
  }

Yep, all we have to do is create and return the task. That's it. Finished. The framework takes care of launching it for us using a thread pool that it creates and manages (actually a ThreadPoolExecutor for you concurrency geeks out there).

Putting it all together.

Since that was too easy, let's dress up the interface just a bit with a label as well.

playground/swing/framework/actions/ActionGame1.java

package playground.swing.framework.actions;

// import a.bunch.of.stuff

public class ActionGame1 extends SingleFrameApplication
{
  @Action
  public CountingTask startCounting( ActionEvent ev )
  {
    return new CountingTask();
  }
 
  protected void startup(String[] args)
  {
    ApplicationActionMap aMap = ApplicationContext.getInstance().getActionMap( getClass(), this );
    ResourceMap rMap = ApplicationContext.getInstance().getResourceMap( getClass() );

    JLabel label = new JLabel();
    label.setName( "titleLabel" );
   
    JButton btn = new JButton();
    btn.setName( "startCountingBtn" );
    btn.setAction( aMap.get( "startCounting" ) );

    JPanel panel = new JPanel();
    panel.setBorder( BorderFactory.createTitledBorder( rMap.getString( "countingDemo" ) ) );
    panel.setLayout( new BorderLayout() );
    panel.add( label, BorderLayout.NORTH );
    panel.add( btn, BorderLayout.CENTER );
    show( panel );
  }

  public static void main( String[] args )
  {
    Application.launch( ActionGame1.class, args );
  }
}

 

playground/swing/framework/actions/resources/ActionGame1.properties

Application.title=Action Game 1
Application.id=ActionGame1

countingDemo=Counting Demonstration
countingIsAllowed=Push the button to start counting.

titleLabel.text=${countingIsAllowed}
titleLabel.font=Arial-BOLD-16

startCountingBtn.font=Arial-BOLD-24
startCountingBtn.foreground=0,0,255

startCounting.Action.text = &Start Counting
startCounting.Action.shortDescription = Start counting in the background

Let's go through these changes real quick:

  1. We included the change to the startCounting action.
  2. A label has been added whose text is retrieved from the resource bundle.
  3. The label's text in the .properties file is a gratuitous example of how one property can refer to another.
  4. A tooltip for the startCounting action was added.
  5. We created a titled border and used BorderLayout to dress things up a bit.
  6. The text for the titled border is retrieved from the resource bundle just like a typical piece of localized text would be.
  7. We've added a mnemonic to the action's text using the '&' character.
  8. We added some Application resources to the .properties file in order to specify the title of the application and an ID string (which is used when storing user preferences and such)

 

This is what our stunning application looks like now.

Simply Stunning-er!
final.png

And this is what we see when the magical button is pushed.

  In the background task and counting...
  0
  1
  2
  3
  4 

Some of you who have done this a time or two are probably anxious to point out that there is a major piece missing here. What happens if we quickly push the button twice?

  In the background task and counting...
  0
  1
  In the background task and counting...
  0
  2
  1
  3
  2
  4
  3
  4

My, what a mess. When performing background tasks we usually have to be careful to disable the interface while a background task is running. Then we have to figure out when the task is finished so we can re-enable it. This can be an error-prone process but the framework gives us an easy way out here too!

There's just one more thing.

With one trivial addition to our @Action annotation, this whole problem just disappears.

  @Action( block = Block.COMPONENT )
  public CountingTask startCounting( ActionEvent ev )
  {
    return new CountingTask();
  }

We simply tell the action to block our component while the task is running. Now the framework handles all of the details of disabling our button when the task begins and re-enabling it when the task ends. Personally, I love frameworks that do more work so I can listen to more Java Posse...oops, I mean be more productive with other things.

This is what we now see after the button has been pushed.

Simply Blocked
finalblocked.png

Hey, boy! How come that don't look like no Mac program?

You have probably noticed that I haven't been using the normal Aqua look and feel even though I'm obviously developing this on a Mac. What you're seeing is the new Nimbus look and feel.

Like most of the other things I've covered, using this with the Swing app framework is a simple one line addition. Is that a coincidence or am I just lazy? You decide. Regardless, if you would like to use a specific look and feel in your app framework application, all you have to do is....

You know what? I'm not going to tell you. Magicians don't divulge all of their secrets!

That's lame!

Very true. But that hasn't stopped me yet in this post!

Oh, alright. Just download the Nimbus jar file and put it in your class path. Then add one extra line to the ActionGame1.properties file:

  ...
  Application.lookAndFeel=org.jdesktop.swingx.plaf.nimbus.NimbusLookAndFeel
  ...

There you have it - no more secrets.

Related Topics >>