Newsletters - MATLAB News & Notes
Programming Patterns in MATLAB
GUI switchyards
by Peter Webb
MATLAB's uicontrol command creates the push buttons, list boxes, pop-up menus, and other controls that make up a MATLAB graphical user interface. A user interface control has various properties that determine its appearance and behavior. One of the most important of these is the callback property, which specifies the action that is taken when the control is activated-when a button is pushed or a selection is made from a menu, for example. The callback property is a string and consists of one or more MATLAB commands. Since the MATLAB interpreter executes callbacks as if they were typed at the MATLAB prompt, callbacks are very powerful. They can create and modify variables in the MATLAB workspace and make calls to any MATLAB M-file or MEX function. However, the unstructured use of callbacks can lead to code that is error-prone and hard to maintain, particularly in programs with large, complex GUIs.
View sample callback code for a group of radio buttons.
Callbacks that use workspace variables to store data are vulnerable to commands the user might execute at the command line. For example, a push button that displays a number that increments every time the button is pushed might have the following callback string:
'x = x + 1; set(gcbo, ''string'', num2str(x));'
This callback stores the value it is incrementing, x, in a workspace variable. If the user somehow modified the value of x in the workspace via the clear command or an assignment to x, the next time the button is pushed, the value displayed will change in a surprising way. It is safer to write this callback as an M-file function with a persistent variable x:
function increment
persistent x;
x = x + 1; set(gcbo, 'string', num2str(x));
The callback string then becomes much simpler: 'increment'. While this solves the problem of unwanted interaction with workspace variables, it has the effect of scattering the GUI callback functions into a large number of very small M-files. This collection of M-files will be hard to maintain. Alternatively, you could use the string property of the uicontrol to store the value of the index, but then the callback string starts to become large and unwieldy:
'x = str2num(get(gcbo, ''string'')); x = x + 1;
set(gcbo, ''string'', num2str(x));'
While small and relatively unsophisticated callbacks like the one shown here can certainly be embedded entirely in the callback string, most callbacks are longer and need to be separate functions. This leads to the M-file proliferation problem again.
There is a better way. Callbacks written in the "GUI switchyard" style are both easier to maintain and immune to workspace-variable changes. A switchyard-style GUI consists of a single M-file. This M-file contains a single entry point (the switchyard) and a collection of local functions that implement the callbacks. Gathering all the callback functions into a single M-file eliminates the problem of M-file proliferation and makes the GUI easier to maintain. And because each callback function manipulates variables in its own workspace, those variables are protected from changes made in the base workspace. Since they concentrate the callbacks for each figure window into a single file, switchyard-style GUIs are most suited to projects for which a single person maintains each switchyard file.
In the following example, the switchyard function takes zero or one arguments. With zero arguments, it calls an initialization function. With one argument, it assumes that the argument is the name of a local callback function and uses feval to call the callback. This example manages a group of radio buttons that are used to set the colormap of the current figure. The switchyard function is called myGui, and it consists of the declaration of a persistent variable and call to feval nested inside a try-catch statement:
function myGui(func)
persistent colormaps;
if nargin == 0
colormaps = initialize;
else
try
feval(func, colormaps);
catch
disp(lasterr);
end
end
Another way to write a GUI switchyard function is to use a switch statement instead of feval. The feval method makes error-checking more complicated, as you need a try-catch statement to handle errors caused by passing in the name of a nonexistent callback. Adding new callbacks, on the other hand, requires no modification of the switchyard function at all; all you need to do is add a new local function for the callback. With a switch statement, error-checking is simpler (you can use otherwise to handle nonexistent callback functions), but adding a new callback requires you to add a new case to the switch statement as well as a local function for the callback.
The initialize function constructs the initial GUI, which consists of a group of four radio buttons and a text label. Then initialize creates four colormap buttons and returns their handles. The creation of the first colormap button is shown below; the others are very similar to this one. Like the callback functions, initialize is a local function.
function cmaps = initialize
cmaps(1) = uicontrol('units', 'normalized',...
'string', 'cool', 'style', 'radio',...
'callback', 'myGui(''coolmap'')',...
'position', [.87, gy, .1, .05], 'value', 1);
Notice the callback property: myGui('coolmap'). This is a call to the function myGui with a single string argument, the name 'coolmap'. myGui will treat 'coolmap' as the name of a callback function, and execute it. The callback functions are all very similar. Each callback first deselects all the buttons in the radio group (all the handles in the colormaps variable) and then activates the button the user selected. This is the standard way to implement a radio group in a MATLAB GUI.
By using the switchyard technique, a large GUI can both be protected from the user's manipulation of the workspace and reduced to a manageable number of files: one switchyard function per figure window. The resulting GUIs are more robust and easier to maintain.