Programming

my faceChris Foley is a computer programming enthusiast. He loves exploring new programming languages and crafting nice solutions. He lives in Glasgow, Scotland and works as a software developer.

Fixing Java on the Desktop

Java has a poor reputation for writing desktop applications. This is despite having no fewer than three GUI toolkits in its core libraries. It has the Abstract Window Toolkit (AWT) which uses the native operating system's UI controls. The AWT has been long superseded by Swing. Swing uses lightweight controls (i.e. drawn by Java, not the OS) and is built on top of the AWT. Swing entered long-term maintenance several years ago. It might get bug fixes but there will be no new features. The future of Java's desktop interface is JavaFX. It brings a lot of really nice stuff to the table: CSS styling is definitely cool and XML configuration seems popular.

Unfortunately, I think the public interfaces of all three are broken. More precisely, they are at the wrong abstraction level. FX seems a little better than AWT and Swing in this regard but it is still quite broken. Let me illustrate by example. Here is a trivial little Swing program to convert from Celcius to Fahrenheit.


package cfoley.swingInterface.problemOutline;

import java.awt.*;

import javax.swing.*;
import javax.swing.event.*;

public class SpinnerConverter extends JFrame implements ChangeListener {
	
	public static void main(String[] args) {
		new SpinnerConverter().setVisible(true);
	}
	
	private JSpinner celcius;
	private JLabel fahrenheit;
	
	public SpinnerConverter() {
		celcius = new JSpinner(new SpinnerNumberModel(0, -273.15, 99999999, 1));
		fahrenheit = new JLabel();
		celcius.addChangeListener(this);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setUpLayout();
	}

	private void setUpLayout() {
		setLayout(new GridLayout(2, 2, 10, 10));
		add(new JLabel("Celcius:"));
		add(celcius);
		add(new JLabel("Fahrenheit:"));
		add(fahrenheit);
		pack();
	}

	@Override
	public void stateChanged(ChangeEvent event) {
		showConversion();
	}

	private void showConversion() {
		try {
			fahrenheit.setText(String.format("%.2f \u00b0F", convert()));
		} catch (ClassCastException ex) {
			fahrenheit.setText("Invalid!");
		}
	}
	
	private double convert() {
		double degreesCelcius = (Double) (celcius.getValue());
		return (1.8 * degreesCelcius) + 32;
	}

}

The class is self-contained so you should be able to run it easily. Basically, there is a spinner and a label. As you change the Celcius value in the spinner, the Fahrenheit value in the label is updated dynamically. So far so good, but what happens if you want to swap the spinner for a text field? Should be easy, right?


package cfoley.swingInterface.problemOutline;

import java.awt.*;

import javax.swing.*;
import javax.swing.event.*;

public class TextFieldConverter extends JFrame implements DocumentListener {
	
	public static void main(String[] args) {
		new TextFieldConverter().setVisible(true);
	}
	
	private JTextField celcius;
	private JLabel fahrenheit;
	
	public TextFieldConverter() {
		celcius = new JTextField();
		fahrenheit = new JLabel();
		celcius.getDocument().addDocumentListener(this);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setUpLayout();
	}

	private void setUpLayout() {
		setLayout(new GridLayout(2, 2, 10, 10));
		add(new JLabel("Celcius:"));
		add(celcius);
		add(new JLabel("Fahrenheit:"));
		add(fahrenheit);
		pack();
	}

	@Override
	public void changedUpdate(DocumentEvent event) {
		showConversion();
	}

	@Override
	public void insertUpdate(DocumentEvent event) {
		showConversion();
	}

	@Override
	public void removeUpdate(DocumentEvent event) {
		showConversion();
	}
	
	private void showConversion() {
		try {
			fahrenheit.setText(String.format("%.2f \u00b0F", convert()));
		} catch (NumberFormatException ex) {
			fahrenheit.setText("Invalid!");
		}
	}
	
	private double convert() {
		String inputString = celcius.getText();
		double degreesCelcius = Double.parseDouble(inputString);
		return (1.8 * degreesCelcius) + 32;
	}

}

This looks very similar to the program above. Obviously, the creation of the TextField is different, and so is the listener, and so is retrieving the value that the user entered, and so is converting it to a number, and so is the kind of exception you might expect. In fact, here is a diff of the two files. It's about the same length as the entire program. Ridiculous!


8c8
< public class SpinnerConverter extends JFrame implements ChangeListener {
---
> public class TextFieldConverter extends JFrame implements DocumentListener {
11c11
< 		new SpinnerConverter().setVisible(true);
---
> 		new TextFieldConverter().setVisible(true);
14c14
< 	private JSpinner celcius;
---
> 	private JTextField celcius;
17,18c17,18
< 	public SpinnerConverter() {
< 		celcius = new JSpinner(new SpinnerNumberModel(0, -273.15, 99999999, 1));
---
> 	public TextFieldConverter() {
> 		celcius = new JTextField();
20c20
< 		celcius.addChangeListener(this);
---
> 		celcius.getDocument().addDocumentListener(this);
35c35
< 	public void stateChanged(ChangeEvent event) {
---
> 	public void changedUpdate(DocumentEvent event) {
38a39,48
> 	@Override
> 	public void insertUpdate(DocumentEvent event) {
> 		showConversion();
> 	}
> 
> 	@Override
> 	public void removeUpdate(DocumentEvent event) {
> 		showConversion();
> 	}
> 	
42c52
< 		} catch (ClassCastException ex) {
---
> 		} catch (NumberFormatException ex) {
48c58,59
< 		double degreesCelcius = (Double) (celcius.getValue());
---
> 		String inputString = celcius.getText();
> 		double degreesCelcius = Double.parseDouble(inputString);

Suggesting A Solution

What's gone wrong here? JSpinner and JTextField both inherit from JComponent which has a slew of methods. The problem is that most of those methods are useless. Once in a blue moon, I may like to change the background colour but I have never felt the desire to remove a vetoable change listener, whatever that is. What I do need to do is get the value from the components and add listeners to watch out for when the value changes. I need these in every application I write and these are the bits that are not in the common superclass. If I could retrofit an interface to the whole of AWT, Swing and JavaFX it would look something like this.


package cfoley.swingInterface;

import javax.swing.JComponent;

public interface Widget<T> {
	
	T value();
	void setValue(T newValue);
	JComponent swingComponent();
	void addWidgetListener(Runnable listener);
	
}

This interface is all I need for 90% of my interactions with the GUI. I can get and set the entered value. The interface is generic, so when I do get and set it I don't have to do further type conversion. I can register listeners. I don't need to worry if it's an item listener, a change listener, a document listener or any of the other listeners. It's the same for all of them and it just calls a method whenever the value changes. Finally, I can get the underlying component. This is needed to add it to a layout but is also useful for the 1% of the time that I end up needing some of the more esoteric functionality of the component.

Here is what the Spinner application would look like if it could use this interface:


package cfoley.swingInterface.solution;

import java.awt.*;

import javax.swing.*;

import cfoley.swingInterface.*;

public class SpinnerConverter extends JFrame {
	
	public static void main(String[] args) {
		new SpinnerConverter().setVisible(true);
	}
	
	private Widget<Double> celcius;
	private JLabel fahrenheit;
	
	public SpinnerConverter() {
		celcius = SpinnerWidget.doubles(0, -273.15, 99999999, 1);
		fahrenheit = new JLabel();
		celcius.addWidgetListener(() -> showConversion());
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setUpLayout();
	}

	private void setUpLayout() {
		setLayout(new GridLayout(2, 2, 10, 10));
		add(new JLabel("Celcius:"));
		add(celcius.swingComponent());
		add(new JLabel("Fahrenheit:"));
		add(fahrenheit);
		pack();
	}
	
	private void showConversion() {
		double degF = (1.8 * celcius.value()) + 32;
		fahrenheit.setText(String.format("%.2f \u00b0F", degF));
	}
	
}

Here is the text box version:


package cfoley.swingInterface.solution;

import java.awt.*;

import javax.swing.*;

import cfoley.swingInterface.*;

public class TextFieldConverter extends JFrame {
	
	public static void main(String[] args) {
		new TextFieldConverter().setVisible(true);
	}
	
	private Widget<Double> celcius;
	private JLabel fahrenheit;
	
	public TextFieldConverter() {
		celcius = TextFieldWidget.makeDoubleInstance(0.0);
		fahrenheit = new JLabel();
		celcius.addWidgetListener(() -> showConversion());
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setUpLayout();
	}

	private void setUpLayout() {
		setLayout(new GridLayout(2, 2, 10, 10));
		add(new JLabel("Celcius:"));
		add(celcius.swingComponent());
		add(new JLabel("Fahrenheit:"));
		add(fahrenheit);
		pack();
	}
	
	private void showConversion() {
		double degF = (1.8 * celcius.value()) + 32;
		fahrenheit.setText(String.format("%.2f \u00b0F", degF));
	}

}

And here is the diff. There are three differences. Two are to do with the different class name. The other is creating the changed widget. The rest of the program is the same. It certainly seems a better way of interacting with the user interface classes.


9c9
< public class SpinnerConverter extends JFrame {
---
> public class TextFieldConverter extends JFrame {
12c12
< 		new SpinnerConverter().setVisible(true);
---
> 		new TextFieldConverter().setVisible(true);
18,19c18,19
< 	public SpinnerConverter() {
< 		celcius = SpinnerWidget.doubles(0, -273.15, 99999999, 1);
---
> 	public TextFieldConverter() {
> 		celcius = TextFieldWidget.makeDoubleInstance(0.0);

If you want to run this program, you'll need the code for the widget implementations. Here they are. Other implementations might be better as I was going for quick and easy with them.


package cfoley.swingInterface;

import javax.swing.*;

public class SpinnerWidget<T> implements Widget<T> {
	
	public static SpinnerWidget<Double> doubles(double value, double minimum, double maximum, double stepSize) {
		return new SpinnerWidget<>(new SpinnerNumberModel(value, minimum, maximum, stepSize));
	}
	
	public static SpinnerWidget<Integer> ints(int value, int minimum, int maximum, int stepSize) {
		return new SpinnerWidget<>(new SpinnerNumberModel(value, minimum, maximum, stepSize));
	}
	
	private JSpinner spinner;
	
	public SpinnerWidget(SpinnerModel model) {
		spinner = new JSpinner(model);
	}

	@Override @SuppressWarnings("unchecked")
	public T value() {
		return (T)spinner.getValue();
	}

	@Override
	public void setValue(T newValue) {
		spinner.setValue(newValue);
	}

	@Override
	public JComponent swingComponent() {
		return spinner;
	}

	@Override
	public void addWidgetListener(Runnable listener) {
		spinner.addChangeListener(e -> listener.run());
	}

}

package cfoley.swingInterface;

import java.util.function.*;

import javax.swing.*;
import javax.swing.event.*;

public class TextFieldWidget<T> implements Widget<T> {
	
	public static TextFieldWidget<String> makeStringInstance() {
		return new TextFieldWidget<>(s -> s);
	}
	
	public static TextFieldWidget<Integer> makeIntegerInstance(Integer defaultValue) {
		return new TextFieldWidget<>(s -> {
			try {
				return Integer.parseInt(s);
			} catch (NumberFormatException e) {
				return defaultValue;
			}
		});
	}
	
	public static TextFieldWidget<Double> makeDoubleInstance(Double defaultValue) {
		return new TextFieldWidget<>(s -> {
			try {
				return Double.parseDouble(s);
			} catch (NumberFormatException e) {
				return defaultValue;
			}
		});
	}
	
	private JTextField textField;
	private Function<String, T> parser;
	
	public TextFieldWidget(Function<String, T> parser) {
		textField = new JTextField();
		this.parser = parser;
	}
	
	@Override
	public T value() {
		return parser.apply(textField.getText());
	}

	@Override
	public void setValue(T newValue) {
		textField.setText(newValue.toString());
	}

	@Override
	public JTextField swingComponent() {
		return textField;
	}

	@Override
	public void addWidgetListener(Runnable listener) {
		textField.getDocument().addDocumentListener(new DocumentListener() {
			@Override
			public void removeUpdate(DocumentEvent e) {
				listener.run();
			}
			@Override
			public void insertUpdate(DocumentEvent e) {
				listener.run();
			}
			@Override
			public void changedUpdate(DocumentEvent e) {
				listener.run();
			}
		});
	}
	
}

Expanding to Other Widgets

You can see how this can easily be extended to sliders, combo boxes, etc. Lists are interesting. For a single selection model, this interface would work. For multiple selections, I initially thought I would need a different interface but now I don't think so. Widget<T> where T is Collection<E> would be much better. Using this idea, a bunch of checkboxes could be grouped as a collection widget and a group of radio buttons could be grouped as a standard-non-collection widget.

I was struggling with labels, since their value is not edited by the user. My current thinking is that they would do better to implement the Widget interface. If, for example, I wanted to change the Fahrenheit label in my example to a text box for a 2-way conversion, it would be easier if label implemented the interface.

Similarly, I think Buttons would be best implementing Widget too. Setting and getting the value might just set and get the text. It's a little clunky but not the end of the world. I'm struggling with what to do about JTable. I could make it a Widget<E[][]> or a Widget<List<List<E>>> but accessing the entire table on each operation seems a little silly to me. Besides, in my experience, JTable is almost always configured with custom renderers and editors when used. The pragmatic solution might be to leave it as a special case.

Did Sun Really Get It Wrong Three Times?

Three GUI toolkits, three APIs with the same problems. Did Sun make a mistake when designing them? I don't think so. Their toolkits are designed to make it possible to make GUIs in Java, and it most certainly is possible. Not only that but Swing and FX are incredibly configurable. You can make custom models, custom renderers, custom editors. You can extend any component to tweak its behaviour. You can write look and feels. Almost anything is possible.

The problem is once the GUI is written it's just not flexible. If you want that kind of flexibility, you need a layer on top. Most of the time you don't need all the awesome features of the toolkit so eliminate all the ones you don't need and dream up your ideal interface. Writing wrappers for the components you actually want to use is quick and easy, and if you get one wrong, it should be pretty easy to debug.

This is Bigger then GUI Toolkits

At the beginning, I said that these GUI toolkits were at the wrong level of abstraction. This is a general problem with other people's libraries. They don't know their customers' problem domains so they focus on something else. What often happens, as in the case of Swing and FX, is they make them ultimately configurable and extendible. There are methods and functions for everything you could possibly need but the way of interacting with them can be clumsy and awkward.

I am in the habit of hiding third party libraries behind my own wrapper. (This is called the Facade pattern.) I get all the advantage of someone else's hard work alongside my own dream API. It also makes it easy to mock out for testing and also to swap in a different library if I find a better one in the future.

In Summary, Java has great GUI toolkits. They suffer the same problems as other third party libraries and frameworks. Fortunately, you can solve these shortcomings with the Facade pattern.

31 January 2015

Comments