Sunday, December 17, 2017

MATLAB GUI Programming: Dynamically Sizing Text Uicontrols

A number of years ago, I was putting together a battery simulation tool written in MATLAB.  Cycling parameters could vary depending on the mode (charge, discharge, rest, or loop) and which of several parameters were to be held constant such as current, power, voltage, or some function of those.  Because of the different possibilities, text labels that described which value should go in a given entry box would change.

Maybe a simple example will show what I mean.  Here is a quick script to produce a window with a popup menu allowing the user to select one of three questions.  The code then writes the question as a text uicontrol and places an edit uicontrol next to it.  One question is long containing many characcters, the other is of medium length, and the last is short.


function static_labels
clear all; close all; clc

f = figure();

%  List of question that will appear to the left of an edit uicontrol.
q = {'How much wood could a woodchuck chuck if a woodchuck could chuck wood? ', ...
     'What is the airspeed velocity of an unladen swallow? ', ...
     'What is the meaning of the universe? '};

%  Create the popup uicontrol and make its entries out list of questions
%  The menu's callback function changes the text in the text uicontrol
%  defined below
pulldown_menu = uicontrol('Parent', f, ...
 'Style', 'popupmenu', ...
 'String', {'Question 1', 'Question 2', 'Question 3'}, ...
 'Callback', @change_question, ...
 'Position', [10, 400, 100, 20]);

%  Create a string variable str which contains the question corresponding
%  to which value is currently set in the popup menu
switch pulldown_menu.Value
 case 1
  str = q{1};
 case 2
  str = q{2};
 case 3
  str = q{3};
end

%  Create a text label whose string is set to str.
label = uicontrol('Parent', f, ...
 'Style', 'text', ...
 'String', str, ...
 'HorizontalAlignment', 'left', ...
 'Position', [10, 300, 400, 17]);

%  Create an entry widget positioned so that it is just to the right of the
%  longest question.  Its position is static not changing if the question
%  changes.
entry = uicontrol('Parent', f, ...
 'Style', 'edit', ...
 'Position', [420, 300, 50, 20]);

%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%  Callback function  %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%
function change_question(source, events)
 %  Get the selected field of the pulldown menu
 N = source.Value;

 %  Assign text depending on which value was chosen
 switch N
  case 1
   str = q{1};
  case 2
   str = q{2};
  case 3
   str = q{3};
 end

 %  Adjust the string of the text uicontrol
 label.String = str;

end

end

Figure 1 shows the pulldown menu.  Question 1, the longest, is selected and an edit uicontrol is just to the right of the text.

Figure 1:  Pulldown menu selects question displayed below.

We can see what happens when the question is changed.  This is shown below in Figs 2a and 2b.  The edit uicontrol stays in the same place while the length of the text changes.  Depending on the project and complexity of the interface being built, this may lead to a less than aesthetic-looking design.


Figure 2a:  Entry edit box next to a long block of text


Figure 2b:  Entry widget is in the same place, though
the length of text in the question is shorter


In my real-world case, there were perhaps 15 different possible configurations, and since this was developed over a period of several months, I handled them manually.  But I always found this unsatisfying.

Unfortunately, MATLAB itself doesn't have a way to estimate the size of a uicontrol based on the text contained.  We need first to get the handle to the Java object using the utility findjobj.   Then, the workaround for the sizing issue is to use the getPreferredSize method.  This method essentially gives hints to the layout manager as to the best size to make the widget

So I added a function,  calculate_widget_positions, that gets the preferred size of the text uicontrol, and returns this number along with the position of the edit control calculated from the text size plus some amount of padding.  This function is shown below.


function [width_label, x_entry] = calculate_widget_positions()

        %  Get the text uicontrol suggested width
        width_label = jlabel.getPreferredSize().getWidth();

        %  Add in some padding.  The edit control starts this amount after
        %  the text control ends
        padding = 15;

        %  Horizontal starting position of the edit uicontrol
        x_entry = label.Position(1) + width_label + padding;
end

The callback function is then modified to adjust the position of the edit uicontrol after setting the text.


function change_question(source, events)
 %  Get the selected field of the pulldown menu
 N = source.Value;

 %  Assign text depending on which value was chosen
 switch N
  case 1
   str = q{1};
  case 2
   str = q{2};
  case 3
   str = q{3};
 end

 %  Adjust the string of the text uicontrol
 label.String = str;

 drawnow;
 [width_label, x_entry] = calculate_widget_positions()
 p = label.Position;
 p(3) = width_label;
 label.Position = p;

 p = entry.Position;
 p(1) = x_entry;
 entry.Position = p;

end

Note also the use of the drawnow function after changing the string of the text control.  This needs to be here so that MATLAB is forced to draw the widget. We can then get its new size for positioning the entry box.   Perhaps if I were a little more clever in coding this, we could avoid this step, but this is more a proof-of-concept thing than something I am about to drop into production code.  Now, for example when the third question is selected, the resulting window looks like Fig. 3 below.

Figure 3.  The position of the edit entry box is determined from the size of the preceding text.

So though MATLAB doesn't naively have a way to address this issue, we can work around it by fiddling under the hood with the Java methods that the uicontrols themselves use.

For the sake of completeness, here is the full code including the dynamic positioning.


function dynamic_labels
clear all; close all; clc

f = figure();

%  List of question that will appear to the left of an edit uicontrol.
q = {'How much wood could a woodchuck chuck if a woodchuck could chuck wood? ', ...
     'What is the airspeed velocity of an unladen swallow? ', ...
     'What is the meaning of the universe? '};

%  Create the popup uicontrol and make its entries out list of questions
%  The menu's callback function changes the text in the text uicontrol
%  defined below
pulldown_menu = uicontrol('Parent', f, ...
 'Style', 'popupmenu', ...
 'String', {'Question 1', 'Question 2', 'Question 3'}, ...
 'Callback', @change_question, ...
 'Position', [10, 400, 100, 20]);

%  Create a string variable str which contains the question corresponding
%  to which value is currently set in the popup menu
switch pulldown_menu.Value
 case 1
  str = q{1};
 case 2
  str = q{2};
 case 3
  str = q{3};
end

%  Create a text label whose string is set to str.
label = uicontrol('Parent', f, ...
 'Style', 'text', ...
 'String', str, ...
 'HorizontalAlignment', 'left', ...
 'Visible', 'on', ...
 'Position', [10, 300, 100, 17]);

%  Get the handle to the Java object
jlabel = findjobj(label);

%  Create an entry widget positioned so that it is just to the right of the
entry = uicontrol('Parent', f, ...
 'Style', 'edit', ...
 'Visible', 'on', ...
 'Position', [420, 300, 50, 20]);

%  Set the initial position of the edit uicontrol
[width_label, x_entry] = calculate_widget_positions();
p = label.Position;
p(3) = width_label;
label.Position = p;

p = entry.Position;
p(1) = x_entry;
entry.Position = p;

%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%  Callback function  %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%
function change_question(source, events)
 %  Get the selected field of the pulldown menu
 N = source.Value;

 %  Assign text depending on which value was chosen
 switch N
  case 1
   str = q{1};
  case 2
   str = q{2};
  case 3
   str = q{3};
 end

 %  Adjust the string of the text uicontrol
 label.String = str;

 drawnow;
 [width_label, x_entry] = calculate_widget_positions()
 p = label.Position;
 p(3) = width_label;
 label.Position = p;

 p = entry.Position;
 p(1) = x_entry;
 entry.Position = p;

end

function [width_label, x_entry] = calculate_widget_positions()
 %  Get the text uicntol suggested width
 width_label = jlabel.getPreferredSize().getWidth();

 %  Add in some padding.  The edit control starts this amount after
 %  the text control ends
 padding = 15;

 %  Horizontal starting position of the edit uicontrol
 x_entry = label.Position(1) + width_label + padding;
end

end

No comments:

Post a Comment