Sunday, July 31, 2016

GUI applications in MATLAB (part 1)

MATLAB is a great tool for scientific and engineering calculations.  Many technical problems in those genres boil down to solving linear algebra problems, and that is this specific niche in which MATLAB excels.  It is very popular in university settings, and while probably not as extensively used in industry due to its price tag, it still enjoys wide use and in certain circles is the de facto standard.


MATLAB also comes with a set of widgets for building graphical user interfaces (GUIs).  While MATLAB itself is hardly the best language to build standalone applications, because it is so easy to prototype mathematical models and I don’t have sufficient time to write everything in C or C++, I do tend to build fairly large and sophisticated applications written solely in MATLAB.  Therefore, I wanted to write a series of posts about the quirks and idiosyncrasies of MATLAB GUI programming.


This first post in the series, however, will deal with writing object oriented (OO) GUI components.  I grew up doing procedural programming rather than OO-programming .  Part of this was that I learned in BASIC back when I was very young and C was my first “real” language.  A second part of this is that arguably OO-programming is not the best paradigm for scientific computing.  It can be done, but care needs to be taken in how to write the classes.  Lastly OO-programming in MATLAB feels added as an afterthought.  The capability exists, but the language wasn’t designed with that in mind.

But in writing standalone GUIs I often found myself reusing large blocks of code or grouping certain things into structures and then writing functions to operate on those structures.  I found myself thinking this should really be rewritten as a class.  Writing classes to handle GUI widgets is pretty much the same as writing any other type of classes, but I didn’t find much online about it.

Here, I will show a simple GUI that adds people’s names along with various other stats about them to a listbox.  We will also provide the ability to edit the information in a given listbox entry.  We will write code to generate an input window used to enter or edit the data.  Since the overwhelming majority of the code would be identical in both the input and editing cases, we’ll wring a class in generate the window and have various methods handle some error checking and widget callbacks. The main program window is shown below.

Main Program window compromised of three buttons to add a person, edit a person's information, and remove a person from the listbox locate to the right
We will write the code for the main window in a very typical MATLAB way-- writing a function with a handful of embedded functions to handle the callbacks. The callbacks can be broken into separate files, and that might be a more useful way to write a more involved application where you want different components that are defined in different files to have access to these functions, but we'll use the simplest case here.


1:  function listbox_demo  
2:    
3:  clear all; close all; clc;  
4:    
5:  % This will be the array where we store people's information  
6:  persons_array = [];  
7:    
8:  % Create the main window and set some of its properties  
9:  main_window = figure();  
10:  set(main_window, 'Name', 'Listbox Demo', 'NumberTitle', 'off');  
11:  set(main_window, 'Tag', 'main_window');  
12:  set(main_window, 'Position', [250 198 500 400]);  
13:  set(main_window, 'ToolBar', 'none');  
14:    
15:  % We are going to use the figure's appdata to store a temporary structure  
16:  % that will be used by the add and edit functionality to pass information  
17:  % between the two windows. This eliminates the need for global variables.  
18:  % We will initialize it as an empty structure.  
19:  setappdata(main_window, 'temp_person_structure', struct());  
20:    



Line 1 is the standard MATLAB function declaration. For the sake of keeping my MATLAB environment clean, I often add a line (line 3 in this case) that clears out the memory in the base workspace, closes any currently open figures and clears the MATLAB console. You may or may not wish to do similarly. It's a matter of taste.

The data we are going to enter will be stored in a variable persons_array. Each element of the array will be a structure containing the information about a particular individual.

Next we set up the main window. I turned off the default toolbar MATLAB has for its figures but left the main menubar in place. We will uses the figure's appdata functionality to store information we need to pass back and forth. We define this on line 19 and set it equal to an empty structure. Next we define the widgets to appear in the main window.


1:  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  
2:  %%% Set up main window widgets %%%  
3:  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  
4:  buttons.add = uicontrol('Style', 'PushButton', ...  
5:      'String' , 'Add Person', ...  
6:      'Position', [10, 350, 100 30]);  
7:  buttons.edit = uicontrol('Style', 'PushButton', ...  
8:      'String', 'Edit Person', ...  
9:      'Position', [10, 310, 100 30]);  
10:  buttons.remove = uicontrol('Style', 'PushButton', ...  
11:      'String', 'Remove Person', ...  
12:      'Position', [10, 270, 100 30]);  
13:    
14:  listbox = uicontrol('Style', 'listbox', ...  
15:      'Position', [150, 180, 300, 200]);  
16:    

These are the standard MATLAB uicontrols. I tend to define the callback functions in a separate location as it makes to a bit easier to track down issues if the definitions are all located in the same place. Again, that's a personal preference. Now let's set up the callbacks:


1:  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  
2:  %%% Set widget callbacks %%%%  
3:  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%  
4:    
5:  set(buttons.add, 'Callback', @add_person);  
6:  set(buttons.edit, 'Callback', @edit_person);  
7:  set(buttons.remove, 'Callback', @remove_person);  
8:    

Above, we've bound callback functions to each of the three buttons. Next, we write the callbacks:


1:  %%%%%%%%%%%%%%%%%%%%%%%%%%%  
2:  %%% Callback functions %%%  
3:  %%%%%%%%%%%%%%%%%%%%%%%%%%%  
4:  function add_person(source, events)  
5:    
6:       % Reset the temp_person_structure to an empty struct  
7:       setappdata(main_window, 'temp_person_structure', struct());  
8:    
9:       % Open up person entry window  
10:       entry_window = InputWindow(main_window);  
11:    
12:       % Explicitly tell MATLAB to wait until the entry window is closed  
13:       % before moving forward  
14:       waitfor(entry_window.window);  
15:    
16:       person_structure = getappdata(main_window, 'temp_person_structure');  
17:    
18:       % If the InputWindow was closed without updating the structure, we  
19:       % will have an empty temp_person_structure with no fields. We  
20:       % cannot append this to our aray so we check to see if the  
21:       % structure is empty. If it is, we return.  
22:       if length( fieldnames( person_structure ) ) == 0  
23:            return;  
24:       end;  
25:    
26:       % Append new person to the persons_array  
27:       persons_array = [persons_array; person_structure];  
28:    
29:       update_listbox(persons_array);  
30:  end;  
31:    
32:  function edit_person(source, events)  
33:    
34:       % Reset the temp_person_structure to an empty struct  
35:       setappdata(main_window, 'temp_person_structure', struct());  
36:    
37:       % Get the value of the selected field  
38:       value = listbox.Value;  
39:    
40:       % By default an empty listbox has a value of 1 when created so we  
41:       % need to make sure we are not trying to edit an empty listbox.  
42:       % We use the length of the cell array of strings to determine of  
43:       % there are entries  
44:       if length(listbox.String) > 0  
45:            entry_window = InputWindow(main_window, persons_array(value));  
46:            waitfor(entry_window.window);  
47:    
48:            person_structure = getappdata(main_window, ...  
49:                 'temp_person_structure');  
50:    
51:            % We want to abort if the person structure wasn't updated  
52:            if length( fieldnames( person_structure ) ) == 0  
53:                 return;  
54:            end;  
55:    
56:            % Modify the persons_array with the correct information  
57:            % and redraw the listbox  
58:            persons_array( value ) = person_structure;  
59:            update_listbox(persons_array);  
60:       end;  
61:              
62:  end;  
63:    
64:  function remove_person(source, events)  
65:       % The listbox value will correspond to the array index of the  
66:       % person to remove. So we get that value and remove it from the  
67:       % array.  
68:    
69:       value = listbox.Value;  
70:       persons_array(value) = [];  
71:    
72:       % If we try deleting the last entry, the listbox value will still  
73:       % be set to that number and will throw a warning as it cannot  
74:       % highlight a non-existant field. So we check if the new array  
75:       % length is less than the value. If so, we set value equal to the  
76:       % length of the array  
77:    
78:       N = length(persons_array);  
79:       if value > N  
80:            listbox.Value = N;  
81:       end;  
82:    
83:       update_listbox(persons_array);  
84:  end;  
85:    

These are pretty straightforward. The simplest is remove_person which just grabs the index of the highlighted listbox entry and removes that index from the persons_array. Then the listbox is redrawn with the new persons_array.

For the sake of completeness, the remaining two functions just generate text strings based on the information pertaining to each individual, and updates the listbox entries.


1:  function str = struct_to_str(person)  
2:       % Take a structure for an input and return a string based on the  
3:       % information therein. These strings will eventually be displayed  
4:       % in the listbox  
5:    
6:       name_str = sprintf('Name: %s %s ', person.firstname, person.lastname);  
7:       sex_str = sprintf('Sex: %s ', person.sex);  
8:       height_str = sprintf('Height: %.1f in. ', person.height);  
9:       weight_str = sprintf('Weight: %.1f lbs. ', person.weight);  
10:    
11:       str = [name_str, sex_str, height_str, weight_str];  
12:  end;  
13:    
14:  function update_listbox(persons_array)  
15:    
16:       % The listbox widget takes a cell array of strings for its string  
17:       % input with each element corresponding to an item in the  
18:       % listbox.. We start with an empty array.  
19:       string_array = {};  
20:    
21:       % Loop over the persons_array and generate a string for each  
22:       % input. Append this string to our cell array of strings to be  
23:       % displayed.  
24:       L = length(persons_array);  
25:       for i = 1:L  
26:            str = struct_to_str( persons_array(i) );  
27:            string_array{ length(string_array) + 1 } = str;  
28:       end;  
29:    
30:       % Update the listbox.  
31:       set(listbox, 'String', string_array);  
32:  end;  
33:    
34:  end  

The final 'end' on line 34 just closes out the original function. Now we we will define a class to create a window where we can add or edit a person's information. This window is shown below.


This is our window for adding or edit the information pertaining to  an individual
This window will allow use to add the person's first and last names, sex, height, and weight to the listbox. Since we will use the same code for both the adding and editing functionality, we need to distinguish between those two modes, and in the case of editing, population the widgets above with the information to be edited. As far as I know, MATLAB does not have the ability to overload class constructors. However, we can create a similar effect by making use of the varargin variable which allows a function to have a variable number of arguments. When the length of varargin is greater than zero, we will take this to be a structure containing information to be edited and populate the widgets in the window accordingly. We will also change the text of the 'Add Person' button to 'Commit Changes' just to give another visual indication of the fact we are editing. There is no code here to check that the actual information passed in in varargin is compatible or correct. In production code, obviously it would be a good idea to add such an argument-checking feature. The code for class in in a separate file, InputWindow.m. We start by defining the class properties which will be the widgets and a handle to the main window. We'll need this handle to get the appdata which we will use to pass back a person structure.


1:  classdef InputWindow  
2:    
3:  properties (SetAccess = private)  
4:    
5:       % GUI widget handles. We will make them properties of the class  
6:       % so we can access them easily from within class methods  
7:       window;  
8:       firstname_label;  
9:       firstname_entry;  
10:       lastname_label;  
11:       lastname_entry;  
12:       sex_label;  
13:       sex_popupmenu;  
14:       height_label;  
15:       height_entry;  
16:       height_units;  
17:       weight_label;  
18:       weight_entry;  
19:       weight_units;  
20:       add;  
21:       cancel;  
22:    
23:       % Handle to the main window. We need this to get the appdata that  
24:       % is stored in the main window.  
25:       root_window_handle;  
26:  end  

Next, we need to define the methods. This class has only three methods, a constructor, a function to close the window, and a function to generate the person structure and store it in the main window's appdata.


1:  methods  
2:    
3:  function obj = InputWindow(root_window_handle, varargin)  
4:    
5:       % We need the handle of the root window to get and set appdata.  
6:       % We will make the handle an object property so it can be accessed  
7:       % from within the class  
8:       obj.root_window_handle = root_window_handle;  
9:         
10:       % We'll clear the appdata setting here just to make sure it is  
11:       % empty if we close out of this window without making any updates.  
12:       setappdata(obj.root_window_handle, 'temp_person_structure', struct());  
13:    
14:       % Set up the main window and widgets  
15:       obj.window = figure();  
16:       set(obj.window, 'Position', [250 198 300 400]);  
17:       set(obj.window, 'ToolBar', 'none');  
18:    
19:       if length(varargin) == 0  
20:            set(obj.window, 'Name', 'Add Person Info', ...  
21:                  'NumberTitle', 'off');  
22:    
23:            firstname = '';  
24:            lastname = '';  
25:            height = '';  
26:            weight = '';  
27:            add_button_text = 'Add Person';  
28:       elseif length(varargin) == 1  
29:            set(obj.window, 'Name', 'Edit Person Info', ...  
30:                  'NumberTitle', 'off');  
31:            person_info = varargin{1};  
32:    
33:            firstname = person_info.firstname;  
34:            lastname = person_info.lastname;  
35:            height = person_info.height;  
36:            weight = person_info.weight;  
37:            add_button_text = 'Commit Changes';  
38:    
39:            height = num2str(height);  
40:            weight = num2str(weight);  
41:       else  
42:            error('InputWidnow constructor called incorrectly');  
43:       end;  
44:    
45:       obj.firstname_label = uicontrol('Style', 'text', ...  
46:            'HorizontalAlignment', 'left', ...  
47:            'String', 'First Name', ...  
48:            'Position', [10, 360, 70, 20]);  
49:       obj.firstname_entry = uicontrol('Style', 'edit', ...  
50:            'String', firstname, ...  
51:            'position', [100, 360, 100, 20]);  
52:    
53:       obj.lastname_label = uicontrol('Style', 'text', ...  
54:            'HorizontalAlignment', 'left', ...  
55:            'String', 'Last Name', ...  
56:            'Position', [10, 330, 70, 20]);  
57:       obj.lastname_entry = uicontrol('Style', 'edit', ...  
58:            'String', lastname, ...  
59:            'position', [100, 330, 100, 20]);  
60:    
61:       obj.sex_label = uicontrol('Style', 'text', ...  
62:            'HorizontalAlignment', 'left', ...  
63:            'String', 'Sex', ...  
64:            'Position', [10, 300, 70, 20]);  
65:       obj.sex_popupmenu = uicontrol('Style', 'popupmenu', ...  
66:            'String', {'Male', 'Female'}, ...  
67:            'Position', [100, 300, 70, 20]);  
68:    
69:       obj.height_label = uicontrol('Style', 'text', ...  
70:            'HorizontalAlignment', 'left', ...  
71:            'String', 'Height', ...  
72:            'Position', [10, 270, 70, 20]);  
73:       obj.height_entry = uicontrol('Style', 'edit', ...  
74:            'String', height, ...  
75:            'position', [100, 270, 100, 20]);  
76:       obj.height_units = uicontrol('Style', 'text', ...  
77:            'HorizontalAlignment', 'left', ...  
78:            'String', 'inches', ...  
79:            'Position', [210, 270, 70, 20]);  
80:    
81:       obj.weight_label = uicontrol('Style', 'text', ...  
82:            'HorizontalAlignment', 'left', ...  
83:            'String', 'Weight', ...  
84:            'Position', [10, 240, 70, 20]);  
85:       obj.weight_entry = uicontrol('Style', 'edit', ...  
86:            'String', weight, ...  
87:            'position', [100, 240, 100, 20]);  
88:       obj.weight_units = uicontrol('Style', 'text', ...  
89:            'HorizontalAlignment', 'left', ...  
90:            'String', 'pounds', ...  
91:            'Position', [210, 240, 70, 20]);  
92:    
93:       obj.add = uicontrol('Style', 'pushbutton', ...  
94:            'String', add_button_text, ...  
95:            'Position', [10, 10, 100, 30], ...  
96:            'Callback', @obj.return_structure);  
97:       obj.cancel = uicontrol('Style', 'pushbutton', ...  
98:            'String', 'Calcel', ...  
99:            'Position', [200, 10, 100, 30], ...  
100:            'Callback', @obj.close_window);  
101:    
102:  end;  
103:    
104:  function close_window(obj, source, events)  
105:       % Close this window  
106:       close(obj.window);  
107:  end;  
108:    
109:  function return_structure(obj, source, events)  
110:    
111:       firstname = obj.firstname_entry.String;  
112:       lastname = obj.lastname_entry.String;  
113:       sex = obj.sex_popupmenu.Value;  
114:       height = obj.height_entry.String;  
115:       weight = obj.weight_entry.String;  
116:    
117:       % We will insist every field be filled out. If not, we display an  
118:       % error dialog.  
119:       if isempty(firstname)  
120:            h = warndlg('Please fill in the persons first name');  
121:            waitfor(h);  
122:            return;  
123:       end;  
124:       if isempty(lastname)  
125:            h = warndlg('Please fill in the persons last name');  
126:            waitfor(h);  
127:            return;  
128:       end;  
129:       if isempty(height)  
130:            h = warndlg('Please fill in the persons height');  
131:            waitfor(h);  
132:            return;  
133:       end;  
134:       if isempty(weight)  
135:            h = warndlg('Please fill in the persons weight');  
136:            waitfor(h);  
137:            return;  
138:       end;  
139:    
140:       % We'll make sure the height and weight are numbers since we will  
141:       % want to add these to the structures as numerics. We will also  
142:       % make sure the numbers are positive.  
143:       [height, height_error] = str2num(height);  
144:       if height_error == 0  
145:            h = warndlg('The height must be a positive numeric value');  
146:            waitfor(h);  
147:            return;  
148:       end;  
149:       if height <= 0  
150:            h = warndlg('The height must be a positive numeric value');  
151:            waitfor(h);  
152:            return;  
153:       end;  
154:    
155:       [weight, weight_error] = str2num(weight);  
156:       if weight_error == 0  
157:            h = warndlg('The weight must be a positive numeric value');  
158:            waitfor(h);  
159:            return;  
160:       end;  
161:       if weight <= 0  
162:            h = warndlg('The weight must be a positive numeric value');  
163:            waitfor(h);  
164:            return;  
165:       end;  
166:    
167:       % We will want the person's sex to be displayed as a string rather  
168:       % than a numeric value  
169:       if sex == 1  
170:            sex = 'male';  
171:       end;  
172:       if sex == 2  
173:            sex = 'female';  
174:       end;  
175:    
176:       % Create the structure  
177:       temp_person_structure.firstname = firstname;  
178:       temp_person_structure.lastname = lastname;  
179:       temp_person_structure.sex = sex;  
180:       temp_person_structure.height = height;  
181:       temp_person_structure.weight = weight;  
182:    
183:       % Add that structure to the main window's appdata  
184:       setappdata(obj.root_window_handle, 'temp_person_structure', ...  
185:            temp_person_structure);  
186:    
187:       % Close this window  
188:       obj.close_window([], []);  
189:  end;  
190:    
191:  end % End of methods section  
192:    
193:  end  

The constructor InputWindow takes two arguments. The first is a handle to the main window. As mentioned before we use this to get access to that window and its properties. In this case, we are interested in appdata. In more complicated applications, you may be interested in other attributes. The second is varargin which, as mentioned, is used to determine if we are in add mode or edit mode. The bulk of the constructor just draws the window and populates it with the needed widgets. Since this window has only two buttons, I defined the callback functions when I created the widgets rather than having a separate callback section as I did on the main window.

The close window function is self-explanatory -- it just deletes the window.

The return_structure function does some simple error checking on the fields: it makes sure they are populated, the numbers are entered as numeric values, and heights and weights are positive numbers. Then it creates a structure and inserts it into the main window's appdata.

Because I've split the code up into several snippets, I've posted links to both files below. So that's it. Pretty simple.

InputWindow.m
listbox_demo.m

No comments:

Post a Comment