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.

editfloat in action

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

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, call editfloat_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!

Leave a Reply

Your email address will not be published. Required fields are marked *