Graphical User Interfaces in Java

Java's Abstract Windows Toolkit package (java.awt) provides platform independent tools for building GUIs.

Java User Interface Structure

The Composite pattern is used to represent recursive tree-structures such as groupable drawings, parse trees, directory structures, assemblies, hierarchies, and user interfaces. Each node (component) in the tree is either childless (a leaf) or a parent (a composite). For uniformity, both leafs and composites are derived from a common, abstract base class. Thus, a composite is a component container. In the case of a user interface, a composite is a container such as a window or control panel, and a leaf is a control such as a button or menu:

Java controls include text boxes, buttons, scroll bars, and menus. Java containers include frames, dialog boxes, panels, and applets. Each dialog box is associated with a parent frame:

Components are added to a container using the add method. Their physical location within the container is determined by a layout manager object associated with the container. Containers also have other attributes that can be set. For example, the following GUI consists of two buttons labeled "inc" and "dec" placed in a 2 x 1 grid:

class GUI extends Frame
{

   public GUI(...) {
      setTitle("Application Control Panel");
      setBackground(Color.pink);
      setSize(200, 100);
      setLayout(new GridLayout(2, 1));
      Button incButton = new Button("inc");
      add(incButton);
      Button decButton = new Button("dec");
      add(decButton);
      // etc.
   }

   // etc.
}

 

Java's Oberver-Observable Framework

In Java, the Publisher-Subscriber pattern is called the Observer-Observable pattern, and is provided as a mini-framework in the utils package. The dynamics are the same: an observer object registers itself with an observable object. When the observable's state changes, it notifies all registered observers by calling each observer's update function, usually passing some information about its new state. The observable doesn't need to know anything about the observer except that it has an update function. This is enforced by making Observer an abstract base class or interface:

interface Observer
{
   public void update(Observable subject, Object state);
}

 

Model-View-Controller in Java

The observer-observable pattern can be used twice in the Model-View-Controller pattern: models observe controls for user input events, and views observe models for state changes:

 

JDK 1.1's misnamed Event Delegation Model uses a modification of the observer-observable pattern to handle user input events. In this context model-observers are called listeners and controls are event sources. For example, if a listener registers itself with a button. It must implement a method called actionPerformed(). When the user pushes the button, the actionPerformed() method of each registered listener is called. An ActionEvent object is passed to the call to indicate which button is making the call.

The Nuclear Reactor Example in Java

A simple representation of a nuclear reactor is a good example of a model. The state of the reactor is a double, called temp, representing the temperature of the reactor's core. A reactor, r, provides services for incrementing r.temp by a fixed amount: Reactor.DELTA1 = 10 degrees, decrementing r.temp by a fixed amount: Reactor.DELTA2 = 3 degrees, and returning r.temp. A predicate, r.isCritical(), returns true if r.temp is above a safe operating temperature: Reactor.CRITICAL = 1000 degrees.

java.util.Observable

Reactor extends the Observable class defined in the java.util package. An instance of Observable encapsulates a list of registered observers (as defined earlier.) From this class the Reactor class inherits:

void addObserver(Observer 0bs) // adds obs to observer list
void deleteObserver(Observer obs) // removes obs from observer list
void setChanged() // observable's "state has changed" = true
void clearChanged() // observable's "state has changed" = false

If the observable's "state has changed" = true, then:

void notifyObservers(state)

calls obs.update(this, state) for each observer on the observer list, then calls clearChanged(). The parameter, state, can be any object, but normally describes some aspect of the observable's new state. This is called the push variant of the Observer-Observable pattern, because the state information is pushed onto (i.e., passed to) observers if they want it or not.

Calling notifyObservers() without a parameter is equivalent to calling notifyObservers(null). (null is Java's null pointer.) Interested observers can get the new state themselves, for example by calling getTemp(). This is called the pull variant of the Observer-Observable pattern, because interested observer's must pull the state information from the observable.

Reactor Declaration

Notice that after modifying temp, the incTemp() and decTemp() methods call the inherited setChanged() method, then the inherited notifyObservers(new Double(temp)) method, passing temp as an instance of the Double class. (This was necessary because temp is a double, not an object.)

class Reactor extends Observable
{

   public Reactor(double t) { temp = t; }

   public void incTemp() {
      temp += DELTA1;
      setChanged();
      notifyObservers(new Double(temp));
   }

   public void decTemp() {
      temp -= DELTA2;
      setChanged();
      notifyObservers(new Double(temp));
   }

   public double getTemp() { return temp; }

   public boolean isCritical() {
      return (temp >= CRITICAL);
   }

   private double temp; // reactor's temperature
   private static double DELTA1 = 10;
   private static double DELTA2 = 3;
   private static double CRITICAL = 1000;

} // Reactor

Views

What objects will be interested in observing the reactor? Perhaps none, but there may be views such as thermometers, alarms, and thermostats that will want to know when the reactor's temperature changes.

Thermometers

A TextField is an awt control that displays a string. It provides the methods:

String getText() // returns displayed string
void setText(String s) // displays s

A thermometer extends TextField and implements Observer. This requires us to implement the update() method:

class Thermometer extends TextField implements Observer
{

   public Thermometer(Reactor r) {
      r.addObserver(this);
      setText("" + r.getTemp());
      myReactor = r;
   }

   public void finalize() {
      myReactor.deleteObserver(this);
   }

   public void update(Observable subject, Object arg) {
      setText(((Double) arg).toString()); // push variant
   }

   private Reactor myReactor;

} // Thermometer

Alarms

There can be many types of alarms. The type defined below extends Frame. These alarms can be independently positioned on the desktop. There may be many of these alarms, so each has an id number that appears on its title bar.

Alarm implements the Observer interface, hence must implement the update() method. What should happen when update() is called? My implementation determines if its associated reactor's temperature is critical, if so, then a flag is set to true, a bell is rung, and the background color of the frame is repainted red. Otherwise the background color is repainted white.

One might complain that I use the associated reactor, myReactor rather than the reactor passed to the update method: (Reactor) subject. This would be a serious design flaw in the unlikely event that my alarm monitors multiple nuclear reactors.

class Alarm extends Frame implements Observer
{

   public Alarm(Reactor r, int id) {
      setTitle("Alarm " + id);
      setBackground(Color.white);
      setSize(100, 100);
      ringing = false;
      r.addObserver(this);
      myReactor = r;
      idNum = id;
   }

   public void finalize() {
      myReactor.deleteObserver(this);
   }

   public void update(Observable obs, Object arg) {
      if (myReactor.isCritical()) { // pull variant
         ringing = true;
         setBackground(Color.red);
         repaint();
         ring();
      }
      else {
         ringing = false;
         setBackground(Color.white);
         repaint();
      }
   }

   private void ring() {
      char bell = (char) 7; // ASCII code for bell
      System.out.println(bell);
   }

   private boolean ringing;
   private int idNum;
   private Reactor myReactor;

} // Alarm

 

Thermostats

Like an alarm, a thermostat also extends Frame and implements Observer. This frame contains a single text field called status. The update method determines if the temperature of its associated reactor is critical. If so, it displays "decrementing temperature" in its status field, then calls myReactor.decTemp(). Otherwise it displays "inactive" in its status field:

class Thermostat extends Frame implements Observer {

   private TextField status;

   public Thermostat(Reactor r) {
      setTitle("Thermostat");
      setBackground(Color.white);
      setSize(100, 100);
      r.addObserver(this);
      myReactor = r;
      status = new TextField("inactive", 40);
      add(status);
      setSize(200, 100);
   }

   public void finalize() {
      myReactor.deleteObserver(this);
   }

   public void update(Observable obs, Object arg) {
      if (myReactor.isCritical()) {
         status.setText("decrementing temperature");
         myReactor.decTemp();
      }
      else
         status.setText("inactive");
   }

   private Reactor myReactor;

} // Thermostat

 

Things get confusing when an observer, x, changes the state of an observable, because the state change triggers a call to notifyObservers(), which calls x.update(), and the cycle repeats. In effect, x.update() becomes recursive.

Of course all of the other observers are notified, too. If an observer, y, follows x on the observer list, then x.update() will schedule multiple calls to y.update() on the control stack. These calls would be performed in reverse order before the original call, but of course the first call to notifyObservers() that terminates will call clearChanged(), which will supress future calls to all update() methods. This can leave views in a confused state. For example, alarms might continue to be red after the reactor's temperature falls below Reactor.CRITICAL. This type of order-sensitive programming probably should be avoided.

Controls & Commands

The reactor GUI creates a 200 x 100 pixel frame containing a thermometer and two buttons (labeled "inc" and "dec") laid out in a 3 x 1 grid. It also creates two alarms (the thermostat was commented out for the reasons mentioned earlier):

class ReactorGUI {

   public ReactorGUI(Reactor r) {

      // main control frame
      Frame control = new Frame();
      control.setTitle("Reactor Control Panel");
      control.setSize(200, 100);
      control.setLayout(new GridLayout(3, 1));

      // explained below
      control.addWindowListener(new MyWindowAdapter());

      Thermometer tempField = new Thermometer(r);
      control.add(tempField);

      Button incButton = new Button("inc");
      IncCommand incCmmd = new IncCommand(r);
      incButton.addActionListener(incCmmd);
      control.add(incButton);

      Button decButton = new Button("dec");
      DecCommand decCmmd = new DecCommand(r);
      decButton.addActionListener(decCmmd);
      control.add(decButton);
      control.show();

      // thermostat
      // Thermostat t = new Thermostat(r);
      // t.show();
      
      // alarms
      Alarm a1 = new Alarm(r, 1);
      a1.show();
      Alarm a2 = new Alarm(r, 2);
      a2.show();

   } // constructor

   // inner classes explained below
   class MyWindowAdapter extends WindowAdapter {
      public void windowClosing(WindowEvent we) {
         dispose();
         System.exit(0);
      };
   }

} // Reactor GUI

The application simply creates a reactor and a GUI. The GUI frame contains an event loop that perpetually listens for user actions:

// ReactorApp.java

import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;

public class ReactorApp {

   public static void main(String[] args) {

      Reactor r = new Reactor(990); // init temperature = 990 degrees
      ReactorGUI gui = new ReactorGUI(r);

   } // main

} // ReactorApp

Without the following lines at the top of each source file:

import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;

all references to JDK classes such as Observable and Button need to be qualified with their package names:

java.util.Observable
java.awt.Button
etc.

Commands

A button, like any control, is an event source. When the user pushes the button labeled "inc", and action event, e, is constructed that encapsulates the string "inc" (thus listeners listening to multiple controls can distinguish which control is notifying them). The button calls the actionPerformed() method each registered listener must implement, with argument e.

To further decouple semantics and control, we introduce command objects. Commands implement listeners or extend adapters. They have special knowledge of the semantic objects they update. For example, a control in a reactor GUI can create a command and add it as a listener without any special knowledge of how to change the temperature of a reactor:

class IncCommand implements ActionListener {

   public IncCommand(Reactor r) { myReactor = r; }

   public void actionPerformed(ActionEvent e) {
      String s = e.getActionCommand();
      if (s.equals("inc")) myReactor.incTemp();
   }

   Reactor myReactor;

}

class DecCommand implements ActionListener {

   public DecCommand(Reactor r) { myReactor = r; }

   public void actionPerformed(ActionEvent e) {
      String s = e.getActionCommand();
      if (s.equals("dec")) myReactor.decTemp();
   }

   Reactor myReactor;

}

Command Factories

Command Processor is a pattern for implementing meta commands such as undo/redo. To be reusable, a layer of abstraction must be inserted between command and semantic objects. This is accomplished using a command factory. A command factory provides virtual constructor methods for manufacturing abstract commands. For example:

class CommandFactory {
   abstract public ActionListener makeCommand(Reactor r);
}

class IncCommandFactory extends CommandFactory {
   public ActionListener makeCommand(Reactor r) {
      return new IncCommand(r);
   }
}

class DecCommandFactory extends CommandFactory {
   public ActionListener makeCommand(Reactor r) {
      return new DecCommand(r);
   }
}

We can type concrete command factories as CommandFactories:

CommandFactory incFactory = new IncCommandFactory();
CommandFactory decFactory = new DecCommandFactory();

Command processors and user interfaces can deal with commands typed as action listeners:

ActionListener incCmmd = incFactory.makeCommand(r);
ActionListener decCmmd = decFactory.makeCommand(r);