|
Welcome to the Core Java Technologies Tech Tips for March 16, 2004. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).
This issue covers:
Customizing JFileChooser
Best Practices in Exception Handling
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.2.
This issue of the Core Java Technologies Tech Tips is written by John Zukowski, president of JZ Ventures, Inc.
See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.
CUSTOMIZING JFILECHOOSER
As is the case with all things in Swing, if you don't like the look of something, you can change it. In this tip, you'll learn how to customize the appearance of the JFileChooser component and its behavior. Let's start with a refresher on how to use the JFileChooser. Then let's examine single and multiple selection with the component, file and directory selection, setting up filters, adding accessories for previewing the currently selected content, and finally updating the file list view area.
Basic Usage
The JFileChooser component gives users a way to choose files and directories that they want to work with in the system. The component presents users with a dialog from which they can select one or more files and directories. Included with the JFileChooser component are a set of methods for showing a modal version of the dialog. Modal means that the methods don't return until the user selects something. These methods are showOpenDialog, showSaveDialog, and showDialog. The three methods control what text appears on the approval button (that is, what the user selects when he or she is finished with the dialog). There are also built-in methods for the common options Open and Save. The displayed file chooser dialog is appropriate for the current look and feel setting. This means that the dialog typically looks like the native file dialog for the user's platform, or the cross-platform Metal look and feel if none is specifically set.
The user selects something in the dialog and clicks the Open or Save button (or double-clicks the selection). The show methods then return a status, indicating whether the approval button was selected or the user closed the dialog without selecting anything (that is, the user clicked the Cancel button or the window adornment for closing). Possible return states are indicated by the class constants CANCEL_OPTION, APPROVE_OPTION, or ERROR_OPTION.
Here's a simple example that demonstrates JFileChooser.
import java.io.File;
import javax.swing.*;
public class Basics {
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFileChooser fileChooser =
new JFileChooser(".");
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
File selectedFile =
fileChooser.getSelectedFile();
System.out.println("Selected: "
+ selectedFile.getParent()
+ " --- "
+ selectedFile.getName());
}
System.exit(0);
}
});
}
}
The program displays a file chooser dialog. After the user selects a directory and file, the dialog identifies the directory and displays the filename.
Single and Multiple Selection
By default, the JFileChooser starts in single-selection mode. This means that a user can select only one directory or file at a time. If you want to allow the user to select multiple items at a time, you need to set the multiSelectionEnabled property to true. Here's an example:
import java.io.File;
import javax.swing.*;
public class Multi {
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFileChooser fileChooser
= new JFileChooser(".");
fileChooser.setMultiSelectionEnabled(true);
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
File selectedFiles[] =
fileChooser.getSelectedFiles();
for (int i=0,
n=selectedFiles.length; i<n; i++) {
System.out.println("Selected: "
+ selectedFiles[i].getParent()
+ " --- "
+ selectedFiles[i].getName());
}
}
System.exit(0);
}
});
}
}
Notice that this uses getSelectedFiles() not getSelectedFile() to retrieve the selection set.
Prior to J2SE SDK version 1.4, there were issues about which look and feels supported multiple selection. Currently, all the system-provided look and feels support the behavior.
File and Directory Selection
Although a user typically selects files with the file chooser, you can also allow a user to select directories. You do this with a simple call to the setFileSelectionMode method. In making the call, you specify one of the following constants:
JFileChooser.FILES_ONLY
JFileChooser.DIRECTORIES_ONLY
JFileChooser.FILES_AND_DIRECTORIES
Each of the constants specifies a mode for the file chooser. For example, the JFileChooser.FILES_AND_DIRECTORIES constant sets the file chooser to allow both file and directory selection (in other words, a user can select files and directories from the same chooser).
You can combine multiple selection with directory selection. This enables a user to select multiple directories in one chooser. This is shown in the following program.
import java.io.File;
import javax.swing.*;
public class Dirs {
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFileChooser fileChooser =
new JFileChooser(".");
fileChooser.setMultiSelectionEnabled(true);
fileChooser.setFileSelectionMode(
JFileChooser.DIRECTORIES_ONLY);
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
File selectedFiles[] =
fileChooser.getSelectedFiles();
for
(int i=0, n=selectedFiles.length; i<n; i++) {
System.out.println("Selected: "
+ selectedFiles[i].getParent()
+ " --- "
+ selectedFiles[i].getName());
}
}
System.exit(0);
}
});
}
}
In directory selection mode, the initial text in the File Name area is the currently selected directory. In file selection mode, the field is empty.
File Filters
File filters (or input masks) allow you to limit which files can be selected in the chooser. For instance, if you only want a user to see .java source files, you can configure a file filter (a subclass of the FileFilter class) for only that file extension. The default file filter is "accept all" (which lists everything in the file chooser). When you add a file filter to the chooser, the filter appears in the appropriate drop-down list. For the Motif version of the chooser, this is above the Folders list. For the Windows and Metal versions, this is at the window bottom.
To add file filters to the file chooser, you need to create a FileFilter instance from the javax.swing.filechooser package. You then call the addChooseableFileFilter method, passing in the new filter. The filter is then available from the drop-down list of filters. You can also select the filter by calling the setFileFilter method. That way, when the chooser is initially displayed, you can have a filter set besides the 'all' one. If you don't explicitly set a file filter, the last one added is the initial selection.
It's important to not confuse the file filter for JFileChooser with the filter in the Java I/O package. Although the names are identical, their packages and usage are not.
Shown below is a program named Filters that demonstrates the use of file filters. To run the program, you need to get the ExampleFileFilter class that comes with the SDK demos. Look in the demo/jfc/FileChooserDemo/src (of course, reverse the slashes for Windows) directory of the SDK. The ExampleFileFilter class lets you create filters for any set of extensions. This makes it simple to install filters. Just add the appropriate file extensions, and add each filter to the chooser.
import java.io.File;
import javax.swing.*;
public class Filters {
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFileChooser fileChooser =
new JFileChooser(".");
ExampleFileFilter filter =
new ExampleFileFilter("java");
filter.setDescription("Java Source Files");
fileChooser.addChoosableFileFilter(filter);
filter = new ExampleFileFilter("class");
filter.setDescription("Class Files");
fileChooser.addChoosableFileFilter(filter);
filter = new ExampleFileFilter("java");
filter.addExtension("class");
filter.setDescription("Source and Class Files");
fileChooser.addChoosableFileFilter(filter);
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
File selectedFile =
fileChooser.getSelectedFile();
System.out.println("Selected: "
+ selectedFile.getParent()
+ " --- "
+ selectedFile.getName());
}
System.exit(0);
}
});
}
}
Accessories
One of the more interesting aspects of JFileChooser is its accessory panel. This is an area of the chooser where you can place a component to coordinate information related to the current selection. For instance, if the current selection is an image type, the component can be an image previewer. For audio files, it could be a sound player. With each change in the file selection, different information is connected to the accessory panel.
To make an accessory panel, you create the appropriate component, attach the component to the JFileChooser with a call to setAccessory. The accessory panel attaches as a PropertyChangeListener to the JFileChooser. The specific property it needs to listen for is SELECTED_FILE_CHANGED_PROPERTY. When that property changes, the accessory component can change its state.
Here is an example. The following LabelAccessory can act as an image previewer:
import javax.swing.*;
import java.beans.*;
import java.awt.*;
import java.io.*;
public class LabelAccessory extends JLabel
implements PropertyChangeListener {
private static final int PREFERRED_WIDTH = 125;
private static final int PREFERRED_HEIGHT = 100;
public LabelAccessory(JFileChooser chooser) {
setVerticalAlignment(JLabel.CENTER);
setHorizontalAlignment(JLabel.CENTER);
chooser.addPropertyChangeListener(this);
setPreferredSize(new Dimension
(PREFERRED_WIDTH, PREFERRED_HEIGHT));
}
public void propertyChange(
PropertyChangeEvent changeEvent) {
String changeName =
changeEvent.getPropertyName();
if (changeName.equals
(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) {
File file = (File)changeEvent.getNewValue();
if (file != null) {
ImageIcon icon =
new ImageIcon(file.getPath());
if (icon.getIconWidth()
> PREFERRED_WIDTH) {
icon = new ImageIcon(
icon.getImage().getScaledInstance
(PREFERRED_WIDTH, -1,
Image.SCALE_DEFAULT));
if (icon.getIconHeight()
> PREFERRED_HEIGHT) {
icon = new ImageIcon(
icon.getImage().getScaledInstance
(-1, PREFERRED_HEIGHT,
Image.SCALE_DEFAULT));
}
}
setIcon(icon);
}
}
}
}
To complete the process, run the following Accessories program. This gives you the accessory panel. When running the program, be sure to enter a directory with images in it. This will allow you to see the images in the preview panel. Notice that nothing appears in the preview panel if the current selection isn't an image.
import java.io.File;
import javax.swing.*;
public class Accessories {
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFileChooser fileChooser =
new JFileChooser(".");
LabelAccessory accessory =
new LabelAccessory(fileChooser);
fileChooser.setAccessory(accessory);
fileChooser.addPropertyChangeListener
(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY,
accessory);
int status = fileChooser.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION) {
File selectedFile =
fileChooser.getSelectedFile();
System.out.println("Selected: "
+ selectedFile.getParent()
+ " --- "
+ selectedFile.getName());
}
System.exit(0);
}
});
}
}
FileView
The area in a file chooser where the filenames are listed is controlled by the abstract FileView class. You can customize how the files are listed by creating a custom subclass. The class has five methods for getting things like icon and name, however you don't have to customize the behavior for everything. When one of the methods returns null, the component examines the look-and-feel specific behavior to get the default value.
To demonstrate, let's create a JFileChooser that shows special icons for Java-related files: a blue icon for .java files, and a green icon for .class files. In addition, for .java files, let's append the file size to the file name.
Here is the definition of the icon:
import javax.swing.*;
import java.awt.*;
public class DiamondIcon implements Icon {
private Color color;
private boolean selected;
private int width;
private int height;
private Polygon poly;
private static final int DEFAULT_WIDTH = 10;
private static final int DEFAULT_HEIGHT = 10;
public DiamondIcon(Color color) {
this(color, true, DEFAULT_WIDTH,
DEFAULT_HEIGHT);
}
public DiamondIcon(Color color, boolean selected) {
this(color, selected, DEFAULT_WIDTH,
DEFAULT_HEIGHT);
}
public DiamondIcon(Color color, boolean selected,
int width, int height) {
this.color = color;
this.selected = selected;
this.width = width;
this.height = height;
initPolygon();
}
private void initPolygon() {
poly = new Polygon();
int halfWidth = width/2;
int halfHeight = height/2;
poly.addPoint(0, halfHeight);
poly.addPoint(halfWidth, 0);
poly.addPoint(width, halfHeight);
poly.addPoint(halfWidth, height);
}
public int getIconHeight() {
return height;
}
public int getIconWidth() {
return width;
}
public void paintIcon(
Component c, Graphics g, int x, int y) {
g.setColor(color);
g.translate(x, y);
if (selected) {
g.fillPolygon(poly);
} else {
g.drawPolygon(poly);
}
g.translate(-x, -y);
}
}
Here is the program that creates the JFileChooser that shows the special icons. If you like, you can comment out the setFileView line to see the default behavior. Then you could add it back to see the customized FileView with the blue and green icons (this assumes that you are in a directory that contains the source and compiled source files).
import java.io.File;
import java.awt.*;
import javax.swing.*;
import javax.swing.filechooser.*;
public class JavaFileView extends FileView {
Icon javaIcon = new DiamondIcon(Color.blue);
Icon classIcon = new DiamondIcon(Color.green);
/**
* If .java file, add length to name
*/
public String getName(File file) {
String filename = file.getName();
if (filename.endsWith(".java")) {
filename += " : " + file.length();
return filename;
}
return null;
}
/**
* Return special icons for .java and .class files
*/
public Icon getIcon(File file) {
// default icons for all directories
if (file.isDirectory()) {
return null;
}
String filename = file.getName();
if (filename.endsWith(".java")) {
return javaIcon;
} else if (filename.endsWith(".class")) {
return classIcon;
}
return null;
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFileChooser fileChooser =
new JFileChooser(".");
FileView view = new JavaFileView();
fileChooser.setFileView(view);
int status = fileChooser.showOpenDialog(null);
System.exit(0);
}
});
}
}
For more information about JFileChooser "see the javadoc".
Also, see "How to Use File Choosers" in the The Java Tutorial. For more information about multithreading and Swing, see "How to Use Threads" in the Java Tutorial.
BEST PRACTICES IN EXCEPTION HANDLING
Exception handling is a built-in aspect of the Java programming language. The concept of handling error cases with exceptions is designed to make code more reliable and easier to maintain and read. This tip looks at some best practices for dealing with and handling exceptions.
As a quick refresher, here's the full construct for exception handling:
try {
// code which could potentially
// throw an exception
} catch (TheException1 e) {
// code to handle exceptional condition
} catch (TheException2 e) {
// code to handle next exceptional condition
} finally {
// code to run whether an exceptional
// condition happened or not
}
Basically, code that might throw an exception goes within a try block. A specific catch block code runs only if the exception identified for it occurs (or its subclass). However, the code in the finally block runs under all conditions -- this even includes the case when the try-catch code calls return to exit out of the current method.
The try block is required. Additionally, one of the two blocks, catch or finally, must be present, or the compiler will complain.
Given that you can have multiple catch clauses, the system finds the first one that matches the exceptional condition. So, if you have a catch block for an IOException, and a FileNotFoundException happens, this is caught by the general IOException catch clause.
This suggests a best practice for exception handling: Always work with as specific an exception as possible. For instance, if you create a method that could generate a FileNotFoundException, don't declare that method to throw an IOException. If you do, it forces the caller to handle all IOExceptions that are possible, not just FileNotFoundExceptions. You shouldn't get too specific though. In particular, the declared exception thrown by a method should never reveal implementation details.
Another best practice is try to avoid empty catch blocks. In other words, don't ignore exceptions when they happen.
// This code is bad
try {
...
} catch (AnException e) {
}
The designers of system libraries throw exceptions for a reason: to tell you some exceptional condition happened. It's good practice to do something in response, even simply logging the problem. For classes you design, only declare methods to throw exceptions where you want the caller to deal with a problem. If you want to ignore an exception, perhaps because of an automatic retry that happens every minute or two, simply place a comment in the catch block to explicitly say why the exceptional condition is ignored.
// Better
try {
...
} catch (AnException ignored) {
// System automatically retries every minute
// Nothing to really do until retry
}
One very important practice related to exceptions thrown from methods is documentation. There is a javadoc tag for documenting which exceptions are thrown by a method. It's @throws. Best practices call for all checked exceptions to be documented. Common runtime exceptions should be documented too. For example, in the following method declaration, both the thrown exception and why the throwable exception could be thrown are documented. Don't just have the @throws ExceptionName bit. That doesn't add any more than the method declaration alone.
/**
* Loads the class
*
* @param name
* Class name
*
* @return Resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If class not found
*/
public Class loadClass(String name)
throws ClassNotFoundException {
...
}
For runtime exceptions, an exception here is the parseInt method of Integer. This can throw a NumberFormatException (this is declared as part of the method declaration). However, because this is a RuntimeException, it doesn't have to be declared.
Not only don't runtime exceptions have to be declared, but neither do they have to be checked. For instance, every array access can throw an ArrayIndexOutOfBoundsException. It is not good practice to wrap all array access code within a try-catch block. Doing that makes code difficult to read and maintain.
// This is bad, don't do it:
try {
for (int i=0, n=args.length; i<n; i++) {
System.out.println(args[i]);
}
} catch (ArrayIndexOutOfBoundsException e) {
System.err.println("Ooops");
}
For recoverable conditions, it is good practice to have catch clauses with actions involved. These are typically checked exceptions, although it's also possible with runtime exceptions. Case in point: the previously mentioned parseInt method of Integer. If you are validating user input with the parseInt method, that is certainly a recoverable operation (assuming the user is still available).
While it is possible to create your own custom exceptions, more typically, you can reuse the system exception classes. These are usually sufficient for most needs, though not for every need.
Runtime exceptions that you might use include the following:
NullPointerException - Tends to be encountered more by accident than by design. If this commonly happens to you, you might think of peer reviews.
IllegalArgumentException - Often encountered when validating arguments to a method. In some cases, they might result from the typical explicit checks at the start of a method. In other cases, they could happen after various operations are performed. Subclasses include NumberFormatException and PatternSyntaxException (for regular expressions).
IndexOutOfBoundsException - The ArrayIndexOutOfBoundsException class is a subclass, so is StringIndexOutOfBoundsException. You can use these in your classes and methods to check for passing in an integer beyond the end (or before the beginning) of your data structure.
UnsupportedOperationException - Typically used by the Collections Framework to indicate that a requested operation is not supported. You can use it to indicate similar usage problems.
Reusing existing exception classes allows developers to keep their codebase smaller and also take advantage of the familiarity they have with the existing exception class hierarchy.
The final aspect of exception handling to explore is exception chaining. Introduced with the 1.4 release of the Java 2 Platform, Standard Edition, exception chaining allows you to attach the underlying cause of a problem to the thrown exception. Chaining is not usually meant for an end user to see. Instead, it allows the person debugging a problem to see what caused the underlying problem.
For instance, in the database world, specifically in the JDBC libraries, it is common to have catch clauses for SQLException. Even prior to exception chaining, the SQLException clause had a getNextException method which allowed chaining of exceptions. To find out all the underlying causes of the SQLException, you could have a loop that looks something like the following:
try {
// JDBC code
} catch (SQLException ex) {
while (ex != null) {
System.err.println(
"SQLState: " + ex.getSQLState());
System.err.println(
"Message: " + ex.getMessage());
System.err.println(
"Vendor: " + ex.getErrorCode());
System.err.println("-----");
ex = ex.getNextException();
}
}
Instead of using the JDBC version of chaining, the standard approach is now the one shown below. Instead of printing the SQL State, message, and error code, just the exception type and message are displayed.
try {
// Normal code
} catch (AnException ex) {
Throwable t = ex;
while (t != null) {
System.err.println(
"Type: " + t.getClass().getName());
System.err.println(
"Message: " + t.getMessage());
System.err.println("-----");
t = t.getCause();
}
}
There is certainly much more that can be done with exceptions. Hopefully, the practices shown here should help get you started toward better understanding and usage.
For additional information on exception handling, see Chapter 8 "Exceptions" in "Effective Java Programming Language Guide" by Joshua Bloch.
IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developers.sun.com/dispatcher.jsp?uid=6910008
Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet
Subscribe to other Java developer Tech Tips:
- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).
To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications page, choose the newsletters you want to subscribe to and click
"Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html
Copyright 2004 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html
Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.
|