[ Previous Section | Next Section | Chapter Index | Main Index ]

Subsections
Model-View-Controller
Lists and ListModels
Tables and TableModels
Documents and Editors
Custom Components

Section 12.4

Complex Components and MVC


Since even buttons turn out to be pretty complex, as seen in the previous section, you might guess that there is a lot more complexity lurking in the Swing API. While this is true, a lot of that complexity works to your benefit as a programmer, since a lot of it is hidden in normal uses of Swing components. For example, you don't have to know about all the complex details of buttons in order to use them effectively in most programs.

Swing defines several component classes that are much more complex than those we have looked at so far, but even the most complex components are not very difficult to use for many purposes. In this section, we'll look at components that support display and manipulation of lists, tables, and text documents. To use these complex components effectively, you'll need to know something about the Model-View-Controller pattern that is used as a basis for the design of many Swing components. This pattern is discussed in the first part of this section.

This section is our last look at Swing components, but there are a number of component classes that have not even been touched on in this book. Some useful ones that you might want to look into include: JTabbedPane, JSplitPane, JTree, JSpinner, JPopupMenu, JProgressBar, JScrollBar, and JPasswordField.

At the end of the section, we'll look briefly at the idea of writing custom component classes -- something that you might consider when even the large variety of components that are already defined in Swing don't do quite what you want.


12.4.1  Model-View-Controller

One of the principles of object-oriented design is division of responsibilities. Ideally, an object should have a single, clearly defined role, with a limited realm of responsibility. One application of this principle to the design of graphical user interfaces is the MVC pattern. "MVC" stands for "Model-View-Controller" and refers to three different realms of responsibility in the design of a graphical user interface.

When the MVC pattern is applied to a component, the model consists of the data that represents the current state of the component. The view is simply the visual presentation of the component on the screen. And the controller is the aspect of the component that carries out actions in response to events generated by the user. The idea is to assign responsibility for the model, the view, and the controller to different objects.

The view is the easiest part of the MVC pattern to understand. It is often represented by the component object itself, and its responsibility is to draw the component on the screen. In doing this, of course, it has to consult the model, since the model represents the state of the component, and that state can determine what appears on the screen. To get at the model data -- which is stored in a separate object according to the MVC pattern -- the component object needs to keep a reference to the model object. Furthermore, when the model changes, the view might have to be redrawn to reflect the changed state. The component needs some way of knowing when changes in the model occur. Typically, in Java, this is done with events and listeners. The model object is set up to generate events when its data changes. The view object registers itself as a listener for those events. When the model changes, an event is generated, the view is notified of that event, and the view responds by updating its appearance on the screen.

When MVC is used for Swing components, the controller is generally not so well defined as the model and view, and its responsibilities are often split among several objects. The controller might include mouse and keyboard listeners that respond to user events on the view; Actions that respond to menu commands or buttons; and listeners for other high-level events, such as those from a slider, that affect the state of the component. Usually, the controller responds to events by making modifications to the model, and the view is changed only indirectly, in response to the changes in the model.

The MVC pattern is used in many places in the design of Swing. It is even used for buttons. The state of a Swing button is stored in an object of type ButtonModel. The model stores such information as whether the button is enabled, whether it is selected, and what ButtonGroup it is part of, if any. If button is of type JButton (or one of the other subclasses of AbstractButton), then its ButtonModel can be obtained by calling button.getModel(). In the case of buttons, you might never need to use the model or even know that it exists. But for the list and table components that we will look at next, knowledge of the model is essential.


12.4.2  Lists and ListModels

A JList is a component that represents a list of items that can be selected by the user. The sample program SillyStamper.java allows the user to select one icon from a JList of Icons. The user selects an icon from the list by clicking on it. The selected icon can be "stamped" onto a drawing area by clicking on the drawing area. (The icons in this program are from the KDE desktop project.) Here is an applet version of the program:

Note that the scrollbar in this program is not part of the JList. To add a scrollbar to a list, the list must be placed into a JScrollPane. See Subsection 6.6.4, where the use of JScrollPane to hold a JTextArea was discussed. Scroll panes are used in the same way with lists and with other components. In this case, the JList, iconList, was added to a scroll pane and the scroll pane was added to a panel with the single command:

add( new JScrollPane(iconList), BorderLayout.EAST );

One way to construct a JList is from an array that contains the objects that will appear in the list. The items can be of any type, but only icons and strings can actually appear in the list; an item that is not of type Icon or String is converted into a string by calling its toString() method. (It's possible to "teach" a JList to display other types of items; see the setCellRenderer() method in the JList class.) In the SillyStamper program, the images for the icons are read from resource files, the icons are placed into an array, and the array is used to construct the list. This is done by the following method:

private JList createIconList() {

   String[] iconNames = new String[] {
      "icon5.png", "icon7.png", "icon8.png", "icon9.png", "icon10.png", 
      "icon11.png", "icon24.png", "icon25.png", "icon26.png", "icon31.png", 
      "icon33.png", "icon34.png"
   };              // Array containing resource file names for the icon images.

   iconImages = new Image[iconNames.length];

   ClassLoader classLoader = getClass().getClassLoader();
   Toolkit toolkit = Toolkit.getDefaultToolkit();
   try {                        // Get the icon images from the resource files.
      for (int i = 0; i < iconNames.length; i++) {
         URL imageURL = classLoader.getResource("stamper_icons/" + iconNames[i]);
         if (imageURL == null)
            throw new Exception();
         iconImages[i] = toolkit.createImage(imageURL);
      }
   }
   catch (Exception e) {
      iconImages = null;
      return null;
   }

   ImageIcon[] icons = new ImageIcon[iconImages.length];
   for (int i = 0; i < iconImages.length; i++)          // Create the icons.
      icons[i] = new ImageIcon(iconImages[i]);
   
   JList list = new JList(icons);         // A list containing the image icons.
   list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
   list.setSelectedIndex(0);   // First item in the list is currently selected.
   
   return list;
}

By default, the user can select any number of items in a list. A single item is selected by clicking on it. Multiple items can be selected by shift-clicking and by either control-clicking or meta-clicking (depending on the platform). In the SillyStamper program, I wanted to restrict the selection so that only one item can be selected at a time. This restriction is imposed by calling

list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

With this selection mode, when the user selects an item, the previously selected item, if any, is deselected. Note that the selection can be changed by the program by calling list.setSelectedIndex(itemNum). Items are numbered starting from zero. To find out the currently selected item in single selection mode, call list.getSelectedIndex(). This returns the item number of the selected item, or -1 if no item is currently selected. If multiple selections are allowed, you can call list.getSelectedIndices(), which returns an array of ints that contains the item numbers of all selected items.

Now, the list that you see on the screen is only the view aspect of the list. The controller consists of the listener objects that respond when the user clicks an item in the list. For its model, a JList uses an object of type ListModel. This is the object that knows the actual list of items. Now, a model is defined not only by the data that it contains but by the set of operations that can be performed on the data. When a JList is constructed from an array of objects, the model that is used is very simple. The model can tell you how many items it contains and what those items are, but it can't do much else. In particular, there is no way to add items to the list or to delete items from the list. If you need that capability, you will have to use a different list model.

The class DefaultListModel defines list models that support adding items to and removing items from the list. (Note that the list model that you get when you create a JList from an array is not of this type.) If dlmodel is of type DefaultListModel, the following methods, among others, are defined:

To use a modifiable JList, you should create a DefaultListModel, add any items to it that should be in the list initially, and pass it to the JList constructor. For example:

DefaultListModel listModel;  // Should probably be instance variables!
JList flavorList;
   
listModel = new DefaultListModel();    // Create the model object.
   
listModel.addElement("Chocolate");     // Add items to the model.
listModel.addElement("Vanilla");
listModel.addElement("Strawberry");
listModel.addElement("Rum Raisin");
   
flavorList = new JList(listModel);     // Create the list component.

By keeping a reference to the model around in an instance variable, you will be able to add and delete flavors as the program is running by calling the appropriate methods in listModel. Keep in mind that changes that are made to the model will automatically be reflected in the view. Behind the scenes, when a list model is modified, it generates an event of type ListDataEvent. The JList registers itself with its model as a listener for these events, and it responds to an event by redrawing itself to reflect the changes in the model. The programmer doesn't have to take any extra action, beyond changing the model.

By the way, the model for a JList actually has another part in addition to the ListModel: An object of type ListSelectionModel stores information about which items in the list are currently selected. When the model is complex, it's not uncommon to use several model objects to store different aspects of the state.


12.4.3  Tables and TableModels

Like a JList, a JTable displays a collection of items to the user. However, tables are much more complicated than lists. Perhaps the most important difference is that it is possible for the user to edit items in the table. Table items are arranged in a grid of rows and columns. Each grid position is called a cell of the table. Each column can have a header, which appears at the top of the column and contains a name for the column.

It is easy to create a JTable from an array that contains the names of the columns and a two-dimensional array that contains the items that go into the cells of the table. As an example, the sample program StatesAndCapitalsTableDemo.java creates a table with two columns named "State" and "Capital City." The first column contains a list of the states of the United States and the second column contains the name of the capital city of each state. The table can be created as follows:

String[][] statesAndCapitals = new String[][] {
           { "Alabama", "Montgomery" },
           { "Alaska", "Juneau" },
           { "Arizona", "Phoenix" },
                .
                .
                .
           { "Wisconsin", "Madison" },
           { "Wyoming", "Cheyenne" }
        };

String[] columnHeads = new String[] { "State", "Capital City" };
   
JTable table = new JTable(statesAndCapitals, columnHeads);

Since a table does not come with its own scroll bars, it is almost always placed in a JScrollPane to make it possible to scroll the table. In the example program this is done with:

add( new JScrollPane(table), BorderLayout.CENTER );

The column headers of a JTable are not actually part of the table; they are in a separate component. But when you add the table to a JScrolPane, the column headers are automatically placed at the top of the pane.

Using the default settings, the user can edit any cell in the table. (To select an item for editing, click it and start typing. The arrow keys can be used to move from one cell to another.) The user can change the order of the columns by dragging a column header to a new position. The user can also change the width of the columns by dragging the line that separates neighboring column headers. Here is an applet version of the program where you can try all this:

Allowing the user to edit all entries in the table is not always appropriate; certainly it's not appropriate in the "states and capitals" example. A JTable uses an object of type TableModel to store information about the contents of the table. The model object is also responsible for deciding whether or not the user should be able to edit any given cell in the table. TableModel includes the method

public boolean isCellEditable(int rowNum, columnNum)

where rowNum and columnNum are the position of a cell in the grid of rows and columns that make up the table. When the controller wants to know whether a certain cell is editable, it calls this method in the table model. If the return value is true, the user is allowed to edit the cell.

The default model that is used when the table is created, as above, from an array of objects allows editing of all cells. For this model, the return value of isCellEditable() is true in all cases. To make some cells non-editable, you have to provide a different model for the table. One way to do this is to create a subclass of DefaultTableModel and override the isCellEditable() method. (DefaultTableModel and some other classes that are discussed in this section are defined in the package javax.swing.table.) Here is how this might be done in the "states and capitals" program to make all cells non-editable:

TableModel model = new DefaultTableModel(statesAndCapitals,columnHeads) {
   public boolean isCellEditable(int row, int col) {
      return false;
   }
};
JTable table = new JTable(model);

Here, an anonymous subclass of DefaultTableModel is created in which the isCellEditable() method returns false in all cases, and the model object that is created from that class is passed as a parameter to the JTable constructor.

The DefaultTableModel class defines many methods that can be used to modify the table, including for example: setValueAt(item,rowNum,colNum) to change the item in a given cell; removeRow(rowNum) to delete a row; and addRow(itemArray) to add a new row at the end of the table that contains items from the array itemArray. Note that if the item in a given cell is null, then that cell will be empty. Remember, again, that when you modify the model, the view is automatically changed to reflect the changes.

In addition to the isCellEditable() method, the table model method that you are most likely to want to override is getColumnClass(), which is defined as

public Class<?> getColumnClass(columnNum)

The purpose of this method is to specify what kind of values are allowed in the specified column. The return value from this method is of type Class. (The "<?>" is there for technical reasons having to do with generic programming. See Section 10.5, but don't worry about understanding it here.) Although class objects have crept into this book in a few places -- in the discussion of ClassLoaders in Subsection 12.1.3 for example -- this is the first time we have directly encountered the class named Class. An object of type Class represents a class. A Class object is usually obtained from the name of the class using expressions of the form "Double.class" or "JTable.class". If you want a three-column table in which the column types are String, Double, and Boolean, you can use a table model in which getColumnClass is defined as:

public Class<?> getColumnClass(columnNum) {
   if (columnNum == 0)
      return String.class;
   else if (columnNum = 1)
      return Double.class;
   else
      return Boolean.class;
}

The table will call this method and use the return value to decide how to display and edit items in the table. For example, if a column is specified to hold Boolean values, the cells in that column will be displayed and edited as check boxes. For numeric types, the table will not accept illegal input when the user types in the value. (It is possible to change the way that a table edits or displays items. See the methods setDefaultEditor() and setDefaultRenderer() in the JTable class.)

As an alternative to using a subclass of DefaultTableModel, a custom table model can also be defined using a subclass of AbstractTableModel. Whereas DefaultTableModel provides a lot of predefined functionality, AbstractTableModel provides very little. However, using AbstractTableModel gives you the freedom to represent the table data any way you want. The sample program ScatterPlotTableDemo.java uses a subclass of AbstractTableModel to define the model for a JTable. In this program, the table has three columns. The first column holds a row number and is not editable. The other columns hold values of type Double; these two columns represent the x- and y-coordinates of points in the plane. The points themselves are graphed in a "scatter plot" next to the table. Initially, the program fills in the first six points with random values. Here is an applet version of the program. Try editing some of the items or typing new ones into the empty cells:

Note, by the way, that in this program, the scatter plot can be considered to be a view of the table model, in the same way that the table itself is. The scatter plot registers itself as a listener with the model, so that it will receive notification whenever the model changes. When that happens, the scatter plot redraws itself to reflect the new state of the model. It is an important property of the MVC pattern that several views can share the same model, offering alternative presentations of the same data. The views don't have to know about each other or communicate with each other except by sharing the model. Although I didn't do it in this program, it would even be possible to add a controller to the scatter plot view. This would let the user drag a point in the scatter plot to change its coordinates. Since the scatter plot and table share the same model, the values in the table would automatically change to match.

Here is the definition of the class that defines the model in the scatter plot program. All the methods in this class must be defined in any subclass of AbstractTableModel except for setValueAt(), which only has to be defined if the table is modifiable.

/**
 * This class defines the TableModel that is used for the JTable in this
 * program.  The table has three columns.  Column 0 simply holds the
 * row number of each row.  Column 1 holds the x-coordinates of the
 * points for the scatter plot, and Column 2 holds the y-coordinates.
 * The table has 25 rows.  No support is provided for adding more rows.
 */
private class CoordInputTableModel extends AbstractTableModel {
   
   private Double[] xCoord = new Double[25];  // Data for Column 1.
   private Double[] yCoord = new Double[25];  // Data for Column 2.
        // Initially, all the values in the array are null, which means
        // that all the cells are empty.
   
   public int getColumnCount() {  // Tells caller how many columns there are.
      return 3;
   }

   public int getRowCount() {  // Tells caller how many rows there are.
      return xCoord.length;
   }

   public Object getValueAt(int row, int col) {  // Get value from cell.
      if (col == 0)
         return (row+1);        // Column 0 holds the row number.
      else if (col == 1)
         return xCoord[row];    // Column 1 holds the x-coordinates.
      else
         return yCoord[row];    // column 2 holds the y-coordinates.
   }

   public Class<?> getColumnClass(int col) {  // Get data type of column.
      if (col == 0)
         return Integer.class;
      else
         return Double.class;
   }

   public String getColumnName(int col) {  // Returns a name for column header.
      if (col == 0)
         return "Num";
      else if (col == 1)
         return "X";
      else
         return "Y";
   }

   public boolean isCellEditable(int row, int col) { // Can user edit cell?
      return col > 0;
   }
   
   public void setValueAt(Object obj, int row, int col) { 
         // (This method is called by the system if the value of the cell
         // needs to be changed because the user has edited the cell.
         // It can also be called to change the value programmatically.
         // In this case, only columns 1 and 2 can be modified, and the data
         // type for obj must be Double.  The method fireTableCellUpdated()
         // has to be called to send an event to registered listeners to
         // notify them of the modification to the table model.)
      if (col == 1) 
         xCoord[row] = (Double)obj;
      else if (col == 2)
         yCoord[row] = (Double)obj;
      fireTableCellUpdated(row, col);
   }
   
}  // end nested class CoordInputTableModel

In addition to defining a custom table model, I customized the appearance of the table in several ways. Because this involves changes to the view, most of the changes are made by calling methods in the JTable object. For example, since the default height of the cells was too small for my taste, I called table.setRowHeight(25) to increase the height. To make lines appear between the rows and columns, I found that I had to call both table.setShowGrid(true) and table.setGridColor(Color.BLACK). Some of the customization has to be done to other objects. For example, to prevent the user from changing the order of the columns by dragging the column headers, I had to use

table.getTableHeader().setReorderingAllowed(false);

Tables are quite complex, and I have only discussed a part of the table API here. Nevertheless, I hope that you have learned enough to start using them and to learn more about them on your own.


12.4.4  Documents and Editors

As a final example of complex components, we look briefly at JTextComponent and its subclasses. A JTextComponent displays text that can, optionally, be edited by the user. Two subclasses, JTextField and JTextArea, were introduced in Subsection 6.6.4. But the real complexity comes in another subclass, JEditorPane, that supports display and editing of styled text, which allows features such as boldface and italic. A JEditorPane can even work with basic HTML documents.

It is almost absurdly easy to write a simple web browser program using a JEditorPane. This is done in the sample program SimpleWebBrowser.java. In this program, the user enters the URL of a web page, and the program tries to load and display the web page at that location. A JEditorPane can handle pages with content type "text/plain", "text/html", and "text/rtf". (The content type "text/rtf" represents styled or "rich text format" text. URLs and content types were covered in Subsection 11.4.1.) If editPane is of type JEditorPane and url is of type URL, then the statement "editPane.setPage(url);" is sufficient to load the page and display it. Since this can generate an exception, the following method is used in SimpleWebBrowser.java to display a page:

private void loadURL(URL url) {
   try {
      editPane.setPage(url);
   }
   catch (Exception e) {
      editPane.setContentType("text/plain"); // Set pane to display plain text.
      editPane.setText( "Sorry, the requested document was not found\n"
            +"or cannot be displayed.\n\nError:" + e);
   }
}

An HTML document can display links to other pages. When the user clicks on a link, the web browser should go to the linked page. A JEditorPane does not do this automatically, but it does generate an event of type HyperLinkEvent when the user clicks a link (provided that the edit pane has been set to be non-editable by the user). A program can register a listener for such events and respond by loading the new page.

There are a lot of web pages that a JEditorPane won't be able to display correctly, but it can be very useful in cases where you have control over the pages that will be displayed. A nice application is to distribute HTML-format help and information files with a program. The files can be stored as resource files in the jar file of the program, and a URL for a resource file can be obtained in the usual way, using the getResource() method of a ClassLoader. (See Subsection 12.1.3.)

It turns out, by the way, that SimpleWebBrowser.java is a little too simple. A modified version, SimpleWebBrowserWithThread.java, improves on the original by using a thread to load a page and by checking the content type of a page before trying to load it. It actually does work as a simple web browser. Here's an applet version, which gives you the unusual experience of seeing a web browser on a web page. However, you'll only be able to access web pages from the same computer from which the applet was loaded:

The model for a JTextComponent is an object of type Document. If you want to be notified of changes in the model, you can add a listener to the model using

textComponent.getDocument().addDocumentListener(listener)

where textComponent is of type JTextComponent and listener is of type DocumentListener. The Document class also has methods that make it easy to read a document from a file and write a document to a file. I won't discuss all the things you can do with text components here. For one more peek at their capabilities, see the sample program SimpleRTFEdit.java, a very minimal editor for files that contain styled text of type "text/rtf."


12.4.5  Custom Components

Java's standard component classes are often all you need to construct a user interface. At some point, however, you might need a component that Java doesn't provide. In that case, you can write your own component class, building on one of the components that Java does provide. We've already done this, actually, every time we've written a subclass of the JPanel class to use as a drawing surface. A JPanel is a blank slate. By defining a subclass, you can make it show any picture you like, and you can program it to respond in any way to mouse and keyboard events. Sometimes, if you are lucky, you don't need such freedom, and you can build on one of Java's more sophisticated component classes.

For example, suppose I have a need for a "stopwatch" component. When the user clicks on the stopwatch, I want it to start timing. When the user clicks again, I want it to display the elapsed time since the first click. The textual display can be done with a JLabel, but we want a JLabel that can respond to mouse clicks. We can get this behavior by defining a StopWatchLabel component as a subclass of the JLabel class. A StopWatchLabel object will listen for mouse clicks on itself. The first time the user clicks, it will change its display to "Timing..." and remember the time when the click occurred. When the user clicks again, it will check the time again, and it will compute and display the elapsed time. (Of course, I don't necessarily have to define a subclass. I could use a regular label in my program, set up a listener to respond to mouse events on the label, and let the program do the work of keeping track of the time and changing the text displayed on the label. However, by writing a new class, I have something that can be reused in other projects. I also have all the code involved in the stopwatch function collected together neatly in one place. For more complicated components, both of these considerations are very important.)

The StopWatchLabel class is not very hard to write. I need an instance variable to record the time when the user starts the stopwatch. Times in Java are measured in milliseconds and are stored in variables of type long (to allow for very large values). In the mousePressed() method, I need to know whether the timer is being started or stopped, so I need a boolean instance variable, running, to keep track of this aspect of the component's state. There is one more item of interest: How do I know what time the mouse was clicked? The method System.currentTimeMillis() returns the current time. But there can be some delay between the time the user clicks the mouse and the time when the mousePressed() routine is called. To make my stopwatch as accurate as possible, I don't want to know the current time. I want to know the exact time when the mouse was pressed. When I wrote the StopWatchLabel class, this need sent me on a search in the Java documentation. I found that if evt is an object of type MouseEvent, then the function evt.getWhen() returns the time when the event occurred. I call this function in the mousePressed() routine to determine the exact time when the user clicked on the label. The complete StopWatch class is rather short:

import java.awt.event.*;
import javax.swing.*;

/**
 * A custom component that acts as a simple stop-watch.  When the user clicks
 * on it, this component starts timing.  When the user clicks again,
 * it displays the time between the two clicks.  Clicking a third time
 * starts another timer, etc.  While it is timing, the label just
 * displays the message "Timing....".
 */
public class StopWatchLabel extends JLabel implements MouseListener {

   private long startTime;   // Start time of timer.
                             //   (Time is measured in milliseconds.)

   private boolean running;  // True when the timer is running.

   /**
    * Constructor sets initial text on the label to
    * "Click to start timer." and sets up a mouse listener
    * so the label can respond to clicks.
    */
   public StopWatchLabel() {
      super("  Click to start timer.  ", JLabel.CENTER);
      addMouseListener(this);
   }
   
   
   /**
    * Tells whether the timer is currently running.
    */
   public boolean isRunning() {
      return running;
   }
   
   
   /**
    * React when the user presses the mouse by starting or stopping
    * the timer and changing the text that is shown on the label.
    */
   public void mousePressed(MouseEvent evt) {
      if (running == false) {
            // Record the time and start the timer.
         running = true;
         startTime = evt.getWhen();  // Time when mouse was clicked.
         setText("Timing....");
      }
      else {
            // Stop the timer.  Compute the elapsed time since the
            // timer was started and display it.
         running = false;
         long endTime = evt.getWhen();
         double seconds = (endTime - startTime) / 1000.0;
         setText("Time: " + seconds + " sec.");
      }
   }
   
   public void mouseReleased(MouseEvent evt) { }
   public void mouseClicked(MouseEvent evt) { }
   public void mouseEntered(MouseEvent evt) { }
   public void mouseExited(MouseEvent evt) { }

}

Don't forget that since StopWatchLabel is a subclass of JLabel, you can do anything with a StopWatchLabel that you can do with a JLabel. You can add it to a container. You can set its font, foreground color, and background color. You can set the text that it displays (although this would interfere with its stopwatch function). You can even add a Border if you want.

Let's look at one more example of defining a custom component. Suppose that -- for no good reason whatsoever -- I want a component that acts like a JLabel except that it displays its text in mirror-reversed form. Since no standard component does anything like this, the MirrorText class is defined as a subclass of JPanel. It has a constructor that specifies the text to be displayed and a setText() method that changes the displayed text. The paintComponent() method draws the text mirror-reversed, in the center of the component. This uses techniques discussed in Subsection 12.1.1 and Subsection 12.2.1. Information from a FontMetrics object is used to center the text in the component. The reversal is achieved by using an off-screen canvas. The text is drawn to the off-screen canvas, in the usual way. Then the image is copied to the screen with the following command, where OSC is the variable that refers to the off-screen canvas, and width and height give the size of both the component and the off-screen canvas:

g.drawImage(OSC, width, 0, 0, height, 0, 0, width, height, this);

This is the version of drawImage() that specifies corners of destination and source rectangles. The corner (0,0) in OSC is matched to the corner (width,0) on the screen, while (width,height) is matched to (0,height). This reverses the image left-to-right. Here is the complete class:

import java.awt.*;
import javax.swing.*;
import java.awt.image.BufferedImage;

/**
 * A component for displaying a mirror-reversed line of text.
 * The text will be centered in the available space.  This component
 * is defined as a subclass of JPanel.  It respects any background 
 * color, foreground color, and font that are set for the JPanel.
 * The setText(String) method can be used to change the displayed
 * text.  Changing the text will also call revalidate() on this
 * component.
 */
public class MirrorText extends JPanel {

   private String text; // The text displayed by this component.
   
   private BufferedImage OSC; // Holds an un-reversed picture of the text.

   /**
    * Construct a MirrorText component that will display the specified
    * text in mirror-reversed form.
    */
   public MirrorText(String text) {
      if (text == null)
         text = "";
      this.text = text;
   }
   
   /**
    * Change the text that is displayed on the label.
    * @param text the new text to display
    */
   public void setText(String text) {
      if (text == null)
         text = "";
      if ( ! text.equals(this.text) ) {
         this.text = text;  // Change the instance variable.
         revalidate();      // Tell container to recompute its layout.
         repaint();         // Make sure component is redrawn.
      }
   }
   
   /**
    * Return the text that is displayed on this component.
    * The return value is non-null but can be an empty string.
    */
   public String getText() {
      return text;
   }

   /**
    * The paintComponent method makes a new off-screen canvas, if necessary,
    * writes the text to the off-screen canvas, then copies the canvas onto
    * the screen in mirror-reversed form.
    */
   public void paintComponent(Graphics g) {
      int width = getWidth();
      int height = getHeight();
      if (OSC == null || width != OSC.getWidth() 
                          || height != OSC.getHeight()) {
         OSC = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
      }
      Graphics OSG = OSC.getGraphics();
      OSG.setColor(getBackground());
      OSG.fillRect(0, 0, width, height);
      OSG.setColor(getForeground()); 
      OSG.setFont(getFont());
      FontMetrics fm = OSG.getFontMetrics(getFont());
      int x = (width - fm.stringWidth(text)) / 2;
      int y = (height + fm.getAscent() - fm.getDescent()) / 2;
      OSG.drawString(text, x, y);
      OSG.dispose();
      g.drawImage(OSC, width, 0, 0, height, 0, 0, width, height, null);
   }

   /**
    * Compute a preferred size that includes the size of the text, plus
    * a boundary of 5 pixels on each edge.
    */
   public Dimension getPreferredSize() {
      FontMetrics fm = getFontMetrics(getFont());
      return new Dimension(fm.stringWidth(text) + 10, 
            fm.getAscent() + fm.getDescent() + 10);
   }

}  // end MirrorText

This class defines the method "public Dimension getPreferredSize()". This method is called by a layout manager when it wants to know how big the component would like to be. Standard components come with a way of computing a preferred size. For a custom component based on a JPanel, it's a good idea to provide a custom preferred size. Every component has a method setPrefferedSize() that can be used to set the preferred size of the component. For our MirrorText component, however, the preferred size depends on the font and the text of the component, and these can change from time to time. We need a way to compute a preferred size on demand, based on the current font and text. That's what we do by defining a getPreferredSize() method. The system calls this method when it wants to know the preferred size of the component. In response, we can compute the preferred size based on the current font and text.

The StopWatchLabel and MirrorText classes define components. Components don't stand on their own. You have to add them to a panel or other container. The sample program CustomComponentTest.java demonstrates using a MirrorText and a StopWatchLabel component. Here is an applet version of the program:

In this program, the two custom components and a button are added to a panel that uses a FlowLayout as its layout manager, so the components are not arranged very neatly. If you click the button labeled "Change Text in this Program", the text in all the components will be changed. You can also click on the stopwatch label to start and stop the stopwatch. When you do any of these things, you will notice that the components will be rearranged to take the new sizes into account. This is known as "validating" the container. This is done automatically when a standard component changes in some way that requires a change in preferred size or location. This may or may not be the behavior that you want. (Validation doesn't always cause as much disruption as it does in this program. For example, in a GridLayout, where all the components are displayed at the same size, it will have no effect at all. I chose a FlowLayout for this example to make the effect more obvious.) When the text is changed in a MirrorText component, there is no automatic validation of its container. A custom component such as MirrorText must call the revalidate() method to indicate that the container that contains the component should be validated. In the MirrorText class, revalidate() is called in the setText() method.


[ Previous Section | Next Section | Chapter Index | Main Index ]