|
|
||||||||||||||||||||||||||||||||||
Dean Iverson's BlogSwing Application Framework Hacks Unleashed For Smarty PantsesPosted 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.
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 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):
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 101Now 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.javapackage 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.
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
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
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 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
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:
This is what our stunning application looks like now.
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( 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.
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. CommentsComments are listed in date ascending order (oldest first) | Post Comment
| ||||||||||||||||||||||||||||||||||
|
|