Skip to main content

Make Your Swing App Go Native, Part 1

December 8, 2003

{cs.r.title}






Java applications, in particular those written with the Swing toolkit,
have a reputation for feeling clunky and out of place, as if they don't
belong on your computer. Often your users can't put their finger on
what it is that doesn't feel quite right. It is often the look of the
buttons, subtle menu bar differences, and a lack of a good launching
mechanism. Swing applications often look and feel different than native
applications.

This is the first of a three part
article series in which we will build a simple application from the ground up
and make it look and feel native. This article will cover designing the
menus and adding visual alerts. The second article will focus on
building native executables and adding file type associations. The third
part will complete the series with icons, dialog boxes, splashscreens,
and a checklist of finishing touches. Throughout the series, we will
make use of several libraries and techniques that automate the process
and make building high-quality Swing applications as easy as
possible.

To illustrate each of these techniques, we will overhaul a faux instant messenger called The Mad
Chatter
. I've chosen to do use an IM program as our ongoing example because it is complex
enough to benefit from native integration while being simple enough to fit in a series of articles. The Mad Chatter will use the basic File and Edit menus to save log files and edit text. Being a chat application, it will also need a way of alerting the user, so we get to exercise dock bouncing and window flashing.

Over the course of these three articles, the Mad Chatter will get a complete makeover, including:

  • A menu bar that feels native
  • Global key combos
  • Alerts when a new message arrives
  • Installer free native packaging
  • Registering file type associations
  • Custom icons
  • A splashscreen
  • Native file dialogs

Some of these tips have been covered in more detail elsewhere, so
I'll try not to rehash them. Instead, I'll cover them briefly and then go
into how you can cleanly integrate them into your development
process.

What is a Native-Feeling Application?

Now first let's talk about what it means to feel native. A simple
explanation would be that the user can't tell the difference between
your Swing application and a native one. But that doesn't tell us the
whole story. Like good photography, feeling native isn't really about
doing anything. It's about not doing a whole bunch of things. Not
being slow, not having widgets that look different, not requiring
special installation procedures. In general, it's about not feeling any
different than any other application. This includes key combos, menu
placement, icons, packaging, and the correct terminology
(Quit vs. Exit, Preferences vs.
Options, etc.). It means all of the little details that have
nothing to do with what the program actually does, but affects how the
program feels. And this makes a huge difference to the users.

Most users have a favorite platform and pre-existing expectations about
what buttons go where, which actions are associated with various
keyboard accelerators, and hundred of other UI niceties that they take
for granted. Although you can create a cross-platform application that
is as simple to use and as reliable as a native application, your
application is more likely to be adopted by end users if its UI meets
their expectations. You will need to consider the expectations of users
on each of your target platforms.

Setting the Look and Feel

The first step to making the application feel native is setting the
current Look and Feel. Swing has the concept of a Look and Feel
(L&F), which is a set of classes and images that completely control
how the standard Swing components look and behave. By default,
Swing applications have a cross-platform L&F called
Metal (Figure 1), which many consider quite ugly.

The standard Metal L&F
Figure 1. The standard Metal L&F

L&Fs are used for theming, but they can also be used to make
on-screen components, from buttons to menus, look just like their
native counterparts. Apple ships an Aqua L&F that makes a Swing app on Mac OS X
virtually indistinguishable from a native OS X application. Sun ships a
similar L&F for Windows. By telling Swing to use the system Look and
Feel, the one provided by the JVM, we help maintain the illusion that
this is a native application.

On OS X, this is already done for us, so we don't need to worry. On
Windows, this is done by getting the system Look and Feel from the
UIManager, as seen below:

try {
    UIManager.setLookAndFeel(
        UIManager.getSystemLookAndFeelClassName());
} catch (UnsupportedLookAndFeelException ex) {
  System.out.println("Unable to load native look and feel");
}

This throws an exception if the system L&F isn't available (or
if there isn't one), so we have to catch it instead of letting the
program quit. Now our application looks like Figure 2 or 3 instead of
Figure 1.

The OS X Aqua L&F
Figure 2. The OS X Aqua L&F

The Windows L&F
Figure 3. The Windows L&F

Managing the Menu Bar

Now that we've got the Components looking native, we need to work on
the menu bar. The main differences between platforms are the single menu
bar and key combos on the Mac.

Unlike Windows, the Macintosh has always given
applications a single menu bar for the entire program instead of one
menu bar at the top of each frame. Since Swing assumes one menu bar per
frame, Apple has designed their Swing L&F to always use the menu bar
of the active frame. To make the menu bar appear as if there is really
only one will require some cleverness as we build up our menu. But I'm getting ahead of myself.

First: we can tell OS X to use a single menu bar by adding a system property
to the command line:

java -Dapple.laf.useScreenMenuBar=true classname

And now we have a single menu bar, as shown in Figure 4:

The Mac OS X single menu bar
Figure 4. The Mac OS X single menu bar

The next Mac-specific item is the Application menu. All OS X
programs have a special menu, titled with the name of the program,
that contains the About, Preferences, and
Quit menu items, along with a few provided by the operating
system (like Hide).

To set the name of our application, and have it appear at the top of the application
menu, we use another system property,
com.apple.mrj.application.apple.menu.about.name. You can put this on
the command line with another -D argument. If you're using the
Project Builder IDE, you'll want to put all of these Java system properties
in your Info.plist file, where they'll look like this:

<key>com.apple.mrj.application.apple.menu.about.name</key>
<string>MadChatter</string>

The Application menu now looks like Figure 5:

OS X Application Name
Figure 5. OS X Application Name

Setting Key Combos

The next step is creating our menu items with the right key combos.
Key combos are keypress combinations that perform menu actions without actually selecting the menu. An example is Control-S for Save. Each platform has default key combos for common functions, and our application should match these defaults wherever possible. Swing also lets us set the menu item name along with the key combo.

We can set each key combo by testing for the platform and using the
right accelerator key (i.e., Control or Command), but we also have to
consider cases where the name of the menu item is different, such as
Quit vs. Exit. This is a lot of conditional logic to develop, much of
which is reusable across applications. To avoid rebuilding this code in
each program, I have created a library called XP for (Cross (X)
Platform
). This library is itself built upon Steve Roy's MRJAdapter library.
(Thanks, Steve.)

Instead of manually creating each menu item appropriately for each
platform, the xp object will create them for us. We just need to add them
to menus and set the ActionListeners. The XP library will
also create the Cut, Copy, and Paste handlers to make sure that
native-to-Java clipboard actions work properly. To use the library, we
just get an XPHelper from the factory and start pulling out menu
items.

XPHelper xp = XPFactory.getXPHelper();
JMenu file = new JMenu("File");

JMenuItem neww = xp.getNewMenu();
JMenuItem open = xp.getOpenMenu();
JMenuItem close = xp.getCloseMenu();
JMenuItem save = xp.getSaveMenu();
JMenuItem quit = xp.getQuitMenu();

file.add(neww);
file.add(open);
file.add(close);
file.add(save);
if(!xp.isMac()) {
  file.add(quit);
}
JMenu edit = new JMenu("Edit");

edit.add(xp.getCutAction());
edit.add(xp.getCopyAction());
edit.add(xp.getPasteAction());

menubar.add(edit);

The Quit menu item is conditionally added to the menu
because OS X will automatically add it to the Application menu, while
under Windows, we need to do it manually.

Attaching the Event Listeners

To make these menus fully functional, we need to add
Actions or ActionListeners. This can be a
little tricky because some actions are tied to a particular window and
some are global. Then we have to add in the complexity of the Application menu under OS X. This gives us three cases to deal with:

  1. An action that is local to a particular window (Save).
  2. An action that is global and is attached to a normal menu item (Help).
  3. An action that is global and may go under the Mac Application menu (Quit).

We'll do this like we would in any normal Swing application, with just
a few changes.

For normal actions, we allocate our ActionListener when we create the JFrame.

ActionListener open_list = new OpenListener(panel,frame);
ActionListener save_list = new SaveListener(panel,frame);
ActionListener close_list = new ActionListener() {
    public void actionPerformed(ActionEvent evt) {
        frame.hide();
        frame.dispose();
    }
};
open.addActionListener(open_list);
save.addActionListener(save_list);
close.addActionListener(close_list);

For global options, we do the same thing, but wrap them in an
initialization flag that makes sure they only get initialized once.
After they are created, we add them to each frame.

if(!initialized) {
    /*   add event handlers  */
    cut = xp.getCutAction();
    copy =xp.getCopyAction();
    paste = xp.getPasteAction();

    new_list = new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
            Startup.newWindow();
        }
    };
    // more global actions
}

JMenu edit = new JMenu("Edit");

edit.add(cut);
edit.add(copy);
edit.add(paste);

For global options that are under the Application menu, we create the
actions globally as before, but we add them conditionally. If it's under
Windows, we add them to each frame. If we are running under OS X, then we
add them once to the application menu.

JMenuItem about = xp.getAboutMenu();
// only manually add on non-mac
if(!xp.isMac()) {
    JMenu help = new JMenu("Help");
    help.add(about);
    menubar.add(help);
    // and add the about for every window
    about.addActionListener(about_list);
} else {
    if(!initialized) {
        about.addActionListener(about_list);
    }
}

Since this conditional menu stuff can get hairy, I recommend creating
functions for each step of the process: init_menu_handlers(),
create_menu_items(), attach_menu_handlers().

Now we have a set of menus that completely hide their cross-platform
nature from the rest of the application. Let's take a look at the
results in Figures 6 and 7.

The finished menu for the Mac
Figure 6. The finished menu for the Mac

The finished menu for Windows
Figure 7. The finished menu for Windows

Native User Alerts for the Mac

One native feature that virtually all instant messenger applications
have is alerts. When a new message comes in, the user may not have our
application active. They may have switched to another application, so we
need to send an alert to notify the user that something is waiting for
them. Each platform has their own way of doing this. In OS X, the dock
icon will bounce. In Windows, the taskbar icon will flash. Sadly, neither
Apple nor Microsoft has provided an API to do this from Java, so what to
do?

On the Mac, there is a third party library called Notifications,
written by Gregory Guerin. His library uses JDirect, Apple's form of
Java native integration, to call the OS X Carbon APIs that control the
dock. It is thoughtfully released under the Artistic License, so we'll
use it here.

The Alert API uses an object called a Notification. This
object can be configured and then posted to send the
alert to the user. Once posted, the alert can be rescinded
to remove the alert and start over. The notification is
actually an abstract object with concrete implementations below it.
Gregory has provided implementations for Mac OS X and the Classic Mac OS.
We just need to tell it which one to load. Here's the basic code to do
it:

Notification.SetFactory(
   "glguerin.notification.imp.mac.ten.TenNotification");
note = Notification.MakeOne();
note.setMarker(true);
note.setIcon(0);
note.setSound(0);
note.setAlert(null);
not.post();

First we initialize the factory with the correct implementation
classname. Then we create a new Notification and configure
it. For our purposes, we set the marker to true so we get dock bouncing
and turn off all other options. Once it's set up, we call post()
to send it to the user. That's it!

I should mention that this library only works under JDirect, not the Java Native Interface (JNI),
which is provided as part of the 1.3 JDK (but not 1.4). It shouldn't
matter how you compile, but make sure you use the 1.3 JVM to run it.
Hopefully, Gregory will port it to JNI in the future. When we do the packaging in the next article, you will need to be sure to use the 1.3 JavaApplicationStub.

So how can we test this puppy? Since we aren't actually running on an
IM network, we'll have to create a fake event with a Test Alert item in
the Tools menu. This will tell the user that an alert will be sent in
five seconds, then launch it. Here's some quick and dirty code to do it;
nothing fancy, since it's just for testing. Note that the actual alert
and screen updates are sent by an separate thread so that we don't block
the main event thread.

public class AlertListener implements ActionListener {
    public void actionPerformed(ActionEvent evt) {
        new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.currentThread().sleep(5000);
                } catch (Exception ex) {
                    System.out.println("error: " + ex);
                }

                final XPHelper xp =
                    XPFactory.getXPHelper();
                if(xp.isMac()) {
                    macAlert();
                }
            }
        }).start();
    }
}

Compile, package, and run, and we get Figure 8:

The OS X Dock bouncing alert
Figure 8. The OS X Dock bouncing alert

That's it for the Mac side; not too bad. With some more work, we
could package that all into one clean .jar and have a nice Java-Mac API
that handles everything. Now on to Windows.

Native User Alerts for Windows

Things aren't so rosy here. No one has made a reusable object for
controlling the window; it looks like we'll have to roll our own.
Another Google search turns up the FlashWindow API. It's a Win32
function that flashes the task bar icon of the provided HWND (a Windows-specific object from the C/C++ world). To make this work, we will need to create a JNI
library from scratch. Don't worry, it's not that hard. It just requires
a little planning.

To use JNI, we create a Java class with
an empty method marked native. Then we create a C function
to implement the empty Java method with native C code. Finally, we compile
and link it into a native library. Our Java method will look like
this:

WindowUtil.java
package org.joshy.jni;
import java.awt.Component;
import javax.swing.*;

public class WindowUtil extends Canvas {
    static { System.loadLibrary("WindowUtil"); }
    public native void flash(Component c,
                             boolean flash);
}

That's it. Just one method. We'll pass in a component contained in
the window we want to flash, and then a Boolean to turn flashing on and
off. The static initializer is to load the native library into which we
will compile our C code.

WindowUtil.c
#include <assert.h>
#include "jawt_md.h"
#include "org_joshy_jni_WindowUtil.h"

JNIEXPORT void JNICALL
Java_org_joshy_jni_WindowUtil_flash(
JNIEnv * env, jobject canvas, jobject component, jboolean bool)
{
/* do native stuff here */
}

The C file has one function with a matching signature. The package and class names have been added to the method name to come up with Java_org_joshy_jni_JNITest_flash. The JNIEnv is for extra environment
information. The canvas is the object making the call (the
WindowUtil class above) and the component is the frame that we want to flash. The jboolean is for the flash variable.

At the top of the C file, you'll notice three includes.
assert.h and jawt_md.h are part of the JDK and
the Windows SDK, both required for this code to compile.org_joshy_jni_WindowUtil.h is the header file of our Java
API in WindowUtil.java. Instead of writing it by hand, this
header file is generated by javah, which we can call with
the javah Ant task.

<javah destdir="jni" class="org.joshy.jni.JNITest" classpath="jni"/>

So far, so good. We've got a Java class, a C file, and a header to
tie them together. Now let's make the C file do something. I found the
following code on Sun's JNI forum; I've adapted it to make it a little safer.

JAWT awt;
JAWT_DrawingSurface* ds;
JAWT_DrawingSurfaceInfo* dsi;
JAWT_Win32DrawingSurfaceInfo* dsi_win;
jboolean result;

jint lock;

// Get the AWT
awt.version = JAWT_VERSION_1_3;
result = JAWT_GetAWT(env, &awt);
assert(result != JNI_FALSE);
// Get the drawing surface
ds = awt.GetDrawingSurface(env, component);
if(ds == NULL)
    return;
// Lock the drawing surface
lock = ds->Lock(ds);
assert((lock & JAWT_LOCK_ERROR) == 0);

// Get the drawing surface info
dsi = ds->GetDrawingSurfaceInfo(ds);

// Get the platform-specific drawing info
dsi_win =
    (JAWT_Win32DrawingSurfaceInfo*)dsi->platformInfo;

FlashWindow(dsi_win->hwnd,bool);

// Free the drawing surface info
ds->FreeDrawingSurfaceInfo(dsi);
// Unlock the drawing surface
ds->Unlock(ds);
// Free the drawing surface
awt.FreeDrawingSurface(ds);

This is mostly boilerplate. JAWT_GetAWT() lets us
get a reference to the AWT subsystem, which we can get use to get a
drawing surface. From there, we can pull out the drawing surface info and
extract an hwnd, which is handle to the window itself. We
can pass this handle to the FlashWindow function to
actually flash the window. Once that's done, we clean up the surfaces and
return. Now we just need to compile and link. C'est facile, non?
Non!

I'd just like to have a quick rant here. I hate dealing with JNI on
Windows. Not that I hate JNI, but I hate C. Specifically, C compilers for
Windows. I spent many hours trying to get this example to compile using
GCC and later, CodeWarrior, not wanting to purchase Visual C++ just to
compile a single library. It took way too much time on Google looking
for help, trying to figure out linking errors, debugging DLL symbols,
and generally making myself crazy. Oh how I longed to be back in the
Java world, where I had only the classpath to worry about. Oh the
humanity! Finally, when I gave up and asked a friend to compile it for
me, I discovered that you can get Microsoft's command-line compiler for
free as part of the .NET SDK. But to install that, you need the .NET
framework runtime. And of course, you'll need the libraries and header
files for Windows itself, so now download the Windows SDK. About three
hundred megs of download later (I'm not kidding), I could compile this
single file. Now I remember why I switched from C to Java eight years ago.
As I go through the compilation step, I don't recommend you try this
at home
.

cl.exe, Microsoft's command-line compiler, can do the compile and
linking in one step, so we just need a single command to do it:

cl.exe \
    /Z7 /Od /nologo \
    -I$JDK\\include \
    -I$JDK\\include\\win32 \
    -I"$MVS\\Vc7\\include" \
    -I"$SDK\\include" \
    WindowUtilImp.c \
    -FeWindowUtil.dll \
    -LDd \
    -MD \
    -link \
    -libpath:"$SDK\\lib" \
    -libpath:"$MVS\\Vc7\\lib" \
    -libpath:"$JDK\\lib" \
    jawt.lib user32.lib gdi32.lib

Now doesn't that look easy!? :) Okay. I'll step through it. I have no
idea what /Z7 /Od and /nologo do. (I pulled
these from examples on the Web.) I think /Od is "optimize
debug." I've created variables for the path to the Java SDK
($JDK), Microsoft's compiler ($MVS), and the
Windows SDK ($SDK). -I adds an include
directory, so I've added the include directories for JNI, the
Windows-specific JNI, the compiler headers, and the standard Windows
headers. The next two lines are the C file and the DLL we want to
make. -LDd tells cl.exe that we want to link and build a
DLL as well. I'm not sure about -MD, but many of the
examples I based this on used it. -link tells the compiler
to pass all of the arguments after it on to the linker, which is why I
placed all of the library stuff after this argument. The
-libpath: arguments add library paths, so we've pulled in
the JNI libraries, the compiler libraries, and the Windows SDK
libraries. Finally, we list the specific libraries we want included. It's
very important that jawt.lib is in there, since it's the
hook that lets the C side get a reference to the JFrame and
extract an HWND.

Okay. Now that we've worked out all of the compilation
difficulties, we can drop it into a script and call it from Ant.

<copy file="${scripts}/makedll.sh" todir="jni"/>
<copy file="${csrc}/WindowUtilImp.c" todir="jni"/>
<exec executable="bash" dir="jni">
    <arg value="makedll.sh"/>
</exec>
<copy file="jni/WindowUtil.dll" todir="."/>

This code copies the scripts and code into a temp directory, compiles
it, and then copies the resulting DLL to the root directory.

So, compile, link, run, and it works!

Windows taskbar flashing
Figure 9. Windows taskbar flashing

This code only does a single flash. To make it a little bit more
usable, I've added an overloaded function to the WindowUtil
class that lets you set the time between the on and off of the flash
and the time between flashes. It also loops count times.
I've put it in a separate thread so that it won't block the event loop,
but this isn't required.

public void flash(final JFrame frame, final int intratime,
    final int intertime, final int count) {
    new Thread(new Runnable() {
        public void run() {
            try {
                // flash on and off each time
                for(int i=0; i<count; i++) {
                    flash(frame,true);
                    Thread.sleep(intratime);
                    flash(frame,true);
                    Thread.sleep(intertime);
                }
                // turn the flash off
                flash(frame,false);
            } catch (Exception ex) {
                System.out.println(ex.getMessage());
            }
    }}).start();
}

So, finally, we have a native method that flashes the window. While
this works, it's a very cumbersome way to do it. It's my hope that in the
future, Sun or the Java community will start building a cross-platform
framework for features like these. Then we could all benefit from native
hooks without having to invest so much time, bandwidth, or money in
native compiler tools.

The Beginning of our Transformation

I hope you've enjoyed this first installment of the Native
Swing series. We have covered getting the menus and toolkit to both
look and behave the same as native applications. Then we added a native feature,
visual alerts, for both the Windows and Macintosh versions. All of these create a
more enjoyable experience for users of each platform.

Special thanks to Kim Swartz of Squeaky Clean Design for creating the consistent icon and logo used in the Mad Chatter.

In the next article I will focus on packaging. We will use Ant and some third-party utilities to create platform-specific executables and add another native feature,
file type associations. I hope you will join me for the next installment.

Editor's Note: Downloadable sample code for this article and others
in the series will be provided at the conclusion of the series. You can also participate in the Mad Chatter project, part of java.net's Java Desktop community.

Josh Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.
Related Topics >> GUI   |   Swing   |   Web Design   |   

Comments

Instead of

Instead of "Thread.currentThread().sleep(5000);" you should use "Thread.sleep(5000);" because sleep() is a static method.