HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

A Web browser is often considered a very complex application. In this article I'll go over the design and implementation of a simple browser offering a look somewhat similar to Navigator and Internet Explorer. I'll use an existing HTML renderer and several classes of the Swing API. This will give us the opportunity to review some classes introduced in the Swing API, such as JPanel, JButton, JLabel, JOptionPane and JProgressBar. Most of the classes will have AWT equivalent classes, but they all provide additional capabilities. For example, JPanel provides the option of displaying a border around the panel. In this article I focus on some of the Swing classes and how to use the additional features they provide as compared to the AWT classes.

A browser such as Navigator is composed of four panels: a panel including buttons controlling the browser, a panel showing the current location and allowing the user to change this location, an HTML renderer and finally, a panel showing whether the browser is loading a page or if the cursor is over a link. I will first describe how to use the HTML renderer and then go over the three remaining panels and present how to set up the underlying look and feel.

HTML Renderer
The tasks of an HTML renderer are basically to parse and render HTML documents loaded from the local file system or over HTTP connections. To implement this simple browser, I have chosen to use the ICE renderer, which is available at www.icesoft.no. Other renderers, such as HotJava and HTMLEditorKit (which is part of the Swing components) and Clue (www.coolclue.com) could also have been used.

The primary class of interest that handles HTML documents is the Browser class, which extends java.awt.Panel through the Document class. I will use the following methods from the Browser class to implement our simple browser:

  • gotoLocation(java.lang.String loc) - load a new HTML document
  • goBack() - go to the previous document in the history list
  • goForward() - go to the next document in the history list
  • reload() - reload the current document
  • getBackHistory() - get a list of previous documents in the history list
  • getForwardHistory() - get a list of next documents in the history list
  • getParsingProgress() - get the parsing progress in the current document

In addition, a document fires a PropertyChangeEvent when important status changes occur in the document. PropertyChangeEvents can be monitored by adding a PropertyChangeListener to the Document. The following properties are used:

  • statusString - The status property is set by the Document when the mouse is over a link, and by applets.
  • documentBase - This property indicates the URL of a document.
  • documentTitle - This property indicates the title of the document.
  • parsingProgress - This property returns a string of the form "frameName charsRead totalChars", where frameName is the name of the current frame, charsRead is the number of characters read and totalChars is the size of the HTML document (if available), or -1 if it's unknown. Parsing is complete when charsRead is equal to totalChars.
Control Panel
The control panel includes a button to open a dialog for file or URL selection, buttons to move forward and backward and to reload the current page, and a button to exit from the browser. Each button contains an icon and text. The button's border is visible only if the cursor rolls over the button. The icon must change whether the button is enabled or disabled, or if the cursor rolls over it. In addition, the tool tip text should appear when the cursor stays idle on a button for several seconds. These buttons should be grouped on the left of the control panel, while an animated icon should appear on the right of the panel. Using the JButton class of the Swing API, implementing such buttons is straightforward. Since all the buttons must have this common behavior, I start first by extending the JButton class by the ControlButton class with a constructor that removes the border and focus, and then aligns the text under the icon:

class ControlButton extends JButton {
ControlButton() {
setBorderPainted(false);
setFocusPainted(false);
setHorizontalTextPosition
(SwingConstants.CENTER);
setVerticalTextPosition(SwingConstants.BOTTOM);
}
}

A mouse listener is used to detect whether the mouse enters or exits a button, and paints the border if necessary. The set of icons used is set by the setIcon, setRolloverIcon and setDisabledIcon methods. The tool tip text is set by the ToolTipText method. Whenever it's appropriate, the tool tip texts of the Back and Forward buttons are updated with the URL of the previous or next page, respectively.

As noted above, the control panel also contains an icon reflecting whether or not the browser is loading a document. To include an icon in a panel, I use the JLabel class and its setIcon method. In addition, it's also possible to specify the border of a label with the setBorder method. Subsequent calls to the setIcon method are used with either a static or an animated GIF icon. Animated GIF images are handled automatically and no additional coding is required.

To select a new URL, the dialog is implemented using the static method showInputDialog of the JOptionPanel class. JOptionPane is designed to make it easy to pop up standard dialog boxes. It includes several showXXXDialog methods to ask a confirming question, prompt for some input or to inform the user. Only a one-line call is necessary to use these methods:

String result;
result = JOptionPane.showInputDialog("Enter the URL ");
if (result != null) {
iceBrowser.gotoLocation(result);
}

To complete the implementation of the control panel, it's necessary to lay the components down correctly. Two internal panels are used, both of them using FlowLayout. The panel containing the buttons uses a LEFT alignment, while the panel containing the icon uses a RIGHT alignment. These two panels are then included in another panel using GridBagLayout. The first panel uses a WEST alignment, while the second panel uses an EAST alignment. In addition, a non-null weightx value is specified to allow components to grow within the panel.

Location Panel
The location panel includes a label containing an icon, text and a textfield. The label is implemented as is described above, except the default alignment is used. The textfield is implemented using the JTextField class and a KeyListener is used to detect characters typed by the user. Once the ENTER key is pressed, a new document is loaded. Again, GridBagLayout is used to lay down the components within the panel. A non-null weight x value is specified to allow the textfield to grow within the panel.

Link Panel
The link panel includes a label specifying whether or not the browser is loading a document, or if the mouse is over a link. A progress bar is used to indicate how much of a document has actually been loaded. The JLabel class is used to display the label. This label is an update function of the state of the browser. The progress bar is implemented by the JProgressBar class. Three methods of the JProgressBar class are used: setMinimum, setMaximum and setValue. They respectively set the progress bar's minimum, maximum and current values. The minimum value is set to zero while the maximum and current values are updated when the parsingProgress property is received. Once again, GridBagLayout is used to allow the label to grow within the panel.

Putting the Pieces Together
I will now present how properties are handled and how the control, location and link panels are updated. It's first necessary to add a PropertyChangeListener to the browser:

iceBrowser = new Browser();
iceBrowser.addPropertyChangeListener(this);

public void propertyChange(PropertyChangeEvent ev) {
String prop = ev.getPropertyName();
if (prop.equals("statusString")) { ...
} else if (prop.equals("documentBase")) {
...
} else if (prop.equals("documentTitle")) {
...
} else if (prop.equals("parsingProgress")) {
...
}
}

A propertyChange method receives updates from the HTML renderer. If the property is statusString, the cursor is updated to a default cursor or to a hand cursor whether the property's value is an empty string or not. If the property is documentBase, the textfield of the location panel is updated with the new URL. If the property is documentTitle, the browser's title is updated with the new title, the animated icon is displayed, the progress bar is reset and the tool tip texts of the Back and Forward buttons are updated. If the property is parsingProgress, the progress bar and the Link panel's label are updated. When all the whole document is loaded, the animated icon is switched back to a static icon.

To lay down all the panels, BorderLayout is used, but first an internal panel is used to group the control and location panels. This panel, as well as the remaining panel are then added to the frame.

Look And Feel
As Swing components do not rely on the GUI manager of the underlying operating system as AWT does, it is possible to provide a look and feel different than the one of the native windowing system. This can be used by an application to provide a common look and feel on different windowing systems. For example, an application can set up a motif look and feel by the following code:
lnf = "com.sun.java.swing.plaf.motif.MotifLookAndFeel"; UIManager.setLookAndFeel(lnfName);

The simple browser using the metal look and feel is shown in Figure 1.

Figure 1
Figure 1:

Conclusion
Using the Swing API, it is possible to painlessly implement a user interface that requires a major effort using the AWT API. I have used only a few of the classes provided by Swing, but Swing also includes advanced GUI components such as trees and tables. Of course, Swing does not eliminate AWT; if some Swing components can be used instead of their equivalent AWT components, a good knowledge of the layout managers is still needed to implement complex user interfaces. As I mentioned above, I've used the ICE HTML renderer. To be able to switch from one look and feel to another, and to ensure a consistent look and feel between the scrollbar of the HTML renderer and the other components, I have used the version of the renderer implemented with the Swing API. This version is still under development but will parse most simple Web pages. The set of icons was developed by Dean Jones and they are available at www.javalobby.org/jfa/projects/icons. I have borrowed the animated and static icons to show if a document is being loaded from the HotJava browser. Finally, it should be straightforward to extend this simple browser with capabilities such as keeping a list of favorite URLs and setting up the home page.

About the Author
Pascal Ledru is a software engineer specializing in networking applications at Netran Interactive Commerce. He is also working on his Ph.D. in computer science at the University of Alabama in Huntsville. Pascal may be reached at [email protected]

	

Listing 1.
 
import javax.swing.*; 
import javax.swing.border.*; 
import javax.swing.plaf.metal.*; 
import com.sun.java.swing.plaf.motif.*; 
import com.sun.java.swing.plaf.windows.*; 
import ice.iblite.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.util.StringTokenizer; 
import java.beans.*; 
public class SimpleBrowser extends JFrame implements 
PropertyChangeListener, WindowListener { 

  Browser iceBrowser; 
  ControlPanel control = new ControlPanel(); 
  LinkPanel link = new LinkPanel(); 
  LocationPanel location = new LocationPanel(); 

  class ControlButton extends JButton { 
    ControlButton() { 
      setBorderPainted(false); 
      setFocusPainted(false); 
      setHorizontalTextPosition(SwingConstants.CENTER); 
      setVerticalTextPosition(SwingConstants.BOTTOM); 
    } 
  } 

  class MouseEventListener implements MouseListener { 
    JButton button; 
    MouseEventListener(JButton button) { 
      this.button = button; 
    } 
    public void mouseClicked(MouseEvent ev) { 
    } 
    public void mouseEntered(MouseEvent ev) { 
      button.setBorderPainted(true); 
    } 
    public void mouseExited(MouseEvent ev) { 
      button.setBorderPainted(false); 
    } 
    public void mousePressed(MouseEvent ev) { 
    } 
    public void mouseReleased(MouseEvent ev) { 
    } 
  } 

  class OpenListener implements ActionListener { 
    public void actionPerformed(ActionEvent ev) { 
      String result; 
      result = JOptionPane.showInputDialog("Enter the 
URL or specify the local file you would like to open : "); 
      if (result != null) { 
        iceBrowser.gotoLocation(result); 
      } 
    } 
  } 

  class BackListener implements ActionListener { 
    public void actionPerformed(ActionEvent ev) { 
      iceBrowser.goBack(); 
    } 
  } 

  class ForwardListener implements ActionListener { 
    public void actionPerformed(ActionEvent ev) { 
      iceBrowser.goForward(); 
    } 
  } 

  class ReloadListener implements ActionListener { 
    public void actionPerformed(ActionEvent ev) { 
      iceBrowser.reload(); 
    } 
  } 

  class ExitListener implements ActionListener { 
    public void actionPerformed(ActionEvent ev) { 
      System.exit(0); 
    } 
  } 

  class ControlPanel extends JPanel { 

    private JLabel status; 
    private boolean idle = false; 
    private ControlButton back; 
    private ControlButton forward; 

    ControlPanel() { 

      setBorder(new EtchedBorder()); 
      GridBagLayout gridbag = new GridBagLayout(); 
      setLayout(gridbag); 
      GridBagConstraints c = new GridBagConstraints(); 

      JPanel panel1 = new JPanel(); 
      panel1.setLayout(new FlowLayout 
(java.awt.FlowLayout.LEFT,0,0)); 

      ControlButton open = new ControlButton(); 
      open.setIcon(new ImageIcon("images/OpenArrow.gif")); 
      open.setRolloverIcon(new ImageIcon 
("images/BrightOpenArrow.gif")); 
      open.setText("Open"); 
      open.addActionListener(new OpenListener()); 
      open.addMouseListener(new MouseEventListener(open)); 
      open.setToolTipText("Open a document"); 

      back = new ControlButton(); 
      back.setIcon(new ImageIcon("images/Left.gif")); 
      back.setRolloverIcon(new ImageIcon 
("images/BrightLeft.gif")); 
      back.setDisabledIcon(new ImageIcon 
("images/DarkLeft.gif")); 
      back.setText("Back"); 
      back.setEnabled(false); 
      back.addActionListener(new BackListener()); 
      back.addMouseListener(new MouseEventListener(back)); 
      back.setToolTipText("Go to previous page"); 

      forward = new ControlButton(); 
      forward.setIcon(new ImageIcon("images/Right.gif")); 
      forward.setRolloverIcon(new ImageIcon 
("images/BrightRight.gif")); 
      forward.setDisabledIcon(new ImageIcon 
("images/DarkRight.gif")); 
      forward.setText("Forward"); 
      forward.setEnabled(false); 
      forward.addActionListener(new ForwardListener()); 
      forward.addMouseListener 
(new MouseEventListener(forward)); 
      forward.setToolTipText("Go to next page"); 

      ControlButton reload = new ControlButton(); 
      reload.setIcon(new ImageIcon("images/RotCCDown.gif")); 
      reload.setRolloverIcon(new ImageIcon 
("images/BrightRotCCDown.gif")); 
      reload.setText("Reload"); 
      reload.setToolTipText("Reload this page from the server"); 
      reload.addActionListener(new ReloadListener()); 
      reload.addMouseListener(new MouseEventListener(reload)); 
      reload.setToolTipText("Reload this page from the server"); 

      ControlButton exit = new ControlButton(); 
      exit.setIcon(new ImageIcon("images/Exit.gif")); 
      exit.setRolloverIcon(new ImageIcon("images/BrightExit.gif")); 
      exit.setText("Exit"); 
      exit.addActionListener(new ExitListener()); 
      exit.addMouseListener(new MouseEventListener(exit)); 
      exit.setToolTipText("Exit from the browser"); 

      panel1.add(open); 
      panel1.add(back); 
      panel1.add(forward); 
      panel1.add(reload); 
      panel1.add(exit); 

      status = new JLabel(); 
      status.setIcon(new ImageIcon("images/fdbk-anim.gif")); 
      status.setBorder(new BevelBorder 
(javax.swing.border.BevelBorder.RAISED)); 

      JPanel panel2 = new JPanel(); 
      panel2.setLayout(new FlowLayout 
(java.awt.FlowLayout.RIGHT,0,0)); 
      panel2.add(status); 

      c.weightx = 1.0; 
      c.anchor = GridBagConstraints.WEST; 
      c.fill = GridBagConstraints.NONE; 
      c.gridx = 1; c.gridy = 1; 
      gridbag.setConstraints(panel1, c); 
      add(panel1); 

      c.anchor = GridBagConstraints.EAST; 
      c.fill = GridBagConstraints.NONE; 
      c.gridx = 2; c.gridy = 1; 
      c.insets = new Insets(0,0,0,10); 
      gridbag.setConstraints(panel2, c); 
      add(panel2); 

    } 

    void setBusy() { 
      status.setIcon(new ImageIcon("images/fdbk-anim.gif")); 
      idle = false; 
    } 

    void setIdle() { 
      if (!(idle)) { 
        status.setIcon(new ImageIcon("images/fdbk-rest.gif")); 
        idle = true; 
      } 
    } 

    void setState() { 
      int s = iceBrowser.getBackHistory().size(); 
      if (s == 0) { 
        back.setEnabled(false); 
        back.setToolTipText("Go to previous page"); 
      } else { 
        back.setEnabled(true); 
        back.setToolTipText((String)iceBrowser.getBackHistory() 
.elementAt(s-1)); 
      } 
      if (iceBrowser.getForwardHistory().size() == 0) { 
        forward.setEnabled(false); 
        forward.setToolTipText("Go to next page"); 
      } else { 
        forward.setEnabled(true); 
        forward.setToolTipText 
((String)iceBrowser.getForwardHistory() 
.elementAt(0)); 
      } 
    } 

  } 

  class LinkPanel extends JPanel { 

    JProgressBar progressBar; 
    JLabel label; 
    JPanel panel1; 

    LinkPanel() { 
      setBorder(new EtchedBorder()); 
      GridBagLayout gridbag = new GridBagLayout(); 
      setLayout(gridbag); 
      GridBagConstraints c = new GridBagConstraints(); 

      label = new JLabel(" "); 
      progressBar = new JProgressBar(); 
      progressBar.setMinimum(0); 

      panel1 = new JPanel(); 
      panel1.setLayout(gridbag); 
      panel1.add(label); 
      c.weightx = 1.0; 
      c.anchor = GridBagConstraints.WEST; 
      c.fill = GridBagConstraints.BOTH; 
      c.gridx = 1; c.gridy = 1; 
      gridbag.setConstraints(label, c); 
      panel1.add(label); 
      c.insets = new Insets(5,5,5,5); 
      gridbag.setConstraints(panel1, c); 

      JPanel panel2 = new JPanel(); 
      panel2.setLayout(new FlowLayout 
(java.awt.FlowLayout.RIGHT,0,0)); 
      panel2.add(progressBar); 
      c.weightx = 0.0; 
      c.anchor = GridBagConstraints.EAST; 
      c.fill = GridBagConstraints.NONE; 
      c.gridx = 2; c.gridy = 1; 
      gridbag.setConstraints(panel2, c); 

      add(panel1); 
      add(panel2); 

    } 

    void setLabel(String s) { 
      label.setText(s); 
    } 

    void setProgressBarMax(int v) { 
      progressBar.setMaximum(v); 
    } 

    void setProgressBarValue(int v) { 
      progressBar.setValue(v); 
    } 

  } 

  class LocationPanel extends JPanel implements KeyListener { 

    JTextField textField; 

    LocationPanel() { 
      setBorder(new EtchedBorder()); 

      GridBagLayout gridbag = new GridBagLayout(); 
      setLayout(gridbag); 
      GridBagConstraints c = new GridBagConstraints(); 
      c.anchor = GridBagConstraints.WEST; 
      c.insets = new Insets(4,4,4,4); 

      JLabel label = new JLabel("Location: "); 
      label.setIcon(new ImageIcon("images/World.gif")); 
      textField = new JTextField(); 
      textField.addKeyListener(this); 

      c.fill = GridBagConstraints.NONE; 
      c.gridx = 1; c.gridy = 1; 
      gridbag.setConstraints(label, c); 

      c.fill = GridBagConstraints.BOTH; 
      c.gridx = 2; c.gridy = 1; 
      c.weightx = 1.0; 
      gridbag.setConstraints(textField, c); 
  

      add(label); 
      add(textField); 
    } 

    public void keyTyped(KeyEvent ev) { 
    } 

    public void keyPressed(KeyEvent ev) { 
      if (ev.getKeyCode() == KeyEvent.VK_ENTER) 
        iceBrowser.gotoLocation(textField.getText()); 
    } 

    public void keyReleased(KeyEvent ev) { 
    } 

    void setText(String s) { 
      textField.setText(s); 
    } 

  } 

  public void propertyChange(PropertyChangeEvent ev) { 
    int max = 0; 
    int currentMax = 0; 
    int current = 0; 
    String currentLabel = "Document : Done"; 
    String prop = ev.getPropertyName(); 
    System.out.println(prop); 
    control.setState(); 
    if (prop.equals("statusString")) { 
      String s = (String)ev.getNewValue(); 
      if (s.equals("")) { 
        setCursor(new Cursor(java.awt.Cursor.DEFAULT_CURSOR)); 
        link.setLabel(currentLabel); 
      } else { 
        setCursor(new Cursor(java.awt.Cursor.HAND_CURSOR)); 
        link.setLabel(s); 
      } 
    } else if (prop.equals("currentLocation")) { 
      System.out.println((String)ev.getNewValue()); 
    } else if (prop.equals("documentBase")) { 
      location.setText((String)ev.getNewValue()); 
    } else if (prop.equals("documentTitle")) { 
      setTitle((String)ev.getNewValue()); 
      control.setBusy(); 
      link.setProgressBarValue(0); 
    } else if (prop.equals("parsingProgress")) { 
      String status = iceBrowser.getParsingProgress(); 
      StringTokenizer st = new StringTokenizer(status, " "); 
      String tokens[] = new String[3]; 
      int i = 0; 
      while (st.hasMoreTokens()) { 
        tokens[i] = st.nextToken(); 
        System.out.println(tokens[i]); 
        i++; 
      } 
      //if (!(tokens[2].equals("-1"))) { 
      //  current = java.lang.Integer.valueOf(tokens[1]) 
.intValue(); 
      //  currentMax = java.lang.Integer.valueOf(tokens[2]) 
.intValue(); 
      // v1.405 seems to return "charsRead totalChars frameName" 
instead of 
      // "frameName charsRead totalChars" 
      if (!(tokens[1].equals("-1"))) { 
        current = java.lang.Integer.valueOf(tokens[0]) 
.intValue(); 
        currentMax = java.lang.Integer.valueOf(tokens[1]) 
.intValue(); 
        if (currentMax == current) { 
          currentLabel = "Document : Done"; 
          control.setIdle(); 
        } else { 
          currentLabel = "Reading"; 
        } 
        link.setLabel(currentLabel); 
        if (max != currentMax) { 
          max = currentMax; 
          link.setProgressBarMax(max); 
        } 
        link.setProgressBarValue(current); 
      } 
    } 
  } 

  public SimpleBrowser() { 

    setSize(800, 500); 
    addWindowListener(this); 

    JPanel panel = new JPanel(); 
    panel.setLayout(new BorderLayout()); 
    panel.add("Center", control); 
    panel.add("South", location); 

    iceBrowser = new Browser(); 
    iceBrowser.addPropertyChangeListener(this); 

    getContentPane().add("North", panel); 
    getContentPane().add("Center", iceBrowser); 
    getContentPane().add("South", link); 

    iceBrowser.gotoLocation("http://www.javasoft.com/"); 

  } 

  public void windowClosing( WindowEvent event ) { dispose(); } 
  public void windowOpened( WindowEvent event ) {} 
  public void windowIconified( WindowEvent event ) {} 
  public void windowDeiconified( WindowEvent event ) {} 
  public void windowClosed( WindowEvent event ) { System.exit(0);} 
  public void windowActivated( WindowEvent event ) {} 
  public void windowDeactivated( WindowEvent event ) {} 

  public static void main(String args[]) { 
    String lnfName; 
    if (new MotifLookAndFeel().isNativeLookAndFeel()) { 
      lnfName = "com.sun.java.swing.plaf.motif.MotifLookAndFeel"; 
    } else if (new WindowsLookAndFeel().isNativeLookAndFeel()) { 
      lnfName = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel"; 
    } else { 
      lnfName = "javax.swing.plaf.metal.MetalLookAndFeel"; 
    } 
    try { 
      System.out.println(lnfName); 
      UIManager.setLookAndFeel(lnfName); 
    } catch (Exception ex) { 
      ex.printStackTrace(); 
    } 
    new SimpleBrowser().show(); 
  } 

} 

  

 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.