Help yourself to some useful Java SE tips Ten years of exploring Java Platform, Standard Edition (Java SE) has provided me with a grab bag loaded with useful tips for enhancing games and other Java programs. This installment of Java Fun and Games shares some of these tips with you. Later in this article, I use all of them to enhance an application that grabs images from Webpages.The simplest sound APINo matter how fast our computers run, it seems we always have to wait for certain tasks to complete, such as downloading large files, performing exhaustive searches, or making extensive mathematical calculations. After one of these lengthy tasks completes, many Java programs alert the user in some fashion, with audible alerts being common.Java provides several sound APIs for creating audible alerts. You can use the Java Speech API to tell the user that the task has finished. If you prefer to accomplish this task with sound effects or music, the Java Sound API is a good choice. However, because Java Speech requires extra distribution files, and because Java Sound requires fairly complex code, you might prefer to use the Audio Clip API. The Audio Clip API is based on java.applet.AudioClip and java.applet.Applet methods such as public static final AudioClip newAudioClip(URL url). Although this API is simpler to use than Java Speech and Java Sound, it is overkill if you only want the computer to emit a simple sound. For this task, consider using Java’s simplest sound API.The simplest sound API consists of java.awt.Toolkit‘s public abstract void beep() method. When this method is called, it emits a simple beep-like sound. To demonstrate beep()‘s usefulness, I’ve created a CalcPi application that calculates pi to a specific number of digits. Check out Listing 1.Listing 1. CalcPi.java // CalcPi.java import java.awt.Toolkit; import java.math.BigDecimal; public class CalcPi { /* constants used in pi computation */ private static final BigDecimal ZERO = BigDecimal.valueOf (0); private static final BigDecimal ONE = BigDecimal.valueOf (1); private static final BigDecimal FOUR = BigDecimal.valueOf (4); /* rounding mode to use during pi computation */ private static final int roundingMode = BigDecimal.ROUND_HALF_EVEN; /* digits of precision after the decimal point */ private static int digits; public static void main (String [] args) { if (args.length != 1) { System.err.println ("usage: java CalcPi digits"); return; } int digits = 0; try { digits = Integer.parseInt (args [0]); } catch (NumberFormatException e) { System.err.println (args [0] + " is not a valid integer"); return; } System.out.println (computePi (digits)); Toolkit.getDefaultToolkit ().beep (); } /* * Compute the value of pi to the specified number of * digits after the decimal point. The value is * computed using Machin's formula: * * pi/4 = 4*arctan(1/5) - arctan(1/239) * * and a power series expansion of arctan(x) to * sufficient precision. */ public static BigDecimal computePi (int digits) { int scale = digits + 5; BigDecimal arctan1_5 = arctan (5, scale); BigDecimal arctan1_239 = arctan (239, scale); BigDecimal pi = arctan1_5.multiply (FOUR). subtract (arctan1_239).multiply (FOUR); return pi.setScale (digits, BigDecimal.ROUND_HALF_UP); } /* * Compute the value, in radians, of the arctangent of * the inverse of the supplied integer to the specified * number of digits after the decimal point. The value * is computed using the power series expansion for the * arc tangent: * * arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 + * (x^9)/9 ... */ public static BigDecimal arctan (int inverseX, int scale) { BigDecimal result, numer, term; BigDecimal invX = BigDecimal.valueOf (inverseX); BigDecimal invX2 = BigDecimal.valueOf (inverseX * inverseX); numer = ONE.divide (invX, scale, roundingMode); result = numer; int i = 1; do { numer = numer.divide (invX2, scale, roundingMode); int denom = 2 * i + 1; term = numer.divide (BigDecimal.valueOf (denom), scale, roundingMode); if ((i % 2) != 0) result = result.subtract (term); else result = result.add (term); i++; } while (term.compareTo (ZERO) != 0); return result; } } Listing 1 calculates pi via an algorithm developed in the early 1700s by English mathematician John Machin. This algorithm first computes pi/4 = 4*arctan(1/5)-arctan(1/239) and then multiplies the result by 4 to achieve the value of pi. Because the arc (inverse) tangent is computed using a power series of terms, a greater number of terms yields a more accurate pi (in terms of digits after the decimal point).NoteListing 1 excerpts much of its code from the “Creating a Client Program” section of Sun’s Remote Method Invocation tutorial. This algorithm’s implementation relies on java.math.BigDecimal and an arc-tangent method. Although the Java SE 5.0 and higher versions of BigDecimal include constants ZERO and ONE, these constants are not present in Java 1.4. Also, the number-of-digits command line argument determines the number of arc-tangent power series terms and pi’s accuracy: java CalcPi 0 3 java CalcPi 1 3.1 java CalcPi 2 3.14 java CalcPi 3 3.142 java CalcPi 4 3.1416 java CalcPi 5 3.14159 More important to this article is Toolkit.getDefaultToolkit ().beep ();, which emits a beep-like sound when the calculation ends. Because larger digit arguments result in longer computations, this single beep lets you know when pi’s computation finishes. If a one-beep audible alert is not sufficient, you can create additional beeps, as shown below: Toolkit tk = Toolkit.getDefaultToolkit (); for (int i = 0; i < NUMBER_OF_BEEPS; i++) { tk.beep (); // On Windows platforms, beep() typically // plays a WAVE file. If beep() is called // before the WAVE sound finishes, the // second WAVE sound will not be heard. A // suitable delay solves this problem. // (I'm not sure if this problem occurs // on other platforms.) try { Thread.sleep (BEEP_DELAY); } catch (InterruptedException e) { } } Window centeringAdd a touch of professionalism to your Java programs by having them center their modal dialog box windows (an “about” dialog box, for example) within parent windows. Accomplish this task with java.awt.Window‘s public void setLocationRelativeTo(Component c) method, which centers a window relative to its component argument—null centers the window on the screen. Check out Listing 2.Listing 2. AboutBox1.java // AboutBox1.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; public class AboutBox1 { public static void main (String [] args) { final JFrame frame = new JFrame ("AboutBox1"); frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); JPanel panel = new JPanel () { { JButton btnWindow = new JButton ("Window center"); ActionListener l = new ActionListener () { public void actionPerformed (ActionEvent e) { new AboutBox (frame, "W").setVisible (true); } }; btnWindow.addActionListener (l); add (btnWindow); JButton btnScreen = new JButton ("Screen center"); l = new ActionListener () { public void actionPerformed (ActionEvent e) { new AboutBox (frame, "S").setVisible (true); } }; btnScreen.addActionListener (l); add (btnScreen); } }; frame.getContentPane ().add (panel); // frame.setLocationRelativeTo (null); frame.pack (); // frame.setLocationRelativeTo (null); frame.setVisible (true); } } class AboutBox extends JDialog { AboutBox (JFrame frame, String centerMode) { super (frame, "AboutBox", true /* modal */); final JButton btnOk = new JButton ("Ok"); btnOk.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent e) { dispose (); } }); getContentPane ().add (new JPanel () {{ add (btnOk); }}); pack (); setLocationRelativeTo (centerMode.equals ("W") ? frame : null); } } Listing 2’s AboutBox1 application creates a GUI whose two buttons establish an about dialog box centered relative to the application’s main window or the screen via setLocationRelativeTo(). The commented-out line before frame.pack (); does not center the main window on the screen because the main window’s size has yet to be determined. However, the second commented-out line centers this window. The getContentPane ().add (new JPanel () {{ add (btnOk); }}); statement might look a bit strange because of its nested pairs of braces. Essentially, I create an object from an anonymous inner class that subclasses javax.swing.JPanel, add a button to this object via the object block initializer identified by the inner braces pair, and add the object to the dialog box’s content pane.Drop shadowsIf you want to make an about dialog box’s title text stand out, consider using a drop shadow—background text drawn at an offset from, and in a specific “shadow” color to give the effect of being behind, the foreground text. Choose a suitable color for the background text to achieve good contrast with foreground text and the background. And use antialiasing to smooth jagged edges. The result appears in Figure 1.Figure 1 reveals an about dialog box with blue text drawn over a black drop shadow and a white background. This dialog box was created within an AboutBox2 application’s AboutBox(JFrame frame, String centerMode) constructor. Because this application’s source code is practically identical to AboutBox1.java, I present only the constructor: AboutBox (JFrame frame, String centerMode) { super (frame, "AboutBox", true /* modal */); // Add a panel that presents some text to the dialog box's content pane. getContentPane ().add (new JPanel () { final static int SHADOW_OFFSET = 3; { // Establish the drawing panel's preferred // size. setPreferredSize (new Dimension (250, 100)); // Create a solid color border that both // surrounds and is part of the drawing // panel. Select the panel background // color that is appropriate to this look // and feel. Color c = UIManager.getColor ("Panel.background"); setBorder (new MatteBorder (5, 5, 5, 5, c)); } public void paintComponent (Graphics g) { // Prevent jagged text. ((Graphics2D) g).setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Because the border is part of the panel, // we need to make sure that we don't draw // over it. Insets insets = getInsets (); // Paint everything but the border white. g.setColor (Color.white); g.fillRect (insets.left, insets.top, getWidth ()-insets.left- insets.right, getHeight ()-insets.top- insets.bottom); // Select an appropriate text font and // obtain the dimensions of the text to be // drawn (for centering purposes). The // getStringBounds() method is used instead // of stringWidth() because antialiasing is // in effect -- and the documentation for // stringWidth() recommends use of this // method whenever the antialiasing or // fractional metrics hints are in effect. g.setFont (new Font ("Verdana", Font.BOLD, 32)); FontMetrics fm = g.getFontMetrics (); Rectangle2D r2d; r2d = fm.getStringBounds ("About Box", g); int width = (int)((Rectangle2D.Float) r2d) .width; int height = fm.getHeight (); // Draw shadow text that is almost // horizontally and vertically (the // baseline) centered within the panel. g.setColor (Color.black); g.drawString ("About Box", (getWidth ()-width)/2+ SHADOW_OFFSET, insets.top+(getHeight()- insets.bottom-insets.top)/2+ SHADOW_OFFSET); // Draw blue text that is horizontally and // vertically (the baseline) centered // within the panel. g.setColor (Color.blue); g.drawString ("About Box", (getWidth ()-width)/2, insets.top+(getHeight()- insets.bottom-insets.top)/2); } }, BorderLayout.NORTH); final JButton btnOk = new JButton ("Ok"); btnOk.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent e) { dispose (); } }); getContentPane ().add (new JPanel () {{ add (btnOk); }}, BorderLayout.SOUTH); pack (); setLocationRelativeTo (centerMode.equals ("W") ? frame : null); } In addition to showing you how to render a drop shadow in the JPanel subclass component’s public void paintComponent(Graphics g) method, the constructor reveals another tip: use UIManager.getColor("Panel.background") to obtain a color for a component’s border that matches the color of the dialog box’s background (for the current look and feel).Hyperlinks and browser launchingMany programs present hyperlinks in their about dialog boxes. When the user clicks a hyperlink, the program launches the default Web browser, taking the user to the program’s Website. Because I think that hyperlinks in about dialog boxes are classy, I created an AboutBox3 application that demonstrates this capability. Examine the source code excerpt below: AboutBox (JFrame frame, String centerMode) { super (frame, "AboutBox", true /* modal */); // Create a pane that presents this dialog box's text. Surround the pane // with a 5-pixel empty border. Pane pane = new Pane (5); pane.setPreferredSize (new Dimension (250, 100)); // Create a title with a drop shadow for the pane. Font font = new Font ("Verdana", Font.BOLD, 32); Pane.TextNode tn = pane.new TextNode ("About Box", font, Color.blue, Pane.TextNode.CENTERX, Pane.TextNode.CENTERY, Color.black); pane.add (tn); // Create a link for the pane. font = new Font ("Verdana", Font.BOLD, 12); tn = pane.new TextNode ("Jeff Friesen", font, Color.blue, Pane.TextNode.CENTERX, 80, null, "http://www.javajeff.mb.ca", Color.red); pane.add (tn); // Add pane to the center region of the dialog box's content pane. getContentPane ().add (pane); // Create a button for disposing the dialog box. final JButton btnOk = new JButton ("Ok"); btnOk.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent e) { dispose (); } }); // Add button via an intermediate panel that causes button to be laid // out at its preferred size to the south region of the dialog box's // content pane. getContentPane ().add (new JPanel () {{ add (btnOk); }}, BorderLayout.SOUTH); // Resize all components to their preferred sizes. pack (); // Center the dialog box with respect to the frame window or the screen. setLocationRelativeTo (centerMode.equals ("W") ? frame : null); } The AboutBox(JFrame frame, String centerMode) constructor creates a Pane component that describes a region for drawing text. This component’s Pane(int borderSize) constructor takes a borderSize argument, identifying the size (in pixels) of the component’s border (the size of the drawing area is equal to the pane’s preferred size minus the border size): Pane (int borderSize) { // Create a solid color border that both surrounds and is part of the // this component. Select the panel background color that is appropriate // to this look and feel. setBorder (new MatteBorder (borderSize, borderSize, borderSize, borderSize, UIManager.getColor ("Panel.background"))); } This component stores text as Pane.TextNode objects in an array list. Each TextNode describes one text item and is created from one of three constructors. The simplest constructor is TextNode(String text, Font font, Color color, int x, int y), which creates a text node describing non-hyperlink text without a drop shadow. It presents five parameters:text specifies the text to be drawnfont specifies the font in which the text is drawncolor specifies the color in which the text is drawnx specifies the starting column of the first charactery specifies the row of each character’s baselineThe next simplest constructor is TextNode(String text, Font font, Color color, int x, int y, Color shadowColor). Along with the previous parameters, it specifies shadowColor, which is the color of the drop shadow. If you pass null, no drop shadow is rendered, which makes this constructor equivalent to the previous constructor. Both of these constructors invoke a third constructor: TextNode (String text, Font font, Color color, int x, int y, Color shadowColor, String url, Color activeLinkColor) { this.text = text; this.font = font; this.color = color; this.x = x; this.y = y; this.shadowColor = shadowColor; this.url = url; this.activeLinkColor = activeLinkColor; if (url != null) { addMouseListener (new MouseAdapter () { public void mousePressed (MouseEvent e) { int mx = e.getX (); int my = e.getY (); if (mx >= TextNode.this.x && mx < TextNode.this.x+width && my > TextNode.this.y-height && my <= TextNode.this.y) { active = false; repaint (); } } public void mouseReleased (MouseEvent e) { int mx = e.getX (); int my = e.getY (); if (mx >= TextNode.this.x && mx < TextNode.this.x+width && my > TextNode.this.y-height && my <= TextNode.this.y) { active = true; repaint (); Launcher. launchBrowser (TextNode.this.url); } } }); addMouseMotionListener (new MouseMotionListener () { public void mouseMoved (MouseEvent e) { int mx = e.getX (); int my = e.getY (); if (mx >= TextNode.this.x && mx < TextNode.this.x+width && my > TextNode.this.y-height && my <= TextNode.this.y) { if (!active) { active = true; repaint (); } } else { if (active) { active = false; repaint (); } } } public void mouseDragged (MouseEvent e) { } }); } } After saving its arguments, the constructor registers a pair of mouse listeners with the pane (provided that url does not contain null). These listeners determine if the mouse pointer is within the hyperlink’s text boundaries. If so, an active variable is manipulated, this and other text nodes are rendered, and the browser launches: class Launcher { static void launchBrowser (String url) { try { // Identify the operating system. String os = System.getProperty ("os.name"); // Launch browser with URL if Windows. Otherwise, just output the url // to the standard output device. if (os.startsWith ("Windows")) Runtime.getRuntime () .exec ("rundll32 url.dll,FileProtocolHandler " + url); else System.out.println (url); } catch (IOException e) { System.err.println ("unable to launch browser"); } } } Pane‘s public void paintComponent(Graphics g) method is called when this component and its text nodes must be rendered. This method enables antialiasing (to prevent jagged text), obtains the component’s insets (so that text is not drawn on the border), clears the drawing area to white, and renders each text node stored in the array list: public void paintComponent (Graphics g) { // Prevent jagged text. ((Graphics2D) g).setRenderingHint (RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // Because the border is part of the panel, we need to make sure that we // don't draw over it. Insets insets = getInsets (); // Paint everything but the border white. g.setColor (Color.white); g.fillRect (insets.left, insets.top, getWidth ()-insets.left-insets.right, getHeight ()-insets.top-insets.bottom); // Render all nodes. Iterator iter = nodes.iterator (); while (iter.hasNext ()) { TextNode tn = (TextNode) iter.next (); tn.render (g, insets); } } Each text node is rendered by TextNode‘s void render(Graphics g, Insets insets) method. This method first establishes the font and then invokes a private strDim() method to obtain the dimensions of the text to be drawn. The optional drop shadow and the non-optional hyperlink or non-hyperlink text are then rendered: void render (Graphics g, Insets insets) { g.setFont (font); Dimension d = strDim (g, text); width = (int) d.width; height = (int) d.height; // Always drop the drop shadow (if specified) first. if (shadowColor != null) { g.setColor (shadowColor); if (x == CENTERX) x = (getWidth ()-d.width)/2; if (y == CENTERY) y = insets.top+(getHeight ()-insets.bottom-insets.top)/2; // Draw the drop shadow. g.drawString (text, x+SHADOW_OFFSET, y+SHADOW_OFFSET); } // If the text is not a link, active can never be true -- the mouse // listeners are not installed. g.setColor ((active) ? activeLinkColor: color); // If a drop shadow was drawn, x and y will never equal CENTERX and // CENTERY (respectively). This is okay because x and y must contain // the same values as specified when drawing the drop shadow. if (x == CENTERX) x = (getWidth ()-d.width)/2; if (y == CENTERY) y = insets.top+(getHeight ()-insets.bottom-insets.top)/2; // Draw the text. g.drawString (text, x, y); } The g.setColor ((active) ? activeLinkColor: color); statement determines whether to draw an active hyperlink’s text (using the active link color specified by activeLinkColor), or to draw inactive hyperlink text (or non-hyperlink text) in the color specified by color. Figure 2 shows the result of this decision: Figure 2. The color of the hyperlink’s text changes to red when the mouse pointer moves over this text. Click on thumbnail to view full-sized image.Status barsMany applications present status bars that typically display a program name and version, menu-related help text, the current time, and other details. Because status bars are so useful, you might think that Java would include a javax.swing.JStatusBar component. However, this is not the case. Fortunately, it is easy to create your own status bar, as Figure 3 reveals.Figure 3 reveals a status bar created by a StatBar application. This application combines a javax.swing.JLabel component with a javax.swing.event.MenuListener and a java.awt.event.MouseListener to display menu-related and menu item-related help text—or default text if there is no selected menu or menu item. Check out Listing 3.Listing 3. StatBar.java // StatBar.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; public class StatBar extends JFrame { // The status label serves as this application's status bar. A description // of the currently highlighted menu/item appears on the status bar. JLabel status; // The default text appears on the status bar at program startup, and when // no other menu/item text appears. String defaultStatusText = "Welcome to StatBar 1.0!"; // The MenuItem helper class conveniently organizes the menu items for each // of the File and Edit menus. This organization reduces the amount of // source code that appears in the StatBar() constructor, which hopefully // makes it easier to study the constructor, and facilitates adding extra // menu items in the future. class MenuItem { String label; // menu text ActionListener al; String desc; // menu description for status bar MenuItem (String label, ActionListener al, String desc) { this.label = label; this.al = al; this.desc = desc; } } // Construct StatBar's GUI and indirectly start AWT helper threads. public StatBar (String title) { // Pass application title to superclass, so that it appears on the title // bar. super (title); // When the user initiates a close operation from the System menu or by // clicking the tiny x window on a Microsoft Windows' window title bar, // terminate this application. setDefaultCloseOperation (EXIT_ON_CLOSE); // Construct the application's menu bar. JMenuBar mb = new JMenuBar (); // Create a menu listener shared by all menus on the menu bar. This menu // listener either displays default text or menu-specific text on the // status bar. MenuListener menul; menul = new MenuListener () { public void menuCanceled (MenuEvent e) { } public void menuDeselected (MenuEvent e) { status.setText (defaultStatusText); } public void menuSelected (MenuEvent e) { JMenu m = (JMenu) e.getSource (); status.setText (m.getActionCommand ()); } }; // Create a mouse listener shared by all menu items on all menus. This // mouse listener displays menu-item specific text on the status bar // whenever the mouse pointer enters the menu item. It displays default // text when the mouse pointer exits a menu item. MouseListener statusl = new MouseAdapter () { public void mouseEntered (MouseEvent e) { JMenuItem mi = (JMenuItem) e.getSource (); status.setText (mi.getActionCommand ()); } public void mouseExited (MouseEvent e) { status.setText (defaultStatusText); } }; // The first menu to appear on the menu bar is File. The user invokes // menu items on this menu to open, save, and print documents, and to // terminate the application. JMenu menuFile = new JMenu ("File"); menuFile.addMenuListener (menul); menuFile.setActionCommand ("Open document, save changes, print document " + "and terminate StatBar."); // Create a listener for each menu item on the File menu. ActionListener openl; openl = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Open listener invoked."); } }; ActionListener saveasl; saveasl = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Save as listener invoked."); } }; ActionListener savel; savel = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Save listener invoked."); } }; ActionListener printl; printl = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Print listener invoked."); } }; ActionListener exitl; exitl = new ActionListener () { public void actionPerformed (ActionEvent e) { System.exit (0); } }; // Identify menu items to be installed on the File menu. MenuItem [] itemsFile = { new MenuItem ("Open...", openl, "Open a document."), new MenuItem ("Save", savel, "Save changes to current document."), new MenuItem ("Save as...", saveasl, "Save current document to new " + "document."), new MenuItem ("Print...", printl, "Print current document."), new MenuItem (null, null, null), new MenuItem ("Exit", exitl, "Terminate StatBar.") }; // Install all of the previous menu items on the File menu. for (int i = 0; i < itemsFile.length; i++) { if (itemsFile [i].label == null) { menuFile.addSeparator (); continue; } JMenuItem mi = new JMenuItem (itemsFile [i].label); mi.addActionListener (itemsFile [i].al); mi.setActionCommand (itemsFile [i].desc); mi.addMouseListener (statusl); menuFile.add (mi); } // Add the file menu to the menu bar. mb.add (menuFile); // The second menu to appear on the menu bar is Edit. The user invokes // menu items on this menu to undo any changes and perform copy/cut/paste // operations on the current document. JMenu menuEdit = new JMenu ("Edit"); menuEdit.addMenuListener (menul); menuEdit.setActionCommand ("Perform various editing tasks and undo " + "changes."); // Create a listener for each menu item on the Edit menu. ActionListener undol; undol = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Undo listener invoked."); } }; ActionListener copyl; copyl = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Copy listener invoked."); } }; ActionListener cutl; cutl = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Cut listener invoked."); } }; ActionListener pastel; pastel = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Paste listener invoked."); } }; // Identify menu items to be installed on the Edit menu. MenuItem [] itemsEdit = { new MenuItem ("Undo", undol, "Restore document."), new MenuItem (null, null, null), new MenuItem ("Copy", copyl, "Copy text to clipboard."), new MenuItem ("Cut", cutl, "Cut text to clipboard."), new MenuItem ("Paste", pastel, "Paste text from clipboard.") }; // Install all of the previous menu items on the Edit menu. for (int i = 0; i < itemsEdit.length; i++) { if (itemsEdit [i].label == null) { menuEdit.addSeparator (); continue; } JMenuItem mi = new JMenuItem (itemsEdit [i].label); mi.addActionListener (itemsEdit [i].al); mi.setActionCommand (itemsEdit [i].desc); mi.addMouseListener (statusl); menuEdit.add (mi); } // Add the edit menu to the menu bar. mb.add (menuEdit); // Install StatBar's menu bar. setJMenuBar (mb); // Create a status bar for displaying help text associated with the menus // and their items. status = new JLabel (defaultStatusText); status.setBorder (BorderFactory.createEtchedBorder ()); // Add the status bar to the bottom of the application's contentpane. getContentPane ().add (status, BorderLayout.SOUTH); // Establish a suitable initial size for displaying a document. setSize (450, 300); // Display GUI and start GUI processing. setVisible (true); } // Application entry point. public static void main (String [] args) { // Create the application's GUI and start the application. new StatBar ("StatBar"); } } After creating the status JLabel with default status bar text, StatBar establishes an etched border (via status.setBorder (BorderFactory.createEtchedBorder ());) to make the status bar label stand out from the rest of the GUI. This label is then added to the southern region of the frame window’s content pane, where status bars normally reside.NoteTo prevent a status bar from displaying default text, specify at least one space character for this text. If you instead specify the empty string (""), the status bar will not appear (although its etched border will display). The reason has to do with the status bar label’s preferred size being inferred from the current font and at least one character. If the status bar label has no characters (the empty string), its preferred size is effectively zero, and it does not display. The MenuItemListener interface describes a listener for the File and Edit menus. Its public void menuSelected(MenuEvent e) method is invoked whenever these menus are selected; then the menu’s help text displays. When a menu is deselected, the public void menuDeselected(MenuEvent e) is invoked—default text is presented.The MouseListener interface describes a listener for each menu item. Its public void mouseEntered(MouseEvent e) method is invoked whenever the mouse pointer enters a menu item. The menu item’s help text is then shown on the status bar. When the mouse pointer exits the menu item, the public void mouseExited(MouseEvent e) is invoked, and default text appears. Each of these listeners depends on javax.swing.JMenu‘s or javax.swing.JMenuItem‘s inherited public void setActionCommand(String command) method having previously been called to assign status bar text to each menu or menu item. This text is retrieved from inside the listener by invoking the associated public String getActionCommand() method.Image grabberA couple of years ago, I built a command line-based GetImages application—check out Listing 4—that grabs a Webpage’s images, saving them to files on my hard drive. This application takes a single URL argument, which it uses to connect to an HTML document. The document is parsed, <img> tag src attribute values are extracted to identify image files, and these files are downloaded.Listing 4. GetImages.java // GetImages.java import java.io.*; import java.net.*; import java.util.regex.*; import javax.swing.text.*; import javax.swing.text.html.*; import javax.swing.text.html.parser.ParserDelegator; public class GetImages { public static void main (String [] args) { // Validate number of command-line arguments. if (args.length != 1) { System.err.println ("usage: java GetImages URL"); return; } // Create a Base URI from the solitary command-line argument. This URI // will be used in the handleSimpleTag() callback method to convert a // potentially relative URI in an <img> tag's src attribute to an // absolute URI. final URI uriBase; try { uriBase = new URI (args [0]); } catch (URISyntaxException e) { System.err.println ("URI is improperly formed"); return; } // Convert the URI to a URL, so that the HTML document can be read and // parsed. URL url; try { url = new URL (args [0]); } catch (MalformedURLException e) { System.err.println ("URL is improperly formed"); return; } // Establish a callback whose handleSimpleTag() method is invoked for // each tag that does not have an end tag. The <img> tag is an example. HTMLEditorKit.ParserCallback callback; callback = new HTMLEditorKit.ParserCallback () { public void handleSimpleTag (HTML.Tag tag, MutableAttributeSet aset, int pos) { // If an <img> tag is encountered ... if (tag == HTML.Tag.IMG) { // Get the value of the src attribute. String src = (String) aset.getAttribute (HTML.Attribute.SRC); // Create a URI based on the src value, and then // resolve this potentially relative URI against // the document's base URI, to obtain an absolute // URI. URI uri = null; try { // Handle this situation: // // 1) http://www.javajeff.mb.ca // // There is no trailing forward slash. // // 2) common/logo.jpg // // There is no leading forward slash. // // 3) http://www.javajeff.mb.cacommon/logo.jpg // // The resolved URI is not valid. if (!uriBase.toString ().endsWith ("/") && !src.startsWith ("/")) src = "/" + src; uri = new URI (src); uri = uriBase.resolve (uri); System.out.println ("uri being " + "processed ... " + uri); } catch (URISyntaxException e) { System.err.println ("Bad URI"); return; } // Convert the URI to a URL so that its input // stream can be obtained. URL url = null; try { url = uri.toURL (); } catch (MalformedURLException e) { System.err.println ("Bad URL"); return; } // Open the URL's input stream. InputStream is; try { is = url.openStream (); } catch (IOException e) { System.err.println ("Unable to open input " + "stream"); return; } // Extract URL's file component and remove path // information -- only the filename and its // extension are wanted. String filename = url.getFile (); int i = filename.lastIndexOf ('/'); if (i != -1) filename = filename.substring (i+1); // Save image to file. saveImage (is, filename); } } }; // Read and parse HTML document. try { // Read HTML document via an input stream reader that assumes the // default character set for decoding bytes into characters. Reader reader = new InputStreamReader (url.openStream ()); // Establish a ParserDelegator whose parse() method causes the // document to be parsed. Various callback methods are called and // the document's character set is not ignored. The parse() method // throws a ChangedCharSetException if it encounters a <meta> tag // with a charset attribute that specifies a character set other // than the default. new ParserDelegator ().parse (reader, callback, false); } catch (ChangedCharSetException e) { // Reparse the entire file using the specified charset. A regexp // pattern is specified to extract the charset name. String csspec = e.getCharSetSpec (); Pattern p = Pattern.compile ("charset="?(.+)"?s*;?", Pattern.CASE_INSENSITIVE); Matcher m = p.matcher (csspec); String charset = m.find () ? m.group (1) : "ISO-8859-1"; // Read and parse HTML document using appropriate character set. try { // Read HTML document via an input stream reader that uses the // specified character set to decode bytes into characters. Reader reader; reader = new InputStreamReader (url.openStream (), charset); // This time, pass true to ignore the <meta> tag with its charset // attribute. new ParserDelegator ().parse (reader, callback, true); } catch (UnsupportedEncodingException e2) { System.err.println ("Invalid charset"); } catch (IOException e2) { System.err.println ("Input/Output problem"); e.printStackTrace (); } } catch (IOException e) { System.err.println ("Input/Output problem"); e.printStackTrace (); } } public static void saveImage (InputStream is, String filename) { FileOutputStream fos = null; try { fos = new FileOutputStream (filename); int bYte; while ((bYte = is.read ()) != -1) fos.write (bYte); } catch (IOException e) { System.err.println ("Unable to save stream to file"); } finally { if (fos != null) try { fos.close (); } catch (IOException e) { } } } } Listing 4 uses the javax.swing.text.html.parser.ParserDelegator class to create a parser for HTML documents, and calls the parser object’s public void parse(Reader r, HTMLEditorKit.ParserCallback cb, boolean ignoreCharSet) method to carry out the parse. This method presents three parameters:r identifies a java.io.Reader object by which the document is readcb identifies a javax.swing.text.html.HTMLEditorKit.ParserCallback object that processes parsed tags and their attributesignoreCharSet identifies whether (true) or not (false) to ignore the charset attribute in the document’s <meta> tag (if present)The parser invokes various ParserCallback methods during the parse. Only one of these methods is required by GetImages: method public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos). This method is called for tags that have no matching end tags (<img> is an example), and presents three parameters:t identifies the tag via an HTML.Tag objecta identifies the attributes that go with this tag via a javax.swing.text.MutableAttributeSet objectpos identifies the current parse positionNow that you have a good idea of how GetImages works, you’ll want to try out this application. For example, you might specify java GetImages http://www.javajeff.mb.ca to download images linked to my Website’s main page. In response, you should see output similar to what appears below (along with several new image files in the current directory): uri being processed ... http://www.javajeff.mb.ca/common/logo.jpg uri being processed ... http://www.javajeff.mb.ca/common/logo.gif uri being processed ... http://www.javajeff.mb.ca/na/images/wom.jpg Although GetImages is useful in a command line context, this application would be even more useful with a GUI that lets you conveniently grab and view images. My IG application, which combines the GetImages source code with the earlier tips and some additional GUI code, does just that. Figure 4 presents IG‘s GUI—I refer you to this article’s code archive (available from Resources) for the source code.ConclusionThe image grabber application nicely combines this article’s tips, but suffers from a couple of problems, the solutions for which I leave as homework. First, image grabber downloads images whose image tags specify relative URLs (as in <img src="/image.gif">), but is unable to download images whose image tags specify absolute URLs—<img src="http://www.abc.com/image.gif"> is an example.The second problem: image grabber is unable to download images where the <img> tag src attributes specify dynamically generated images. The following src attribute from an <img> tag that I recently found on JavaWorld‘s main page is an example: src="http://ad.doubleclick.net/ad/idg.us.nwf.jw_home/;abr=!ie;pos=top;sz=728x90;ptile=1;type=;ord=063423?".Do you have any of your own tips for enhancing Java games? Submit them in the discussion thread below. Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology. JavaSoftware DevelopmentTechnology Industry