How to use the CLOS Heater/Thermostat Simulation w/Java GUI
CLOS Heater / Thermostat Simulation with Java GUI
============================================================================
1. General Structure
There are five sets of components which comprise this application.
a) The CLOS code in ThermSimulator.l which defines the CLOS objects
to support the simulation and the methods used to invoke behavior on
those objects. It is slightly modified from the original. This is
where execution of the application begins. It loads the code for the
secondary pipe process for communicating with the GUI from Pipe.l,
creates the objects for a simple one-room simulation, then sends over
the pipe the request to execute the Java GUI side of the application.
b) The main function in ThermSimulator.java is what the LISP code
starts up. It creates the GUI window and the "shadow" model that
maintains a copy of the CLOS simulation state and handles communication
therewith. The main function then enters an infinite loop that reads
strings from the pipe and determines what action in the GUI model to
take.
c) The shadow model in ThermSimulatorModel.java is a single class
that maintains an up to date copy of the important values in the
CLOS object simulation. It provides the methods that the main
function calls based on codes it reads from the pipe to change the
GUI from events in the CLOS simulation. There are also methods for
the GUI to use to update the model from user input. When these occur
the model sends s-expressions to CLOS which change the model there.
d) The GUI components are in the remaining .java files.
ThermSimulatorGUI.java is the main frame and there are several components
that comprise it. These components are typically panels with further
substructure. Two of these components need to communicate the events
inside them to the model and so are passed the model object when they
are created.
f) Finally there is the Pipe.l code that manages the UNIX pipe-based
communication. Using the MP (multiple process) package of Common
LISP, this package maintains a second process that perpetually listens
for s-expressions being sent to CLOS code and executes them. It also
provides a function tk-put for sending strings over the pipe to the
Java GUI code.
============================================================================
2. How To Run This Package
1. You need to be logged into a cs linux box running a Swing-compatible
Java and the appropriate Allegro LISP. See the "Accessing Java on
the CS Machines" link on the Languages link from the course web page
for a list of Linux Debian boxes with the correct installations. Of
those, some may not be accessible. The following have had this
ThermSimulator application tested successfully and are certain to work:
tigger, umwert, sunset, mercy, gig-X (where X is 1-9), mashadar,
lanfear, dragonfly, spider, portico, crash.
2. If you are not at the machine you are logging into,
the ssh negotiation is frequently insufficient for Java to be able
to open windows on your remote terminal. To be safe then, you
should determine the name of the machine from which you are ssh-ing
and execute the following command after you have logged
in:
setenv DISPLAY <the name of your machine>:0.0
For example if you are logging into, say, sunset from lennon,
then after you ssh to sunset you would execute
setenv DISPLAY lennon.cs.unm.edu:0.0
Always use the complete name (i.e. with .cs.unm.edu after it).
3. Make a local directory of your own where you will put the files
for the application, say, thermsim, and then copy the files there:
cd thermsim
cp ~jalewis/451/sim/*.java .
cp ~jalewis/451/sim/*.class .
cp ~jalewis/451/sim/*.l .
cp ~jalewis/451/sim/*.README .
You could just load the *.java files and compile them yourself, or
since the *.class files are compiled for the sparky JDK virtual
machine you could just load the *.class files and run them. You
always need the *.l files. Since you may want to use the code as
a guide for your own development, it seems reasonable to bring over
the *.java and this file also.
4. Start Common LISP by issuing to the UNIX prompt as usual:
lisp
5. Once Allegro CL is running, load the ThermSimulator.l file which
will load the Pipe.l file and start the GUI. The GUI will tell
the CLOS simulation to initialize, and once it is finished drawing
the screen, you can begin to interact with it.
(load "ThermSimulator.l")
6. Note: The interpreter will probably spit out a screenful of message
having to do with unknown fonts. You should simply ignore these.
============================================================================
3. Interacting With the GUI
Once the GUI is running you will see essentially six main panels. At
the top is simply a label "Heater Thermostat Simulation -- CLOS Engine
/ Java GUI". In sequence across the center are three panels: a set of
radio buttons showing the heater state is first, then a graphics panel
with an up to date "temperature bar" for the temperature in the room,
and finally a scrollable text pane that contains the "transcript" for
the simulation. At the bottom are the control panels. The first is
the temperature control panel. Its temperature value is also kept up
to date with the CLOS simulation (through the up to date Java shadow
model). The user may also "play God" and change the temperature in the
room with the increase or decrease buttons and then submitting the
change with the change button. This triggers an appropriate change
in the heater state and the transcript and temperature readouts show
the change in room temperature over time (cooling back down with the
heater off if the temperature is raised and warming up with the heater
on if the termperature is lowered). The lower control panel is for the
thermostat setting. The user can increase or decrease this value and
then submit the change with the change button. The GUI then shows the
change in temperature over time to meet the setting. Note that the
radio buttons for heater state are not changeable by the user, since
only the simulation has that capability.
============================================================================
4. Discussion of Implementation
We have created this application in an attempt to show you some of the
basic approaches to creating GUIs with Java, as well as to demonstrate the
use of the pipe-based communication between CLOS and Java. Without this
pedagogical inclination one might make different choices in the
implementation. Furthermore, there are a number of variations in design
that are appropriate depending on how heavily one relies on the
communication and on the details of the application itself. We will
discuss these options as we go through the code.
First of all we note that the invocation of methods between the two
sides of the application, the interface (Java GUI with its shadow model)
and the model (CLOS code), is not symmetric. The GUI can invoke a change
in temperature in the CLOS model when the user clicks on the change
button in the temperature control panel. The model can also invoke a
change in the temperature from the heat-rooms method, which the GUI
cannot do. These temperature changes need to be communicated back to
the GUI to update the temperature displays there. If the same change-temp
method were used for both situations a loop would be created when the GUI
sends a change that would normally be echoed back (in case it came from
the model). To remedy this, there are separate methods for each "direction"
of invocation of a particular action. This situation arises frequently
between a GUI and its model (even if they are not implemented in different
languages). The amount of information that is kept in the shadow model
impacts how this asymmetry arises. At one extreme we might keep no
information at all in the GUI (in the shadow model) and simply send a
request for information across the pipe every time we need to redraw even
the smallest bit of the screen. At the other extreme, as in our case,
we keep as much information as possible copied in the shadow model.
This gives rise to these different methods for the flow of information
bidirectionally between the shadow model and the real model and between
the shadow model and the GUI. Note however that the shadow model is
the only point of communication between the Java side and the CLOS side.
The loop in the main function reads from the pipe, but always invokes
methods on the shadow model. And the shadow model is the only thing that
sends s-expressions across the pipe to the CLOS side. This is a wise and
typical design.
With those ideas in mind, we are in position to understand the details
of the code. The code in ThermSimulator.java is fairly straightforward,
as we have already mentioned. It creates a BufferedReader on standard
input, instantiates the main window of the GUI (letting it know about the
shadow model in the constructor), lets the model know about the GUI, shows
the main window, initializes its values, and then begins the infinite
loop listening to the pipe for commands from the CLOS side. The command
language understood by the GUI consists of "newTemp" and "setHeaterState"
each with appropriate parameters. Reading one of these from the pipe
invokes a method on the shadow model to update it. The shadow model will
then trigger GUI updates as necessary. Deciding on how much information
is in the shadow model and on the command language are the two most
important parts of creating the link between the two implementations.
The ThermSimulatorModel contains values for the thermostat setting,
the temperature, and the heater state, as well as a the GUI window itself.
The initialize method sets the startup state of the shadow model and
calls the initialize method of the CLOS code to set its values to the
same. This is done by emitting to the pipe (standard input which has
been piped to the LISP process) a string that is an s-expression to
execute the desired behavior in the CLOS code. There is a method called
by the GUI to set a new thermostat setting in the shadow model and then
to send that change in setting over to the CLOS code. There is a similar
method called by the GUI to se a new temperature value in the shadow
model and then to send that change in setting over to the CLOS code.
There is also a method to change the temperature setting in the shadow
model because the CLOS model has undergone a temperature change; this
method must also invoke methods in the GUI to update its display.
The final "set" method allows the CLOS model to update the shadow model
about the state of the heater, and this method also triggers an update
to the GUI display. There are also "get" methods for each of these
values which the GUI uses when told about a change to request the
actual value to be displayed from the shadow model.
The details of the methods provided for communication back and
forth between the shadow model and the GUI have been discussed in
the context of ThermSimulatorModel.java, the shadow model. There
are also comments before each of these methods that describe their
purpose, use, and particular variations. We will not discuss them
further here.
The details of interest in ThermSimulatorGUI.java for those learning
to use Java as a GUI development tool have to do with creating
interface components and assigning them to screen real estate
appropriately. There are some guidelines to follow, but as with
most GUI development, getting a good interface requires some
experimentation. This is also the best way to acquire the skill of
using the LayoutManagers, which are probably the most difficult
part of Java's GUI capabilities to use. In ThermSimulatorGUI.java
we first note that it implements the ActionListener interface.
This means it has a method called actionPerformed that allows it
to respond to GUI events inside it that have registered the window
as the appropriate handler of those events. We will be discussing
this aspect of Java in great detail in the last few weeks of the
semester. The sizing stuff is fairly self-explanatory: an instance
of the built-in class Toolkit gets access to operating system
information, such as the size of the screent (in terms of an object
of type Dimension). From there we calculate how big the frame will
be. Despite the facilities of LayoutManagers and automatic re-
adjustment of a resized window, whenever you have a fairly complicated
interface it is a good idea to restrict the user from resizing it.
The next lines make it possible for the window to be closed (otherwise
the only way to kill it is by kill the process that opened it). Next
the menubar is being constructed, bottom-up. The action command for
a component is the string that will identify which component has been
affected by the user when the action performed method responds. Adding
"this" window as an action listener to a menu item means that when the
user clicks on that menu item "this" window's action performed method
will be the one to respond.
Whenever you want to add things to a frame you actually add it to the
panel for that frame known as its contentPane, which the getContentPane
method returns. You can then set the LayoutManager for that pane
which controls how you place things on the panel. We will talk more
about LayoutManagers later. For now you only need to know about two:
the FlowLayout manager which places things in horizontal succession in
the order you add them, wrapping around when needed; and the BorderLayout
manager which places things as specified in the five regions of "Center"
and each of the directions "North", "South", etc. The BorderLayout
manager will resize added panels to fill the regions of the screen.
Sometimes this results in poor arrangement or even certain components
covering others. Typically we create subpanels to group components
together using either manager and then place those components on the
top panel using the BorderLayout manager, given that our subpanels
are each roughly big enough to occupy their regions evenly.
Back in ThermSimulatorGUI.java we create a panel to go in the North
position of the main frame. We add to that panel a label, then we add
the panel to the contentPane of the frame at the North position. Next
we create the graphical TemperatureGaugePanel, which uses the size of
the main frame to calculate its own size and the sizes of the boxes it
draws. The HeaterStatePanel and the TranscriptPanel are simple components
that are created next. Then we create a panel just for those three
components, which we place in the West, Center, and East, respectively.
The North and South regions of this subpanel are not used, so the components
grow to fill them, creating a row effect. We could use the FlowLayout
manager for this, but it sizes things slightly differently and did not
make good use of the frame real estate. This is an example of where you
have to experiment to get good GUI arrangement. Once our subpanel with
these three components is finished we add it to the main contentPane in
the Center. Finally we create the temperature and thermostat control
panels, which get passed the shadow model so they can communicate to it
whatever changes the users invokes in them. Then we make a panel for
those adding them to the North and South, respectively, then we add
this whole panel to the South of the main contentPane. One of the best
ways to create good GUIs is with the frequent use of panels to collect
related components together.
Most of the rest of the code is simply putting GUI components together
and providing methods for the interaction with the rest of the program.
TemperatureGaugePanel.java has local variables for pixel locations on
the screen for drawing a temperature bar and uses the graphics object in
paintComponent to draw this. It provides one method that the GUI calls
to tell it that its temperature must be changed and the panel redrawn.
TemperatureControlPanel.java can be updated by the rest of the program
(ultimately from the CLOS model, through the shadow model) and it also
can be changed by the user. So it is its own action listener, responding
to the clicks on increase/decrease buttons by changing its local value
and its display, and responding to a click on the change button by
informing the shadow model of the new temperature value. The shadow model,
as we have seen, is responsible for sending this information to the
CLOS model. There is a setTemperature method called by the GUI whenever
it has been informed by the shadow model that the CLOS model has changed
its temperature. Each of these that changes the temperature also calls
repaint so that the display will be redrawn with the new value. The details
of the ThermostatControlPanel are completely analagous, except that the
setThermsetting method is only called once at initialization time, since
the model never changes that--only the user does with clicks whose results
are displayed immediately. Once again the amount of work in keeping the
redundant information between the components of the system up to date is a
trade-off with the availability of the information locally to the GUI for
quick updates of the display, which may be needed more frequently than the
model itself changes if the application is being run in a typical
multi-window environment.
The HeaterStatePanel is a simple use of radio buttons. It is again its
own action listener. One could not provide any action response to clicking
on these buttons, since the user is not supposed to have that capability.
However, without a listener, the radio button components do still redraw
themselves to show that a button has been pressed. So the user would be
able to change the display, taking it out of sync with the underlying
model. Here we implement a listener that immediately undoes any button
pressing the user might do. As usual we repaint whenever we make a change
that we want to see in a visual component. The TranscriptPanel creates a
text area of a certain size then places that text area into a scroll pane.
This component is combined with a label using the BorderLayout. A method
append has been provided which takes a string from the GUI which calls it
and appends it to the text area, calling repaint to show the change.
Let's finally take a look at the CLOS code in ThermSimulator.l. There are
only a few differences from the original code we looked at in class. First
it loads the pipe code for communicating with the GUI. Another difference
is the change-temp-from-gui method that updates the model and begins the
processing of the change without informing the GUI of the first temperature
change, since it came from there. Then there is the regular change-temp
method called by the model itself each time it updates its temperature and
echoes that to the GUI. Where before there were lines written to the screen
as part of the "transcript", there are now calls to tk-put to send information
to the GUI. The GUI handles generating actual text for its transcript based
on what commands are sent. The initialize function which gets called by the
GUI at startup creates the instances that will be used. The last line is
creates the pipe for communication with the GUI and starts up the Java GUI
application subordinate to this LISP process with its stdin and stdout connected
to the pipe.