ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Prototyping Desktop Applications Prototyping Desktop Applications

by Andrei Cioroianu
04/28/2004

Since you are reading this article, you have probably decided to use Java on desktops. If you haven't made this decision yet, you could read my previous article, where I talked about the benefits that Java provides on the client side. In this article, I'll start presenting a desktop application prototype, which I called JImaging. I'm going to focus on the application's architecture, explaining how I made my technical decisions and how I solved the problems that occurred during development. If you are new to Java desktop development, you should start with The Java Tutorial. You can also find many interesting articles on The Swing Connection.

Why Would You Build a Prototype?

The development of most applications usually starts with a prototype for several reasons. First of all, you have to make sure that the user's requirements can be satisfied using existing technologies. For example, Windows integration cannot be implemented in a Swing application without using native code, which leads to the loss of Java's cross-platform advantage. SWT provides a limited integration with the operating system, while allowing you to run the same application on most native platforms. In many cases, however, the J2SE platform, with its rich set of features (Swing, Java 2D, Image I/O, etc.), provides everything you need for building complex desktop applications. Before committing to a big Java desktop project, you should always build a prototype to see if J2SE satisfies the application's needs.

In addition to proving that your ideas can be implemented and your technical decisions are correct, a prototype can be used to gather user feedback very early in the development process. The prototype can also help you to estimate the time and resources needed to complete your project. It takes a lot of work to build a polished user interface with menus, dialogs, drag-and-drop features, clipboard support, undo management, printing, etc. Before starting all of this work, you should find out how difficult it will be to build the core functionality of your application. If you have to use third-party custom components, you should test them to see if they work properly together. If you have to solve scalability or performance problems, you should find the solutions during the prototyping phase.

User Requirements

The JImaging prototype (see "Resources" below for the source) is a desktop application that lets you annotate images. Email might be the most popular "collaboration tool," but the ability to comment on screenshots is enhanced by a graphic tool that lets you draw lines, rectangles, ellipses, and text notes on an image.

Java is a natural choice for such an application if its users are working with more than one operating system. While Windows dominates the desktop market, there are user groups where the Mac or Linux are preferred. For example, when Java developers are working together on a project over the Internet, there are good chances they aren't using the same operating system.

The user interface is very simple, containing a toolbar and a drawing area, which is sufficient for testing the main functionality of the application. Figure 1 shows what this looks like.

JImaging Prototype
Figure 1. JImaging prototype, being used to annotate its own screenshot

Packages and Classes

Figure 2 shows the arrangement of the code for the prototype. The root package of the application contains only the Main class, which is described in the next section. I'll present the rest of the classes in future articles of this series.

The frames package contains classes that represent the application's main frame, the main panel that is based on JDesktopPane, and the text notes based on JInternalFrame. These three classes are named MainFrame, MainPanel, and NoteFrame.

The paint package groups the PaintView component, its data model (named PaintModel), and the ToolBarBuilder class that creates the application's toolbar. The tools sub-package has a set of tool classes that paint the graphic objects.

The ResourcesSupport class of the resources package is a utility class for handling the ToolBarResources.properties resource bundle and image icons such as those from the images directory.

Prototype Tree
Figure 2. The packages and classes of the prototype

The Main Class

This class implements the application's main() method and was packed together with all other classes and resources in a JAR file named JImaging.jar using the following command:

jar cfm JImaging.jar m.txt com

The com directory contains the packages with their classes, .properties resources, and .gif image icons. The m.txt file indicates the main class of the application with

Main-Class: com.devsphere.articles.desktop.Main

The jar tool copies the content of the m.txt file into the META-INF/manifest.mf file created automatically in JImaging.jar.

The following import declarations indicate the classes used by Main:

package com.devsphere.articles.desktop;
 
import com.devsphere.articles.desktop.frames.MainFrame;
import com.devsphere.articles.desktop.frames.MainPanel;
 
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
 
import javax.imageio.ImageIO;
 
import java.awt.image.BufferedImage;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
 
import java.io.File;
import java.io.IOException;
 
import java.util.logging.Logger;

The main() method calls the Main() constructor, sets the system look and feel of Swing, creates the main window, and then shows it:

public class Main {
    private String args[];
    private MainFrame mainFrame;
    private MainPanel mainPanel;
 
    private Main(String args[]) {
        this.args = args;
    }
 
    public static void main(String args[]) {
        Main main = new Main(args);
        main.setSystemLookAndFeel();
        main.createFrame();
        main.showFrame();
    }
 
    ...
}

The command line may contain one or two arguments. The user can specify the path of a source image as the first argument in the command line. The application loads and shows the source image, allowing the user to annotate it. If the second argument is present, the application saves the annotated image to a file whose path is specified in the command line. To launch the application, run the following command:

java   -jar JImaging.jar   sourceImage  annotatedImage

J2SE 1.4 can load GIF, JPEG, and PNG files, but it can save images using only the JPEG and PNG formats. You may not use the GIF format to save the annotated image.

The remainder of this section describes the methods of the Main class.

Setting the System Look and Feel

The following setSystemLookAndFeel() method calls the setLookAndFeel() method of the javax.swing.UIManager class.

It requests Swing to switch from the default look and feel, named Metal, to the native look and feel:

private void setSystemLookAndFeel() {
    try {
        UIManager.setLookAndFeel(
            UIManager.getSystemLookAndFeelClassName());
    } catch (UnsupportedLookAndFeelException x) {
        log(x);
    } catch (ClassNotFoundException x) {
        log(x);
    } catch (IllegalAccessException x) {
        log(x);
    } catch (InstantiationException x) {
        log(x);
    }
}

Normally, no exception is thrown by setLookAndFeel() because the parameter is guaranteed to have a valid value, since it's returned by the getSystemLookAndFeelClassName() method of javax.swing.UIManager. However, using the standard logging API, any exception would be logged as a severe error message:

private static void log(Exception x) {
    Logger.global.severe(x.getMessage());
}

Using the global logger is okay in the case of a prototype, but a production application would have to use its own logger, saving the error messages in a file.

Java Swing

Related Reading

Java Swing
By Marc Loy, Robert Eckstein, Dave Wood, James Elliott, Brian Cole

Creating and Showing the Main Window

The createFrame() method creates a MainFrame instance, packs the frame, and loads the source image:

private void createFrame() {
    mainFrame = new MainFrame();
    mainPanel = mainFrame.getMainPanel();
    mainPanel.updateSize();
    mainFrame.pack();
    loadImage();
}

The invocation of updateSize() sets the preferred size of the main panel that is obtained with getMainPanel(). The pack() method causes the main frame to be sized to fit the preferred size of the main panel and the application's toolbar. Note that getMainPanel() and updateSize() are application methods implemented by the MainFrame and MainPanel classes. The pack() method is inherited from java.awt.Window.

The showFrame() method shows the application's main frame and invokes the requestFocus() method of the main panel. Without the requestFocus() call, the focus would be gained by the toolbar's zoom combo box, which is not the main component of the frame. When the application starts, its main component should have the focus, even if the main panel doesn't handle any keyboard events.

The setDefaultCloseOperation() call with the DO_NOTHING_ON_CLOSE parameter disables the default action that happens when the window is closed. The showFrame() method registers its own window listener in order to handle the window-closing event. When the user closes the main frame, the listener saves the annotated image, disposes the frame, and ends the execution of the application with System.exit(0):

private void showFrame() {
     mainFrame.setDefaultCloseOperation(
         MainFrame.DO_NOTHING_ON_CLOSE);
     mainFrame.addWindowListener(new WindowAdapter() {
         public void windowClosing(WindowEvent e) {
             saveImage();
             mainFrame.dispose();
             System.exit(0);
         }
     });
     mainFrame.show();
     mainPanel.requestFocus();
}

Loading and Saving Images

A finished product would use file dialogs to load the source image and save the annotated image. Ideally, the "File Open" dialog would let users preview images and the "File Save" dialog would allow them to provide various parameters, such as the compression quality of the saved image. The standard file dialogs of Swing are based on a component called JFileChooser, which can be customized with its setAccessory() method that lets you add your own component to the file dialog.

In the case of a prototype, the focus is on the main functionality. Therefore, instead of using a customized file dialog, the prototype gets the paths of the loaded and saved images from the command line. The simple read() and write() methods of the javax.imageio.ImageIO class are used to load and save the image. Note that the Image I/O API lets you find out which graphic formats are supported, and you can set parameters such as the compression quality. You would need these features for a customized file dialog.

The loadImage() method reads the image file, whose path is provided as the first command line argument, and sets the background image of the main panel:

private void loadImage() {
    if (args.length >= 1) 
        try {
            File file = new File(args[0]);
             BufferedImage image = ImageIO.read(file);
             mainPanel.getPaintView().getModel().setBackImage(image);
        } catch (IOException x) {
             log(x);
        }
}

The saveImage() method gets the annotated image of the main panel and writes this image to a file whose path is given as the second argument of the command line:

private void saveImage() {
     if (args.length >= 2)
         try {
             File file = new File(args[1]);
             String name = file.getName();
             int k = name.lastIndexOf('.') + 1;
             String ext = name.substring(k);
             BufferedImage image
                 = mainPanel.getAnnotatedImage();
             ImageIO.write(image, ext, file);
         } catch (IOException x) {
             log(x);
         }
}

Making Technical Decisions

During development, I had to solve a few technical problems and I made several technical decisions. The following code fragments are explained only briefly here, but they will be detailed in my future articles. The important thing is to understand the role of a prototype. Use your prototypes to find solutions to technical problems, to test less-used APIs, and to make sure that your application's performance is good.

Using Multi-Layered Panels

Building a graphic application such as Windows Paint is not a very complicated task. You have to handle mouse events and draw lines, rectangles, and ellipses. Transforming such a basic application into a professional graphic editor that lets you move, resize, reorder, delete, copy, cut, and paste the graphic objects requires more work, but can be done in a reasonable amount of time. You might also want to include text boxes that support editing, resizing, word wrapping, etc. Building your own styled-text editor is not necessary since Swing already provides a few text components.

How would you integrate one of the Swing's text editors with your own painting component? I can think of two solutions. One is to implement a mechanism similar to the JTextField cell editors used by JTable, but things are a bit trickier if you want to be able to resize and move the text boxes. The other solution is to use JDesktopPane, placing the text components within JInternalFrame instances.

With the second solution, the resizing and moving are already provided by Swing, but the next question is: how do you paint the source image under the internal frames, which contain the text notes? And how can you draw the other primitives such as lines, rectangles, and ellipses on the JDesktopPane? Fortunately, there is an easy solution, because JDesktopPane is actually a multi-layered panel. The prototype's MainPanel class, which extends JDesktopPane, has two layers. One of them contains the PaintView custom component that lets you draw graphic primitives. The other layer contains the text notes. Of course, this solution would be useless if the annotated image cannot be captured programmatically. The getAnnotatedImage() method of MainPanel does this with the following code:

BufferedImage image = new BufferedImage(
     width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
printAll(g);
g.dispose();

Painting Outside of paint()

The painting of the Swing components is normally done within paint() or within methods called by paint(). When drawing an object on screen with the mouse, however, you don't want to repaint any components, because this can significantly slow down the application. For example, when the user plays with the pencil, the application draws a small line for each mouse event. There can be hundreds of MOUSE_DRAGGED events between MOUSE_PRESSED and MOUSE_RELEASED.

Repainting the PaintView component a few hundred times when the user just draws something on screen is not acceptable. Note that PaintView handles most of the drawing operations and a single repaint requires the redrawing of all annotations including the text notes, which are painted over the PaintView component. The right solution is to obtain a graphic context outside of paint() with getGraphics() when each mouse event is handled:

protected void toolAction(MouseEvent e) {
    e.consume();
    Graphics2D g2 = (Graphics2D) getGraphics();
    float zoomFactor = model.getZoomFactor();
    g2.scale(zoomFactor, zoomFactor);
    float x = e.getX() / zoomFactor;
    float y = e.getY() / zoomFactor;
    currentTool.action(e.getID(), x, y, g2);
    g2.dispose();
}

The PaintView component uses mouse listeners to handle the mouse events. The above method is called for each event, delegating the painting to the currentTool object. When the mouse is released, the repaint() method is called to request the refreshing of the whole component. Therefore, paint() is called only once after the user has finished the drawing of the graphic object. Here is the code that registers the mouse listeners:

protected void registerListeners() {
    addMouseListener(new MouseAdapter() {
        public void mousePressed(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                 requestFocus();
                 currentTool = model.createTool(
                      AbstractTool.DRAW_STYLE);
                 toolAction(e);
            }
        }
 
        public void mouseReleased(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                 toolAction(e);
                 model.setLastTool(currentTool);
                 currentTool = null;
                 repaint();
            }
        }
    });
 
    addMouseMotionListener(new MouseMotionAdapter() {
        public void mouseDragged(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e))
                 toolAction(e);
        }
    });
 
    ...
}

The entire code of the PaintView class will be explained in a future article. The above code fragments are included here only to show how a prototype can be used to make technical decisions. The development of a professional graphic editor would not be possible without the ability to paint outside of paint().

Summary

Prototypes have an important role during the application development process, allowing you to test your ideas and to get user feedback very early. I do not view a prototype as a piece of code that you can throw away when the "real" development starts. Instead, the prototype should be the foundation of your product or application. This means that you should code it carefully, even if some of your classes or methods will be rewritten later. In the later articles of this series, I'll continue to present the source code of the JImaging prototype.

Source Code

Download the source code of the JImaging prototype.

Andrei Cioroianu is the founder of Devsphere and an author of many Java articles published by ONJava, JavaWorld, and Java Developer's Journal.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.