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 |
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 |
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