Code covered by the BSD License  

Highlights from
GridBagLayout

GridBagLayout

by

 

13 Feb 2009 (Updated )

A MATLAB class which controls layout and resize of a figure/uipanel/uicontainer.

Editor's Notes:

This file was selected as MATLAB Central Pick of the Week

layout.GridBagLayout
classdef GridBagLayout < layout.AbstractLayout
    %GridBagLayout - Constructs a GridBagLayout layout manager.
    %   layout.GridBagLayout(H) Constructs a GridBagLayout object to manage
    %   the Handle Graphics object H.  H can be a figure, uicontainer or
    %   uipanel.
    %
    %   GridBagLayout methods:
    %       add            - Add a component to the layout.
    %       clean          - Remove excess spacing.
    %       insert         - Insert a row or column of empty space.
    %       remove         - Remove a component.
    %       setConstraints - Set various spacing constraints.
    %       update         - Force an update.
    %
    %   GridBagLayout public fields:
    %       VerticalGap       - Vertical gap between components.
    %       HorizontalGap     - Horizontal gap between components.
    %       VerticalWeights   - Vertical weights used for spacing.
    %       HorizontalWeights - Horizontal weights used for spacing.
    %       Grid              - Stores all the components in a matrix.
    %       Panel             - Stores the parent of the components.
    %
    %   % Example
    %       hl = layout.GridBagLayout(figure);
    %       hl.add(uicontrol, 1, [1 2], 'Fill', 'Both');
    %       hl.add(uicontrol, 2, 1, 'Fill', 'Horizontal', ...
    %           'Anchor', 'North');
    %       hl.add(uicontrol, 2, 2, 'Fill', 'Vertical', ...
    %           'Anchor', 'West');
    
    properties
        
        %VerticalGap The vertical gap between widgets.  This is the
        %   uniform gap between the widgets and the widgets and the edges
        %   of the frame.  To specify addition gap for specific widgets use
        %   the IPad or Inset constraints.
        VerticalGap = 0;
        
        %HorizontalGap The horizontal gap between widgets.  This is the
        %   uniform gap between the widgets and the widgets and the edges
        %   of the frame.  To specify addition gap for specific widgets use
        %   the IPad or Inset constraints.
        HorizontalGap = 0;
        
        %VerticalWeights The weights used to calculate where to use extra
        %   pixels among the rows.  The higher the weight for a specific
        %   row results in more extra pixels being given to the widgets in
        %   that row.  This value is always of length equal to size(H.Grid,
        %   1).  If additional rows are added to the Grid a value of 0 will
        %   be added to the end of the weights.  If the Grid is reduced,
        %   extra weights will be discarded.
        VerticalWeights = [];
        
        %HorizontalWeights The weights used to calculate where to use extra
        %   pixels among the columns.  The higher the weight for a specific
        %   column results in more extra pixels being given to the widgets
        %   in that column.  This value is always of length equal to
        %   size(H.Grid, 2).  If additional columns are added to the Grid a
        %   value of 0 will be added to the end of the weights.  If the
        %   Grid is reduced, extra weights will be discarded.
        HorizontalWeights = [];
    end
    
    properties (SetAccess = protected)
        
        %Grid A matrix storing all of the widgets parented to the specified
        %   container.  NaN values are used for empty cells.
        Grid;
    end
    
    methods
        
        function this = GridBagLayout(hPanel, varargin)
            %GridBagLayout   Construct the GridBagLayout class.
            
            this@layout.AbstractLayout(hPanel);
            
            for indx = 1:2:length(varargin)
                this.(varargin{indx}) = varargin{indx+1};
            end
        end
        
        function add(this, h, row, col, varargin)
            %add Add a component to the layout manager.
            %   add(H, HCOMP, ROW, COL) Add the HG object HCOMP to the
            %   layout manager H in the row (ROW) and column (COL)
            %   specified.  ROW and COL must be scalars or vectors of
            %   length two.  When specified as a scalar that exact position
            %   in the grid is used.  When specified as a vector of length
            %   two, they are used to determine the limits of the row or
            %   column span, e.g. a single component can be placed over
            %   multiple cells in the grid.
            %
            %   add(H, ..., CONSTRAINT1, VALUE1, etc.) Optional constraints
            %   can be passed as additional arguments.  See setConstraints
            %   for the complete list of constraints.
            %
            %   Children of the managed container are not automatically
            %   added to the layout.  They must be explicitly added via the
            %   add method.
            %
            %   See also setConstraints, remove.
            
            error(nargchk(4, inf, nargin, 'struct'));
            
            add@layout.AbstractLayout(this, h, row, col);
            
            g = this.Grid;
            
            g(min(row):max(row), min(col):max(col)) = h;
            
            if ~isappdata(h, this.CONSTRAINTSTAG)
                setappdata(h, this.CONSTRAINTSTAG, layout.GridBagConstraints);
            end
            
            % Convert any added zeros to NaN.
            g(g == 0) = NaN;
            
            this.Grid = g;
            
            if nargin > 4
                setConstraints(this, row, col, varargin{:});
            end
        end
        
        function insert(this, type, indx)
            %insert   Insert a row or a column at an index.
            %   insert(H, TYPE, INDX) Insert either a 'row' or 'column'
            %   (TYPE) of NaNs into the Grid at the row or column INDX.
            %
            %   See also add, remove.
            
            g = this.Grid;
            
            [rows cols] = size(g);
            
            switch lower(type)
                case 'row'
                    if indx > rows + 1
                        g = [g; NaN(indx-rows, cols)];
                    else
                        g = [g(1:indx-1,:); NaN(1, cols); g(indx:end,:)];
                    end
                case 'column'
                    if indx > cols + 1
                        g = [g NaN(rows, indx-cols)];
                    else
                        g = [g(:, 1:indx-1) NaN(rows, 1) g(:, indx:end)];
                    end
            end
            
            this.Grid = g;
        end
        
        function remove(this, indx, jndx)
            %remove   Remove the handle from the manager.
            %   remove(H, HCOMP) Removes the specified HG object HCOMP from
            %   the layout manager H.
            %
            %   remove(H, ROW, COL) Removes the object stored in the Grid
            %   at the ROW and COL specified.
            %
            %   See also add, insert.
            
            g = this.Grid;
            
            if nargin == 2
                h = indx;
                
                g(g == h) = NaN;
                
                % Reset the grid and clean up the listeners vector.
                this.Grid = g;
            else
                remove(this, g(indx, jndx));
            end
        end
        
        function clean(this)
            %clean   Remove Trailing rows/columns with only NaN values.
            %
            %   See also add, insert, remove.
            
            g = this.Grid;
            
            [rows cols] = size(g);
            
            % Clean up any extra rows in the grid.
            indx = rows;
            while indx > 0 && all(isnan(g(indx,:)))
                g(indx,:) = [];
                indx      = indx-1;
            end
            
            indx = cols;
            while indx > 0 && all(isnan(g(:,indx)))
                g(:,indx) = [];
                indx      = indx-1;
            end
            
            this.Grid = g;
        end
        
        function setConstraints(this, row, col, varargin)
            %setConstraints   Set the constraints for the specified component.
            %   setConstraints(HLAYOUT, LOCATION, PARAM1, VALUE1, etc.) Set the
            %   constraints for the component in LOCATION.
            %
            %   SETCONSTRAINTS(HLAYOUT, LOCATION, 'default') when the string 'default'
            %   is passed to SETCONSTRAINTS the stored constraints are reset to their
            %   default values.
            %
            %   Parameter Name      Valid Values        Default
            %   MinimumHeight       Positive Numbers    20
            %   MinimumWidth        Positive Numbers    20
            %   PreferredHeight     Positive Numbers    20
            %   PreferredWidth      Positive Numbers    20
            %   MaximumHeight       Positive Numbers    inf
            %   MaximumWidth        Positive Numbers    inf
            %   IPadX               Real Numbers        0
            %   IPadY               Real Numbers        0
            %   LeftInset           Real Numbers        0
            %   RightInset          Real Numbers        0
            %   TopInset            Real Numbers        0
            %   BottomInset         Real Numbers        0
            %   Fill                'None'              'None'
            %                       'Horizontal'
            %                       'Vertical'
            %                       'Both'
            %   Anchor              'Center'            'Center'
            %                       'Northwest'
            %                       'North'
            %                       'Northeast'
            %                       'East'
            %                       'Southeast'
            %                       'South'
            %                       'Southwest'
            %                       'West'
            
            error(nargchk(3, inf, nargin, 'struct'));
            
            % Do not error out if no constraints are passed.
            if nargin < 5
                return;
            end
            
            % Get the component from the subclass.
            hComponent = getComponent(this, row, col);
            
            % Get the old constraints.
            ctag           = this.CONSTRAINTSTAG;
            oldConstraints = getappdata(hComponent, ctag);
            
            if strcmpi(varargin{1}, 'default')
                
                % If the pv pairs is just 'default' remove all constraints.
                if ~isempty(oldConstraints)
                    rmappdata(hComponent, ctag);
                end
            else
                
                % If there are no old constraints, create a new object.
                if isempty(oldConstraints)
                    c = GridBagConstraints(varargin{:});
                    setappdata(hComponent, ctag, c);
                else
                    
                    % If there are old constraints, just set the object with the new
                    % constraints, don't throw away any old ones.
                    for indx = 1:2:length(varargin)-1
                        oldConstraints.(varargin{indx}) = varargin{indx+1};
                    end
                end
            end
            
            this.Invalid = true;
            
            % Force a call to update.
            update(this);
        end
    end
    
    methods (Access = protected)
        
        function component = getComponent(this, row, col)
            
            g = this.Grid;
            
            component = [];
            
            if max(row) <= size(g, 1) && max(col) <= size(g, 2)
                for indx = 1:length(row)
                    for jndx = 1:length(col)
                        if ~isnan(g(row(indx), col(jndx)))
                            component = [component; g(row(indx), col(jndx))]; %#ok<AGROW>
                        end
                    end
                end
            end
            component = unique(component);
        end
        
        function [m, n] = getComponentSize(this, indx, jndx)
            %GETCOMPONENTSIZE   Get the componentsize.
            
            g = this.Grid;
            
            h = g(indx, jndx);
            
            if isnan(h)
                m = 0;
                n = 0;
            else
                m = find(g(:, jndx) == h, 1, 'last' ) - indx + 1;
                n = find(g(indx,:) == h, 1, 'last' )  - jndx + 1;
            end
            
            if nargout < 2
                m = [m n];
            end
        end
        
        function layout(this)
            %LAYOUT   Layout the container.
            
            grid = this.Grid;
            if isempty(grid)
                return;
            end
            
            panelpos    = this.PanelPosition;
            ctag        = this.CONSTRAINTSTAG;
            [rows cols] = size(grid);
            
            hg = this.HorizontalGap;
            vg = this.VerticalGap;
            vw = this.VerticalWeights;
            hw = this.HorizontalWeights;
            
            % If all of the weights are zero convert them all to ones so that we can do
            % easier math on them, because 0/sum([0 0 0]) doesn't work.
            if all(hw == 0)
                hw = ones(size(hw));
            end
            if all(vw == 0)
                vw = ones(size(vw));
            end
            
            minheight = zeros(size(grid));
            minwidth  = minheight;
            
            % Get all the heights
            for indx = 1:rows
                for jndx = 1:cols
                    if ishghandle(grid(indx,jndx))
                        
                        [n, m] = getComponentSize(this, indx, jndx);
                        
                        hC = getappdata(grid(indx,jndx), ctag);
                        if isempty(hC)
                            minh = 20;
                            minw = 20;
                        else
                            % Each grid location has a minimum height of the insets +
                            % the minimum dimension of the component.
                            minh = (hC.MinimumHeight+hC.BottomInset+hC.TopInset)/n;
                            minw = (hC.MinimumWidth+hC.LeftInset+hC.RightInset)/m;
                        end
                        
                        % Remove the control from the grid.
                        grid(indx:indx+n-1,jndx:jndx+m-1) = NaN;
                        
                        minheight(indx:indx+n-1,jndx) = minh;
                        minwidth(indx,jndx:jndx+m-1)  = minw;
                    end
                end
            end
            
            % The minimum height for each row is the max of all the minimum heights in
            % each column.  Vice-versa for the minimum width.
            minheight = max(minheight, [], 2);
            minwidth  = max(minwidth,  [], 1);
            
            % Calculate the final widths by determining the number of leftover pixels
            % and dividing them according to the weights property for the given
            % dimension.
            if cols == 1
                
                % If the is just one column, the width is just the panel width minus
                % two horizontal gaps
                widths = panelpos(3)-2*hg;
            else
                
                % Subtract the sum of the minimum widths from the panel width and then
                % one extra horizontal gap so that we have one on each side.
                leftoverwidth  = panelpos(3)-sum(minwidth)-hg*(cols+1);
                widths = minwidth+leftoverwidth*hw/sum(hw);
            end
            
            if rows == 1
                
                % If there is just one row, the height is the panel height minus two
                % vertical gaps.
                heights = panelpos(4)-2*vg;
            else
                
                % Subtract the sum of the minimum heights from the panel height and
                % then one extra vertical gap so that we have one on each side.
                leftoverheight = panelpos(4)-sum(minheight)-vg*(rows+1);
                heights = minheight+leftoverheight*vw/sum(vw);
            end
            
            grid = this.Grid;
            for indx = 1:rows
                for jndx = 1:cols
                    if ishghandle(grid(indx,jndx))
                        
                        [n m] = getComponentSize(this, indx, jndx);
                        
                        % Calculate the grid position given the grids width and height.
                        gridpos = [ ...
                            sum(widths(1:jndx-1))+hg*jndx+1 ...
                            panelpos(4)-sum(heights(1:indx+n-1))-vg*(indx+n-1)+1 ...
                            sum(widths(jndx:jndx+m-1))+hg*(m-1) ...
                            sum(heights(indx:indx+n-1))+vg*(n-1)];
                        
                        if isappdata(grid(indx, jndx), ctag)
                            hC = getappdata(grid(indx, jndx), ctag);
                            
                            % Add the insets to the grid position.
                            gridpos = gridpos + [...
                                hC.LeftInset ...
                                hC.BottomInset ...
                                -hC.LeftInset-hC.RightInset ...
                                -hC.BottomInset-hC.TopInset];
                            
                            % Get the final width and height from the Fill and
                            % Preferred Dimension constraints.
                            pos = gridpos;
                            switch lower(hC.Fill)
                                case 'none'
                                    % Start with an anchor of southwest
                                    if pos(3) > hC.PreferredWidth
                                        pos(3) = hC.PreferredWidth;
                                    end
                                    if pos(4) > hC.PreferredHeight
                                        pos(4) = hC.PreferredHeight;
                                    end
                                case 'horizontal'
                                    if pos(4) > hC.PreferredHeight
                                        pos(4) = hC.PreferredHeight;
                                    end
                                case 'vertical'
                                    % Start with an anchor of southwest
                                    if pos(3) > hC.PreferredWidth
                                        pos(3) = hC.PreferredWidth;
                                    end
                                case 'both'
                                    % This is a no-op, let it fill the whole area.
                            end
                            
                            % Get the x and y from the anchor.
                            switch lower(hC.Anchor)
                                case 'southwest'
                                    % NO OP, already at the origin of the grid area.
                                case 'west'
                                    pos(2) = pos(2)+(gridpos(4)-pos(4))/2;
                                case 'northwest'
                                    pos(2) = pos(2)+gridpos(4)-pos(4);
                                case 'north'
                                    pos(1) = pos(1)+(gridpos(3)-pos(3))/2;
                                    pos(2) = pos(2)+gridpos(4)-pos(4);
                                case 'northeast'
                                    pos(1) = pos(1)+gridpos(3)-pos(3);
                                    pos(2) = pos(2)+gridpos(4)-pos(4);
                                case 'east'
                                    pos(1) = pos(1)+gridpos(3)-pos(3);
                                    pos(2) = pos(2)+(gridpos(4)-pos(4))/2;
                                case 'southeast'
                                    pos(1) = pos(1)+gridpos(3)-pos(3);
                                case 'south'
                                    pos(1) = pos(1)+(gridpos(3)-pos(3))/2;
                                case 'center'
                                    pos(1) = pos(1)+(gridpos(3)-pos(3))/2;
                                    pos(2) = pos(2)+(gridpos(4)-pos(4))/2;
                            end
                        else
                            
                            % Without a constraints object use the defaults of 20/20
                            % 'center' and 'none'.
                            pos = gridpos;
                            if pos(3) > 20
                                pos(3) = 20;
                            end
                            if pos(4) > 20
                                pos(4) = 20;
                            end
                            pos(1) = pos(1)+(gridpos(3)-pos(3))/2;
                            pos(2) = pos(2)+(gridpos(4)-pos(4))/2;
                        end
                        
                        % Make sure that we use 1 pixel for everything.  Avoid errors.
                        pos(pos < 1) = 1;
                        
                        % Set the components new position.
                        if ishghandle(grid(indx, jndx), 'axes') && ...
                                strcmp(get(grid(indx, jndx), 'ActivePositionProperty'), 'outerposition')
                            oldUnits = get(grid(indx, jndx), 'Units');
                            set(grid(indx, jndx), 'Units', 'Pixels', 'OuterPosition', pos);
                            set(grid(indx, jndx), 'Units', oldUnits);
                        else
                            setpixelposition(grid(indx,jndx), pos);
                        end
                        
                        % Remove the control from the grid.
                        grid(indx:indx+n-1,jndx:jndx+m-1) = NaN;
                    end
                end
            end
        end
    end
    
    methods
        
        function hWeights = get.HorizontalWeights(this)
            
            % Trim the weights to match the size of the Grid.  Add zeros if
            % the grid grows.
            hWeights = resizeWeights(this, this.HorizontalWeights, 2);
            
        end
        
        function vWeights = get.VerticalWeights(this)
            
            vWeights = resizeWeights(this, this.VerticalWeights, 1);
            
        end
        
        function set.VerticalGap(this, vGap)
            
            if ~isscalar(vGap) || ~isnumeric(vGap) || isnan(vGap) || isinf(vGap)
                error('VerticalGap must be a scalar numeric.');
            end
            
            this.VerticalGap = vGap;
            this.Invalid = true;
            update(this);
        end
        
        function set.HorizontalGap(this, hGap)
            
            if ~isscalar(hGap) || ~isnumeric(hGap) || isnan(hGap) || isinf(hGap)
                error('HorizontalGap must be a scalar numeric.');
            end
            
            this.HorizontalGap = hGap;
            this.Invalid = true;
            update(this);
        end
        
        function set.VerticalWeights(this, vWeights)
            if ~any(isnumeric(vWeights)) || any(isnan(vWeights)) || any(isinf(vWeights))
                error('VerticalWeights must be a scalar numeric.');
            end
            
            this.VerticalWeights = vWeights;
            this.Invalid = true;
            update(this);
        end
        
        function set.HorizontalWeights(this, hWeights)
            if ~any(isnumeric(hWeights)) || any(isnan(hWeights)) || any(isinf(hWeights))
                error('HorizontalWeights must be a scalar numeric.');
            end
            
            this.HorizontalWeights = hWeights;
            this.Invalid = true;
            update(this);
        end
        
        function set.Grid(this, grid)
            this.Grid = grid;
            this.Invalid = true;
            update(this);
        end
    end
end

function weights = resizeWeights(this, weights, dimension)

nw = size(this.Grid, dimension);

% Only return a number of weights equal to the width of the grid.
weights = [weights zeros(1, nw-length(weights))];
weights = weights(1:nw);

weights = weights(:);
if dimension == 2
    weights = weights';
end
end

% [EOF]

Contact us