|
|
||||||||||||||||||
by Joshua Marinacci | ||||||||||||||||||
| ||||||||
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.
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. Default Icon for Mac OS X |
Figure 2. Default Icon for Windows |
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. 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. Mac OS X Application 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. Windows Application Icon
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>
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);
}
}
});
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. 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 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.
Joshua Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.
Make Your Swing App Go Native, Part 2
Swing applications don't often feel or behave like native apps. It doesn't have to be this way. Joshua Marinacci's continues with a look at providing double-clickable executables and filetype associations.
Make Your Swing App Go Native, Part 1
Swing applications don't often feel or behave like native apps. It doesn't have to be this way. Joshua Marinacci's three-part series begins by improving an app's appearance and menus, and offers a way to get attention via the Windows taskbar and Mac OS X dock.
View all java.net Articles.
Showing messages 1 through 18 of 18.
I have posted the code to the project under the filesharing section. I am also working on pulling the code into CVS.
If you are interested in this project then please sign up for the mailing list. In a week I'm going to kick off the discussion of what we'd like this project to turn in to.
Thanks
- Joshua
As far as I know you need Mac's dev tool to do it. However, Photoshop might be able to save to it. I think it's a reasonably open format (derived from PNGs perhaps?) so maybe we could write a cross platform tool that would do it.
....
A little research on Google turns up that .icns are actually container files holding multiple icons, each of which can be different sizes. The file format for the icons has not been document by Apple, but it was reverse engineered for the ResCafe project so maybe that would be a place to start. - Joshua - Joshua
|
|