The Problem
Consider the following “gotcha” code:
package com.implementsblog; import javax.swing.JFrame; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; /** * An example of odd JSpinner size. */ public class JSpinnerGotcha { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JFrame frame = new JFrame("JSpinner Gotcha"); frame.getContentPane().add( new JSpinner( new SpinnerNumberModel( 0, // Initial 0, // Minimum Double.MAX_VALUE, // Maximum 1 // Step ))); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setVisible(true); } }); } }
On the surface, the output seems fairly simple: a JFrame
containing a single component: a JSpinner
whose model has been specified. However, when executed, the JFrame
is so wide it falls off the side of the screen.
The JSpinner
‘s preferred size is unexpectedly large. But why?
The Investigation
Let’s explore the JSpinner
source code. For each implementation of SpinnerModel
, there’s a corresponding Editor
. For example, the SpinnerDateModel
has a DateEditor
. Let’s follow line 24 of the gotcha, the call to the JSpinner
constructor:
public JSpinner(SpinnerModel model) { if (model == null) { throw new NullPointerException("model cannot be null"); } this.model = model; this.editor = createEditor(model); setUIProperty("opaque",true); updateUI(); }
From here we’ll follow the createEditor
call on line 155:
protected JComponent createEditor(SpinnerModel model) { if (model instanceof SpinnerDateModel) { return new DateEditor(this); } else if (model instanceof SpinnerListModel) { return new ListEditor(this); } else if (model instanceof SpinnerNumberModel) { return new NumberEditor(this); } else { return new DefaultEditor(this); } }
The gotcha uses SpinnerNumberModel
, so we’ll follow line 249. After a few nested constructor calls, we end up at:
private NumberEditor(JSpinner spinner, DecimalFormat format) { super(spinner); if (!(spinner.getModel() instanceof SpinnerNumberModel)) { throw new IllegalArgumentException( "model not a SpinnerNumberModel"); } SpinnerNumberModel model = (SpinnerNumberModel)spinner.getModel(); NumberFormatter formatter = new NumberEditorFormatter(model, format); DefaultFormatterFactory factory = new DefaultFormatterFactory( formatter); JFormattedTextField ftf = getTextField(); ftf.setEditable(true); ftf.setFormatterFactory(factory); ftf.setHorizontalAlignment(JTextField.RIGHT); /* TBD - initializing the column width of the text field * is imprecise and doing it here is tricky because * the developer may configure the formatter later. */ try { String maxString = formatter.valueToString(model.getMinimum()); String minString = formatter.valueToString(model.getMaximum()); ftf.setColumns(Math.max(maxString.length(), minString.length())); } catch (ParseException e) { // TBD should throw a chained error here } }
Our problem lies on lines 1220 and 1221; in the gotcha, the JSpinner
‘s model returns Double.MAX_VALUE
for model.getMaximum()
. Thus, formatter.valueToString(model.getMaximum())
returns the evaluated String
of , a very long number indeed.
The Workaround
Now that we know the problem, what’s the workaround? One way would be to get the preferred size of the JSpinner
before adding the model, and then reset the size after setting the model, as this snippet from the gotcha illustrates:
JFrame frame = new JFrame("JSpinner Gotcha"); JSpinner spinner = new JSpinner(); Dimension size = spinner.getPreferredSize(); spinner.setModel(new SpinnerNumberModel( 0, // Initial 0, // Minimum Double.MAX_VALUE, // Maximum 1 // Step )); spinner.setPreferredSize(size); frame.getContentPane().add(spinner);
Rather than being a bug in JSpinner
, however, this is probably a misuse of JSpinner
. Is a user really expected to press the up arrow button until he or she reaches Double.MAX_VALUE
?