Course Notes Table of Contents
Exercises | Online Training Index< /a>

Swing Short Course, Part II
from MageLang Institute

In Part II of this course you will:
  • Learn the basic conceptual model for the Swing component set of the Java Foundation Classes (JFC)
  • Explore complex GUI architectures with Swing
  • Gain an understanding of the Swing text document model
  • Learn about create your own pluggable look-and-feel

In Part I of this course, you learned about the general component makeup, new layout managers, and new events that make up the Swing component set. In this second part, you'll take a look at some of the more advanced pieces of Swing. You'll start with an introduction to the Model-View-Controller (MVC) architecture, which is always working behind the scenes for you with all the Swing components. Then, the course describes how to use MVC to better design user interfaces. Next, the components that take advantage of MVC are more fully described:

  • JTree
  • JList
  • JComboBox
  • JTextPane
  • JTable

Finally, the last part of the course describes the steps necessary to create your own pluggable look-and-feel. By the time you have completed this course, you'll be well on your way to becoming a Swing guru.

Model-View-Controller Architecture

There are a number of ways to approach using Swing to develop GUIs. As shown in the first part of this course, you can use most of the Swing widgets in the same way AWT widgets are used. If you take this approach, the transition from AWT programming is very easy.

However, Swing gives the programmer a more powerful interface to the widgets. Employing a technology called the Model-View-Controller (MVC) architecture, the Swing team has given you the ability to control how widgets look, how they respond to input, and, for some more complex widgets, how data is represented.

This section provides some background information on MVC and its relationship to Swing. If you want to see how to properly create a GUI with Swing, skip ahead to the next section, "Three Ways to design a Swing GUI."

Aside from a richer collection of widgets, the primary advantage of Swing over AWT is its use of MVC. MVC is a design pattern often used in building user interfaces. In an MVC UI, there are three communicating objects, the model, view, and controller. The model is the underlying logical representation, the view is the visual representation, and the controller specifies how to handle user input. When a model changes, it notifies all views that depend on it. This separation of state and presentation allows for two very powerful features.

  1. You can base multiple views on the same model. For instance, you can present a set of data in both table form and chart form. As you update the data model, the model notifies both views and gives each an opportunity to update itself.
  2. Because models specify nothing about presentation, you can modify or create views without affecting the underlying model.

A view uses a controller to specify its response mechanism. For instance, the controller determines what action to take when receiving keyboard input.

Although the primary purpose of MVC is for building UIs, it can be used to establish an analogous notification protocol between non-visual objects. A model object can send change notifications to an arbitrary set of interested objects without knowing details about those objects. The Observer/Observable objects in java.util have served this need well, since Java 1.0.

Swing Component Architecture and MVC

Swing represents components by a common variation of MVC in which view and controller are combined into an object called a delegate. Delegates both represent the model, as a view does, and translate user input into the model, as a controller does. Communication between view and controller is very complex. Combining the two simplifies the job of component design.

As an example, consider a checkbox widget. Regardless of visual representation, it has a state that can be either true or false. This corresponds to the checkbox's model. The way you represent these two states on the screen refers to its delegate-view. When a user clicks the mouse on the checkbox, the delegate-controller is responsible for notifying the model of the intended state change. Commonly, the delegate associated with a checkbox will use a checked box to represent the true state and an unchecked box to represent the false state and will toggle the state when a user clicks within the box. In this way, the delegate-view reflects the model and the delegate-controller translates user input into the model.

Swing widgets are subclasses of JComponent, such as JButton. At any given time, a JComponent has a single model and a single delegate associated with it. Possible models for a particular JComponent are classes that implement a model interface specific to that JComponent. For a class to act as a JButton's model, it must implement the ButtonModel interface. Likewise, delegates are implementations of a delegate interface specific to the JComponent. The ButtonUI interface defines a JButton's delegate.

As stated earlier, a JComponent can have different models and delegates. You access models with the setModel and getModel methods, and delegates can be accessed with the setUI and getUI methods.

Delegates and the ComponentUI Interface

All delegates, such as ButtonUI, extend the ComponentUI interface and are part of the com.sun.java.swing.plaf package. ComponentUI contains basic functionality to define how a delegate renders a JComponent. The primary method in this interface is one seen with applets, the paint method. Along with other methods, such as getPreferredSize and getMinimumSize, these ComponentUI methods describe the view portion of a delegate. Specific subinterfaces of ComponentUI determine the controller aspects of a delegate.

To take the JButton example to completion, look more closely at the ButtonModel and ButtonUI interfaces and their default implementations. ButtonModel has methods such as isPressed and setPressed to reflect the state of a button, independent of visual representation. ButtonUI inherits most of its functionality from ComponentUI. The only additional information the ButtonUI offers is the inset size, via the getDefaultMargin method.

DefaultButtonModel is JButton's default model. It is rare that its model will change; after all, a button is a button.

BasicButtonUI is the default delegate for JButton on Microsoft Windows platforms. This is the Windows 95-like representation of a button and is supported only on MS Windows platforms. MotifButtonUI is the default delegate on Unix platforms. The MacButtonUI, or something similarly named, will be the default delegate for Macintosh platforms; however, no Macintosh look-and-feel is available at this time.

Look and Feel

Common to both AWT and Swing is the concept of decoupling the rendering of a GUI from the Java classes that build the GUI. In AWT, each component has an associated native peer class that translates between a Java component and a native operating system widget. For instance, this means that the java.awt.Button component appears like a Windows 95 button when running under Windows 95 and like a Motif button when running under Solaris. However, the developer has no input into this process. There is only one way to render a button under Windows 95. The decoupling is simply a way to allow for platform independence.

The basis of Swing components is the lightweight component architecture introduced in AWT 1.1. As such, components no longer have these peer classes nor do they use native operating system widgets. Instead, they participate in the MVC framework described above.

Unlike AWT components, Swing components can appear multiple ways on the same platform. This concept describes its look and feel (L&F).

Look and Feel is "Pluggable"

Because of the modular nature of MVC, you can make Swing-based GUIs, as a unit, look like Windows, the Java Look & Feel, Motif, or other user-defined views with minimal programming effort. You call this property a Pluggable Look and Feel. You can make such changes in visual representation at run-time. You accomplish this with an object called an AbstractLookAndFeel, which maintains a mapping of JComponents with ComponentUIs. Setting the AbstractLookAndFeel for an application switches the entire GUI. To make an application's interface appear like Java Look & Feel components, use the following:

try {
  UIManager.setLookAndFeel (
    "com.sun.java.swing.jlf.JLFLookAndFeel");
} catch (java.lang.ClassNotFoundException e) {
  // Can't change factories
}

The Java Look & Feel is a Java-native look and feel provided with Swing. First seen in Swing 0.6, it is an attempt to create a common appearance across different computing environments. If you are wondering what happened to the Rose Look and Feel, read A Message to our Swing developers, from the Swing development team. Later, you'll see how to create your own look and feel.

Three ways to design a Swing GUI

Fortunately, you can ignore much of the MVC widget internals discussed above for simple GUI design. You can approach widget placement in a GUI with Swing in exactly the same way as AWT: by instantiating widgets and adding them to containers. Additionally, two techniques use MVC to design flexible, powerful GUIs.

  1. Simple GUI Design: Instantiate widgets, add them to a container, select a look and feel for the entire GUI.
  2. Complex GUI Design: Although Swing doesn't directly address this point, widgets can interact among themselves in an MVC framework. One widget can act as a controller by responding to user input and changing a data model, which, in turn, forwards changes to other widgets acting as views.
  3. Complex Widget Architecture: Some swing widgets such as JList, JTree, and the text widgets are most useful when the programmer defines and specifies the model and part of the delegate. This way, for instance, you can represent a group of line-item objects in a JList widget.

The first two methods of GUI design are treated in depth below. In the individual widget descriptions is a discussion of the third.

Simple GUI Design with Swing

GUI design with Swing can be approached in the same way as AWT, by instantiating components, adding them to a container, and setting up events among them.

// import the symbols from awt and swing packages
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;

// Subclass JPanel to place widgets in a panel
class SimplePanel extends JPanel {

  // Declare the two components
  JTextField textField;
  JButton button;

  // Add a constructor for our JPanel
  // This is where most of the work will be done
  public SimplePanel() {

    // Create a JButton
    button = new JButton("Clear Text");
    
    // Add the JButton to the JPanel
    add(button);
    
    // Create a JTextField with 10 visible columns
    textField = new JTextField(10);
    
    // Add the JTextField to the JPanel
    add(textField);

    // Add a listener to the JButton
    // that clears the JTextField
    button.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        textField.setText("");
      }
    });
  }
}
    
// Next, create a simple framework
// for displaying our panel
// This framework may be used for displaying other
// panels with minor modifications

// Subclass JFrame so you can display a window
public class SimplePanelTest extends JFrame {

  // Set up constants for width and height of frame
  static final int WIDTH = 300;
  static final int HEIGHT = 100;
   
  // Add a constructor for our frame.
  SimplePanelTest(String title) {
    // Set the title of the frame
    super(title);
    
    // Set the background of the frame
    setBackground(Color.lightGray);
    
    // Instantiate and add the SimplePanel to the frame
    SimplePanel simplePanel = new SimplePanel();
    Container c = getContentPane();
    c.add(simplePanel, BorderLayout.CENTER);
  }
      
  // Create main method to execute the application
  public static void main(String args[]) {

    // instantiate a SimplePanelTest object 
    // so you can display it
    JFrame frame = 
      new SimplePanelTest("SimplePanel Example");
    
    // Create a WindowAdapter so the application 
    // is exited when the window is closed.
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    
    // Set the size of the frame and show it
    frame.setSize(WIDTH, HEIGHT);
    frame.setVisible(true);
  }   
}

Complex GUI Design with Swing

This section deals primarily with how you handle events in a GUI. Two examples are given. In the first, you handle events in a very simple AWT 1.1 style, with adapters (similar to the previous example). Following this, there is a discussion of problems with this model related to its flexibility. Some alternatives are examined, and finally an example using MVC to design the GUI is given. It is far more complex, but also more maintainable and flexible in the long run.

/* This is a basic application that demonstrates a
 * simple way to establish interaction among widgets
 * in a GUI. Its event framework is fine for simple
 * applications. Some shortcomings will be outlined
 * below. It places a JTextField and a JTextArea on
 * the screen. An ActionListener is added to the
 * JTextField, so that, upon entering text into the
 * JTextField, a line with the same text is appended
 * to the JTextArea.
 */

// First, import the swing and awt symbols
import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;

/* The class is going to extend JFrame
* Most of the work of setting up the GUI
* will be done in the constructor for the frame
* Additionally, add a main method so you can
* run it as an application
*/ 
public class SimpleEvents extends JFrame {

  // Constants to specify width and height of frame
  // Used below in the main method
  static final int WIDTH=350;
  static final int HEIGHT=180;

  // Declare a JTextField for getting user input
  JTextField textField;

  // Declare a JTextArea for receiving lines of
  // text from textField
  JTextArea textList;

  // Declare a JScrollPane to hold the JTextArea
  JScrollPane pane;

  // Constructor for the frame class
  public SimpleEvents(String lab) {
    // Call JFrame's constructor
    // This will set the label of the JFrame
    super(lab);
  
    // Set the layout to FlowLayout
    setLayout(new FlowLayout());

    // Set the background color to lightGray,
    // a constant in the Color class
    setBackground(Color.lightGray);

/********** Create a container for the textField *****/

    // Instantiate a JPanel
    JPanel textPanel = new JPanel();

    // Give it a border so it stands out
    // By default, panels have no border
    textPanel.setBorder (
      BorderFactory.createEtchedBorder());
  
    // Set the layout of the textPanel to a BorderLayout
    textPanel.setLayout(new BorderLayout());

    // Create a label and add it to the panel
    JLabel textTitle = 
      new JLabel("Type and hit ");
    textPanel.add(textTitle, BorderLayout.NORTH);

    // Instantiate JTextField and add it to the textPanel
    textField = new JTextField();
    textPanel.add(textField, BorderLayout.SOUTH);

    // Add a strut to the textPanel as a bottom margin
    textPanel.add(Box.createVerticalStrut(6));

/******** Create a container for the textArea ********/

    // Instantiate a JPanel
    JPanel listPanel = new JPanel();

    // Give it a border so it stands out
    listPanel.setBorder (
      BorderFactory.createEtchedBorder());

    // Set the layout of the textPanel to a BoxLayout
    // BoxLayouts are discussed below (ignore for now)
    listPanel.setLayout(
      new BoxLayout(listPanel,BoxLayout.Y_AXIS));

    // Create a label and add it to the panel
    JLabel title = new JLabel("Text List");
    listPanel.add(title);

    // Add a strut to the BoxLayout
    listPanel.add(Box.createVerticalStrut(10));

    // Instantiate the JTextArea with no initial text,
    // 6 rows, 10 columns, and vertical scrollbars
    textList=new JTextArea("", 6, 10);

    // Make it read-only
    textList.setEditable(false);

    // Add the the textList to the listPanel
    pane = new JScrollPane (textList);
    listPanel.add(pane);

    // Add a strut to the listPanel as a bottom margin
    listPanel.add(Box.createVerticalStrut(6));

/***** Add a listener to the textField ***************/

    /* The listener will respond to user input by 
     * copying the textField's text to the textList.
     * The ENTER key causes an ActionEvent to be
     * generated. Notice how the two widgets are
     * becoming intertwined. 
     * Changes to one will likely affect the other
     */
    textField.addActionListener(new ActionListener() {           
      public void actionPerformed(ActionEvent e) {
        // Append the textField's text to textList
        textList.append(textField.getText());
        textList.append("\n");
        // Validate for scrollbar
        pane.validate();
        // Reset the textField
        textField.setText("");
      }
    });

    // Add two panels to frame, separated by a strut
    Container c = getContentPane();
    c.setLayout (new FlowLayout());
    c.add(textPanel);
    c.add(Box.createHorizontalStrut(30));
    c.add(listPanel);
 }  

/* Create a main method for invoking as application **/
  public static void main(String args[]) {
    // Instantiate instance of the SimpleEvents class
    // This is where constructor is executed, and the
    // GUI built - JFrame title is passed as parameter
    SimpleEvents frame = 
      new SimpleEvents("Simple Events Example");

    // This is a standard adapter that should be 
    // in most applications. It closes the window
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });

    // Set the size of the JFrame and show it
    frame.setSize(WIDTH, HEIGHT);
    frame.setVisible(true);
 }
}

This GUI framework will work well for simple applications. If, however, the widgets in an application have a more complicated relationship, the simple adapter approach has some shortcomings.

Consider another scenario in which you may want to create a third widget, an avgField that averages numbers entered into the textList and displays the result. Using the same simple adapter architecture, you could accomplish this in three ways. Each, however, has weaknesses. Look at these three ways and then a fourth approach, using MVC to solve the problem.

  1. You could add another adapter to link user input from the textField object to an avgField object. So far, this is fine. In that listener, however, you will need to refer to the textList object to perform the average function. Now, the textList acts in two roles. It shows numbers entered in the textField, and it feeds the avgField object a list of numbers to average. In MVC terms, the textList is now a view and a model.

    Furthermore, the AWT event model doesn't guarantee this arrangement to work. The textField object will have two ActionListeners. There is no guarantee of event ordering in AWT. The avgField could process the event before the textList object. In this case, the most recent number entered won't participate in the average function. This problem can be overcome with a special type of adapter that can handle ordered multicasting of events, but the mixed role of the textList still remains.

  2. You could add an adapter to the textList object rather than the textField object. When its contents are changed, the textList will refresh the avgField. This doesn't eliminate the problem of textList acting in two roles. It, however, does solve the problem of update order. A new problem with this design is that you begin to lose track of where events are occurring. You are chaining events from one widget to another. This can be hard to follow.

  3. You could add the code to update the avgField to the same adapter that updates the textList. This will ensure ordering of events, but, again, binds the avgField to the textList.

This association of the avgField with the textList is where the primary problem resides. If you decide that you no longer want to display the list, only the average, this causes a problem - you calculate the average from the textList. What you really need here is a List data structure for any number of widgets to observe. When data changes in that List, you want to notify these observers. You can add some methods to the List data structure to deal with notification of observers. Finally, when you enter a number in the textField, you add it to the List rather than to the textList.

This is the MVC architecture. The List is acting as a model for two views, avgField and the textList. The textField is acting as a controller, passing user input into the List.

You could design such an arrangement in the following manner:

Both avgField and textList implement the ChangeListener interface and add themselves as listeners to the List model. By doing so, they have a stateChanged method to process any changes in the list.

The textField (controller) uses an ActionListener to change data in the list.

The List maintains a ChangeListener list and notifies them any time data has changed (calls their stateChanged methods).

The problem with using this architecture is that the stateChanged method doesn't contain any relevant data (it contains a ChangeEvent). The view objects have to be able to go get the model's data.

Ideally, what you want to do is pass some model data to the stateChanged method. In this case, you could simply pass the complete, updated list. The reason Swing does not allow this probably has to do with Java's strong typing. Perhaps you could create another interface called ObjectStateChanged, and an object could be passed, in addition to the ChangeEvent. This is less attractive in Java since the object has to be downcast and a method call made from within the view to the model. It's best to keep the model and view "ignorant" of each other.

You can solve this problem using adapters to establish the model-view relationship. The adapter acts as a ChangeListener of the model, rather than the view. The essential difference from the previous scenario is that the adapter contains type and method information about the model rather than the view. The constructor for the adapter has handles to both model and view as its parameters.

The following is an implementation of such an arrangement. It is similar to the prior example with an additional view, avgView that maintains a running average of entered numbers. The other two objects acquired new names to reflect their roles in the MVC relationship. The textField object is now called controller, while the textList object is now called listView.

There are three primary classes involved:

  1. ListView contains a view based on a JTextArea, as before.
  2. IntVectorModel is a vector model that holds the numbers
  3. FirstMVC contains the frame, adapters and main method
(The controller and avgView objects are JTextField objects.)

ListView

The ListView will act as a view to the IntVectorModel. It is a simple extension of a JTextArea. It does some initialization in the constructor and contains a changed method, which the adapter knows to call. This changed method receives data from the List (model) in the form of a Vector.

import java.util.*;
import com.sun.java.swing.*;

public class ListView extends JTextArea {
  public ListView(int n) {
    super("", n, 10);
    setEditable(false);
  }

  /* This is NOT tied to a particular model's event.
   * An adaptor is used to isolate the model's type
   * from the view.
   *
   * Method called by adapter
   * resets JTextArea and copies the data model
   * Vector back in
   */
  public void changed (Vector v) {
    setText("");
    Enumeration e = v.elements();
    while (e.hasMoreElements()) {
      Integer i = (Integer)e.nextElement();
      append (i.toString() + "\n");
    }
    // Ensure scrollbar visible if necessary
    getTopLevelAncestor().validate();
  }
}

IntVectorModel

The IntVectorModel class contains the list of numbers and tracks and notifies ChangeListener objects using an EventListenerList. Take a close look at how to maintain a event listener list with EventListenerList.

import java.util.*;
import com.sun.java.swing.*;
import com.sun.java.swing.event.*;

public class IntVectorModel {
  protected Vector data = new Vector();
  protected EventListenerList changeListeners =
    new EventListenerList();

  public IntVectorModel() {
  }

  public void addElement(int i) {
    data.addElement(new Integer(i));
    fireChange();
  }

  public Vector getData() {
    return data;
  }

  // Listener notification support
  public void addChangeListener(ChangeListener x) {
    changeListeners.add (ChangeListener.class, x);

    // bring it up to date with current state
    x.stateChanged(new ChangeEvent(this));
  }

  public void removeChangeListener(ChangeListener x) {
    changeListeners.remove (ChangeListener.class, x);
  }

  protected void fireChange() {
    // Create the event:
    ChangeEvent c = new ChangeEvent(this);
    // Get the listener list
    Object[] listeners = 
      changeListeners.getListenerList();
    // Process the listeners last to first
    // List is in pairs, Class and instance
    for (int i = listeners.length-2; i >= 0; i -= 2) {
      if (listeners[i] == ChangeListener.class) {
        ChangeListener cl = 
          (ChangeListener)listeners[i+1];
        cl.stateChanged(c);
      }
    }
  }
}

FirstMVC

The FirstMVC class is where the MVC framework is assembled. It creates a view and two models and places adapters between them.

/* Demonstrates use of MVC for GUI design: interaction 
 * *between* components.  The model is a Vector of
 * numbers.  The views are a list of the numbers and
 * the average of the numbers. The Views do not
 * directly listen for changes from the model. Adaptors
 * are used to isolate type information (promoting
 * flexibility) from the model/views.
 *
 * Really the only Swing part is the ChangeListener
 * stuff (plus a BoxLayout).
 */

import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import com.sun.java.swing.event.*;
import java.util.*;

public class FirstMVC extends JFrame {

  // The initial width and height of the frame
  public static int WIDTH = 300;
  public static int HEIGHT = 200;
   
  // a View
  ListView listView = new ListView(5);

  // Another View
  TextField avgView = new TextField(10);

  // the Model
  IntVectorModel model = new IntVectorModel();

  // the Controller
  TextField controller = new TextField(10);

  /**Adaptor mapping IntVector to ListView;
   * Hide specific types in adaptor rather 
   * than having view/model know about each other.
   *
   * A real system would allow the model to indicate
   * WHAT had changed (for efficiency of execution
   * and simpler design).
   */
  private static class IntVectorToListviewAdaptor
    implements ChangeListener {
    IntVectorModel model;
    ListView view;
    public IntVectorToListviewAdaptor(
      IntVectorModel m, ListView v) {
      model = m;
      view = v;
    }
    public void stateChanged(ChangeEvent e) {
      view.changed(model.getData());
    }
  }

  private static class IntVectorToAvgViewAdaptor 
    implements ChangeListener {
    IntVectorModel model;
    TextField view;
    public IntVectorToAvgViewAdaptor(
        IntVectorModel m, TextField v) {
      model = m;
      view = v;
    }

    public void stateChanged(ChangeEvent e) {
      double avg = 0.0;
      Vector d = model.getData();
      Enumeration enum = d.elements();
      while (enum.hasMoreElements()) {
        Integer i = (Integer)enum.nextElement();
        avg += i.intValue();
      }
      if (d.size()>0)
        avg = avg / d.size();
      view.setText(""+avg);
    }
  }

  private static class TextFieldToIntVectorAdaptor 
      implements ActionListener {
    IntVectorModel model;
    TextField controller;
    public TextFieldToIntVectorAdaptor(
        TextField c, IntVectorModel m) {
      model = m;
      controller = c;
    }
    public void actionPerformed(ActionEvent e) {
      String n = controller.getText();
      n = n.substring(0, n.length());     // remove \n
      controller.setText("");       // clear txt field
      try {
        model.addElement(Integer.parseInt(n));
      } catch(NumberFormatException nfe) {
        System.err.println("bad num: '"+n+"'");
      }
    }
  }

  public FirstMVC(String lab) {
    super(lab);
    setLayout(new FlowLayout());
    setBackground(Color.lightGray);

    // Display Controller
    JPanel controlPanel = new JPanel();
    controlPanel.setBorder (
      BorderFactory.createEtchedBorder());
    controlPanel.setLayout(new
       BoxLayout(controlPanel,BoxLayout.Y_AXIS));
    JLabel ctitle = new JLabel("Control");
    ctitle.setHorizontalTextPosition(JLabel.CENTER);
    controlPanel.add(ctitle);
    controlPanel.add(Box.createVerticalStrut(10));
    controlPanel.add(controller);
    Container c = getContentPane();
    c.setLayout (new FlowLayout ());
    c.add(controlPanel);

    c.add(Box.createHorizontalStrut(30));

    // Display Views
    JPanel viewPanel = new JPanel();
    viewPanel.setBorder (
      BorderFactory.createEtchedBorder());
    viewPanel.setLayout(
      new BoxLayout(viewPanel,BoxLayout.Y_AXIS));
    JLabel title = new JLabel("Views");
    viewPanel.add(title);
    title.setHorizontalAlignment(JLabel.CENTER);
    title.setHorizontalTextPosition(JLabel.CENTER);
    viewPanel.add(Box.createVerticalStrut(10));
    viewPanel.add(new JScrollPane(listView));
    viewPanel.add(Box.createVerticalStrut(10));
    viewPanel.add(avgView);
    c.add(viewPanel);

    // Hook the Controller up to the Model
    TextFieldToIntVectorAdaptor CM =
      new TextFieldToIntVectorAdaptor(controller, model);
    controller.addActionListener(CM);
   
    // Hook up the simple avg View up to the Model
    IntVectorToAvgViewAdaptor MV1 =
       new IntVectorToAvgViewAdaptor(model,avgView);
    model.addChangeListener(MV1);

    // Connect the View to the Model via the adapter,
    // which isolates type information from each other.
    IntVectorToListviewAdaptor MV2 =
      new IntVectorToListviewAdaptor(model,listView);
    model.addChangeListener(MV2);
  }

  public static void main(String args[]) {
    FirstMVC frame = new FirstMVC("First MVC Example");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });

    frame.setSize(WIDTH, HEIGHT);
    frame.setVisible(true);
  }
}

Now that you have a general feeling for the Model/View/Controller architecture, take a look at some of the JComponent objects that take advantage of them.

JTree

Swing has a very flexible set of classes for creating tree controls. The JTree class is the basis to present hierarchical data.

JTree objects are built from TreeNode objects, which are simple representations of a tree node. They have zero or one parent nodes and zero or more child nodes. There is a rich set of methods in the DefaultMutableTreeNode class for viewing and manipulating nodes in a tree. This class implements the MutableTreeNode interface, which extends the TreeNode interface. This is a partial listing of some of the more useful methods:

TreeNode
getParent() Returns the the parent TreeNode
children() Returns an enumeration of the node's children
isLeaf() Returns true if the node has no children
getChildAt(int) Returns the child at index in this node's child array
MutableTreeNode
insert (MutableTreeNode, int) Inserts a node as a child at index
remove(int) Removes the child at index in the tree's child array and sets parent to null
remove (MutableTreeNode) Removes the child node from the tree and sets parent to null
setParent (TreeNode) Moves the node and its descendants to a different place in the tree
DefaultMutableTreeNode
add (MutableTreeNode) Inserts node as child at end of the tree's child array
getRoot() Returns the root TreeNode
getLevel() Returns the node's tree level
getNextSibling() Returns the next sibling of the node
isNodeSibling (TreeNode) Returns true if node is a sibling to the TreeNode parameter
getSharedAncestor (DefaultMutableTreeNode) Returns the nearest common ancestor between the node and TreeNode parameter
isNodeChild (TreeNode) Returns true if the TreeNode is a child of the TreeNode parameter
preorderEnumeration() Conducts a preorder traversal rooted at this node and returns an enumeration of TreeNodes
postorderEnumeration() Conducts a postorder traversal rooted at this node and returns an enumeration of TreeNodes
pathFromAncestorEnumeration (TreeNode) Returns an enumeration of the path from ancestor to node

Optionally, a MutableTreeNode can hold a handle to an arbitrary object with the userObject property and the setUserObject access method. The getUserObject method is a part of DefaultMutableTreeNode. This way, a JTree can hold any objects. The toString method of TreeNode returns the result of the userObject.toString method or null if userObject is null.

Three interfaces work in conjunction to allow developers to customize the model and view of a tree, TreeModel, TreeSelectionModel, and TreeCellRenderer.

TreeModel

The TreeModel interface describes a JTree's underlying data model. JTree contains a property, Model, with access methods getModel and setModel, that determine which TreeModel a JTree uses. The TreeModel interface specifies how a tree is mapped over a data structure with the following methods:

getChild (Object parent, int index)
getChildCount (Object parent)
getIndexOfChild (Object parent, Object child)
getRoot()
isLeaf (Object node)

These are similar to their analogous JTree methods, except that they deal with Objects rather than TreeNodes.

Three additional methods, addTreeModelListener, removeTreeModelListener, and valueForPathChanged deal with adding, removing, and notifying listeners respectively. These listeners are notified of changes in the TreeModel by receiving TreeModelEvent messages.

An object that defines these methods can operate as a model for a JTree.

DefaultTreeModel is a simple implementation of TreeModel that explicitly uses TreeNode and MutableTreeNode objects.

TreeSelectionModel

TreeSelectionModel is an interface that specifies how the user may select a path of arbitrary objects. JTree uses it to set up selection rules.

DefaultTreeSelectionModel is a simple implementation of TreeSelectionModel. It allows for the usual selection paradigm that one is accustomed to (i.e. selecting files in a directory).

TreeCellRenderer

The TreeCellRenderer interface is used by JTree to specify a component that will visually represent nodes in the tree. For instance, the default cell renderer is BasicTreeCellRenderer, which uses folders as root and internal nodes and filled circles as leaf nodes (see example below). Custom appearance can be defined by creating classes that implement this interface that contains only one methods:

  • getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) - Returns the component that will be used for rendering nodes.

Using the default model and view of a JTree, you can easily create a file directory style tree. You do have to make sure you place the tree in a JScrollPane in case there is insufficient display space.

public class JTreePanel extends JPanel {

  JTreePanel() {
    // Set the layout to hold only one component
    setLayout(new BorderLayout());

    // Create root node of tree
    DefaultMutableTreeNode root = 
      new DefaultMutableTreeNode("Contacts");

    // Create 1st level child
    DefaultMutableTreeNode level1 = 
      new DefaultMutableTreeNode("Business");

    // Add 1st level child under root node
    root.add(level1);

    // Create and add 2nd level child
    DefaultMutableTreeNode level2 = 
      new DefaultMutableTreeNode("JavaSoft");
    level1.add(level2);

    // Create and add some 3rd level leaf nodes
    level2.add(new DefaultMutableTreeNode(
      "James Gosling"));
    level2.add(new DefaultMutableTreeNode(
      "Frank Yellin"));
    level2.add(new DefaultMutableTreeNode(
      "Tim Lindholm"));

    // Create and add another 2nd level child
    level2 = new DefaultMutableTreeNode(
      "Disney");
    level1.add(level2);

    // Create and add some 3rd level leaf nodes
    level2.add(new DefaultMutableTreeNode(
      "Goofy"));
    level2.add(new DefaultMutableTreeNode(
      "Mickey Mouse"));
    level2.add(new DefaultMutableTreeNode(
      "Donald Duck"));

    // Create and add another 1st level child
    level1 = new DefaultMutableTreeNode(
      "Personal");
    root.add(level1);

    // Create and add some 2nd level leaf nodes
    level1.add(new DefaultMutableTreeNode(
      "Justin"));
    level1.add(new DefaultMutableTreeNode(
      "Andrew"));
    level1.add(new DefaultMutableTreeNode(
      "Denice"));

    // Create a tree from the root
    JTree tree = new JTree(root);

    // Place tree in JScrollPane
    JScrollPane pane = new JScrollPane();
    pane.getViewport().add (tree);

    add(pane, BorderLayout.CENTER);
  }
}

Exercises

  1. Tree Views
  2. Custom JTree Rendering

JList/JComboBox Revisited

With the introduction of MVC, you can do more with a JList or JComboBox, as well as just about every other JComponent. By associating a data model to the component, and a way to render a view of the model, you can create more complex display components. To demonstrate MVC within these two components, you can use the same data model for both a JList and JComboBox because the ComboBoxModel extends the ListModel. Also, they both have the same renderer interface: ListCellRenderer.

public class MVCListPanel extends JPanel {
  MVCListPanel() {
    ImageListModel ilm = new ImageListModel();
    JComboBox combo = new JComboBox(ilm);
    combo.setRenderer(new ImageCellRenderer());
    combo.setSelectedIndex(0);
    add(combo);
    JList list = new JList (ilm);
    list.setCellRenderer(new ImageCellRenderer());
    list.setSelectedIndex(0);
    list.setVisibleRowCount(4);
    JScrollPane pane = new JScrollPane ();
    pane.setBorder (
      BorderFactory.createLoweredBevelBorder());
    pane.getViewport().add (list);
    add(pane);
  }

  class ImageListModel extends DefaultListModel 
      implements ComboBoxModel, Serializable {
    Object currentValue;
    Icon icon[];
    Hashtable cache[];
    final int SIZE = 6;
    Color color[] = {Color.red, Color.orange, 
      Color.yellow, Color.green, Color.blue, 
      Color.magenta};
    String label [] = {"Cranberry", "Orange", 
      "Banana", "Kiwi", "Blueberry", "Pomegranate"};
    public ImageListModel () {
      cache = new Hashtable[getSize()];
      icon = new AnOvalIcon[SIZE];
      for (int i=0;i<SIZE;i++) 
        icon[i] = new AnOvalIcon (color[i]);
    }
    // ListModel methods
    public int getSize() {
      // Use constant for performance reasons
      return SIZE;
    }
    public Object getElementAt(int index) {
      // Cache data items to avoid recreating
      if (cache[index] != null) {
        return cache[index];
      } else {
        Hashtable result = new Hashtable();
        result.put ("label", label[index]);
        result.put ("icon",  icon[index ]);
        cache[index] = result;
	  return result;
      }
    }

    // ComboBoxModel methods
    // DefaultComboBoxModel does this for you

    public Object getSelectedItem() {
      return currentValue;
    }
    public void setSelectedItem(Object anObject) {
      currentValue = anObject;
	fireContentsChanged(this, -1, -1);
    }
  }

  class ImageCellRenderer extends JLabel 
      implements ListCellRenderer {
    private boolean focused = false;
    public ImageCellRenderer () {
      setOpaque (true);
    }
    public Component getListCellRendererComponent(
        JList list, Object value, int index, 
        boolean isSelected, boolean cellHasFocus) {
      Hashtable h = (Hashtable) value;
      if (value == null) {
        setText("");
        setIcon(null);
      } else {
        setText((String)h.get ("label"));
        setIcon((Icon)h.get ("icon"));
      }
      setBackground (isSelected ? 
        SystemColor.textHighlight : 
        SystemColor.text);
      setForeground (isSelected ? 
        SystemColor.textHighlightText : 
        SystemColor.textText);
      return this;
    }
    public Dimension getPreferredSize() {
      Dimension dim = super.getPreferredSize();
      dim.width += 15;  // widen
      return dim;
    }
  }

  class AnOvalIcon implements Icon {
    Color color;
    public AnOvalIcon (Color c) {
      color = c;
    }
    public void paintIcon (Component c, Graphics g, 
        int x, int y) {
      g.setColor(color);
      g.fillOval (x, y, 
        getIconWidth(), getIconHeight());
    }
    public int getIconWidth() {
      return 20;
    }
    public int getIconHeight() { 
      return 10;
    }
  }
}

As just demonstrated, the JList in Swing can be very different from its AWT equivalent when it participates in an MVC relationship.

Three interfaces work in conjunction to allow developers to customize the model and view of a list box, ListModel, ListSelectionModel, and ListCellRenderer.

ListModel

The ListModel interface is a general model for a list of objects. It specifies the data to be represented by the JList. It is a fairly simple interface, containing four methods:

Object getElementAt(int index);
int getSize();
void addListDataListener(ListDataListener listener);
void removeListDataListener(ListDataListener listener);

The getElementAt method returns a single data element representing a position in the ListBox at index. The getSize method returns the number of elements in the model, and therefore in the JList. The final two methods maintain a list of view objects that are interested in changes to the model. The DefaultListModel class manages the listener list for you, through its superclass AbstractListModel. Then, when a ListDataEvent happens, you notify the listeners with one of the following methods:

  • fireContentsChanged - To be called after an item in list changes
  • fireIntervalAdded - To be called after a set of items is added to list
  • fireIntervalRemoved - To be called after a set of items is removed to list

ListSelectionModel

ListSelectionModel is an interface that specifies how the user may select a set of arbitrary ranges of objects. JList uses it to set up selection rules.

DefaultListSelectionModel is a simple implementation of ListSelectionModel. The selection model describes whether or not a JList is in single or multi-selection mode.

ListCellRenderer

Another interface, ListCellRenderer, specifies how to visually represent items in a list. Similar to other widgets with "renderer" interfaces, it contains one methods:

  • getListCellRendererComponent (JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) - Returns the type of component to be used for rendering a list item.

There is a default ListCellRenderer available, so you can just add a String[] (or Vector) to the JList and it will use the default renderer.

Swing Text Framework

Warning: The following describes the 0.6.1 version of Swing. There will be changes to the com.sun.java.swing.text package in 0.7. These changes should not affect the general functionality of the text components. However, until 0.7 is released, the following is subject to change.

The way Swing treats text-based widgets is another example of the Complex Widget Architecture application of MVC above. Textual content (model) and its representation (view) are decoupled. In order for an object to play the role of a model, it must implement the Document interface or, more likely, extend one of its "canned" implementations that ship with Swing. Observers of a document extend the abstract View class or one of its subclasses. A View usually takes the form of a rendered component on the screen.

The illustration below shows how documents and views interact. UI events are usually sent to the document. If a change occurs that a view is interested in, the system generates a DocumentEvent and passes it to it. This allows for the synchronization of the document and view. Events, such as selection of text with the mouse, send the document event directly to the view for processing.

Document Interface

The Document interface describes an implementation independent structure for holding text. It supports markup of styles, notification of changes and tracking of changes to allow for "undo" functionality. Text is marked up with structures called elements, a concept taken from SGML. Elements describe the state of a document with an arbitrary set of attributes. You build a view from a type of element structure. Documents also contain methods to describe the number of lines and paragraphs of text.

In most cases, a single document structure can describe a text component's model. The Document interface however, does allow for multiple structural representations of the text data. To do this, you create a document that has multiple root elements, one for each structural representation. The Swing team gives the following examples of where such an arrangement might be useful:

  • Logical document structure
  • View projections
  • Lexical token streams
  • Parse trees
  • Conversion to a format other than the native format
  • Modification specifications
  • Annotations

Document Implemented

Several convenience implementations of Document ship with Swing. The simplest of them, AbstractDocument, is intended primarily as a superclass to extend and form models that are more complete. The primary contribution of AbstractDocument is its locking mechanism. It implements the readers/writers model to allow either one writer or multiple readers access to the content. Writers must wait for notification of all observers of a previous change before they can begin another mutation cycle.

AbstractDocument is the abstract superclass of two full-featured document models, PlainDocument and DefaultStyledDocument. The purpose of PlainDocument is for fairly short and simple text. It manages textual content as a string, and does not support history or undo operations. DefaultStyledDocument allows for storage of formatted text similar to Rich Text Format (RTF). It relies on structure elements to mark up the text into styles. These style elements are associated with paragraph marker elements.

Basic Swing Text Widgets

This MVC based text framework is very powerful but also very complex. After all, most developers simply want to throw some text components into a container and accept their pre-defined behavior. Fortunately, Swing can hide the MVC mechanics from you. If you simply instantiate a text widget and add it to a container a default document is generated, initialized, and maintained for you. You can think of text widgets as JTextComponents that have a ready-to-use delegate (View) and a ready-to-use model (Document) operating behind the scenes.

Using JTextPane and DefaultStyledDocument

The JTextPane component provides support for multi-attributed text. No longer are you restricted to the single color or font limitations of TextArea. With the help of a DefaultStyledDocument for its model, and a good understanding of the com.sun.java.swing.text package, you are well on your way to creating the next word processor or language-sensitive editor.

Creating a JTextPane for complex text display requires two simple steps:

  1. Create a DefaultStyledDocument to model for the data
    DefaultStyledDocument doc = new DefaultStyledDocument();
    
  2. Create a JTextPane using the DefaultStyledDocument
    JTextPane pane = new JTextPane (doc);
    

Once you have a your document, you can make various AttributeSet objects to describe the content style:

SimpleAttributeSet defaultStyle = 
  new SimpleAttributeSet();

SimpleAttributeSet italicStyle = 
  new SimpleAttributeSet();
StyleConstants.setItalic(attr, true);

SimpleAttributeSet bigStyle = 
  new SimpleAttributeSet();
StyleConstants.setFontSize(attr, 36);

and fill up the JTextPane, associating an attribute set with each paragraph in the StyledDocument:

doc.insertString (doc.getLength(), 
  "Hello World\n", bigStyle);
doc.insertString (doc.getLength(), 
  "What's up Doc?\n", italicStyle);
doc.insertString (doc.getLength(), 
  "Boring...\n", defaultStyle);

Then, at the appropriate time, you can use the various methods of StyleConstants to change the style of the selected contents within the JTextPane or use StyledDocument methods like setCharacterAttributes, setParagraphAttributes, or just plain setLogicalStyle, to change the document characteristics. Just create a SimpleAttributeSet, and configure any attribute you would like:

void setAlignment()
void setBold()
void setComponent()
void setFirstLineIndent()
void setFontFamily()
void setFontSize()
void setForeground()
void setIcon()
void setItalic()
void setLeftIndent()
void setLineSpacing()
void setRightIndent()
void setSpaceAbove()
void setSpaceBelow()
void setUnderline()
StyleConstants methods for changing attributes of currently selected content. You will also need to setCharacterAttributes or setParagraphAttributes the JTextPane.
void replaceSelection(String c)
void insertComponent(Component c)
void insertIcon(Icon g)
JTextPane methods to replace currently selected content with a String, Component, or Icon
void setLogicalStyle(Style s) Changes Style of current paragraph

For certain functionality, you just need to wrap one of the style changing methods into an ActionListener and make it available on a menu or a button. To make things easier, most of these adapters are already created for you. Just make an instance and add one as an ActionListener to a menu or button. With either of these methods, you won't have to worry about finding the selected text to figure out what to change. There is even a third way, so you don't want to have to worry about specific class names, just functionality. The StyledEditorKit class provides a minimal set of text actions as a series of inner classes that are covered next.

StyledEditorKit.AlignmentAction AlignmentAction (String textAction, int alignment)
StyledEditorKit.BoldAction BoldAction()
StyledEditorKit.FontFamilyAction FontFamilyAction (String textAction, String family)
StyledEditorKit.FontSizeAction FontSizeAction (String textAction, int size)
StyledEditorKit.ForegroundAction ForegroundAction (String textAction, Color color)
StyledEditorKit.ItalicAction ItalicAction()

In addition to the StyledEditorKit inner classes, there are a whole slew of other ones. Most of these are useful when you want to provide alternative input mechanisms for traversal within the JTextPane. However, none of them are public.

DefaultEditorKit.BackwardAction
DefaultEditorKit.BeginAction
DefaultEditorKit.BeginParagraphAction
DefaultEditorKit.BeepAction
DefaultEditorKit.CopyAction
DefaultEditorKit.CutAction
DefaultEditorKit.DeleteNextCharAction
DefaultEditorKit.DeletePrevCharAction
DefaultEditorKit.DownAction
DefaultEditorKit.DumpModelAction
DefaultEditorKit.EndAction
DefaultEditorKit.EndParagraphAction
DefaultEditorKit.ForwardAction
DefaultEditorKit.InsertBreakAction
DefaultEditorKit.InsertContentAction
DefaultEditorKit.PageDownAction
DefaultEditorKit.PageUpAction
DefaultEditorKit.PasteAction
DefaultEditorKit.ReadOnlyAction
DefaultEditorKit.SelectionBackwardAction
DefaultEditorKit.SelectionForwardAction
DefaultEditorKit.UpAction
DefaultEditorKit.WritableAction
JTextField.NotifyAction

TextActions

Since none of the inner classes outside of StyledEditorKit are public, you need to access this functionality in another way. To perform these operations you can ask a JTextComponent how to do some functionality and it passes back something that implements ActionListener. You usually just get back an inner class, but you never need to know that. What you get back is an Action that happens to implement the ActionListener interface. You then just add this listener to your MenuItem, Button, or other class. To demonstrate, the following program shows how to support Cut and Paste operations for a JTextArea.

public class CutPaste extends JPanel {
  CutPaste() {
    setLayout (new BorderLayout (5, 5));
    JTextArea jt = new JTextArea();
    JScrollPane pane = new JScrollPane();
    pane.setBorder (
      BorderFactory.createLoweredBevelBorder());
    pane.getViewport().add(jt);
    add(pane, BorderLayout.CENTER);

    // get the command table
    Hashtable commands = new Hashtable();
    Action[] actions = jt.getActions();
    for (int i = 0; i < actions.length; i++) {
      Action a = actions[i];
      commands.put(a.getText(Action.NAME), a);
    }

    JButton cut = new JButton("Cut");
    cut.setBackground (SystemColor.control);
    Action cutAction = 
      (Action)commands.get (DefaultEditorKit.cutAction);
    if (cutAction == null) {
      cut.setEnabled (false);
    } else {
      cut.setActionCommand (DefaultEditorKit.cutAction);
      cut.addActionListener (cutAction);
    }
    JButton paste = new JButton("Paste");
    paste.setBackground (SystemColor.control);
    Action pasteAction = 
      (Action)commands.get (DefaultEditorKit.pasteAction);
    if (pasteAction == null) {
      paste.setEnabled (false);
    } else {
      paste.setActionCommand (DefaultEditorKit.pasteAction);
      paste.addActionListener (pasteAction);
    }
    JPanel p = new JPanel();
    p.add(cut);
    p.add(paste);
    add (p, BorderLayout.SOUTH);
  }
}

TextAction Table

There are String constants available in the various classes to help in working with most of the text actions. The following table shows the commands with built-in support in the text components, along with where the constants are located:

JTextComponent and DefaultEditorKit (class variables) backwardAction
beepAction
beginAction
beginParagraphAction
copyAction
cutAction
deleteNextCharAction
deletePrevCharAction
downAction
endAction
endParagraphAction
forwardAction
insertBreakAction
insertContentAction
pageDownAction
pageUpAction
pasteAction
readOnlyAction
selectionBackwardAction
selectionForwardAction
upAction
writableAction
JTextField adds (class variable) notifyAction
JTextPane adds (strings, not class variables) center-justify
left-justify
right-justify
font-italic
font-bold
font-size-8
font-size-10
font-size-12
font-size-14
font-size-16
font-size-18
font-size-24
font-size-36
font-size-48
font-family-Courier
font-family-Helvetica
font-family-TimesRoman

The way Action objects work within Swing is you can programmatically enable or disable components when you enable or disable an Action. This involves associating the component to the Action as a JavaBeans PropertyChangeListener

and reacting accordingly to the change. For instance, if boldAction is the Action to make selected text bold, you can toggle the functionality with the following code:
JButton b1 = new JButton ("Toggle Bold");
b1.addActionListener (new ActionListener() {
  public void actionPerformed (ActionEvent e) {
    boldAction.setEnabled(!boldAction.isEnabled());
  }
});
...
class MyButton extends JButton implements PropertyChangeListener {
  public void propertyChange (PropertyChangeEvent e) {
    if (e.getPropertyName().equals ("enabled")) {
      if (!(e.getNewValue() == e.getOldValue()))
        setEnabled (((Boolean)e.getNewValue()).booleanValue());
    }
  }
};
MyButton b2 = new MyButton ();
b2.setText ("Do Bold1");
b2.addActionListener (boldAction);
boldAction.addPropertyChangeListener (b2);

MyButton b3 = new MyButton ();
b3.setText ("Do Bold2");
b3.addActionListener (boldAction);
boldAction.addPropertyChangeListener (b3);

Then, when the Toggle Bold button is selected, the components associated with boldAction are jointly enabled or disabled.

View and ViewFactory Interfaces

The View interface specifies a representation based on part or all of a document. It contains a paint method for rendering and layout. ViewFactory describes how Views are mapped to structure elements. It contains a method called create that returns a View, given an Element. Often a ViewFactory is passed to a method with an accompanying Shape object, allowing for dynamic generation of views.

Exercise

  1. Using JTextPane

Swing Table Framework

Warning: The following describes the 0.6.1 version of Swing. There will be MAJOR changes to the com.sun.java.swing.table package in 0.7. These changes will affect the general functionality of the table components, rendering the following description useless. Read the README-JTable.txt file that comes with the Swing release for a preview of things to come. Basically, JTable is NOT a spreadsheet and shouldn't be used like one.

Table support for Swing is found in the com.sun.java.swing.table package. The package consists of a series of classes and interfaces to handle the creation and display of columner data. The way Swing supports tables is another example of MVC. The table data model is found in the TableModel interface, while the View/Controller part is found in the JTable class.

TableModel

The TableModel interface specifies how to describe the data in the table cells, and requires the maintaining of a TableModelListener list. Because whenever you create the table's data model you need to maintain this list, there is a AbstractTableModel class which maintains the list for you. The methods of TableModel consist of the following:

int getColumnCount()
Returns count of columns in data model
int getRowCount()
Returns count of rows in data model
Class getColumnClass(int column)
Returns the class of the column
Object getColumnIdentifier (int column)
Returns the unique identifier for the column. This is necessary because the display of the data may change the display column order.
String getColumnName (int column)
Returns a non-unique name for the column header label
Object getValueAt(int row, int column)
Returns the current cell value for a row / column combination
void setValueAt(Object aValue, int row, int column)
Changes a cell value for a specific row / columns. Need to notify listener list after change via fireTableChanged method of AbstractTableModel.
void addTableModelListener(TableModelListener l)
Add listener to TableModelListener list
void removeTableModelListener(TableModelListener l)
Remove listener to TableModelListener list

As mentioned above, AbstractTableModel maintains the listener list for you. The list is available from the protected listenerList variable. Then, when you subclass the adapter, you only need to implement getColumnCount, getRowCount, getValueAt, and setValueAt. If you are creating a read-only table, the setValueAt method is stubbed out for you in AbstractTableModel, so you don't have to implement that either. However, when you do implement setValueAt, you have to remember to notify the listener about the change. Besides changing the data, use the fireTableChanged method of AbstractTableModel to notify those interested in cell-level changes:

class SomeDataModel extends AbstractTableModel {
...
  public void setValueAt (Object aValue, 
      int row, int column) {
...
    foo[row][column] = aValue;
    fireTableChanged (new TableModelEvent (this, row));
...
}

JTable

Once you have the table's data model in something that implements the TableModel interface, you can actually create and display the table. There are actually two steps involved here:

  1. Create table and associate data model. This can been done in one of two ways:
    JTable table = new JTable();
    table.setModel (theModel);
    

    or, more simply, just

    JTable table = new JTable(theModel);
    
  2. Display the table in a JScrollPane in case there is insufficient display space. This also creates and displays a set of column headers for the table.
    JScrollPane scrollPane = 
      JTable.createScrollPaneForTable(table);
    

To make life even easier, you don't even have to worry about TableModel. If you have your data in a set of Vector objects or Object arrays, you can pass that off to the JTable constructor.

More About JTable

There are many more capabilities available for JTable, like support for editing, colorizing, simultaneous row-column selection, and getting information about the selected entries. Be sure to examine the API documentation for information on these additional capabilities.

The source for the example used above is shown below:

public class TablePanel extends JPanel {
  TablePanel() {
    setLayout (new BorderLayout());

    // Create data model
    EmployeeDataModel employeeModel = 
      new EmployeeDataModel();

    // Create/setup table
    JTable table = new JTable (employeeModel);

    // Resize columns
    table.sizeColumnsToFit (false);

    // Place table in JScrollPane
    JScrollPane scrollPane = 
      JTable.createScrollPaneForTable(table);

    // Add to Screen
    add(scrollPane, BorderLayout.CENTER);

  }
}

class EmployeeDataModel extends AbstractTableModel {
    
  // By extending AbstractTableModel, instead of 
  // implementing TableModel yourself, 
  // AbstractTableModel takes care of
  // TableModelListener list management

  String columns[] = {"Employee ID", "First Name", 
                      "Last Name", "Department"};
  String rows[][] = {
    {"0181", "Bill", "Cat", "Political Candidate"},
    {"0915", "Opus", "Penguin", "Lost and Found"},
    {"1912", "Milo", "Bloom", "Reporter"},
    {"3182", "Steve", "Dallas", "Legal"},
    {"4104", "Hodge", "Podge", "Style"},
    {"5476", "Billy", "Boinger", "Entertainment"},
    {"6289", "Oliver", "Jones", "Science"},
    {"7268", "Cutter", "John", "Travel"},
    {"8133", "W. A.", "Thornhump", "C.E.O"},
    {"9923", "Berke", "Breathed", "Editor"}
  };

  private int numColumns = columns.length;
  private int numRows = rows.length;

  public int getColumnCount() {
    return numColumns;
  }

  public int getRowCount() {
    return numRows;
  }

  public Object getValueAt (int row, int column) {
    return rows[row][column];
  }

  public void setValueAt (Object aValue, 
      int row, int column) {
    String cellValue;
    if (aValue instanceof String)
      cellValue = (String)aValue;
    else
      cellValue = aValue.toString();
    rows[row][column] = cellValue;
    fireTableChanged (new TableModelEvent (this, row));
  }

  public String getColumnName (int columnIndex) {
    return columns[columnIndex];
  }

}

Instead of using the EmployeeDataModel, the JTable could have been created with:

  String columnNames[] = ...
  String data[][] = ...
  JTable table = new JTable (data, columnNames);

Exercise

  1. Using Tables

Creating a New Look

Creating a different look and feel is not for everyone. Most people will just work with what Swing already provides. For those interface designers who want total control of what the interface looks like, Swing provides that control. The AbstractLookAndFeel is where you start. However, what you will probably do is just extend an existing look-and-feel (BasicLookAndFeel) class to support some of your own components. That way, you won't have to provide everything at once. To demonstrate, you can create your own look and feel, MyLookAndFeel, that changes the look of the JButton to have multi-color triangular right and left borders.

The LookAndFeel Class

If you extend BasicLookAndFeel, the only thing you need to do with your LookAndFeel class is map your UI classes to the appropriate UI names. These names can be found in the com.sun.java.swing.basic package, or you can ask a particular component with the getUIClassID method.

package my;
import java.awt.Color;
import com.sun.java.swing.UIDefaults;
import com.sun.java.swing.basic.BasicLookAndFeel;

public class MyLookAndFeel extends BasicLookAndFeel {

  public String getName() {
    return "My Look and Feel";
  }
  public String getDescription() {
    return "The My Look and Feel";
  }
  public boolean isNativeLookAndFeel() {
    return false;
  }
  public boolean isSupportedLookAndFeel() {
    return true;
  }
  protected void initClassDefaults (UIDefaults table) {
    super.initClassDefaults(table);
    table.put ("ButtonUI", "my.MyButtonUI");
  }
}

The ButtonUI Class

After defining what UI classes you are going to create, you need to create them. For the most part, the UI classes are identical, with the exception of the paint method. The rest is just general house-keeping routines that have minor changes between components. For the particular ButtonUI that you are creating, you will need two support classes: the MyButtonBorder class draws the borders around the button and the MyButtonListener class will describe how your UI component responds to internal mouse and keyboard input.

Here is the code for the ButtonUI. The installUI, uninstallUI, and createUI methods should be the only thing new to you. The installation routines basically just install your listener and border that you want for your interface. Obviously, paint draws the object. However, the majority of the work is left for the border class.

package my;

import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import com.sun.java.swing.border.*;
import com.sun.java.swing.plaf.ComponentUI;
import com.sun.java.swing.basic.*;
import java.io.Serializable;

import com.sun.java.swing.plaf.ButtonUI;

public class MyButtonUI extends ButtonUI 
    implements Serializable {

  protected final static Insets defaultMargin = 
      new Insets (2, 5, 2, 5);
  protected final static Font defaultFont =
      new Font ("Serif", Font.BOLD, 10);

  private final static Border defaultBorder = 
    new CompoundBorder(
      MyButtonBorder.getButtonBorder(),
      BasicMarginBorder.getMarginBorder());

  protected static final int textIconGap = 3;

  protected MyButtonListener listener;
  protected static ButtonUI buttonUI;

  public static ComponentUI createUI (JComponent c) {
    if (buttonUI == null) {
      buttonUI = new MyButtonUI();
    }
    return buttonUI;
  }

  public void installUI (JComponent c) {
    listener = new MyButtonListener(c);
    c.addMouseListener(listener);
    c.addMouseMotionListener(listener);
    c.setFont (defaultFont);
    if (c.getBorder() == null)
      c.setBorder (defaultBorder);
  }

  public void uninstallUI (JComponent c) {
    c.removeMouseListener (listener);
    c.removeMouseMotionListener (listener);
    if (c.getBorder() == defaultBorder)
      c.setBorder(null);	
  }
  
  public void paint (Graphics g, JComponent c) {
    AbstractButton ab = (AbstractButton) c;
    ButtonModel bm = ab.getModel();

    Dimension size = ab.getSize();
    g.setFont (c.getFont());
    FontMetrics fm = g.getFontMetrics();

    int shiftOffset = 0;

    Rectangle viewRect = new Rectangle(size);
    Rectangle iconRect = new Rectangle();
    Rectangle textRect = new Rectangle();

    String text = SwingUtilities.layoutCompoundLabel (
      fm, ab.getText(), ab.getIcon(), 
      ab.getVerticalAlignment(), 
      ab.getHorizontalAlignment(),
      ab.getVerticalTextPosition(), 
      ab.getHorizontalTextPosition(),
      viewRect, iconRect, textRect, textIconGap);

    if (bm.isArmed() && bm.isPressed()) {
      shiftOffset = 1;
    }
	  
    // Paint background
    if (c.isOpaque()) {
      g.setColor (ab.getBackground());
      g.fillRect (0, 0, size.width, size.height);
    }


    // Draw Icon

    if (ab.getIcon() != null) { 
      Icon icon = null;
      if (!bm.isEnabled()) {
        icon = ab.getDisabledIcon();
      } else if (bm.isPressed() && bm.isArmed()) {
        icon = ab.getPressedIcon();
      } else if (bm.isRollover()) {
        icon = ab.getRolloverIcon();
      } 

      if (icon == null) {
        icon = ab.getIcon();
      }

      if (bm.isPressed() && bm.isArmed()) {
        icon.paintIcon (c, g, iconRect.x + shiftOffset,
          iconRect.y + shiftOffset);
      } else {
        icon.paintIcon (c, g, iconRect.x, iconRect.y);
      }
    }

    // Draw Text

    if ((text != null) && (text.length() != 0)) {
      if (bm.isEnabled()) {
        g.setColor (ab.getForeground());
        BasicGraphicsUtils.drawString (g, text, 
          bm.getKeyAccelerator(),
          textRect.x + shiftOffset,
          textRect.y + fm.getAscent() + shiftOffset);
      } else {
        g.setColor (ab.getBackground().brighter());
        BasicGraphicsUtils.drawString (g, text, 
          bm.getKeyAccelerator(),
          textRect.x, textRect.y + fm.getAscent());
        g.setColor (ab.getBackground().darker());
        BasicGraphicsUtils.drawString (g, text,
          bm.getKeyAccelerator(),
          textRect.x - 1, textRect.y + fm.getAscent() - 1);
      }
    }
  }
      
  public Dimension getMinimumSize (JComponent c) {
    return getPreferredSize (c);
  }

  public Dimension getMaximumSize (JComponent c) {
    return getPreferredSize (c);
  }

  public Dimension getPreferredSize (JComponent c) {
    if ((c.getComponentCount() > 0) ||
        !(c instanceof AbstractButton)) {
      return null;
    }

    AbstractButton ab = (AbstractButton) c;
    Icon icon = ab.getIcon();
    String text = ab.getText();

    Font font = ab.getFont();
    FontMetrics fm = ab.getToolkit().getFontMetrics (font);
	  
    Rectangle viewRect = new Rectangle (
      Short.MAX_VALUE, Short.MAX_VALUE);
    Rectangle iconRect = new Rectangle();
    Rectangle textRect = new Rectangle();

    SwingUtilities.layoutCompoundLabel (
      fm, text, icon,
      ab.getVerticalAlignment(),
      ab.getHorizontalAlignment(),
      ab.getVerticalTextPosition(), 
      ab.getHorizontalTextPosition(),
      viewRect, iconRect, textRect, textIconGap);

    // find union of icon and text rectangles
    Rectangle rect = iconRect.union (textRect);

    Insets insets = getInsets (c);

    rect.width += insets.left + insets.right;
    rect.height += insets.top + insets.bottom;

    return rect.getSize();
  }

  public Insets getDefaultMargin (AbstractButton b) { 
    return defaultMargin;
  }

  public Insets getInsets (JComponent c) { 
    Border border = c.getBorder();
    Insets insets = ((border != null) ? 
      border.getBorderInsets (c) : 
      new Insets (0,0,0,0));
    return insets;
  }
}

The Mouse Listener

Unless you are doing special events for keyboard events and/or drawing focus, your listener code can practically be shared across multiple objects. Basically, you have to say things like: the mouse press event causes the button to be armed and pressed. This in turn triggers the appropriate behavior of the JButton object listeners. You can have any activity you want trigger these behaviors. For instance, if you want a ButtonUI that requires your user to fly a plane between two buildings before the button is activated, you can do that. In this case here, the MyButtonUI supports the general mouse events for selection.

package my;

import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;
import java.io.Serializable;

public class MyButtonListener implements MouseListener,
    MouseMotionListener, Serializable {
  AbstractButton ab;

  public MyButtonListener (JComponent c) {
    ab = (AbstractButton) c;
  }

  public void mouseMoved (MouseEvent e) {
  }

  public void mouseClicked (MouseEvent e) {
  }
 
  public void mouseDragged (MouseEvent e) {
    ButtonModel bm = ab.getModel();
    if (bm.isPressed()) {
      Graphics g = ab.getGraphics();
      if (g != null) {
        Rectangle r = g.getClipBounds();
        if (r.contains (e.getPoint()))
          bm.setArmed(true);
        else
          bm.setArmed(false);
      }
    }
  }

  public void mousePressed (MouseEvent e) {
    ButtonModel bm = ab.getModel();
    bm.setArmed(true);
    bm.setPressed(true);
  }

  public void mouseReleased (MouseEvent e) {
    ButtonModel bm = ab.getModel();
    bm.setPressed (false);
  }

  public void mouseEntered (MouseEvent e) {
    if (ab.getRolloverIcon() != null)
      ab.getModel().setRollover (true);
  }

  public void mouseExited (MouseEvent e) {
    if (ab.getRolloverIcon() != null)
      ab.getModel().setRollover (false);
  }
}

The Button Border

The MyButtonBorder class is the worker class of this new user interface object. Here, the border needs to draw triangles in the insets of the component. Depending upon the state of the component, determines the color actually drawn.

package my;

import java.awt.*;
import com.sun.java.swing.*;
import com.sun.java.swing.border.*;
import com.sun.java.swing.basic.BasicGraphicsUtils;

public class MyButtonBorder extends AbstractBorder {
  private static Border buttonBorder = 
    new MyButtonBorder();

  public static Border getButtonBorder() {
    return buttonBorder;
  }

  public void paintBorder (Component c, Graphics g, 
      int x, int y, int width, int height) {
    boolean pressed = false;
    boolean focused = false;

    if (c instanceof AbstractButton) {
      AbstractButton b = (AbstractButton)c;
      ButtonModel bm = b.getModel();

      pressed = bm.isPressed();
      focused = (pressed && bm.isArmed()) || 
                (b.isFocusPainted() && b.hasFocus());
    }

    Insets in = getBorderInsets(c);
    Polygon p1 = new Polygon ();
    p1.addPoint (x+in.left, y);
    p1.addPoint (x, y+(height/2));
    p1.addPoint (x+in.left, y+height);

    Polygon p2 = new Polygon ();
    p2.addPoint (x+width-in.right, y);
    p2.addPoint (x+width, y+(height/2));
    p2.addPoint (x+width-in.right, y+height);

    if (pressed) {
      g.setColor (c.getForeground());
    } else if (focused) {
      g.setColor (SystemColor.green);
    } else {
      g.setColor (SystemColor.red);
    }
    g.fillPolygon (p1);
    g.fillPolygon (p2);

  }

  public Insets getBorderInsets (Component c) {
    return new Insets (5, 10, 5, 10);
  }
}

The Example

And, that is all there is to it. Borrowing heavily from the Simple example that comes with Swing, hear is an example that demonstrates all the hard work. You can extend it as you add more user interfaces to MyLookAndFeel. Notice that the interaction with the JButton doesn't change.

import java.awt.*;
import java.awt.event.*;
import com.sun.java.swing.*;

public class MyExample extends JPanel {

  public MyExample() {

    // Create the buttons.
    JButton button = new JButton ("Hello, world");

    ActionListener myListener = new ActionListener() {
      public void actionPerformed (ActionEvent e) {
        String lnfName = null;

        if (e.getActionCommand().equals ("My")) {
          lnfName = "my.MyLookAndFeel";
        } else {
          lnfName = 
            "com.sun.java.swing.basic.BasicLookAndFeel";
        }

        try {
          UIManager.setLookAndFeel(lnfName);
          SwingUtilities.updateComponentTreeUI (
            MyExample.this);
          MyExample.this.validate();
        } catch (Exception ex) {
          System.err.println (
           "Could not swap LookAndFeel: " + lnfName);
        }
      }
    };

    ButtonGroup group = new ButtonGroup();
    JRadioButton basicButton = 
      new JRadioButton ("Basic");
    basicButton.setSelected(true);
    basicButton.addActionListener (myListener);
    group.add (basicButton);

    JRadioButton myButton = 
      new JRadioButton ("My");
    myButton.addActionListener (myListener);
    group.add (myButton);

    add (button);
    add (basicButton);
    add (myButton);
  }

  public static void main (String args[]) {
    JFrame f = new JFrame ("LnF Example");
    JPanel j = new MyExample();
    f.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    f.getContentPane().add (j, BorderLayout.CENTER);
    f.setSize (300, 100);
    f.show();
  }
}

Conclusion

In conclusion, remember that Swing is just a small part of the Java Foundation Classes (JFC). Other new parts of JFC include the Accessibility API, Java2D API, Drag and Drop (Glasgow/Beans), and various application services (Undo, Custom Cursors, Keyboard Navigation).


Copyright © 1997 MageLang Institute. All Rights Reserved
May-97 Copyright © 1996, 19 97 Sun Microsystems Inc. All Rights Reserved