Reputation: 4855
In some GUI of my own I have created many controls (using uicontrol
) to allow users configuring a filter used during later processing stage.
The filter edition consist in a combobox to select the filter type, plus many editboxes that update upon selected filter type and many callbacks to react upon user inputs.
I now need to add this filter selection in another GUI, and, of course, I don't want to copy-paste all the logic I have already done and would prefer to create some custom control that I can reuse as easily as:
filterEditor = uifilter('Parent', gcf);
set(filterEditor, 'FilterDescription', 'Cylinder (r = 45 cm, h = 1 m)');
set(filterEditor, 'Callback', @onFilterEditModified);
Is there a standard procedure to create custom "uicontrol
" objects ? I searched the internet and matlab's documentation but did not find any good pointer yet ...
Currently I'm thinking to create custom class deriving from hgsetget
:
classdef uifilter < hgsetget
properties
% Local properties
FilterDescription;
Callback;
end
properties(SetAccess=private, GetAccess=private)
% Internal controls
globalContainer;
comboFilterType;
edit1;
end
methods
function [this] = uifilter(varargin)
% Create a global `uicontainer` to hold my controls
[localPVpairs, genericPVpairs] = separatePVpairs(varargin{:});
this.container = uicontainer(genericPVpairs{:});
% Create my own controls and logic
this.comboFilterType = uicontrol('Parent', this.container, ...);
this.edit1 = ...
end
end
end
in order to mimic uicontrol
behavior (set
, get
, findobj
, etc...) but maybe there's more standard approach or some base class other than hgsetget
to start from (i.e. some base class with Visible
, Enable
, HitTest
etc... already defined with default implementation)?
Upvotes: 2
Views: 767
Reputation: 4855
Coming back on the issue, a very simple approach (equivalent to define a custom class inheriting from hgsetget
or possibly some uicontrolbase
class to have default behavior for Enable
, Position
, etc...) is to create a class inheriting from uiextras.Container
in the GUI Layout toolbox.
Indeed this class is fully equivalent to the idea of having a uicontrolbase
class. It exposes a protected UIContainer
property which is the panel in which to put all child elements, so it is very easy to build reusable compound component from it:
classdef uimyfilter < uiextras.Container
%% --- Exposed properties
% NB: Can be accessed with set/get routines
properties(Dependent, Transient)
FilterDescription;
Callback;
end
methods
{% ... own custom set/get logic for exposed properties ... %}
end
%% --- Lifetime
methods
function [this] = uimyfilter(varargin)
% Consume or init local properties from varargin list
[c, otherPvPairs] = uimyfilter.extractOrInitPvPairs(varargin, { ...
'FilterDescription', @()'Cylinder (r = 10 cm, h = 42 cm)'; ...
'Callback', @()[]; ...
});
% Call superclass with other pv pairs
[email protected](otherPvPairs{:});
% Build interface
grid = uiextras.Grid('Parent', this.UIContainer, 'Spacing', 5, 'Padding', 5);
c.handles.cbFilterType = uicontrol('Parent', grid, 'Style', 'Popup', 'String', { 'Cylinder', 'Sphere' }, 'Callback', @(s,e)onFilterTypeChanged(this,s,e));
uiextras.Empty('Parent', grid);
c.handles.cardFilterParams = uiextras.CardPanel('Parent', grid);
uiextras.Empty('Parent', grid);
set(grid, 'ColumnSizes', [90, -1]);
set(grid, 'RowSizes', [23, -1]);
uicontrol('Parent', c.handles.cardFilterParams, 'Style', 'Text', 'String', '... todo: params for cylinder ...', 'BackgroundColor', 'r');
uicontrol('Parent', c.handles.cardFilterParams, 'Style', 'Text', 'String', '... todo: params for sphere ...', 'BackgroundColor', 'r');
% Store local properties and handles for later calls
this.state = c;
% Init Gui
this.refresh();
end
end
%% --- Internal logic
methods(Access=private)
function [] = refresh(this)
set(this.state.handles.cardFilterParams, 'SelectedChild', get(this.state.handles.cbFilterType, 'Value'));
end
function [] = onFilterTypeChanged(this, s, e) %#ok
this.refresh();
if (~isempty(this.state.Callback)),
this.state.Callback(this);
end
end
end
methods(Access = protected)
function [] = redraw(this) %#ok
end
end
properties(GetAccess=private, SetAccess=private)
state;
end
%% --- Helpers
methods(Static, Access=protected)
function [c, otherPvPairs] = extractOrInitPvPairs(pvPairs, consumeDescriptor)
% Check arguments
if (nargin < 2),
error('Not enough input arguments.');
end
if (~isempty(consumeDescriptor) && ...
(~iscell(consumeDescriptor) || ~ismatrix(consumeDescriptor) || ...
~iscellstr(consumeDescriptor(:, 1)) || ~all(cell2mat(cellfun(@(x)isa(x, 'function_handle'), consumeDescriptor(:,2), 'UniformOutput', false)))))
error('Invalid descriptor for properties to consume.');
end
if (~iscell(pvPairs) || (~isvector(pvPairs) && ~isempty(pvPairs)) || (length(pvPairs(1:2:end)) ~= length(pvPairs(2:2:end))) || ~iscellstr(pvPairs(1:2:end)))
error('Invalid list or property names/values pairs.');
end
% Consume local properties
c = struct();
otherNames = pvPairs(1:2:end);
otherValues = pvPairs(2:2:end);
for ki = 1:size(consumeDescriptor, 1),
pname = consumeDescriptor{ki,1};
pinit = consumeDescriptor{ki,2};
idx = strcmpi(otherNames, pname);
if (isempty(idx)),
c.(pname) = pinit();
elseif (isscalar(idx)),
c.(pname) = otherValues{idx};
otherNames(idx) = []; otherValues(idx) = [];
else
error('Property `%s` appears more than once.', pname);
end
end
% Recompose other pv
otherPvPairs = cell(1, 2*length(otherNames));
otherPvPairs(1:2:end) = otherNames(:);
otherPvPairs(2:2:end) = otherValues(:);
end
end
end
Exposed properties and internal logic is of course fully tied to the compound component to have anyway, building interface is as simple as adding uicontrol
or uiextras.(...)
objects to this.UIContainer
.
PS: For R2014b and later, you have to inherit from uix.Container
in GUI Layout toolbox for HG2 anyway the idea is similar.
Upvotes: 0
Reputation: 24127
I think this would be the right approach.
To do it properly, you'll probably need to implement your own set
and get
methods for each uicontrol
property. These set
and get
methods will mostly just pass through values to and from the underlying uicontrol
. You can probably get away without implementing some of the less-used properties in your first draft (e.g. FontAngle
), adding them in as necessary and just living with the uicontrol
defaults until then.
In some cases though, they'll need to do more, and you'll need to exercise a bit of care when you implement things such as set
for the Parent
property (it may need to destroy the original uicontrol and create a new one for the new parent). You'll also need to exercise care when implementing set
for the Position
and Units
properties - for normal uicontrol
s they interact in quite a complicated way, and I think the outcome can sometimes depend on which is set first.
I'd also suggest that for the internal properties, as well as setting them to private
, you might also set them to Hidden
, to prevent users from trying to meddle with them.
One last point - I think, from some of your other questions, that you're making use of GUI Layout Toolbox. I haven't thought it through much, but you might need to think ahead about whether you need to do anything special to enable that.
Upvotes: 2