The JSpinner works like a cross between a JListor JComboBox component with a JFormattedTextField. In either the JList and JComboBox control, the user can select input from a predetermined set of values. The JSpinner also allows this type of selection. The other half of the component is the JFormattedTextField. How to display or enter the value isn't controlled by a list cell renderer, as in a JList; instead, you get a JFormattedTextFieldfor entry and a couple of arrows on the side to navigate through the different values available for the text field.

Figure 1 shows what the spinner looks like for several different types of input. At the top of Figure 1 is a JSpinner with the days of the week in French provided to a SpinnerListModel. In the middle, you have a JSpinnerfor a date via the SpinnerDateModelclass. On the bottom is the JSpinnerusage with the SpinnerNumberModel. Each of these three works in its own mysterious way, as you'll learn later in this article.

Figure 1. JSpinner examples

Many classes are involved when creating and manipulating JSpinnercomponents, foremost, the JSpinnerclass itself. The primary two sets of classes involved are the SpinnerModel interface, for containing the set of selectable items for the control, and, the JSpinner.DefaultEditorimplementations, for catching all the selections. Thankfully, many of the other classes involved work behind the scenes, so, for example, once you provide the numeric range in a SpinnerNumberModel and associate the spinner with its model, your work is essentially done.

Creating JSpinner components

The JSpinner class includes two constructors for initializing the component:

                                           public JSpinner()JSpinner spinner = new JSpinner();
                        public JSpinner(SpinnerModel model)SpinnerModel model = new SpinnerListModel(args);
JSpinner spinner = new JSpinner(model);
               

You can start with no data model and associate it later with the tracking method of JSpinner. Alternatively, you can start up the component with a full model, in an implementation of the SpinnerModel interface, of which three concrete subclasses are available: SpinnerDateModel, SpinnerListModel, and SpinnerNumberModel, along with their abstract parent class AbstractSpinnerModel. If you don't specify a model, the SpinnerNumberModel is used. While the renderer and editor for the component is a JFormattedTextField, the editing is basically done through a series of inner classes of JSpinner: DateEditor, ListEditor, and NumberFormat, with its support class in its parent DefaultEditor.

JSpinner properties

In addition to creating the JSpinner object, you can certainly reconfigure it through one of the nine properties listed in Table 1.

Table 1. JSpinner properties

Property name Data type Access
accessibleContext AccessibleContext Read-only
changeListeners ChangeListener[ ] Read-only
editor JComponent Read-write bound
model SpinnerModel Read-write bound
nextValue Object Read-only
previousValue Object Read-only
UI SpinnerUI Read-write
UIClassID String Read-only
value Object Read-write

The value property allows you to change the current setting for the component. The nextValue and previousValueproperties allow you to peek at entries of the model in the different directions without changing the selection within the application itself.

Listening for JSpinner events with a ChangeListener

The JSpinner directly supports a single type of event listener: ChangeListener. Among other places, the listener is notified when the commitEdit() method is called for the associated component, telling you the spinner value changed. To demonstrate, Listing 1 attaches a custom ChangeListenerto the source used to generate the program associated with Figure 1.

Listing 1. JSpinner with ChangeListener

                    

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

public class SpinnerSample { public static void main (String args[]) { Runnable runner = new Runnable() { public void run() { JFrame frame = new JFrame("JSpinner Sample"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); DateFormatSymbols symbols = new DateFormatSymbols(Locale.FRENCH); ChangeListener listener = new ChangeListener() { public void stateChanged(ChangeEvent e) { System.out.println("Source: " + e.getSource()); } };

String days[] = symbols.getWeekdays(); SpinnerModel model1 = new SpinnerListModel(days); JSpinner spinner1 = new JSpinner(model1); spinner1.addChangeListener(listener); JLabel label1 = new JLabel("French Days/List"); JPanel panel1 = new JPanel(new BorderLayout()); panel1.add(label1, BorderLayout.WEST); panel1.add(spinner1, BorderLayout.CENTER); frame.add(panel1, BorderLayout.NORTH); SpinnerModel model2 = new SpinnerDateModel(); JSpinner spinner2 = new JSpinner(model2); spinner2.addChangeListener(listener); JLabel label2 = new JLabel("Dates/Date"); JPanel panel2 = new JPanel(new BorderLayout()); panel2.add(label2, BorderLayout.WEST); panel2.add(spinner2, BorderLayout.CENTER); frame.add(panel2, BorderLayout.CENTER);

SpinnerModel model3 = new SpinnerNumberModel(); JSpinner spinner3 = new JSpinner(model3); spinner3.addChangeListener(listener); JLabel label3 = new JLabel("Numbers"); JPanel panel3 = new JPanel(new BorderLayout()); panel3.add(label3, BorderLayout.WEST); panel3.add(spinner3, BorderLayout.CENTER); frame.add(panel3, BorderLayout.SOUTH);

frame.setSize(200, 90); frame.setVisible (true); } }; EventQueue.invokeLater(runner); }

Running this program demonstrates the use of this listener (of course, you'll find far more meaningful ways to use a ChangeListener).

Customizing a JSpinner look and feel

As with all Swing components, the JSpinner control has a different appearance under each of the system-defined look-and-feel types, as shown in Figure 2. The component primarily looks just like a text field; the difference is in the drawing of the arrows.

Figure 2. JSpinner under different look-and-feel types. Click on thumbnail to view full-sized image.

The set of 11 UIResource properties for a JSpinneris shown in Table 2. These are limited to drawing the text field and the arrows.

Table 2. JSpinner UIResource elements

Property string Object type
Spinner.actionMap ActionMap
Spinner.ancestorInputMap InputMap
Spinner.arrowButtonBorder Border
Spinner.arrowButtonInsets Insets
Spinner.arrowButtonSize Dimension
Spinner.background Color
Spinner.border Border
Spinner.editorBorderPainted Boolean
Spinner.font Font
Spinner.foreground Color
SpinnerUI String

SpinnerModel interface

So far, you've seen how to interact with the main JSpinnerclass. The SpinnerModel interface is the data model for the component. The definition of SpinnerModelfollows:

                    public interface SpinnerModel {
   // Properties
   public Object getValue();
   public void setValue(Object);
   public Object getNextValue();
   public Object getPreviousValue();
   // Listeners
   public void addChangeListener(ChangeListener);
   public void removeChangeListener(ChangeListener);
}
               

The six methods of SpinnerModel map directly to those of JSpinner. The JSpinner methods just redirect the method calls to that of the model, though, in the case of the listener methods, the event source is where you attach the listener.

AbstractSpinnerModel class

The base implementation of the SpinnerModel interface is the AbstractSpinnerModel class. It provides for the management and notification of the listener list. Subclasses must implement the other four value-related methods of the interface. Three concrete implementations of the SpinnerModelinterface are provided: SpinnerDateModel, SpinnerListModel, and SpinnerNumberModel.

SpinnerDateModel class

As might be inferred from its name, the SpinnerDateModelprovides for the selection of dates. This class has two constructors: one that defaults to selecting all dates and another that allows you to limit the range.

                    

public SpinnerDateModel()SpinnerModel model = new SpinnerDateModel(); JSpinner spinner = new JSpinner(model);

public SpinnerDateModel(Date value, Comparable start, Comparable end, int calendarField)

Calendar cal = Calendar.getInstance(); Date now = cal.getTime(); cal.add(Calendar.YEAR, -50); Date startDate = cal.getTime(); cal.add(Calendar.YEAR, 100); Date endDate = cal.getTime(); SpinnerModel model = new SpinnerDateModel(now, startDate, endDate, Calendar.YEAR); JSpinner spinner = new JSpinner(model);

If you don't specify any parameters, there is no start or end point. The example shown here uses parameters to provide a 100-year range. The last field should be one of the following constants from the Calendar class:

  • Calendar.AM_PM
  • Calendar.DAY_OF_MONTH
  • Calendar.DAY_OF_WEEK
  • Calendar.DAY_OF_WEEK_IN_MONTH
  • Calendar.DAY_OF_YEAR
  • Calendar.ERA
  • Calendar.HOUR
  • Calendar.HOUR_OF_DAY
  • Calendar.MILLISECOND
  • Calendar.MINUTE
  • Calendar.MONTH
  • Calendar.SECOND
  • Calendar.WEEK_OF_MONTH
  • Calendar.WEEK_OF_YEAR
  • Calendar.YEAR

Note:The SpinnerDateModel does not include any of the time zone-related constants of Calendar. You cannot scroll through those within a JSpinnervia a SpinnerDateModel.

Table 3 lists the three properties from the SpinnerModelinterface and four specific to the SpinnerDateModel.

Table 3. SpinnerDateModel properties

Property name Data type Access
calendarField int Read-write
date Date Read-only
end Comparable Read-write
nextValue Object Read-only
previousValue Object Read-only
start Comparable Read-write
value Object Read-only

Typically, the only new property you'll use is for getting the final date, although all that does is wrap the result of getValue()in the appropriate data type. If you've provided a range of dates to the constructor, the previous or next values will be null when the current value is at an edge condition.

SpinnerListModel class

The SpinnerListModel provides for selection from a list of entries or at least their string representation. This class has three constructors:

                    

public SpinnerListModel()SpinnerModel model = new SpinnerListModel(); JSpinner spinner = new JSpinner(model);

public SpinnerListModel(List<?> values)List<String> list = args; SpinnerModel model = new SpinnerListModel(list); JSpinner spinner = new JSpinner(model);

public SpinnerListModel(Object[] values)

SpinnerModel model = new SpinnerListModel(args); JSpinner spinner = new JSpinner(model);

When no arguments are provided, the model contains a single element: the string empty. The List version retains a reference to the list. It does not copy the list. If you change the list, you change the elements in the model. The array version creates a private inner class instance of a Listthat can't be added to. For both the Listand array versions, the initial selection will be the first element. If either is empty, an IllegalArgumentExceptionwill be thrown.

As shown in Table 4, the only property added beyond those from the interface is to get or set the list.

Table 4. SpinnerListModel properties

Property name Data type Access
list List<?> Read-write
nextValue Object Read-only
previousValue Object Read-only
value Object Read-write

SpinnerNumberModel class

The SpinnerNumberModel provides for the selection of a number from an open or closed range of values. That number can be any of the subclasses of Number, including Integerand Double. It has four constructors, with the first three provided just as convenience methods to the last.

                    

public SpinnerNumberModel()SpinnerModel model = new SpinnerNumberModel(); JSpinner spinner = new JSpinner(model);

public SpinnerNumberModel(double value, double minimum, double maximum, double stepSize)SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25); JSpinner spinner = new JSpinner(model);

public SpinnerNumberModel(int value, int minimum, int maximum, int stepSize)SpinnerModel model = new SpinnerNumberModel(50, 0, 100, 1); JSpinner spinner = new JSpinner(model);

public SpinnerNumberModel(Number value, Comparable minimum, Comparable maximum, Number stepSize)

Number value = new Integer(50); Number min = new Integer(0); Number max = new Integer(100); Number step = new Integer(1); SpinnerModel model = new SpinnerNumberModel(value, min, max, step); JSpinner spinner = new JSpinner(model);

If the minimum or maximum value is null, the range is open-ended. For the no-argument version, the initial value is 0and step is 1. The step size is literal, so if you set the step to .333, there will be no rounding off.

Table 5 shows the properties for SpinnerNumberModel. The added properties are the same as those provided by the constructor.

Table 5. SpinnerNumberModel properties

Page 2 of 2
Property name Data type Access
maximum Comparable Read-write
minimum Comparable Read-write
nextValue Object Read-only
number Number Read-only
previousValue Object Read-only
stepSize Number Read-write
value Object Read-write

Custom models

Typically, the available models for the JSpinner are sufficient, so you don't need to subclass. However, that isn't always the case. For example, you might want to use a custom model that wraps the SpinnerListModel—instead of stopping at the first or last element, it wraps around to the other end. One such implementation is shown in Listing 2.

Listing 2. RolloverSpinnerListModel class

                    

import javax.swing.*; import java.util.*;

public class RolloverSpinnerListModel extends SpinnerListModel {

public RolloverSpinnerListModel(List<??> values) { super(values); }

public RolloverSpinnerListModel(Object[] values) { super(values); } public Object getNextValue() { Object returnValue = super.getNextValue(); if (returnValue == null) { returnValue = getList().get(0); } return returnValue; }

public Object getPreviousValue() { Object returnValue = super.getPreviousValue(); if (returnValue == null) { List list = getList(); returnValue = list.get(list.size() - 1); } return returnValue; } }

JSpinner editors

For each of the models available for a JSpinner, a secondary support class, an inner class of JSpinner, is available. Whereas the model allows you to control what is selectable for the component, the spinner editors allow you to control how to display and edit each selectable value.

JSpinner.DefaultEditor class

The setEditor() method of JSpinner allows you to have any JComponent as the editor for the JSpinner. While you certainly can do that, more typically, you will work with a subclass of JSpinner.DefaultEditor. It provides the basics you will need when working with simple editors based on JFormattedTextField. It contains a single constructor:

                                           public JSpinner.DefaultEditor(JSpinner spinner)JSpinner spinner = new JSpinner();
JComponent editor = JSpinner.DefaultEditor(spinner);
spinner.setEditor(editor);
               

As Table 6 shows, there are two properties for the editor.

Table 6. JSpinner.DefaultEditor properties

Property name Data type Access
spinner JSpinner Read-only
textField JFormattedTextField Read-only

Without knowing which type of model you were working with, what you might do at this level is change some display characteristic of the JFormattedTextField. More typically, though, you'll change a custom aspect for the model's editor.

JSpinner.DateEditor class

The DateEditor allows you to customize the date display (and entry) using various aspects of the SimpleDateFormatclass of the java.text package. See the Javadoc for SimpleDateFormat for a complete listing of the available formatting patterns. If you don't like the default display output, you can modify it by passing in a new format to the second constructor.

                    

public JSpinner.DateEditor(JSpinner spinner)SpinnerModel model = new SpinnerDateModel(); JSpinner spinner = new JSpinner(model); JComponent editor = JSpinner.DateEditor(spinner); spinner.setEditor(editor);

public JSpinner.DateEditor(JSpinner spinner, String dateFormatPattern)

SpinnerModel model = new SpinnerDateModel(); JSpinner spinner = new JSpinner(model); JComponent editor = JSpinner.DateEditor(spinner, "MMMM yyyy"); spinner.setEditor(editor);

By default, the format is M/d/yy h:mm a or 12/25/04 12:34 PM for some time on Christmas in 2004 (or 1904, 1804, and so on). The latter example will show December 2004.

The editor has the two properties shown in Table 7.

Table 7. JSpinner.DateEditor properties

Property name Data type Access
format SimpleDateFormat Read-only
model SpinnerDateModel Read-only

JSpinner.ListEditor class

When working with the SpinnerListModel, the ListEditorprovides no special formatting support. Instead, it offers type-ahead support. Since all entries of the model are known, the editor tries to match the characters the user has already entered with the start of one of those entries. There is only one constructor, but you should never need to access it.

                    public JSpinner.ListEditor(JSpinner spinner)
               

As shown in Table 8, ListEditor has only a single property.

Table 8. JSpinner.ListEditor properties

Property name Data type Access
model SpinnerListModel Read-only

JSpinner.NumberEditor Class

The NumberEditor works in a manner similar to the DateEditor, allowing you to enter strings to customize the display format. Instead of working with the SimpleDateFormat, the NumberEditor is associated with the DecimalFormat class of the java.textpackage. Just like DateEditor, it has two constructors:

                    

public JSpinner.NumberEditor(JSpinner spinner)SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25); JSpinner spinner = new JSpinner(model); JComponent editor = JSpinner.NumberEditor(spinner); spinner.setEditor(editor);

public JSpinner.NumberEditor(JSpinner spinner, String decimalFormatPattern)

SpinnerModel model = new SpinnerNumberModel(50, 0, 100, .25); JSpinner spinner = new JSpinner(model); JComponent editor = JSpinner.NumberEditor(spinner, "#,##0.###"); spinner.setEditor(editor);

The second constructor usage shows the default formatting string. It will try to use commas if the number is large enough, and it will not show decimals if the value is a whole number.

As shown in Table 9, this editor has two properties.

Table 9. JSpinner.NumberEditor properties

Property name Data type Access
format DecimalFormat Read-only
model SpinnerNumberModel Read-only

Summary

In this article, you learned about Swing's JSpinner component. When your set of choices is limited to a fixed set or a range of values, a JSpinner allows you to select a value by spinning through the different choices. You learned how that set of choices could be provided: from a set of dates with the SpinnerDateModeland DateEditor, with the SpinnerListModeland ListEditor, or via the SpinnerNumberModel and NumberEditor.

John Zukowskiis a freelance writer and strategic Java consultant for JZ Ventures. He serves as the resident guru for a number of jGuru's community-driven FAQs (www.jguru.com). He has served as a Java columnist for Sun's Java Developer Connection, IBM's developerWorks, and JavaWorld. His latest books are Java Collections (Apress, 2001), Definitive Guide to Swing for Java 2 (Apress, 2000), and Mastering Java 1.4 (Sybex, 2002). He's also the author of Borland's JBuilder: No Experience Required (Sybex, 1997).

Learn more about this topic