Skip to main content

Using Swing's JFormattedTextField for integers is not as trivial as it should be.

Posted by hansmuller on August 25, 2005 at 2:18 PM PDT

Earlier this year I was fiddling around with the new
J2SE network

ProxySelector

APIs as part of a small demo-project. Sadly, the project just wouldn't
stay small and I didn't have time for something big. So
after a few days it disappeared into one of the many corners of
my laptop's hard disk, where it's been quietly moldering away.

One part of the old demo was a small GUI for
collecting network proxy host names and port numbers. I'd used
JFormattedTextFields for the latter. You might think that doing
so would have been trivial, since port numbers are just integers
between 0 and 65534 and JFormattedTextField is very, well, flexible.
It turns out to have been not so trivial and at the time I was
inspired to write a blog-sized article about exactly what I'd done.
That article would have remained buried with everything else from
the project if it
hadn't been for some interesting

JFormattedTextField threads

on the javadesktop.org JDNC forum that cropped up recently. The
problem that inspired the JDNC JFormattedTextField thread had to do
with decimals (like 123.45). Since I'd spent some time in the
trenches with a similar problem, I thought it might be fun to exhume
my old article and toss it on the pyre. So here it is.

ProxyPanel: A Swing Component for Network Proxies

Warning: if you're looking for the material about
JFormattedTextField you can skip the first couple of paragraphs.
I've left the first couple of paragraphs the way they were out
of respect for the dead. Plus, I'm too lazy to edit them out.

Before writing more than a few lines of code I considered structuring
the ProxyPanel component conventionally: with careful separation of
model and view, and with great flexibility for all dimensions of both.
The model would be a Java Bean that included all of the data required
to completely specify the usual set of networking proxies along with
all of the secondary data like overrides and user names and passwords.
The bean's API would be specified as an interface, so that the
ProxyPanel could operate directly on application data, and an abstract
class would provide a simple backing store for the
data along with all of the change listener machinery required to keep
the GUI view in sync. The view would be equally overdesigned. It
would be configurable, to accommodate applications that wanted a
compact or subsetted presentation. And just before I awoke from my
second system syndrome induced reveries, I imagined providing an XML
schema that could be used to completely configure and (cue the Mormon
Tabernacle choir) even localize the GUI.

This was supposed to be a tiny project aimed at highlighting the new
ProxySelector APIs and providing a small coding diversion for yours
truly. So, after I'd calmed down, I decided to write a simple GUI
that wasn't terribly configurable and that lacked a pluggable model.
That's right: no model/view separation here. If there are MVC gods,
I'm sure I'll be in for some smiting. And if the gods can't be bothered,
then I'm confident that my more dogmatic brethren will take up the slack.
Please don't send your self-righteous segregationist rantings about the
merits of MVC to me. I know, I know.

My first cut at structuring the code for the four pairs of proxy
host/port fields that correspond to the bulk of the GUI was to create
a little internal class that defined the GUI for just one proxy, in
terms of four components:

public class ProxyPanel extends JPanel {
    private ProxyUI httpUI;
    private ProxyUI httpsUI;
    // ProxyPanel constructor initializes httpUI etc ...

    private static class ProxyUI {
        private final JLabel hostLabel;
        private final JTextField hostField;
        private final JLabel portLabel;
        private final JFormattedTextField portField;

        ProxyUI (ProxyPanel panel, String hostTitle, String host, String portTitle, int port) {
           // create labels, fields, and update the GridBagLayout
        }
        String getHostName() {
            return hostField.getText();
        }
        // ...
    }
}

The ProxyPanel created four ProxyUI instances and squirreled them away
in four private ProxyPanel ivars. The ProxyUI class did
encapsulate the details of how one proxy was presented to the user.
On the down side, had to assume that the ProxyPanel had a
GridBagLayout (no encapsulation there) and it felt gratuitously
complicated.

One lesson I learned as part of building this first revision of
ProxyPanel was how to configure a JFormattedTextField that accepted
either a integer between 0 and 65534 or an empty string. The latter
indicated that the user hadn't provided a valid value. It seemed like
it would a little less surprising for users to map no-value or invalid
values to a blank than to insert a valid default value like 0.

JFormattedTextFields are eminently configurable and if you'd like to
get acquainted with the API I'd recommend the href="http://java.sun.com/docs/books/tutorial/uiswing/components/formattedtextfield.html">
Java Tutorial.
The specific problem I was trying to solve isn't
covered there however with a little help from the local cognoscenti I
was able to work things out.

The Swing class that takes care of converting to and from strings as
well as validating same, is called a formatter and the subclass needed
for numbers is called NumberFormatter. A separate java.text class
called DecimalFormat is delegated the job of doing the actual string
conversions and it provides its own myriad of options for specifying
exactly how our decimal is to be presented. Fortunately in this case
we don't need to avail ourselves of much of that, in fact we're going
to defeat DecimalFormat's very capable features for rendering numbers
in a locale specific way. What we need is just a geek friendly 16 bit
unsigned integer. Or a blank.

Here's the code for our JFormattedTextField instance. We override
NumberFormatter's stringToValue method to map "" (empty string) to
null. The ProxyPanel.getPort() method that reads this field will map
null to -1, to indicate that the user hasn't provided a valid value.

DecimalFormat df = new DecimalFormat("#####");
NumberFormatter nf = new NumberFormatter(df) {
    public String valueToString(Object iv) throws ParseException {
        if ((iv == null) || (((Integer)iv).intValue() == -1)) {
            return "";
        }
        else {
            return super.valueToString(iv);
        }
    }
    public Object stringToValue(String text) throws ParseException {
        if ("".equals(text)) {
            return null;
        }
        return super.stringToValue(text);
    }
};
nf.setMinimum(0);
nf.setMaximum(65534);
nf.setValueClass(Integer.class);
portField = new JFormattedTextField(nf);
portField.setColumns(5);

It occurred to me that perhaps an IntegerTextField would be
worthwhile. That way one could write:

IntegerTextField inf = new IntegerTextField();
itf.setMinimum(0);
itf.setMaximum(65534);
itf.setEmptyOK(true);
itf.setEmptyValue(-1);  // new feature, "" => -1
itf.setValue(0);

I don't think that's a vast improvement however developers might have
an easier time sorting out how to create an IntegerTextField than
assembling the right combination of DecimalFormat, NumberFormatter,
and FormattedTextField. Of course, having gone so far as to create
IntegerTextField we'd want similar classes for currency values, real
numbers, dates, and so on. Some of this is already covered by
JSpinner although spinners are better suited to cycling through
relatively small sets of values.

Eight months later ...

It's been a long time since I wrote all of that. Looking back
I'd have to say that a set of classes, like IntegerTextField,
would certainly make life more straightforward for Swing developers.
Hopefully the

SwingLabs
project will take up the cause and maybe in the future
a collection of battle-hardened special purpose text fields will
find their way into the JDK. If they do, I'll use them.

Related Topics >>