Skip to main content

Make Your Swing App Go Native, Part 3

January 29, 2004

{cs.r.title}








Contents
Setting the Icon
Creating an OS X Icon
Creating a Windows Icon
Compressed Packaging
Native Open and Save Dialogs
Adding a Splash Screen
Conclusion

Welcome back. This is the final article in my series on making Swing
applications feel native. In Part 1, we set up custom menus, the appropriate L&F, and native user alerts. In Part 2, we built double-clickable applications and added file-type associations. In this last installment, we will create custom icons and add some final polish to really make our application shine.

Setting the Icon

Now that the application looks and launches native, what about the icon? Both Mac and Windows have a default executable icon for all Java applications (Figures 1 and 2). These icons are pretty useless, since they don't look any different than any other generic application, Java or otherwise, much less reflect anything special about your app. So let's get rid of that right away.

Figure 1 Figure 2

Figure 1. Default Icon for Mac OS X

Figure 2. Default Icon for Windows

Creating an OS X Icon

To get a better-looking application, first we have to create the icon
and then attach it to the executable.

For the Mac, we can use Apple's IconComposer utility, located in
/Developer/Applications. You can see it in action in Figure 3. You will need to install the free developer tools from Apple. This program doesn't have command-line access, unfortunately, but it shouldn't matter, since we probably won't be changing our icon very often.

To generate an icon, just drag the source image from the Finder into each thumbnail well and save it
into the src/images directory. IconComposer should accept any
image format that QuickTime understands, which includes all of the major formats, such as GIF, TIFF, PNG, and JPEG. I have had the best success with PNGs because
IconComposer can use the alpha channel in your PNG (assuming you created one)
to generate the hit mask. A hit mask is a second bitmap that indicates which part of the image can be clicked on. The operating system will use this for drawing selections and doing transparency on the desktop. Once
saved, this will create a .icns file.

Figure 3
Figure 3. IconComposer in action

With the icon created, we just need to attach it to the program.
We need to set another property in our
Info.plist file to tell the Finder which icon to use.
(The Info.plist file was introduced in Part 2 of our series.)

<key>CFBundleIconFile</key>
<string>MadChatter.icns</string>

Finally, we add a copy task to the osx.app task of our Ant build
file to copy the image into the Contents/Resources
directory of our application bundle.

<copy todir="${dist-mac}/${app-name}.app/Contents/Resources">
  <fileset dir="${src}/images">
    <include name="*"/>
  </fileset>
</copy>

Now run the osx.app target, and we get an application with an icon in Figure 4.

Figure 4
Figure 4. Mac OS X Application Icon

Creating a Windows Icon

As with OS X, Windows uses its own proprietary and incompatible
icon format, this time called a .ico. Under Windows,
we will use an open source program, png2ico, which
can generate a .ico file from a PNG image. We will use the same
PNG image that we used with IconComposer for OSX. One more Ant task
will do the trick.

<target name="win-icon">
  <exec executable="bin/png2ico.exe" dir=".">
    <arg value="${build}/icon.ico"/>
    <arg value="${images}/icon.png"/>
  </exec>
</target>

Unfortunately, attaching the icon to the executable isn't as easy as it is on
the Macintosh. There isn't an officially
sanctioned API to do it, and it doesn't look like there's going to be one
any time soon. Fortunately, our packager, JExePack, can create a
.exe with any icon we specify. We just need to add this line to our
jexepack.ini file:

/icon:images\icon.ico

Notice the use of a backslash: this is the platform-specific separator for Windows.

Run the exe task and we get another desktop application with a real
icon, as seen in figure 5.

Figure 5
Figure 5. Windows Application Icon

Compressed Packaging

We want to make installation as easy for our users as possible, so
the final step is to compress our program for easy download. The .exe
will go into a .zip file, since .zip is the most common compression
format for Windows, and XP supports it natively in the File Explorer.
For the Mac, we will use the .tar.gz format, since it's also built-in and can handle
preserving the executable bit that lets OS X know it's a program. We
won't used StuffIt because it's commercial and not as easily
scriptable. I've also decided not to use a disk image, because it's
difficult to automate the disk creator program and get the volume sizes
right. Perhaps in the next release of the developer tools this will be
an option.

Here are our final two build targets:

<target name="dist-mac" depends="OS X.app">
  <exec command=
"tar -C ${dist-mac} -cvf ${dist-mac}/${app-name}.tar ${app-name}.app"/>
  <gzip zipfile="${dist-mac}/${app-name}.tar.gz"
    src="${dist-mac}/${app-name}.tar"/>
</target>
<target name="dist-win" depends="exe">
  <zip zipfile="${build}/${app-name}.zip"
    basedir="${dist-win"/>
</target>

Native Open and Save Dialogs

Now we want to do a few final things to make the application feel native. The
first is to use native file dialogs. The Swing file open and close dialogs are
very powerful but not identical to the native dialogs. An alternative is to use
the AWT FileDialog instead of the JFileChooser, since the AWT version uses a
native component. However, recent versions of the Windows JDK have made great
strides in perfecting the Swing JFileChooser. Now it actually looks better than
the standard native file chooser and more like the advanced file browser you
would find in modern Microsoft applications (like Outlook). And if you do use the
AWT version, you will have to give up the customization of the JFileChooser. It's
a command decision. We could go with native for all platforms, but I've chosen
to only use it on the Mac, per Apple's recommendations, and to use a JFileChooser on other platforms. The code is pretty straightforward:

save.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent evt) {
        if(xp.isMac()) {
            // use the native file dialog on the mac
            FileDialog dialog =
               new FileDialog(frame, "Save Log",FileDialog.SAVE);
            dialog.show();
        } else {
            // use a swing file dialog on the other platforms
            JFileChooser chooser = new JFileChooser();
            chooser.showOpenDialog(frame);
        }
    }
});

Adding a Splash Screen

While not strictly a native application issue, it would be nice
to have a splash screen. It makes our program feel a little bit more
professional. Splash screens were originally invented to mask the
loading process, but our app will probably load pretty quickly, so we'll
just give it a time delay.

Our splash screen will be a JWindow without any decorations
(title bar, close and minimize buttons, etc.), centered
on the screen. We want an image (from our fantastic graphic design
department, right?) smack in the middle of the panel. While we could
subclass Panel to draw it, the quickest way to get an Image on the
screen is to use an ImageIcon in a textless JLabel. Then we turn off
decorations and do some quick calculations to center the frame on the
screen. A quick note here: we have to do a pack() before our
calculations, because the width and height are undefined until pack() has
been called.

public class SplashScreen extends JWindow {
    public SplashScreen() {
        ImageIcon image = null;
        JLabel label = new JLabel(image);
        try {
            image = new ImageIcon("logo.png");
            label = new JLabel(image);
        } catch (Exception ex) {
            u.p(ex);
            label = new JLabel("unable to load: " + res);
        }
        getContentPane().add(label);
        pack();
        Dimension dim =
            Toolkit.getDefaultToolkit().getScreenSize();
        int x = (int)(dim.getWidth() - getWidth())/2;
        int y = (int)(dim.getHeight() - getHeight())/2;
        setLocation(x,y);
    }
}

Notice something here: we are loading the image from a string. This string
represents the filename relative to the current working directory. Our entire
philosophy up to this point has been that we should avoid having extra files
lying around; anything extra can be lost or corrupted. We could package the
image up using the mechanisms built into JExePack and Application Bundles, but
Java actually has its own way of doing it: resource bundles. You can load a
resource from the classpath, the same way a class is loaded. Then it won't
matter if our application is compressed into a .jar or loaded across the network,
as long as we put the resource with the classes, we can get to it. We just copy
the resource, an image in our case, into the directory with our classfiles and
then use a different function to load it.

First we need to modify the build file to put the images into the right
place. To make sure the image gets picked up by all of the different packaging
tasks we'll just put it in the compile target, which is required by all
of them. Now the target looks like this:

<target name="compile" depends="init" description="compile">
    <mkdir dir="${classes}"/>
    <javac srcdir="${java}" destdir="${classes}" debug="on">
        <classpath>
            <fileset dir="${lib}">
                <include name="*.jar"/>
            </fileset>
        </classpath>
    </javac>
    <copy file="${src}//images/2004/01/logo.png" todir="${classes}"/>
</target>

Finally, we change the call to ImageIcon:

ImageIcon image = new
  ImageIcon(this.getClass().getResource("/logo.png"));

Note that I've put a / at the front of the logo path.
If we didn't put in a / the class loader would assume
that it was relative to the current class, and would prepend it with the
package name. Thus it would look for
/org/joshy/oreilly/swingnative/logo.png and would return
null when it couldn't find the image.

We can launch SplashScreen with a thread that just
sleeps for three seconds and then closes the window. Since this is a
separate thread, the main initialization code (say, connecting to the chat
server) can continue to run in the background. In a more sophisticated
application, we would have the initialization code actually close the splash
screen instead of just waiting three seconds.

final SplashScreen splash = new SplashScreen();
splash.pack();
splash.show();
new Thread(new Runnable() {
    public void run() {
        try {
            Thread.currentThread().sleep(3000);
            splash.hide();
        } catch (InterruptedException ex) {
        }
    }
}).start();

The splash screen looks like Figure 6:

Figure 6
Figure 6. The Splash Screen

Conclusion

It's a lot of work to make Java applications cross platforms and still feel natural to users on those platforms,
but with some help from the platform provider and the use of
open source libraries, we can make it happen. We have created native
menus, file-type associations, user alerts, a splash screen, and most
importantly, native executables. All of these add up to a much more
pleasant experience for the end user.

With the hope that we can make this easier, I have created a Java.net project where we can develop reusable technology for native integration. It will begin with the
codebase for The Mad Chatter; we will add to it as the community grows.

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