Make Your Swing App Go Native, Part 3 Blog



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

    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. InPart 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 1Figure 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/imagesdirectory. 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 task of our Ant build file to copy the image into theContents/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 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:


    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"> <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);; } 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 anImageIcon 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 apack() 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();; 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


    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 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.