Matlab GUI: Numeric input for serious work
Design goals
Need GUI element with the following properties:
- Accepts numeric input between a min and max
- Changes value on arrow up/arrow down
- The value change should depend on the caret position
- Bad input makes string and number default to suitable value.
- Doesn't waste screen real estate.
Why a JSpinner doesn't cut it
You can easily create Java Swing objects in your GUI, and the JSpinner is pretty close to what I need. But: To my knowledge, it does not support incrementing/decrementing the value based on the current caret position. The caret always jumps to the beginning of the string.
Example:
f = figure;
jSpinner = javaObjectEDT('javax.swing.JSpinner');
javacomponent(jSpinner);
About "editfloats"
I hacked on edittext fields until they became a bit like JSpinner, but without the superfluous arrows on the right-hand side. Arrow up/arrow down increments/decrements the digit to the right of the caret position. Because the hack is based on edittexts, I call these fields editfloats. The format, minimum, maximum, default number, and default string properties are stored inside the 'UserData' property of the edittext.
The callbacks I needed are not accessible with the Matlab handles. Instead, I use the handles of the underlying Java swing components. Yair Altman wrote an excellent FindJObj function for finding those handles.
I use these edifloat elements in all my GUIs that I had made earlier
(using GUIDE). It's just as easy to put them into GUIs that were
created programmatically (without GUIDE). Caveat: Using java handles
often needs the GUI to be
visible. Therefore I
placed the FindJObj function in GUIDE's autocreated ..._OutputFcn
,
which is called after the GUI is made visible. If you instead use the
..._OpeningFcn
(called when the figure is invisible), the figure will
be made visible/invisible multiple times, and figure creation will take
a long time.
Preparations for creating editfloats
I moved the meaty stuff into external function files, because many of my GUIs and GUI elements will share this code.
The function editfloat_setup.m
takes the handle of an edittext and
sets up the appropriate Callbacks to make it an editfloat.
function floatstr_edit_setup(h, options)
% Takes the handle of an edit_text, and sets up callbacks such that it
% becomes a float_edit box, with arrowup/arrowdown functionality.
% Needs an options struct, which is attached to h as user_data. Must
% contain fields:
% - .format Format of the floating point number
% - .min Minimum number
% - .max Maximum Number
% - .default_num Default Number (for bad input)
% - .default_str Default String (for bad input)
set(h, 'UserData', options)
set(h, 'FontName', 'Courier', 'FontSize', 10)
% Sets callback, calls it to make sure string is in suitable format
set(h,'Callback',@editfloat_Callback);
editfloat_Callback(h, 0)
% Note: 'persistent' fails when the GUI is closed and started again. You need to
% restart Matlab in this case. I stopped using 'persistent'.
% Sets up Callback of underlying Java Swing object.
jh = findjobj(h, 'nomenu'); % external function by Yair Altman.
set(jh,'KeyPressedCallback', ...
{@editfloat_KeyPressedCallback_Java, h});
Because the user is explicity allowed to enter any text into the
editfloat field, we need a function editfloat_Callback
to make sure
the user input is valid.
function editfloat_Callback(hObject,~)
% Called on "commit" event ("Enter" and "LostFocus" or so).
% Parses the element's input and updates the string with parsed input.
edit_str = get(hObject, 'String');
user_data = get(hObject, 'UserData');
[~, clean_str] = editfloat_str_parser(edit_str, user_data);
set(hObject, 'String', clean_str)
Arrow up/arrow down functionality happens inside the
editfloat_KeyPressedCallback_Java Callback
. This checks if the key
pressed was an arrow up or arrow down key, and then
increments/decrements the current value by a value that depends on the
caret position.
function editfloat_KeyPressedCallback_Java(jhObject, event, hObject)
% Called on each button press inside edit text.
% Increments/decrements the value at the current caret position.
% Needs the following fields in the "user_data" struct:
% - .format Format of the floating point number
% - .min Minimum number
% - .max Maximum Number
% - .default_num Default Number (for bad input)
% - .default_str Default String (for bad input)
keynum = get(event, 'keyCode');
if keynum==38 || keynum==40 % arrow up or arrow down
user_data = get(hObject, 'UserData');
% format = user_data.format;
caret_position = get(jhObject, 'CaretPosition') + 1; %matlab indexing
current_str = get(jhObject, 'Text'); %using jhObject, because during edition, hObject contains old stuff
[old_num, parsed_str] = editfloat_str_parser(current_str, user_data);
if caret_position > length(parsed_str)
% we're at the end of the string. do nothing
return
end
if length(parsed_str) ~= length(current_str)
% User was during input. Update field, return
% Update: In the current version, this is impossible, I think
set(hObject, 'String', parsed_str)
drawnow;
return
end
% Prepare a delta_str -> delta_num from the given string and the caret
% position.
numeric_period = int16('.');
delta_str = parsed_str;
delta_str(parsed_str ~= numeric_period) = '0'; % -> 00000.00000 or so
new_num = old_num;
if ~strcmp(delta_str(caret_position), '.')
delta_str(caret_position) = '1'; % -> 00100.00000 or so
delta_num = str2double(delta_str);
if keynum == 38 % up
new_num = new_num + delta_num;
elseif keynum == 40 %down
new_num = new_num - delta_num;
end
end
new_str = sprintf(user_data.format, new_num);
[~, new_str] = editfloat_str_parser(new_str, user_data);
set(hObject, 'String', new_str) %using hObject here, because jhObject led to weird errors
drawnow; % otherwise set() takes place after the setCaretPosition!
% Set correct caret position again. Adjust for possibly new string
% length.
length_difference = length(new_str) - length(current_str);
jhObject.setCaretPosition(caret_position - 1+length_difference);
drawnow; % I was getting more errors. Maybe this helps?
end
The fourth function you need is one that parses a string (usually the content of editfloat) and returns the number that the string represents together with a clean version of that string.
function [num, clean_str] = editfloat_str_parser(float_str, options)
% Converts float_str to a number. Returns this number as
% numeric and as a string.
%
% The input "options" must be a struct that contains the fields:
% - .format Output format of the number string.
% - .min Minimum number
% - .max Maximum Number
% - .default_num Default Number (for bad input)
% - .default_str Default String (for bad input)
% Example output_format: '%016.6f'
% - 0 padded to field length
% - 16 field length (including decimal point! -> 15 digits)
% - 6 digits after the decimal points
% - floating point
no_commas = strrep(float_str, ',', '.'); % having "," as decimal is fine
num = str2double(no_commas);
if isnan(num) % if contained text
num = options.default_num;
clean_str = options.default_str;
return
end
num = real(num); % only keep the real part
if num < options.min num = options.min; elseif num > options.max
num = options.max;
end
clean_str = sprintf(options.format, num);
Adding an editfloat to a GUI
- Save each function outlined above into its own file and put those file into Matlab's PATH.
- Using
GUIDE
, create an edittext field. (Mine has the tag ("name")FreqOffset_edittext
.) - In the GUI's
..._OutputFcn
, calleditfloat_setup
on the edittext.
% --- Outputs from this function are returned to the command line.
function varargout = Advanced_THEs_Plot_GUI_OutputFcn(hObject, eventdata, handles)
% varargout cell array for returning output args (see VARARGOUT);
% This function is executed just after the GUI is made visible. All the
% java swing object stuff needs the object to be visible, which is why the
% following code bits are here.
% Note:
% Makes edittext into editfloats by setting up the right callbacks (for
% arrowup/arrowdown functionality)
% FreqOffset field
editfloat_options.format = '%12.3f';
editfloat_options.min = 0;
editfloat_options.max = 100e6-1e-3;
editfloat_options.default_num = 0;
editfloat_options.default_str = '0.000';
editfloat_setup(handles.FreqOffset_edittext, editfloat_options)
You're done!