CitizenInsane
CitizenInsane

Reputation: 4855

Creating custom and reusable user interface control

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

Answers (2)

CitizenInsane
CitizenInsane

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

Sam Roberts
Sam Roberts

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

Related Questions