The Source for Java Technology Collaboration
User: Password:



Dean Iverson

Dean Iverson's Blog

Swing Application Framework Hacks Unleashed For Smarty Pantses

Posted by diverson on April 02, 2007 at 01:06 PM | Comments (7)

Title: Swing Application Framework Hacks Unleashed For Smarty Pantses

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#FF0000Red
#AARRGGBB#80FF0000Half transparent red
R, G, B0, 255, 0Green
R, G, B, A0, 0, 255, 128Half transparent blue
FontStringConverter fontName-style-size Arial-BOLD-12 This converter is just a wrapper around the Font#decode method
fontName style sizetimes italic 18Dashes 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 JButton'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.



Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • How about support for DPI-aware font sizes?

    Posted by: kirillcool on April 02, 2007 at 01:09 PM

  • One case i'm not seeing in the app framework, is what happens if the properties that i want to persist can be modified on runtime? The app framework doesn't magically replace the text in the resource bundle does it? In a not hyphotetical example, what whapens if the property is something map based like KeyStrokes that are utterly bitchy to serialize (can't must serialize must toString and reconstruct on load) are utterly bitchy to rebind (a map property and reflection) and really not designed for it at all (even thought inputmap tries to be a map, it doesn't implement the interface, and that means its values is not accessible, so you can't save it like a collection directly). However setting the keyStrokes(shortcuts) for a action in a program is a very common option and must be saved.

    Posted by: i30817 on April 02, 2007 at 01:25 PM

  • Nice post. Here's a question that has been on my mind ever since the Swing App Framework has been released. Maybe you can answer it. Since one of the Swing App Framework most important roles, just like any other app framework, is like you mentioned, support for resource injection, be it text, font, colors, actions, LnF, etc., why hasn't it been built on top of a Dependency Injection (DI) container? Why do we have to extend the Application class instead? DI based frameworks have shown to be the best approach for this sort of resource injection. They are well proven in the industry as far scalability, power, and flexibility, and can be made very simple to use. Why did the Swing App Framework have to go it its own way, rather then leverage one these well designed DI containers out there (Guice, etc.)? Extending the Application class to write your application is so last century in case you haven't been checking.

    Posted by: mikeazzi on April 02, 2007 at 02:25 PM

  • Very nice!

    I have a couple concerns:

    • Can anything be done to simplify maintaining the string literal references to component names and Action method names? What happens when I use a component name that's not in the resource file? Maybe a build-time verifier.
    • I like the property file format as a self-explanatory format ... but perhaps there is a more appropriate format? CSS would be a nice way to skin the application and build the resource map at the same time.
      #titleLabel {
      	text: "Counting Demonstration";
      	font: bold 16pt Arial;
      }
      

    Posted by: jsando on April 03, 2007 at 08:03 AM

  • Dean,
    This writeup was great. It was funny (kept things light hearted, so I didn't feel like I was reading a API spec) and so well detailed. You covered all the beginner questions I had and then dug down further into the "what ifs" that I was sure to have after getting started with the framework. You really nailed it.

    Thanks for putting things together.

    Riyad

    Posted by: rkalla on April 03, 2007 at 08:08 AM

  • mikeazzi: You, I and many others have been wondering the same. I have actually wired a Swing app together with Spring. And I've recently wondered how much more interesting it would be to do so with Guice. Interestingly, an IOC solves a lot of intricate Swing dependency issues, removes the need to write component and/or action glue code and allows for extremely well-factored Swing applications. Sigh....

    Posted by: javawerks on April 03, 2007 at 08:10 AM

  • Nice article, and very useful.

    Any hints on how you might set a particular theme for a LookAndFeel.
    For example, to use one of the JGoodies LookAndFeel's:

    Application.lookAndFeel=com.jgoodies.looks.plastic.PlasticXPLookAndFeel

    However, how would you then set a theme like ExperienceBlue for this LookAndFeel?

    Posted by: cforker on April 03, 2007 at 08:36 AM



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds