CSCI213
Autumn Session, 2007

Lab Exercise 3

This exercise is intended to help you get used to building graphical user interfaces (GUIs) for Java programs. Some explanations first, then the coding exercises:

Finally, there are a couple of demonstrations that relate to assignment tasks. There is an additional exercise that involves creating a variant of one of these demonstrations.

Applets and Applications

There are two types of graphical Java program - Applets, and applications. Applets were the feature used to sell Java initially; they are (smallish) programs for which the class files can be downloaded from a WWW host and run in a web browser. Applets never achieved the predicted level of usage; alternative technologies were found more appropriate for use on the browser (e.g. "Flash" for animated graphics, Javascripting for dynamic HTML). Mostly GUIs are developed for stand-alone Java applications.

There aren't actually major differences between Applets and GUI-applications. Both use "event-driven" program architectures wherein the user's interactions (mouse-clicks and keystrokes) are packaged as "event objects" that are delivered to call-back functions that the programmer has registered to receive these events. Both are built with the same collection of standardized user interface objects - text fields that can be used to enter short text strings, action buttons, checkboxes, scrolling panes with scroller controls, and graphic areas for drawing application specific data. Both use the same graphics objects that support functions for drawing text and filling in colored regions. Both use the same color model.

The real difference is that an Applet gains access to a subpart of a window that belongs to the browser in which it runs, an application has to negotiate with the operating system to open a window. These differences result in the use of different base classes for the graphical parts of the program. The developer of an Applet will create an application specific subclass of one of the Sun supplied "applet base classes"; the developer will add all the text fields, checkboxes, panels and buttons as contents of this customized applet. The applet's GUI interface appears when the browser displays the HTML page that references it. The developer of an application will create an instance of one of Sun's supplied "frame classes" and then add all the text fields etc as the contents of this "frame". When the interface has been built, the application will explicitly show the new frame and its contents.

Applets are often popular as assignments in Java courses, but they are problematic in real use. First, your browser has to contain a JVM (Java interpreter) that has up to date versions of standard packages like java.lang and java.util. In practice, most browsers have old versions (e.g. Java 1.1 circa 1998) and programs written to current Java APIs (1.4 etc) simply will not run. Java applets only run reliably if the user of the browser has installed a Sun plugin with a current JVM and basic set of packages. Even if you do have a valid Java interpreter in your browser, you can experience performance problems resulting from the need to download individual class file and "jar" (java archive) files with packages.

These exercises include applets and applications. The applets should be run with the appletviewer tool (you can try available browsers, but the applets will most likely only work with appletviewer).

Graphical packages

There are several libraries (packages) of graphical classes for Java. Sun has two - AWT and Swing. IBM supplies another package with its Eclipse development system. Netscape invented another library that you may still encounter. There are others.

The applets and applications that you will develop will use AWT or Swing (which itself makes some very limited use of AWT). The different packages all have some standard components - so you will find class Button (AWT) and JButton (Swing), TextField (AWT) and JTextField (Swing), Panel (AWT) and JPanel (Swing). Swing has a larger selection of classes than AWT; for example, AWT has a simple class for display and editing of block text in a single font, but Swing also has classes for formatted text in multiple fonts and classes for display of HTML text.

The different packages take quite different approaches to graphics rendering. The AWT package relies a lot on C-code in the supporting Java runtime engine. An AWT class such as Button does not actually handle the display of an action button control; instead, this work is done by system specific code in the C runtime. This C code is different for a Solaris system, for a Macintosh, and for a Windows system; the result is that the button renders as an X-windows button, a Macintosh style button, or Windows button. Similarly, the AWT run-time code for event-handling links into that supplied by the operating system, so that a button press starts as an XButton event or a Windows button event as generated by the OS. In contrast, Swing minimizes its reliance on the underlying platform; basically, the Swing system simply asks the platform for a graphic area for which it will take total responsibility. Swing draws its own buttons, textfields etc (using user specified preferences to determine style); and Swing picks up generic key and mouse events and handles all their translation. Most of the Swing code is Java. The result is that Swing built interfaces often look better, but the code runs slower.

The exercises will use both Swing and AWT.

Laying out a GUI

By the time Java was invented, there were already extensive class libraries (in C++ and "Object Pascal") that could be used to build GUI interfaces for applications on Macintosh and Windows systems. These earlier class libraries had many of the same classes as you find in the Java packages - classes for buttons, scrollpanes, scrollbars, checkboxes, etc, etc. The developer of a graphical interface would start with "frame" or "window" class, and place labels, buttons, textfields etc at specific coordinate locations.

The use of specific coordinates caused problems for Java's "write once, run anywhere" policy. The problems relate to the fact that things like buttons are different sizes on different platforms - a GUI might look fine on a Mac, but transfer to Windows and the various fields overlap. The Java designers decided to follow a different approach - one pioneered for the C-libraries used with X-windows on Unix systems. In this approach, it is not the programmer who decides where components like buttons should appear - instead this becomes the responsibility of the runtime system.

In the Java version, one has "containers", "simple components", and "layout managers". Containers are graphical components that can contain other graphical components. One always has a top-level container such as the Applet itself, or a Frame. There are others. A "Panel" is a container that can be put into a top-level Applet or Frame and can be used to hold subordinate components (other panels or simple components like buttons, textfields, or checkboxes). Each "container" has an associated "layout manager" (either explicitly defined by the programmer or obtained by default from the Java graphic system). It is this layout manager that determines the arrangement of subordinate components.

There are several layout managers defined (Swing and AWT share many of the same layout manager classes, Swing has a few extra layout managers). Some, like FlowLayout, are simple but produce unattractive interfaces. Others, like GridBagLayout, are fiendishly complex, but give the programmer a great deal of control and enable the construction of complex interfaces. (A few people cheat, setting the layout manager to null and using explicit coordinates when creating an interface; such cheating is frowned on by Java purists and assignment markers.)

In CSCI213, you are expected to build some of your GUI interfaces programmatically. You create your GUI objects; you create containers and layout managers; you assign GUI objects to containers and provide specifications to the layout manager that help it place the components. It is tiresome boring code; but you need to work through it to understand how GUIs are created and how they work. Once you have built some GUIs "from the ground up", and you understand the basic concepts, then you can rely on GUI builders to do much of the coding for you.

Graphics and colors

Application specific display classes are also necessary. The AWT package contains a "Canvas" class for this purpose - you create a subclass of Canvas and link it to your data. When the windows system wants data drawn, it invokes the "paint" method of your Canvas subclass. Your code then renders some pictorial representation of your data. For some reason, Swing does not have a corresponding class - so instead one typically creates an application specific subclass of JPanel and links this to the data.

When you need to render data, you will first obtain a reference to a "Graphics" object (you can get this from your Canvas or JPanel class). The Graphics class defines operations like drawString, fillRect, and frameOval. You use these to draw an image of your data.

The Color system is simple - you define Color objects by specifying their red, green, and blue intensity values. (Java is American, so colour is consistently spelt as color). There are a few constant color object defined by the Color class - e.g. Color.RED; you can invent others by specifying the r, g, b values (each in range 0...255). If you have a decent graphics card, the colors will display properly (some of the Sun workstations in the Java lab may still have old graphics cards that have restricted color displays). You set foreground and background colors for a Graphics object prior to invoking an operation like drawString or fillRect (remember to set the colors back to whatever they were originally, the Graphics object is shared, and if you don't reset colors then you may end up with oddly colored buttons, scrollbars etc).


Exercises

  1. Run demonstration GUI application

    Compile, run, and try to understand a simple GUI application.

    The code for the application is shown below. The code defines an application class and some additional classes; all the classes are defined in a single file. The code can be cut-and-pasted into a file (EventDemo1.java); this file can be compiled and run using the command line compiler java and then run using java. Rather than run at the command line, you should adapt this code for execution in the NetBeans environment (you simply create separate NetBeans files for each class and "cut&paste" the example code.

    The "EventDemo1" application illustrates, in a limited way, a basic GUI-style program that has some data that are to be displayed, with some controls that change the rendering of the data.

    The data in this case are a set of rectangles. They are displayed by simply drawing the rectangles in a Canvas. The controls can be used to shift the coordinates of these rectangles. The data will be redisplayed after each shift. The controls consist of a set of action buttons - one each for moving the rectangles "north", "east", "south", or "west".

    The main (public) class is EventDemo1. The mainline creates a "data object" and a "display object". The other classes (all defined in the one file) are DataObject, MyCanvas, and Display.

    The DataObject class has data members for an origin for a coordinate system, a vector used as a collection class for rectangle objects, and a link to its Canvas. The DataObject class processes the events coming from the action button controls; each such event results in a shift of the coordinate origin. When the rectangles have been shifted, any existing display is out of date; so, the "repaint" operation is invoked on the associated Canvas object. (Repaint doesn't actually cause the painting to be done immediately, instead a paint event is scheduled at the convenience of the AWT library code.) Since the DataObject class is handling the action events from the controls, it has been defined as an ActionListener. The DataObject class also defines a paint operation; it is here that one finds the code that draws the rectangles with appropriate positions. (In this example, the DataObject is both "Model" and "Control".)

    The Canvas class defines a visual component that can be placed in container (such as an Applet or a Frame) and occupies space. It is for display of application specific data. It obtains a link to its data when it is constructed. It gets told to perform a paint operation when the window is first opened and after any subsequent repaint invocations. Its paint operation simply defers the actual work to the related data object.

    The Display class builds the complete GUI - a frame that contains other component that will be arranged using a "BorderLayout". The main area is for the MyCanvas. The "East" area is for a panel containing action buttons. (The GUI classes are defined. In this example, you do not build them using the NetBeans GUI editor. You simply add the class definitions to a NetBeans project.)

    import java.awt.*;
    import java.awt.event.*;
    import java.util.*;
    
    class DataObject implements ActionListener {
    
    	public DataObject() {
    		myCollection = new Vector<Rectangle>();
    		Rectangle r1;
    		r1 = new Rectangle(10, 20, 40, 40);
    		myCollection.addElement(r1);
    		r1 = new Rectangle(110,80, 60, 30);
    		myCollection.addElement(r1);
    		r1 = new Rectangle(220, 180, 10, 85);
    		myCollection.addElement(r1);
    	}
    
    	public void LinkToCanvas(Canvas c) { theCanvas = c; }
    
    	public void paint(Graphics g)
    	{
    		for(Rectangle r : myCollection) {
    			int x;
    			int y;
    			x = xOrigin + r.x;
    			y = yOrigin + r.y;
    			g.drawRect(x, y, r.width, r.height);
    		}
    	}
    
    	public void actionPerformed(ActionEvent e)
    	{
    		String s = e.getActionCommand();
    		if(s.equals("North")) { yOrigin -= 5; theCanvas.repaint(); }
    		else
    		if(s.equals("East")) { xOrigin += 5; theCanvas.repaint(); }
    		else
    		if(s.equals("South")) { yOrigin += 5; theCanvas.repaint(); }
    		else
    		if(s.equals("West")) { xOrigin -= 5; theCanvas.repaint(); }
    
    	}
    
    	private Vector<Rectangle> myCollection;
    	private int xOrigin;
    	private int yOrigin;
    
    	private Canvas theCanvas;
    
    }
    
    class MyCanvas extends Canvas {
    	public MyCanvas(DataObject d) { myData = d; setSize(400, 400); }
    
    	public void paint(Graphics g) { myData.paint(g); }
    
    	private DataObject myData;
    }
    
    class Display implements WindowListener {
    	public void windowClosing(WindowEvent e) { System.exit(0); }
    	public void windowClosed(WindowEvent e) { }
    	public void windowIconified(WindowEvent e) { }
    	public void windowOpened(WindowEvent e) { }
    	public void windowDeiconified(WindowEvent e) { }
    	public void windowActivated(WindowEvent e) { }
    	public void windowDeactivated(WindowEvent e) { }
    	
    	public Display(DataObject d) 
    	{
    		theFrame = new Frame();
    		theFrame.setLayout(new FlowLayout());
    		theCanvas = new MyCanvas(d);
    		d.LinkToCanvas(theCanvas);
    
    		theFrame.add(theCanvas);
    		Button b;
    
    		b = new Button("North");
    		b.addActionListener(d);
    		theFrame.add(b);
    
    		b = new Button("East");
    		b.addActionListener(d);
    		theFrame.add(b);
    
    		b = new Button("South");
    		b.addActionListener(d);
    		theFrame.add(b);
    
    		b = new Button("West");
    		b.addActionListener(d);
    		theFrame.add(b);
    
    		theFrame.pack();
    		theFrame.addWindowListener(this);
    	}
    
    	public void show() { theFrame.setVisible(true); }
    
    	private Frame theFrame;
    	private MyCanvas theCanvas;
    
    }
    
    public class EventDemo1 {
    
    	public static void main(String args[])
    	{
    		DataObject aData;
    		Display aDisplay;
    		aData = new DataObject();
    		aDisplay = new Display(aData);
    		aDisplay.show();
    	}
    }
    
    
    

    In this example, the Display class owns an instance of the java.awt.Frame class (an equally good design would have the Display class defined as a subclass of the standard Frame class). The constructor of the Display class

    The display object is responsible for WindowEvents such as activation, deactivation, closing etc. So, it must implement the WindowListener interface. In this application, most window events (e.g. activation) are unimportant - so there are just empty methods defined (as WindowListener is an interface, it is necessary to provide some definition for each method that it declares). The only operation that matters is the window closing event - this signifies that the application should exit.

    There is another way in which these Window events could be handled. It would involve creation of a WindowAdapter object. You will find this approach illustrated in textbooks such as the Horstmann and Cornell Core Java reference. Code using a WindowAdapter is shorter - but a lot more obscure! You can try using Adapter classes in your exercises and assignments, but you are advised to stick to the simpler Listener style until you have more experience with Java.

    Compile the program, make sure it runs in NetBeans.

  2. Try creating a Swing graphics version of EventDemo1

    Change the code of the EventDemo1 program to use classes from the Swing library; at the same time, improve the layout by placing the action buttons inside an extra "Box" component (a Swing container that, coupled with a BoxLayout manager, can arrange components in neat vertical columns).

    You will need to change the code to use a JFrame instead of a Frame, and use a specialized JPanel subclass instead of a specialized Canvas subclass.

    The JFrame class has some limited functionality for handling Window events, so you will no longer need to have anything that implements WindowListener. You simply use a method in JFrame that specifies what should be done when the close-box is clicked on a window (by default, the window is just hidden - you will need to change that behaviour to get the application to exit). You cannot add components directly to the JFrame object itself - ask the JFrame to return a reference to its content pane and add subordinate components to this.

    Your main hassles will be with the JPanel that you will have to substitute for AWT's Canvas class. Swing expects a JPanel to work out its size from the dimensions of subordinate components - but here there are none, as simply requiring a component in which one can draw application data. Since there are no subordinate components, Swing decides the JPanel's size is zero (even if you invoke setSize()) and you won't see anything drawn.

    You can however set a "preferred size" for a JPanel and Swing will honor this setting. (You will find some messy interfaces in the graphics classes. Some functions require Dimension objects, others require x- and y- coordinate values. It is very inconsistent. Things like Component and Container were among the first Java classes defined and there were inconsistencies - too much code now depends on the existing interfaces so Sun can't clean them up and make anything better.) Although a JPanel with a preferred size will sometimes work, there are problems if components are re-sized (try replacing the inheritted Dimension getMinimumSize() method with one that returns the minimum size that you are prepared to use for your picture). There really isn't a good Swing equivalent to AWT Canvas (and don't try mixing Swing and AWT - that leads to some weird visual glitches).

    The other thing you will notice is that the drawing is incorrect. You will need to add code that clears any existing data from the display before redrawing your data.

  3. Hand crafted Applet version

    The Applet version of the same example consists of the Java code and an associated HTML page that has an <applet> tag.
    The DataObject and MyCanvas classes remain the same. The principal class is now a subclass of Applet. It constructs the GUI components - since it is an Applet (and therefor a GUI container class), it adds these components to itself. There is no code for dealing with window events (because there is no separate window and the browser will handle window events for the window in which the entire page is displayed).
    
    public class EventDemo1Applet extends Applet {
    
    	public  void init()
    	{
    		DataObject aData = new DataObject();
    		this.setLayout(new FlowLayout());
    		MyCanvas aCanvas = new MyCanvas(aData);
    		aData.LinkToCanvas(aCanvas);
    
    		this.add(aCanvas);
    		Button b;
    
    		b = new Button("North");
    		b.addActionListener(aData);
    		this.add(b);
    
    		b = new Button("East");
    		b.addActionListener(aData);
    		this.add(b);
    
    		b = new Button("South");
    		b.addActionListener(aData);
    		this.add(b);
    
    		b = new Button("West");
    		b.addActionListener(aData);
    		this.add(b);
    
    	}
    
    }
    
    

    NetBeans will create a HTML page and run the Applet in appletviewer. A HTML page that uses this applet is:

    <html>
    <head>
    <title>Applet demo</title>
    </head>
    <body>
    <h1>An applet</h1>
    <p>HTML text will appear in a browser
    but not when the page is displayed with 
    appletviewer.</p>
    <applet code="EventDemo1Applet.class"
    	width="500" height="500">
    </applet>
    </body>
    </html>
    
    
    

    Try building a "distribution" version of the Applet (in a Java archive .jar file) and running it with a browser.

    .

    In NetBeans the Applet should appear something like the following:

  4. JApplet

    Try to create a Swing Applet (JApplet) using the NetBeans GUI editor.

    This task is similar to that illustrated in the lecture notes except that you will work with Swing classes rather than AWT classes.

    You should arrange that the DataObject monitors the buttons - not the JApplet itself.

    I found the GUI editor rather picky on this, it kept adding buttons to incorrect panels if I was off by a pixel. In the end, I created a JApplet; set its layout to BorderLayout; added a JPanel to "East" area; added the JButtons to this panel, and only then changed the layout of the panel to BoxLayout. The JButtons have "post create" code that adds the data object as an action listener. The main area of the JApplet is given to a "MyJPanel" (subclass of JPanel).; it needs special "create code" to instantiate it and link it to the data object.

    The generated code (open up initComponents to see it) involved some non-standard classes. These are classes that the GUI editor uses to help it lay out GUIs that it builds. If you want to deploy a JApplet built in this way, you have to get a .jar file with the library containing these non-standard GUI classes.

  5. Mouse listening

    The following code illustrates a different kind of user interaction. The ColourPaletteTest program is a "mouse tracker" - every time the user moves the mouse over the window an event is sent to the program. In this example, the program uses the mouse coordinates to identify the colour tile containing the pointer and then displays the colour name.

    The program is using ~100 standard colours and many standard shades of grey. These colours were standardized long ago (late 1980s) for X-window displays. Developers are generally encouraged to stick to this standard colour palette.

    The main program is:

    import java.awt.*;
    import java.awt.event.*;
    
    
    class MyCanvas extends Canvas implements MouseMotionListener {
    	public void mouseDragged(MouseEvent e) { }     
    
    	public void mouseMoved(MouseEvent e) {
    		int x = e.getX();
    		int y = e.getY();
    		int width =  getSize().width / 16;
    		int height = getSize().height / 16;
    
    		x /= width;
    		y /= height;
    
    		int which = 16*y + x;
    		if(which<ColourTable.colourcount) fName.setText(ColourTable.names[which]);
    		else fName.setText("xxxxxxxxxxxxxxxxxxxxxxxxxxxx");
    		
    }
    
    	public MyCanvas(Label l) 
    	{ fName = l; setSize(512,512); addMouseMotionListener(this);}
    
    	public void paint(Graphics g) {
    		int width = getSize().width / 16;
    		int height = getSize().height / 16;
    		int x = 0; 
    		int y = 0;
    		int col = 0;
    		for(int i=0; i < ColourTable.colourcount; i++) {
    			g.setColor(ColourTable.spectrum[i]);
    			g.fillRect(x,y, width, height);
    			g.setColor(Color.black);
    			g.drawRect(x,y, width, height);
    			x+=width; 
    			col++;
    			if(col==16) { x = 0; y+= height; col = 0; }
    			}
    		}
    
    	Label fName;
    
    }
    
    class GUI extends Frame implements WindowListener {
    
    	public void windowClosing(WindowEvent e) { System.exit(0); }
    	public void windowClosed(WindowEvent e) { }
    	public void windowIconified(WindowEvent e) { }
    	public void windowOpened(WindowEvent e) { }
    	public void windowDeiconified(WindowEvent e) { }
    	public void windowActivated(WindowEvent e) { }
    	public void windowDeactivated(WindowEvent e) { }
    
    	
    	public GUI() {
    		super("Color Palette");
    		Panel p = new Panel();
    		p.add(new Label("color name "));
    		Label l = new Label("xxxxxxxxxxxxxxxxxxxxxxxxxxxx");
    		p.add(l);
    		add(p, "South");
    		fCanvas = new MyCanvas(l);
    		add(fCanvas, "Center");
    		pack();
    		addWindowListener(this);
    	}
    
    	private MyCanvas fCanvas;
    
    }
    
    public class ColourPaletteTest {
    	public static void main(String argv[]) {
    		GUI g = new GUI();
    		g.show();
    	}
    }
    
    The program structure is similar to the first version of the EventDemo program and will have to be reorganized to suit NetBeans (separate files for classes etc). The main program constructs a GUI interface object and displays it. In this case the GUI class is defined as a subclass of the generic Frame class. Its gets a BorderLayout by default. It has a label in the bottom field and a different MyCanvas object occupying most of the display area.

    Here the MyCanvas class handles the mouse events and deals with the data display. "MouseDragged" events occur when the mouse is moved with a button held down. "MouseMotion" events occur when the mouse is simply moved. Every mouse motion event returned to the listener contains mouse coordinates.

    The program relies on a static class - ColourTable:

    import java.awt.*;
    
    public class ColourTable {
    	static public Color[] spectrum = new Color[256];
    	static public String[] names = new String[256];
    	static public int colourcount;
            static {
    		int i = 0;
    		spectrum[i] = new Color(255,250,250);  names[i] = new String("snow"); i++;
    		spectrum[i] = new Color(248,248,255);  names[i] = new String("ghost white"); i++;
    		spectrum[i] = new Color(245,245,245);  names[i] = new String("white smoke"); i++;
    		spectrum[i] = new Color(220,220,220);  names[i] = new String("gainsboro"); i++;
    		...
    		spectrum[i] = new Color(144,238,144);  names[i] = new String("light green"); i++;
    		colourcount = i;
    	}
    }
    

    The standard colours do have romanticised names like "white smoke". (The class uses a static initializer block - probably the first one that you have seen. Remind yourself as to the purpose of this construct.)

    A full listing of the ColourTable class is available.

  6. Colour chooser

    Create a little Swing application that allows you to view any chosen color. (Actually, there is a Swing colour chooser class - after you have built this application, try using that class).

    The program should use a "pseudo-Canvas" class that extends JPanel and a "GUI" class that extends JFrame. The mainline program will create and show an instance of the GUI class.

    The GUI class will use a BorderLayout. The central area is used to display an instance of the "pseudo-Canvas" JPanel class. The "South" area holds a JButton; the GUI class itself acts as the listener for this JButton. The "North" area should hold a JPanel within which there are JLabel and JTextField fields for red, green, and blue values.

    When the "Show Color" button is activated, the GUI object should handle the action event. Read the strings from the textfields for the red, green, and blue values. If data are erroneous (either cannot be converted to an integer, or integer value lies outside the permitted range) use a JOptionPane to show an error message (look up JOptionPane - the documentation includes examples). If the data are valid, create a new Color object and pass it to the "pseudo-canvas" JPanel object. Get the pseudo-canvas redrawn.

    The pseudo-canvas class fills its entire area with a chosen color. Initially, this should be Color.WHITE. It will be changed if valid data are entered via the GUI class.


Demonstrations

Tabbed Pane (Swing)

The application for this demonstration is supplied as a NetBeans project (in /share/cs-pub/csci213/Lab3). The project includes source code and a data file. The GUI interface classes were composed as code, they were not built using the GUI editor (the related exercise does require use of the GUI editor).

One common style of interface uses a "tabbed pane". The same main window is used for displays of different data and controls. A set of "tabs" allows the user to select the particular display form required.
Info Pane Messages Pane Table Pane

This example has three tab-panes - "info", "messages", and "table". The program exists merely to illustrate tabbed panes (and a "table model" for a Swing JTable display) so it doesn't really do anything. The "info" pane has an input field (used for a file name) and an action button. The "messages" pane is used to display any error messages. The "table" pane shows a tabular listing of data; the table is set to allow a single row to be selected, an action button causes a selected row to be printed.

The table supposedly shows suburbs and their postcodes. The necessary data are read from a file, with format such as the fragment listed here:

Pyrmont:2009
Mortdale:2223
Kogarah:2217
Gordon:2072
St. Ives:2075
Glebe:2037
Lilyfield:2040
Manly:2095
Cremorne:2090

When the action button in the "info" pane is pressed, a file name is read from the textfield. The program then attempts to read data from the file constructing a vector of "AddressRecord" objects.

public class AddressRecord {
	// Data fields intentionally public
	public String suburb;
	public String postcode;	
	public AddressRecord(String sub, String pc){
		suburb = sub;
		postcode = pc;
	}
}

The main program simply builds the interface:

public class TabPaneDemo {
	public static void main(String[] args) {
		try {		
			JFrame aFrame = new JFrame("Tab pane demo");
			// Avoid using a window listener - built in functions in JFrame
			// suffice
			aFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE );
			
			ATabbedPaneThing displayComponent = new ATabbedPaneThing();
	
			aFrame.getContentPane().add(displayComponent);
			aFrame.setSize(new Dimension(500,500));

			aFrame.show();
	   }
	   catch(Exception e) {
		System.out.println("Failed because : " + e) ;
	   }
	}
}

The program is run by the thread created in Java's graphic runtime system.

The "control and view" parts of the program are handled by a set of four classes - ATabbedPaneThing, InfoPanel, TablePanel, and MessagesPanel. The ATabbedPaneThing is the component that creates the tabbedpane structure, and its subordinate panels. It also handles actual input of data from a file and most of the work of switching among the panels - so it is really the principal control component. The MessagesPanel exists purely to view error messages. The InfoPanel and TablePanel have action buttons and other input elements (a textfield and a table with selectable rows); these panels handle their own inputs.

public class ATabbedPaneThing extends JPanel {
	private JTabbedPane 		tabbedPane;
	private MessagesPanel		mp;
	private TablePanel		tp;
	private	InfoPanel		ip;

	public ATabbedPaneThing() {
		// This JPanel subclass holds a "tabbed pane" that occupies
		// entire panel (GridLayout(1,1) forces this).
		// The tabbed pane displays itself as a set of "tabs"
		// across top of panel; the rest of the panel is for the
		// tab components.  Each tabcomponent is itself a specialized
		// subclass of JPanel - InfoPanel, MessagesPanel or TablePanel;
		tabbedPane = new JTabbedPane();
		ip = new InfoPanel(this);
		tabbedPane.addTab("Info", ip);	// index 0 - panel for text entry
		mp = new MessagesPanel();
		tabbedPane.addTab("Messages", mp); // index 1 - messages
		tp = new TablePanel();
		tabbedPane.addTab("Tabular data", tp); // index 2 - table
		tabbedPane.setEnabledAt(2, false); // table not ready until have data
		setLayout(new GridLayout(1, 1));
		add(tabbedPane);	
		try {
			tabbedPane.setSelectedComponent(ip);	
		}
		catch(Exception e){}
	}
	
	public void switchToTable() {
		try {
			tabbedPane.setSelectedComponent(tp);	
		}
		catch(Exception e){}
	}
	
	public void switchToMessages() {
		try {
			tabbedPane.setSelectedComponent(mp);	
		}
		catch(Exception e){}
	}
	
	public MessagesPanel getMP() { 
		return mp;
	}
	
	public TablePanel getTP() {
		return tp;
	}

	public void tryLoadFile(String name)
	{
		try {
			Vector<AddressRecord> newData = new Vector<AddressRecord>();
			BufferedReader input = new BufferedReader(
				new FileReader(name));
			for(;;) {
				String line = input.readLine();
				if(line == null) break;
				String[] parts = line.split(":");
				AddressRecord arec = new AddressRecord(parts[0], parts[1]);
				newData.add(arec);
				tp.updateDataSet(newData);
				tabbedPane.setEnabledAt(2, true); // can now tab to pane, though auto switch anyway
				switchToTable();
			}
			
		}
		catch(IOException ioe) {
			tabbedPane.setEnabledAt(2, false); // table not ready until have valid data
			
			mp.setMsgString("Unable to load file "
				+ name +
				" because " +
				ioe.getMessage());
			switchToMessages();
		}
	}

}

The messages panel simply owns some text data that are displayed in a chosen font. Mutator methods exist to change the text.

public class MessagesPanel extends JPanel
{
	public MessagesPanel() {
		super();
		Font myFont = new Font("Lucida Bright Demibold", Font.PLAIN, 18);
		setLayout(new GridLayout(1,1));
		txta = new JTextArea();
		txta.setFont(myFont);
		JScrollPane sp = new JScrollPane(txta);
		add(sp);
	}
	
	public void setMsgString(String astr) { txta.setText(astr); }
	public void appendMsgString(String astr)
	{
		txta.append(astr);
	}
	
	public void clear() { txta.setText(""); }
	
	private JTextArea txta;

    
}

The Info panel has a text field and an action button. The Info panel handles the action event from its action button by reading the textfield and passing a request to the main control object to read a file. A GridBayLayout is used to center the textfield and actionbutton controls in a resizable area - the code to setup the GridBagLayout constraints makes this class fairly lengthy! (Each component added to the container panel has to have "constraints" defining its positioning; these constraints define the grid squares that it occupies - here it is a grid 3 wide, 5 high. As well as defining the grid squares, the constraints include information about how components are to be scaled if the window is resized, and how a component is actually positioned within its grid squares.) This is the kind of GUI coding that one wants to avoid - a place where the GUI builder becomes really useful.

public class InfoPanel extends JPanel implements ActionListener {
	private ATabbedPaneThing	myGUI;
	private JTextField	myTextField;
	private JButton		myDoITButton;
	public InfoPanel(ATabbedPaneThing atabpanegui) { 
		myGUI=atabpanegui;
		myTextField = new JTextField(8);
		myDoITButton = new JButton("Do it");
		GridBagLayout gbl = new GridBagLayout();
		GridBagConstraints gbc = new GridBagConstraints();
		setLayout(gbl);


		// Layout has leftmost and rightmost columns as
		// expandable blank areas, holding JPanels
		JPanel leftPanel = new JPanel();
		gbc.weightx = 100;
		gbc.weighty = 100;
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.gridheight = 5;
		gbc.gridwidth = 1;
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.BOTH;
		add(leftPanel, gbc);		
		
		JPanel topPanel = new JPanel();
		gbc.weightx = 0;
		gbc.weighty = 50;
		gbc.gridx = 1;
		gbc.gridy = 0;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.VERTICAL;
		add(topPanel, gbc);
		
		JPanel rightPanel = new JPanel();
		gbc.weightx = 100;
		gbc.weighty = 100;
		gbc.gridx = 2;
		gbc.gridy = 0;
		gbc.gridheight = 5;
		gbc.gridwidth = 1;
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.BOTH;
		add(rightPanel, gbc);

		JPanel bottomPanel = new JPanel();
		gbc.weightx = 0;
		gbc.weighty = 50;
		gbc.gridx = 1;
		gbc.gridy = 4;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.VERTICAL;
		add(bottomPanel, gbc);
		
		JLabel lbl1 = new JLabel("File name");
		gbc.weightx = 0;
		gbc.weighty = 0;
		gbc.gridx = 1;
		gbc.gridy = 1;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.insets = new Insets(2,2,2,2);
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.NONE;
		add(lbl1, gbc);

		gbc.weightx = 0;
		gbc.weighty = 0;
		gbc.gridx = 1;
		gbc.gridy = 2;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.insets = new Insets(2,2,2,2);
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.NONE;
		add(myTextField, gbc);


		gbc.weightx = 0;
		gbc.weighty = 0;
		gbc.gridx = 1;
		gbc.gridy = 3;
		gbc.gridheight = 1;
		gbc.gridwidth = 1;
		gbc.insets = new Insets(2,2,2,2);
		gbc.anchor = GridBagConstraints.CENTER;
		gbc.fill = GridBagConstraints.NONE;
		add(myDoITButton, gbc);

		myDoITButton.addActionListener(this);

	}
	
	
	public void actionPerformed(ActionEvent aev) {
		String str = myTextField.getText();
		if(str.equals("")) {
			MessagesPanel mp = myGUI.getMP();
			mp.setMsgString("You must supply a filename");
			myGUI.switchToMessages();
			return;	
		}

		myGUI.tryLoadFile(str);
		
	}

    
}

The table panel creates a JTable and sets options so that individual rows in a displayed table can be selected. This panel also contains, and handles events from an action button. Here, a simple BorderLayout is used to allocate space for a scrollpane with the table and an action button.

public class TablePanel extends JPanel implements ActionListener {
    private JButton             selectButton;
    private JScrollPane			sp;
    private AddressDataTable	data;
    private JTable				theTable;
    
    public TablePanel(){
		
        data = new AddressDataTable();
		theTable = new JTable(data);
		theTable.setColumnSelectionAllowed(false);
		theTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		selectButton = new JButton("Print selected record");
		selectButton.addActionListener(this);
		sp = new JScrollPane(theTable);
	
		setLayout(new BorderLayout());
		
		add(sp, BorderLayout.CENTER);
		
		add(selectButton, BorderLayout.SOUTH);

	}

	public void actionPerformed(ActionEvent aev) {
		int selection = theTable.getSelectedRow();
		if(selection<0) return;
		data.printRow(selection);
	}
	
	public void updateDataSet(Vector<AddressRecord> v) { data.replaceData(v); }
    
}

The JTable class used to display data is generally useful - but there is a complication. You have to define a "table model" for your data (how else would the JTable code know how many columns there were, how many rows, etc etc). The table model also has to be given the data - as a collection of objects of some type (here, it is a Vector containing AddressRecord objects).

public class AddressDataTable extends AbstractTableModel {

	private static final String[]	columnNames = { "Suburb", "Postcode" };
	
	private Vector<AddressRecord> data;

	public int getRowCount() { 
            return data.size(); 
	}
        
	public int getColumnCount() { 
            return 2; 
	}
        
	public String getColumnName(int i) { 
            return columnNames[i]; 
	}
	
	public AddressDataTable()
	{
            data = new Vector<AddressRecord>();
	}
	
	public Object getValueAt(int row, int col) 
	{
            AddressRecord ar = data.elementAt(row);
            String response = null;
            switch(col) {
                case 0: response = ar.suburb; break;
				case 1: response = ar.postcode; break;
            }
            return response;
	}
	
	public void printRow(int row) {
            AddressRecord ar = data.elementAt(row);

            System.out.println(ar.suburb + "\t" +
			ar.postcode);
	}
        
	public void replaceData(Vector<AddressRecord> newData) { 
		data = newData;
		fireTableDataChanged(); 
	}
 
}

Exercise - variant tabbed pane application

Build a variation of the tabbed pane example. This version is to keep a collection of records with name, initials, phone#. Obviously, some minor changes are needed to the table-model. The main changes are to be:

  1. A redesigned "InfoPanel" used to enter a file name.
  2. An extra panel - "DataEntryPanel" - that is used to enter additional data. This is to have input fields for name, initials and phone number and an action button.
    When the action button is activated, the program updates its collection of records. If there is an existing entry for the given name and initials, the existing phone number is changed; the program then displays an acknowledgement in the messages panel. If the combination of name and initials is new, an additional record is added to the collection. Whenever the collection is changed, the file on disk is rewritten to contain the updated information.
  3. It will probably be useful to define a more elaborate data model than a simple Vector of records; you will want a class that can load data from a file and store data back to that file and which can add/update records.

The revised and new panels for the tabbed pane are to be built using the NetBeans GUI editor. You create JPanel forms in NetBeans, set a layout, and add components.

You have two choices - the default layout system that NetBeans supports or use of GridBagLayout. The default layout system depends on non-standard classes. An application built using these classes has to have an additional .jar file with these classes; this .jar file must be included in any version of the application that is deployed outside of the NetBeans IDE. Unfortunately, if you aren't very careful with your layout, even a GridBagLayout based approach draws in some of these non-standard classes. The default layout system is definitely easier to use; you can try working with GridBagLayout.

This image shows the revised InfoPanel being built with NetBeans and the default layout mechanisms:

Under the NetBeans Help menu, you will find links to tutorials. One of these illustrates use of the GUI builder with the default layout.


Threads in GUIs

In the examples shown so far, there is only one thread of control. This is the "AWT-thread" (it gets created when AWT or Swing code starts up the graphical interface). Most of the time, this thread is blocked waiting for inputs. It gets reawakened after a user action involving the mouse or keyboard. Such actions are picked up by the operating system which creates system specific events that are passed to the C-code part of the Java runtime. The C-code converts the OS events into things more standardized and suitable for Java; these Java events then work their way up through the Java runtime and java.awt library code. Eventually the events get to registered listeners. Things like textfields listen for keystrokes - and automatically fill in text. It is usually only action button events that get passed on to application specific code (sometimes mouse motion events get to application code).

The application code handles the event by performing some processing that changes a data model and then causing a view of the data to be refreshed.

This single thread style is fine for all applications that are basically data entry and display programs.

Sometimes, one needs more. Typically, more elaborate multi-threaded GUIs are needed for programs that involve some form of simulation - there are data whose values change with time, these data are to be displayed as they change, and there are controls that change the way the data evolve. In such situations, one needs one thread to look after the evolution of the data over time, and a second thread that waits for control actions.

An example program illustrating threads is developed here in two stages. First, there is a version that works with one thread - a simulated system that will change with time, a start button that starts it running, a run loop that runs for a fixed time and then terminates.

The program for this demonstration supposedly illustrates a simulation of a water tank gradually filling with water.
Simulation

Version 1 - single AWT thread

The application consists of a little driver program, an instance of a GUI display class that creates a window with a drawing area and a control button, a specialized panel for application-defined graphical output and a data object.

The GUI class uses a border layout. It handles events from the action button in its "south" area. The action button starts the simulation (the button is disabled until the simulation finishes).

The specialized JPanel class simple acts as a drawing area linked to the data object.

The data object owns the data for the simulation and runs the simulation. The simulation starts with the water tank empty. The program then loops - increasing the water level, updating the display, and "sleeping" a little (to slow the simulation so that it can be observed by a user).

There is one decidely odd aspect in the code - direct calls to paint functions. The normal Swing/AWT style is to change data, then request repainting of views to be scheduled by the AWT graphics engine. These repaint calls get translated into events that are posted and eventually get picked up by the AWT-thread when it next gets to its idle-loop. That does not work here. The AWT-thread is running the simulation; it will not get back to the point of picking up normal events until the simulation finishes. So one cannot rely on a "repaint" request and an eventual "paint" action. Painting (redrawing) must be forced when needed.

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

class Data implements Runnable
{
	private static final int left = 100;
	private static final int top = 100;
	private static final int width = 200;
	private static final int height = 150;
	private static final int inset = 10;
	
	private static final Color tankColor = new Color(5,5,5);
	private static final Color fillColor = new Color( 25, 25,112);
	private static final Color emptyColor = new Color(240,248,255);
	private static final int xstr = 20;
	private static final int ystr = 40;
	
	private JPanel	displayPanel;
	private int currentDepth;
	private String state;
	private int increment;
	private int sleepTime;
	
	public Data()
	{
		restart();
	}
	
	public void restart() { currentDepth = 0;  state = "Restarting"; increment = 1; sleepTime = 250; }
	
	public void setLinkToPanel(JPanel aPanel) { displayPanel = aPanel; }
	public void paint(Graphics g)
	{
		Color c = g.getColor();
		g.setColor(Color.WHITE); 
		g.fillRect(0,0,displayPanel.getWidth(),displayPanel.getHeight()); 
		g.setColor(Color.BLACK);
		g.drawString(state, xstr,ystr);

		
		g.setColor(tankColor);
		g.fillRect(left, top, width, height);
		g.setColor(emptyColor);
		g.fillRect(left+inset, top, width-2*inset, height-inset);
		g.setColor(fillColor);
		g.fillRect(left+inset, top+height-inset-currentDepth, width-2*inset, currentDepth);
		
		g.setColor(c);
	}
	
	public void forcepaint()
	{
		Graphics g = displayPanel.getGraphics();
		paint(g);
	}
	
	
	public void run()
	{
		state = "Running";
		
		while(currentDepth<(height-inset)) {
			currentDepth+=increment;
			forcepaint();
			try { Thread.sleep(sleepTime); } catch(InterruptedException ie) {}
		}
		
		state = "Finished";
		forcepaint();
	}
	
}


class MyPanel extends JPanel {
	Data fData;
	public MyPanel(Data aData) {  
		setPreferredSize(new Dimension(400,400));
		fData = aData;
	}

	public void paint(Graphics g) { 
		fData.paint(g);
	}


}


class GUI  extends JFrame implements ActionListener {
	
	private MyPanel fPanel;
	private JButton fStartButton;
	private Data fData;
	
	public void actionPerformed(ActionEvent e)
	{
		// Run the "simulation
		// First, disable the button
		// Tell the data object to restart
		// brief pause
		// tell the data object to run
		//     it runs using the current main AWT thread
		// re-enable the button
		fStartButton.setEnabled(false);
		fStartButton.setText("Wait!");
		fStartButton.paint(fStartButton.getGraphics()); // force painting now
		fData.restart();
		fData.forcepaint();
		try { Thread.sleep(500); } catch (InterruptedException ie) {}
		fData.run(); // loop inside run, using the only thread (the AWT thread)
		
		fStartButton.setEnabled(true);
		fStartButton.setText("Start again");
	}

	public GUI()
	{
		fData = new Data();
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLayout(new BorderLayout());
		fPanel = new MyPanel(fData);
		this.getContentPane().add(fPanel, BorderLayout.CENTER);
		fData.setLinkToPanel(fPanel);
		
		fStartButton = new JButton("Start again");
		fStartButton.addActionListener(this);
		this.getContentPane().add(fStartButton, BorderLayout.SOUTH);

		this.pack();
	}
	
}
public class V1 {
	public static void main(String[] args)
	{
		GUI aGUI = new GUI();
		aGUI.setSize(500,500);
		aGUI.show();
	}
	
}

(Playing around with the repaint/paint mechanism like this can cause anomalies. It works in simple cases but you may have troubles with things like windows being temporarily obscured and then redisplayed - things may not get drawn correctly.)

A threaded version - with thread "suspend" and "resume"

This has a slightly more complex simulation. The tank is cyclically filled, and then emptied; this is handled by a dedicated thread. The control button is now used to pause/resume this simulation thread. Drawing is handled by the AWT thread through the normal repaint mechanism.

This first variant uses Thread.suspend and Thread.resume operations for simplicity. Use of these operations will result in warning messages from the Java compiler.

In a trivial case like this, there are no problems associated with stopping and restarting a worker thread. However, in real applications such operations on threads are error prone. A thread may "lock" some shared data while performing an update; if another thread invokes a "suspend" operation on the worker thread, the data remain locked. This can easily lead to deadlock. Further, if a thread is forced to stop unexpectedly, it may leave data in unsafe states. The next example shows the basics of an approach for safer thread control.

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

class Data2 implements Runnable
{
	private static final int left = 100;
	private static final int top = 100;
	private static final int width = 200;
	private static final int height = 150;
	private static final int inset = 10;
	
	private static final Color tankColor = new Color(5,5,5);
	private static final Color fillColor = new Color( 25, 25,112);
	private static final Color emptyColor = new Color(240,248,255);
	private static final int xstr = 20;
	private static final int ystr = 40;
	
	private JPanel	displayPanel;
	private int currentDepth;
	private String state;
	private int increment;
	private int sleepTime;
	private int limit;
	
	public Data2()
	{
		restart();
	}
	
	public void restart() { 
		currentDepth = 0;  
		state = "Restarting"; 
		increment = 1; 
		sleepTime = 250; 
		limit = height - inset;
	}
	
	public void setLinkToPanel(JPanel aPanel) { displayPanel = aPanel; }
	
	public void paint(Graphics g)
	{
		Color c = g.getColor();
		g.setColor(Color.WHITE); 
		g.fillRect(0,0,displayPanel.getWidth(),displayPanel.getHeight()); 
		g.setColor(Color.BLACK);
		g.drawString(state, xstr,ystr);

		
		g.setColor(tankColor);
		g.fillRect(left, top, width, height);
		g.setColor(emptyColor);
		g.fillRect(left+inset, top, width-2*inset, height-inset);
		g.setColor(fillColor);
		g.fillRect(left+inset, top+height-inset-currentDepth, width-2*inset, currentDepth);
		
		g.setColor(c);
	}
	

	
	public void run()
	{
		state = "Waiting";
		
		Thread.currentThread().suspend(); // ready to run, wait for button
		
		state = "Filling";
		for(;;) {
			if(increment>0) {
				// Tank is filling
				if(currentDepth<limit) {
					currentDepth += increment;
					if(currentDepth>limit) currentDepth = limit;
				}
				else {
					// Tank is full, switch to emptying
					limit = 0;
					increment = - increment;
					state = "Emptying";
				}
			}
			else {
				// Tank is Emptying
				if(currentDepth>0) {
					currentDepth += increment;  // -ve increment makes it go down!
					if(currentDepth<0) currentDepth = 0;
				}
				else {
					// Tank is empty, switch to emptying
					limit = height - inset;
					increment = - increment;
					state = "Filling";
				}
				
			}
			displayPanel.repaint();
			try { Thread.sleep(sleepTime); } catch(InterruptedException ie) {}
		}
		

	}
	
}


class MyPanel extends JPanel {
	Data2 fData;
	public MyPanel(Data2 aData) {  
		setPreferredSize(new Dimension(400,400));
		fData = aData;
	}

	public void paint(Graphics g) { 
		fData.paint(g);
	}


}


class GUI  extends JFrame implements ActionListener {
	private static final String suspend = "Suspend animation";
	private static final String resume = "Resume animation";
	private boolean running;
	private MyPanel fPanel;
	private JButton fStartButton;
	private Data2 fData;
	private Thread fWorkThread;
	
	public void actionPerformed(ActionEvent e)
	{
		if(running) {
			running = false;
			fWorkThread.suspend();
			fStartButton.setText(resume);
		}
		else {
			running = true;
			fWorkThread.resume();
			fStartButton.setText(suspend);
		}
	}

	public GUI()
	{
		fData = new Data2();
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLayout(new BorderLayout());
		fPanel = new MyPanel(fData);
		this.getContentPane().add(fPanel, BorderLayout.CENTER);
		fData.setLinkToPanel(fPanel);
		
		fStartButton = new JButton(resume);
		fStartButton.addActionListener(this);
		this.getContentPane().add(fStartButton, BorderLayout.SOUTH);

		this.pack();
		running = false;
		fWorkThread = new Thread(fData);
		fWorkThread.start();
	}
	
}
public class V2 {
	public static void main(String[] args)
	{
		GUI aGUI = new GUI();
		aGUI.setSize(500,500);
		aGUI.show();
	}
	
}

A threaded version - with a coordination object

The safer approach is to have a worker thread check at appropriate times whether it should continue or pause. Such checks are done using a coordination object - this will contain a boolean ("okToContinue") variable. A control thread (the one that handles user events) will be able to change the setting of this variable. The worker thread will check the boolean, continuing if it is set to true. If the boolean is false, the worker thread pauses until it is notified that the value of the control variable may have changed. (The lecture notes will contain a similar example relating to a "bounded buffer" structure.)

The revised code is shown here. The changed parts are highlighted in bold.

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

class RunControl {
	public boolean runOK;
}

class Data3 implements Runnable
{
	private static final int left = 100;
	private static final int top = 100;
	private static final int width = 200;
	private static final int height = 150;
	private static final int inset = 10;
	
	private static final Color tankColor = new Color(5,5,5);
	private static final Color fillColor = new Color( 25, 25,112);
	private static final Color emptyColor = new Color(240,248,255);
	private static final int xstr = 20;
	private static final int ystr = 40;
	
	private JPanel	displayPanel;
	private int currentDepth;
	private String state;
	private int increment;
	private int sleepTime;
	private int limit;
	private RunControl check;
	
	public Data3(RunControl rc)
	{
		check = rc;
		restart();
	}
	
	public void restart() { 
		currentDepth = 0;  
		state = "Restarting"; 
		increment = 1; 
		sleepTime = 250; 
		limit = height - inset;
	}
	
	public void setLinkToPanel(JPanel aPanel) { displayPanel = aPanel; }
	
	public void paint(Graphics g)
	{
		Color c = g.getColor();
		g.setColor(Color.WHITE); 
		g.fillRect(0,0,displayPanel.getWidth(),displayPanel.getHeight()); 
		g.setColor(Color.BLACK);
		g.drawString(state, xstr,ystr);

		
		g.setColor(tankColor);
		g.fillRect(left, top, width, height);
		g.setColor(emptyColor);
		g.fillRect(left+inset, top, width-2*inset, height-inset);
		g.setColor(fillColor);
		g.fillRect(left+inset, top+height-inset-currentDepth, width-2*inset, currentDepth);
		
		g.setColor(c);
	}
	

	
	public void run()
	{
		state = "Waiting";
		
		
		state = "Filling";
		for(;;) {

Claim (and temporarily at least Lock) the coordination object.
If it isn't ok to run, then wait for the object to be changed.  While
this thread is waiting, release the lock on the coordination object.
This thread will resume after a "notify" is performed by some other thread.
This thread should then check whether it is ok to continue.

		
			synchronized(check) {
				while(!check.runOK) { 
					try { check.wait(); } catch(InterruptedException ie) { }
				}
			}
		
			if(increment>0) {
				// Tank is filling
				if(currentDepth<limit) {
					currentDepth += increment;
					if(currentDepth>limit) currentDepth = limit;
				}
				else {
					// Tank is full, switch to emptying
					limit = 0;
					increment = - increment;
					state = "Emptying";
				}
			}
			else {
				// Tank is Emptying
				if(currentDepth>0) {
					currentDepth += increment;  // -ve increment makes it go down!
					if(currentDepth<0) currentDepth = 0;
				}
				else {
					// Tank is empty, switch to emptying
					limit = height - inset;
					increment = - increment;
					state = "Filling";
				}
				
			}
			displayPanel.repaint();
			try { Thread.sleep(sleepTime); } catch(InterruptedException ie) {}
		}
		

	}
	
}


class MyPanel extends JPanel {
	Data3 fData;
	public MyPanel(Data3 aData) {  
		setPreferredSize(new Dimension(400,400));
		fData = aData;
	}

	public void paint(Graphics g) { 
		fData.paint(g);
	}


}


class GUI  extends JFrame implements ActionListener {
	private static final String suspend = "Suspend animation";
	private static final String resume = "Resume animation";
	private boolean running;
	private MyPanel fPanel;
	private JButton fStartButton;
	private Data3 fData;
	private Thread fWorkThread;
	private RunControl fCheck;
	
	public void actionPerformed(ActionEvent e)
	{

You might want to lock the coordination variable while changing 
a value in its data field.  But in this case the value
is just a boolean so there won't be any issues of partial
updates etc.

		if(running) {
			running = false;
			fCheck.runOK = false;
			fStartButton.setText(resume);
		}
		else {
			running = true;
			fCheck.runOK = true;
			fStartButton.setText(suspend);

Have changed the coordination variable, implicitly releasing any thread
blocked waiting for it to be ok to resume running.
Must also notify any such threads that it is now
appropriate for them to re-examine their state.
 			
			synchronized(fCheck) { fCheck.notify(); }
		}
	}

	public GUI()
	{
		fCheck = new RunControl();
		fData = new Data3(fCheck);
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		this.setLayout(new BorderLayout());
		fPanel = new MyPanel(fData);
		this.getContentPane().add(fPanel, BorderLayout.CENTER);
		fData.setLinkToPanel(fPanel);
		
		fStartButton = new JButton(resume);
		fStartButton.addActionListener(this);
		this.getContentPane().add(fStartButton, BorderLayout.SOUTH);

		this.pack();
		running = false;
		fWorkThread = new Thread(fData);
		fWorkThread.start();
	}
	
}
public class V3 {
	public static void main(String[] args)
	{
		GUI aGUI = new GUI();
		aGUI.setSize(500,500);
		aGUI.show();
	}
	
}