GUI with Matlab: A Tutorial
1. Introduction
-
Matlab GUI vs others
-
Similar to RAD such as C++ builder and VB
-
Most GUI work across platforms
-
Can perform most functions as traditional GUI through tricks
-
Can link platform dependent code using MEX programs
-
Problems with GUI
-
Not as flexible
-
Cross platform appearance may not be the same
-
Often must use tricks and unfriendly techniques
-
MEX code GUI eliminates cross platform operation
2. GUI elements
-
GUI elements are treated as objects. They encapsulate data and method.
-
Menu: uimenu
-
Controls: pushbutton, checkbox, radio buttons, text (static), slider (scroll
bar), edit (interactive), listbox, popupmenu, frame : uicontrol
-
Graphics: plots, text, axis, image, surface, shading : axes, image,
line, patch, surface, text, light
-
Others: almost all visible elements on screen except command window : e.g.,
msgbox
3. Finding out more about GUI elements
-
Use "helpdesk"
-
launch default web browser, usually Netscape
-
enter "helpdesk" from matlab prompt. Click on handle graphics objects
-
Experiment with properties
-
example: create a plot and enter "set(gcf),pause,set(gca)"
-
"get" only shows the properties, "set" can set and show
-
Try "guide", "cbedit", "toolpal", "aligntool"; good for simple GUI generation
-
"guide" is the mother of all tools
4. Simplest GUI: message box
-
Simple syntax: msgbox(message,title,'modal' or 'nonmodal')
-
Used to pop up a dialog box with Ok button on it
-
Message and title are strings; title is optional, default is 'nonmodal'
-
For example,
msgbox('I am afraid I CAN do that, Mik.')
-
Other types of message boxes are
-
errordlg(message,title,'modal' or 'nonmodal'): msgbox with error icon
-
warndlg(message,title,'modal' or 'nonmodal'): msgbox with warning icon
5. Simpler GUI: input box
-
Used to ask the user to input information
-
Simple syntax: answer=inputdlg(Prompt,Title,LineNo,DefaultAnswer)
-
Uses cells as parameters for prompt and default answer; must convert to
useful data before and after
-
For example,
prompt={'name', 'function to call', 'number'}
defans={'mik', 'hamming', '123'}
fields = {'name','func', 'num'}
info = inputdlg(prompt, 'give it to me!', 1, defans)
if ~isempty(info) %see if user hit cancel
info = cell2struct(info,fields)
myname = info.name
myfunc = info.func
mynum = str2num(info.num) %convert string to number
mycall = [myfunc '(' num2str(mynum) ')']
eval(mycall) %evaluate expression
msgbox([myname ', I hope ' mycall ' was what you wanted.'], 'Cool!')
end
%you can and should put ; on all line endings in practical program
%also, str2num and num2str are not necessary, just there for demo purpose

6. Simple GUI generation
-
Close all open figures using "close all" command
-
Start guide and place a pushbutton on "Figure No.1"
-
Double click on pushbutton; place check mark on "show property list"
-
Click on "Callback" and put " 'msgbox(''hello mik'')' "
-
Click on "String" and put " 'greetings' "
-
Select "Options->Activate Figure" from "Figure No. 1" window; save and
name it "test.m"
-
Click on "greetings" button and see the message box
-
Get back to editing more by double clicking on " Active #1 " in "Guide-Controlled
Figure List"
-
Summary of simple GUI generation
-
Created button: create object
-
Specify method: specify what the button will do in callback
-
Specify data: specify the label for the button
7. Analysis of simple GUI code 1
-
Starts with file name as function name
function test()
% This is the machine-generated representation of a MATLAB object
% and its children. Note that handle values may change when these
% objects are re-created. This may cause problems with some callbacks.
% The command syntax may be supported in the future, but is currently
% incomplete and subject to change.
%
% To re-open this system, just type the name of the m-file at the MATLAB
% prompt. The M-file and its associtated MAT-file must be on your path.
-
Comment lines tell you it may cause problems; also note the spelling error
-
Load variable values stored in file; not very good since its difficult
to change
load test
8. Analysis of simple GUI code 2
-
Create figure (figure is a window) using specific parameters
a = figure('Color',[0.8 0.8 0.8], ...
'Colormap',mat0, ...
'PointerShapeCData',mat1, ...
'Position',[564 166 672 504], ...
'Tag','Fig1');
-
a is the handle of the window; all objects have handles associated with
them
-
Handle is nothing more than a number referencing the object
-
gcf is the handle of current figure and gca is the handle of current axis
-
other "current" handles are gcbo (get current callback object), gco (get
current object), gcbf (get current callback figure), but not often used
in practice
-
Color is RGB values between 0 and 1
-
Colormap is matrix of defined colors for the window
-
Position is the position of the window in pixels, [left, bottom, width,
height]
-
default units is pixels, but can be changed to other units using "units"
property
-
Tag is user defined name for the object which can be used to located the
object easily using "findobj" function without knowing the handle
9. Analysis of simple GUI code 3
-
Create the pushbutton object using specific parameters
b = uicontrol('Parent',a, ...
'Units','points', ...
'Callback','msgbox(''hello mik'')', ...
'Position',[124.759 62.6897 83.1724 30.4138], ...
'String','greetings', ...
'Tag','Pushbutton1');
-
b is the handle of pushbutton
-
Parent points to a, the figure; this means the object belongs to this figure
and should be placed in it
-
Callback is the method that should be executed
-
Units is points which means the locations are specified as 1/72 inch
-
Position is the location of the control in 1/72 inch. Format is [left,
bottom, width, height]
-
Tag works the same for all objects and this works the same as for figure
(for use with findobj function)
10. Analysis of simple GUI code 4
-
From Matlab command prompt, execute test.m
-
Enter " get(gcf) "; note the many default settings already in place
-
Enter " get(get(gcf, 'children')) "; note the many settings for the child
-
It is not necessary to set all parameters when creating a control
-
For our example, only the callback and name for the button was necessary
and we didn't care about anything else (maybe position)
-
The new code could be
function test1()
uicontrol('style', 'pushbutton', ...
'parent', figure, ...
'string', 'greetings', ...
'callback','msgbox(''hello mik'')', ...
'position', [10 10 100 30]);
return
11. Analysis of simple GUI code 5: summary
-
"guide" is good for basic GUI generation
-
positioning of the controls
-
generating template for larger GUI programs
-
simple code for copy paste into other m files
-
"guide" drawbacks
-
Very slow in changing and viewing properties
-
Cannot make modifications outside of "guide" environment; will cause problems
-
Cannot easily change default properties since some values are stored in
mat file
-
Generated code doesn't use informative handle names (a for figure, b for
pushbutton, etc.)
12. List of properties for UIcontrol
-
BackgroundColor
-
BusyAction
-
ButtonDownFcn
-
Callback
-
Children
-
Clipping
-
CreateFcn
-
DeleteFcn
-
Enable
-
Extent
-
FontAngle
-
FontName
-
FontSize
-
FontUnits
-
FontWeight
-
ForegroundColor
-
HandleVisibility
-
HorizontalAlignment
-
Interruptible
-
ListboxTop
-
Max
-
Min
-
Parent
-
Position
-
Selected
-
SelectionHighlight
-
SliderStep
-
String
-
Style
-
Tag
-
Type
-
Units
-
UserData
-
Value
-
Visible
13. Useful simple GUI commands
-
zoom on / zoom off
-
Zooms in on a 2D graph using mouse drag; double click moves to full graph
-
For example,
plot(sin(0:.1:20));
zoom on
-
ginput(numberOfPoints): mouse input
-
Allows the user to click on the graph (or figure or image) and returns
the x and y coordinates
-
Useful for finding the number shown on the graph
-
For example, combined with text command for placing some text on the figure
and 45 degree rotation,
[x y] = ginput(1);
txthandle = text(x, y, [num2str(x) ', ' num2str(y)]);
set(txthandle, 'rotation', 45);
-
rotate3d on / rotate3d off
-
Allows interactive rotation of 3D graphics; does work with 2D graphs, but
not very useful
x = 0:.01:.6;
[x y] = meshgrid(x,x);
z = sin(x^2 - y^2);
surf(x, y, z);
rotate3d on;
-
uigetfile : opens the file/open dialog box
-
uiputfile : opens the file/save dialog box
-
waitforbuttonpress : suspects all activity until user presses a key or
click the mouse
14. Sophisticated GUI program
-
Problems with callback property
-
Works on global scope; does not recognize functions in local file
-
Without workaround, each callback must exist as separate files
-
Work around: recursion and global
-
Place all or most functions within one file and call them using recursive
calls
-
Use global variable to pass information
-
Need to preserve some information such as object handles
-
Matlab doesn't support static variable
-
An example of what can be done with Matlab
15. Using recursion
-
Call itself with parameter description what would execute code other than
the originating code
-
Switch / case statement is your friend
-
For example
function recurs(param)
switch param
case 'first'
msgbox('this is 1');
recurs('second');
case 'second'
msgbox('this is 2');
otherwise
msgbox('this is zero');
recurs('first');
end
return
16. Parameters to recursion
-
Parameters should contain what action to take
-
Function call without parameter should also be handled
-
Use nargin, number of input parameters
function recurs(param)
if nargin == 0
param = '???';
end
switch param
case 'first'
msgbox('this is 1');
recurs('second');
case 'second'
msgbox('this is 2');
otherwise
msgbox('this is zero');
recurs('first');
end
return
-
nargout returns the number of output parameters
17. Using global variables
-
Necessary due to Matlab not having static variables
-
Must declare in each function used
-
Should declare early for memory efficiency
-
Good place is to declare just before first executable code
-
For example,
function recurs(param)
global x y z;
if nargin == 0
param = '???';
end
switch param
.
.
.
return
18. Sophisticated program example
-
Name: general purpose graph pal (gpgp)
-
Add buttons to any figure
-
Display value clicked on the plot
-
Remove a text generated by gpgp with mouse click
-
Remove all text generated by gpgp
-
Edit text with mouse click
-
Zoom on/off
-
Quit gpgp, leave text behind
-
Drag and move generated text
19. Create buttons
-
Need 6 buttons
-
use guide to place them and save as gpgp.m
-
do not use alignment tool or anything else other than guide
-
Open gpgp.m in text editor and make changes
-
Remove all comments and line "load gpgp"
-
Remove a = figure(...) since we will only be working with existing figure
-
For each button, change the parent to gcf
20. Give meaningful names to buttons
-
Add "string" property to each button with appropriate titles
-
PlotVal
-
Remove
-
RemoveAll
-
Edit
-
Zoom Off
-
Quit
-
Give meaningful names to handles (use modified Hungarian notation)
-
hbtn_plotval
-
hbtn_remove
-
hbtn_removeall
-
hbtn_edit
-
hbtn_zoom
-
hbtn_quit
-
Declare the handle names as global variable
function gpgp()
global hbtn_plotval hbtn_remove hbtn_removeall hbtn_edit hbtn_zoom hbtn_quit;
-
Delete references to "tag" property for all buttons; it's not used in this
program
-
Save as gpgp and call gpgp from Matlab; positioning of buttons is next
21. Position buttons
-
All buttons are next to each other and same size
-
Use variable to specify bottom, width, and height of the buttons; also
the left of first button
btnlef = 2.48276;
btnbot = 3.10345;
btnwid = 47.1724;
btnhei = 14.8966;
-
Change the button positions using the variables in gpgp.m
'Position',[btnlef+0*btnwid btnbot btnwid btnhei], ...
'Position',[btnlef+1*btnwid btnbot btnwid btnhei], ...
'Position',[btnlef+2*btnwid btnbot btnwid btnhei], ...
'Position',[btnlef+3*btnwid btnbot btnwid btnhei], ...
'Position',[btnlef+4*btnwid btnbot btnwid btnhei], ...
'Position',[btnlef+5*btnwid btnbot btnwid btnhei], ...
-
Save and call gpgp and verify the buttons look the way you want
22. Change the function for recursion
-
Change the function declaration with one parameter
-
Add the statements to handle no parameter case; no parameter means initialize
function gpgp(param)
global hbtn_plotval hbtn_remove hbtn_removeall hbtn_edit hbtn_zoom hbtn_quit;
if nargin == 0
param = 'init';
end
{rest of the code from previous step}
-
Add switch case statement around button generation routine for case 'init'
-
Add return statement at the end of the function
switch(param)
case 'init'
btnlef = 2.48276;
.
.
.
{make button routines from previous step}
end
return
23. Source code so far
-
Function with one parameter, few global variables, and switch statement
function gpgp(param)
global hbtn_plotval hbtn_remove hbtn_removeall hbtn_edit hbtn_zoom hbtn_quit;
if nargin == 0
param = 'init';
end
switch(param)
case 'init'
btnlef = 2.48276;
btnbot = 3.10345;
btnwid = 47.1724;
btnhei = 14.8966;
hbtn_plotval = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+0*btnwid btnbot btnwid btnhei], ...
'String','PlotVal');
hbtn_remove = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+1*btnwid btnbot btnwid btnhei], ...
'String','Remove');
hbtn_removeall = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+2*btnwid btnbot btnwid btnhei], ...
'String','RemoveAll');
hbtn_edit = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+3*btnwid btnbot btnwid btnhei], ...
'String','Edit');
hbtn_zoom = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+4*btnwid btnbot btnwid btnhei], ...
'String','Zoom Off');
hbtn_quit = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+5*btnwid btnbot btnwid btnhei], ...
'String','Quit');
end
return
24. Method for PlotVal button
-
Assume the user knows he / she should click on the graph
-
Add callback property to PlotVal button; use modified Hungarian notation
hbtn_plotval = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+0*btnwid btnbot btnwid btnhei], ...
'String','PlotVal', ...
'callback', 'gpgp(''hbtn_plotval_click'');');
-
Another case statement to handle the button click; use Hungarian notation
case 'hbtn_plotval_click'
[x y] = ginput(1);
if isempty(x)
return;
end
txt = [num2str(x) num2str(y)];
text(x, y, txt);
-
Save it and try it out on any graph
25. Keeping track of text object
-
Text object is "lost" since its handle isn't kept
-
Add global variable to keep track of text handles
global hbtn_plotval hbtn_remove hbtn_removeall hbtn_edit hbtn_zoom hbtn_quit htxt;
Add text handles in callback function for PlotVal button (case 'hbtn_plotval_click')
-
replace the line where text is created to save the handle in global variable
text(x, y, txt);
gets replaced with
htxt = [htxt text(x, y, txt)];
26. Method for RemoveAll button
-
Step 1: specify the callback function property
hbtn_removeall = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+2*btnwid btnbot btnwid btnhei], ...
'String','RemoveAll', ...
'callback', 'gpgp(''hbtn_removeall_click'');');
-
Step 2: tell all buttons to die
case 'hbtn_removeall_click'
delete(htxt(ishandle(htxt)));
htxt = [];
-
Note: htxt is a vector (array), so set operates on all values in vector
without having to explicitly specify each value
27. Method for Quit button
-
Quit only eliminates the buttons, but keeps text
-
If gpgp is called again, it should remember existing text
-
Need to do memory clean up
-
Step 1: add callback
hbtn_quit = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+5*btnwid btnbot btnwid btnhei], ...
'String','Quit', ...
'callback', 'gpgp(''hbtn_quit_click'');');
-
Step 2: specify what callback should do
-
delete all objects, then free the memory taken by objects
-
check to see if htxt is empty; if so, remove it from memory
case 'hbtn_quit_click'
delete(hbtn_plotval);
delete(hbtn_remove);
delete(hbtn_removeall);
delete(hbtn_edit);
delete(hbtn_zoom);
delete(hbtn_quit);
clear global hbtn_plotval;
clear global hbtn_remove;
clear global hbtn_removeall;
clear global hbtn_edit;
clear global hbtn_zoom;
clear global hbtn_quit;
htxt(~ishandle(htxt)) = [];
if isempty(htxt)
clear global htxt;
end
-
Step 3: test it
-
Enter gpgp; click on PlotVal and create some text
-
Click on quit; restart gpgp and see if RemoveAll deletes existing text
28. Summary of easy buttons, PlotVal, RemoveAll, Quit
-
Step 1: add callback to each button
-
Step 2: specify what the callback should do
-
Step 3: optional; rarely (if ever) practiced by software developers
-
Source code so far
function gpgp(param)
global hbtn_plotval hbtn_remove hbtn_removeall hbtn_edit hbtn_zoom hbtn_quit htxt;
if nargin == 0
param = 'init';
end
switch(param)
case 'init'
btnlef = 2.48276;
btnbot = 3.10345;
btnwid = 47.1724;
btnhei = 14.8966;
hbtn_plotval = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+0*btnwid btnbot btnwid btnhei], ...
'String','PlotVal', ...
'callback', 'gpgp(''hbtn_plotval_click'');');
hbtn_remove = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+1*btnwid btnbot btnwid btnhei], ...
'String','Remove');
hbtn_removeall = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+2*btnwid btnbot btnwid btnhei], ...
'String','RemoveAll', ...
'callback', 'gpgp(''hbtn_removeall_click'');');
hbtn_edit = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+3*btnwid btnbot btnwid btnhei], ...
'String','Edit');
hbtn_zoom = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+4*btnwid btnbot btnwid btnhei], ...
'String','Zoom Off');
hbtn_quit = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+5*btnwid btnbot btnwid btnhei], ...
'String','Quit', ...
'callback', 'gpgp(''hbtn_quit_click'');');
case 'hbtn_plotval_click'
[x y] = ginput(1);
if isempty(x)
return;
end
txt = [num2str(x) num2str(y)];
htxt = [htxt text(x, y, txt)];
case 'hbtn_removeall_click'
delete(htxt(ishandle(htxt)));
htxt = [];
case 'hbtn_quit_click'
delete(hbtn_plotval);
delete(hbtn_remove);
delete(hbtn_removeall);
delete(hbtn_edit);
delete(hbtn_zoom);
delete(hbtn_quit);
clear global hbtn_plotval;
clear global hbtn_remove;
clear global hbtn_removeall;
clear global hbtn_edit;
clear global hbtn_zoom;
clear global hbtn_quit;
htxt(~ishandle(htxt)) = [];
if isempty(htxt)
clear global htxt;
end
end
return
29. Enhancement to creation
-
More than one instance of gpgp could create multiple controls
-
Need to check if controls exist; if so, delete existing buttons before
creating new ones
-
Since the default zoom mode is off, explicitly turn off zoom
case 'init'
if ishandle(hbtn_plotval)
delete(hbtn_plotval);
delete(hbtn_remove);
delete(hbtn_removeall);
delete(hbtn_edit);
delete(hbtn_zoom);
delete(hbtn_quit);
end
zoom off;
.
.
.
30. Method for Zoom button
-
Step 1: Specify callback on creation
hbtn_zoom = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+4*btnwid btnbot btnwid btnhei], ...
'String','Zoom Off', ...
'callback', 'gpgp(''hbtn_zoom_click'');');
-
Step 2: Change zoom state depending on current state of button (can you
guess the significance of the comment?)
case 'hbtn_zoom_click'%error: zoom will distort movement and remove/edit
a_str = get(hbtn_zoom, 'string');
switch a_str
case 'Zoom Off'
zoom on;
set(hbtn_zoom, 'string', 'Zoom On');
otherwise
zoom off;
set(hbtn_zoom, 'string', 'Zoom Off');
end
31. Method for Remove button
-
Step 1: Specify the function to execute when user clicks on the button
hbtn_remove = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+1*btnwid btnbot btnwid btnhei], ...
'String','Remove', ...
'callback', 'gpgp(''hbtn_remove_click'');');
-
Step 2: Specify what the function should do when the button is clicked
case 'hbtn_remove_click'
if ~ishandle(htxt)
return
end
set(gcf, 'pointer', 'crosshair');
set(htxt, 'ButtonDownFcn', 'gpgp(''fcn_removetxt'')');
-
Step 3: Specify the button object to kill itself when clicked
case 'fcn_removetxt'
set(gcf, 'pointer', 'arrow');
set(htxt, 'ButtonDownFcn', 'gpgp(''fcn_text_click'');');
htxt(htxt == gcbo) = [];
delete(gcbo);
32. Method for Edit button
-
Pretty much identical to the way Remove was implemented as 3 step process
-
Step 1: creation
hbtn_edit = uicontrol('Parent',gcf, ...
'Units','points', ...
'Position',[btnlef+3*btnwid btnbot btnwid btnhei], ...
'String','Edit', ...
'callback', 'gpgp(''hbtn_edit_click'');');
-
Step 2 and 3: to do in callback functions
case 'hbtn_edit_click'
if ~ishandle(htxt)
return
end
set(gcf, 'pointer', 'Ibeam');
set(htxt, 'ButtonDownFcn', 'gpgp(''fcn_edittxt'')');
case 'fcn_edittxt'
obj = gcbo;
txt = {get(obj, 'string')};
txtcell = inputdlg({'What is thy bidding, my master?'}, ...
'New text value', 1, txt);
set(gcf, 'pointer', 'arrow');
set(htxt, 'ButtonDownFcn', 'gpgp(''fcn_text_click'');');
if isempty(txtcell)
return
end
txt = cell2struct(txtcell, {'txt'}, 1);
set(obj, 'string', txt.txt);
33. Dragging text around
-
Allow the user to drag the text around with mouse drag
-
Problem in Matlab: drag event is not available for text object
-
Workaround
-
Step 1: specify on text creation what should happen on mouse button down;
need modification to hbtn_plotval_click callback (note: the code also contains
method for removing orphaned handles)
case 'hbtn_plotval_click'
[x y] = ginput(1);
if isempty(x)
return;
end
txt = [num2str(x) ', ' num2str(y)];
txt = text(x, y, txt);
set(txt, 'ButtonDownFcn', 'gpgp(''fcn_text_click'');');
htxt = [htxt txt];
htxt(~ishandle(htxt)) = [];
-
Step 2: specify in callback for mouse button down to set the following
-
Store which text handle should be manipulated for callbacks; use another
global or use figure's userdata property
-
Specify current figure callback for mouse motion
-
Specify current figure callback for mouse button up
case 'fcn_text_click'
obj = gcf;
set(obj, 'userdata', gcbo);
set(obj, 'windowbuttonmotionfcn', 'gpgp(''fcn_text_drag'')');
set(obj, 'windowbuttonupfcn', 'gpgp(''fcn_text_drag_end'')');
-
Step 3: create callback for mouse motion so that text location is according
to mouse location
case 'fcn_text_drag'
objfig = gcf;
pt = get(gca, 'CurrentPoint');
pt = pt(1, 1:2);
objtxt = get(objfig, 'userdata');
set(objtxt, 'position', pt);
-
Step 4: create callback for mouse button up so that both previous callbacks
are cleared in figure
case 'fcn_text_drag_end'
objfig = gcf;
set(objfig, 'windowbuttonmotionfcn', '');
set(objfig, 'windowbuttonupfcn', '');
34. Enhancements needed: left as exercise for the reader
When figure is closed, all objects are deleted but the global variables
remain
Moving the text around in zoom mode can cause confusion
When remove or edit is clicked and then new plot values is clicked without
deleting a value, the new text objects don't initially behave the same
as old ones
Consolidate all global variables into one global structure
Even better, get rid of all global variables in favor of using user data
and/or children properties
35. Conclusion
-
Matlab can generate most GUI of standard program
-
The GUI elements are objects
-
Matlab guide isn't too helpful in making large programs
-
Most simple GUI are easy to generate
-
Complex GUI programs need more work using recursion and data preserving
techniques
-
Some tasks require use of "twisted" method to generate appropriate responses
-
Please e-mail mikkim@bigfoot.com
for requests for code. However, you should be able to construct it using
copy paste of various pieces from this tutorial.