Canvas class - APP DESIGNER (uiaxes/uifigure)
This is an abstract class to facilitate the development of graphical applications that deal with MATLAB axes objects as canvases to draw models, plot graphs and enable mouse interactivity.
Contents
Authors
- Pedro Cortez F Lopes (pedrocortez@id.uff.br)
- Rafael Lopez Rangel (rafaelrangel@tecgraf.puc-rio.br)
- Luiz Fernando Martha (lfm@tecgraf.puc-rio.br)
History
@version 1.00
Initial version: September 2020
Initially prepared for the course CIV 2801 - Fundamentos de Computação Gráfica, 2020, second term, Department of Civil Engineering, PUC-Rio.
Class definition
classdef Canvas < handle
Protected attributes
properties (SetAccess = protected, GetAccess = protected) ax = []; % handle to axes object that defines canvas fig = []; % handle to figure where canvas is located updateFcn = []; % handle to function that shows how to update % drawing on canvas % ATTENTION: updateFcn MUST have a handle to % uiaxes as first input. boundBoxFcn = []; % handle to function that computes model bound % box bBoxWasSet = false; % flag to indicate that bound box was provided axIsEq = 1; % flag indicating if axes style is 'equal' freeBorder = 0.1; % free border propotion end
Constructor method
methods %------------------------------------------------------------------ % Input: % * ax = handle to axes associated to canvas % * fig = handle to figure where canvas is placed % * fcn = handle to function that details how to update drawing on % canvas function this = Canvas(ax,fig,fcn) % Check input if (nargin == 0) % If no axes or figure were provided, initialize both (commented out) %fig ax = uiaxes(); fig = ax.Parent; ax.Position = [fig.Position(3)*0.05,fig.Position(4)*0.05,... fig.Position(3)*0.9,fig.Position(4)*0.9]; elseif isempty(ax) if ~isempty(fig) % If no handles to axes object were provided, but a % handle to figure was, initialize axes on this figure ax = uiaxes(fig); else % If no axes or figure were provided, initialize both ax = uiaxes(); fig = ax.Parent; end ax.Position = [fig.Position(3)*0.05,fig.Position(4)*0.05,... fig.Position(3)*0.9,fig.Position(4)*0.9]; end % By default, set 'hold' axes property to 'on' % This avoids unwanted deletion of plots, when dealing with % multiple plots on the same canvas. hold(ax,'on'); % Set canvas object properties this.ax = ax; this.fig = fig; if (nargin > 2) this.updateFcn = fcn; end % By default, define canvas as equal (proportional). % Users may change this by calling 'this.Equal(false)' after % initializing Canvas object. this.Equal(true); % Set handle to axes.DeleteFcn. Ensures that this canvas object % will be erased if axes is closed. this.ax.DeleteFcn = @this.onDeletion; % Set a handle back to canvas from uiaxes object this.ax.addprop('canvas'); this.ax.canvas = this; end end
Public methods
methods %------------------------------------------------------------------ % DOES NOT WORK FOR APP DESIGNER - MATLAB R2019a % Override ButtonDownFcn callback, associated to 'ax'. % If user provides a handle to a function, that will be set as the % callback, otherwise, it will be directed to the abstract method % 'ax_onButtonDown'. function overrideAxButtonDownFcn(~,~) %(this,fcn) %if (nargin < 2) % fcn = @this.ax_onButtonDown; %end %this.ax.ButtonDownFcn = fcn; end %------------------------------------------------------------------ % Store handle to function that updates canvas % Input arguments: % fcn: handle to update function to be used by canvas function setUpdateFcn(this,fcn) this.updateFcn = fcn; end %------------------------------------------------------------------ % Store handle to function that computes bounding box % Input arguments: % fcn: handle to bound box function to be used by canvas function setBoundBoxFcn(this,fcn) this.boundBoxFcn = fcn; end %------------------------------------------------------------------ % Method for users to signalize if a new bounding box must be % computed, regardless if one was already provided. function resetBoundBox(this,bBoxMustBeComputed) if (nargin < 2) % By default, admit that bBoxMustBeComputed == true this.bBoxWasSet = false; return end if bBoxMustBeComputed this.bBoxWasSet = false; else this.bBoxWasSet = true; end end %------------------------------------------------------------------ % Calls function to update canvas % Input arguments: % varargin: list with a variable number of input arguments, % related to the redraw function to be used % Output arguments: % output: true (canvas successfully updated), false (nothing was % done) function output = update(this,varargin) % If no redraw function was previously specified, do nothing if isempty(this.updateFcn) output = false; this.fit2view(evalBoundBox(this)); return end % Ensures this.ax is the current axes (commented out) %axes(this.ax); % If user provided a bounding box function, call it now if ~isempty(this.boundBoxFcn) this.fit2view(evalBoundBox(this)); end % Updates canvas with specified redraw function if (nargin > 1) this.updateFcn(this.ax,varargin{:}); else this.updateFcn(this.ax); end % If no bounding box function was provided, call fit2view if ~this.bBoxWasSet this.fit2view(); end % Reinitialize flag this.bBoxWasSet = false; % Set flag to be returned output = true; end %------------------------------------------------------------------ % Calls function to set bounding box function bBox = evalBoundBox(this,varargin) if ~isempty(this.boundBoxFcn) if (nargin > 1) bBox = this.boundBoxFcn(varargin{:}); else bBox = this.boundBoxFcn(); end else bBox = []; end end %------------------------------------------------------------------ function setFreeBorder(this,b) % Free border property is a proportion of the bounding box % size, so it makes no sense to allow users to set -1 or less. % Any value between (-1,0) results on a zoom centered on the % drawing's midpoint coordinates. if b <= -1 return end this.freeBorder = b; this.fit2view(evalBoundBox(this)); end %------------------------------------------------------------------ % Returns handle to axes associated to canvas object function ax = getContext(this) ax = this.ax; end %------------------------------------------------------------------ % Returns handle to figure associated to canvas object function fig = getDialog(this) fig = this.fig; end %------------------------------------------------------------------ function Equal(this,eq) this.axIsEq = eq; if eq axis(this.ax,'equal'); else axis(this.ax,'normal'); end this.fit2view(evalBoundBox(this)); end %------------------------------------------------------------------ function Grid(this,turnOnorOff) this.ax.XGrid = turnOnorOff; this.ax.YGrid = turnOnorOff; this.ax.ZGrid = turnOnorOff; end %------------------------------------------------------------------ function Ruler(this,turnOn) if turnOn this.ax.XAxis.Visible = 'on'; this.ax.YAxis.Visible = 'on'; this.ax.ZAxis.Visible = 'on'; else this.ax.XAxis.Visible = 'off'; this.ax.YAxis.Visible = 'off'; this.ax.ZAxis.Visible = 'off'; end end %------------------------------------------------------------------ function Clipping(this,turnOn) if turnOn this.ax.Clipping = 'on'; else this.ax.Clipping = 'off'; end end %------------------------------------------------------------------ function Clean(this) cla(this.ax); end end
Protectted methods
methods (Access = protected) %------------------------------------------------------------------ function onDeletion(this,~,~) delete(this.ax); this.kill(); end end
Abstract methods
Declaration of abstract methods implemented in derived sub-classes.
methods (Abstract) %------------------------------------------------------------------ % Fits axis limits to display everything drawn on canvas. % Input arguments: % bBox: bounding box vector % 2D : [left bottom right up] - xy % 3D : [near left bottom far right up] - xyz fit2view(this,bBox); %------------------------------------------------------------------ % Mouse button down callback - related to axes object % Input: % * this = handle to this canvas object % * obj = handle to graphical object that was clicked on % * event = struct with event data ax_onButtonDown(this,obj,event); %------------------------------------------------------------------ % Mouse button down callback - generic callback, should be called % from figure related events. % Input: % * this = handle to this canvas object % * pt = current cursor coordinates % * whichMouseButton = 'left', 'right', 'center', 'double click' onButtonDown(this,pt,whichMouseButton); %------------------------------------------------------------------ % Mouse button up callback - generic callback, should be called % from figure related events. % Input: % * this = handle to this canvas object % * pt = current cursor coordinates % * whichMouseButton = 'left', 'right', 'center', 'double click' onButtonUp(this,pt,whichMouseButton); %------------------------------------------------------------------ % Mouse move callback - generic callback, should be called % from figure related events. % Input: % * this = handle to this canvas object % * pt = current cursor coordinates onMouseMove(this,pt); %------------------------------------------------------------------ % Mouse scroll callback - generic callback, should be called % from figure related events. % Input: % * this = handle to this canvas object % * pt = current cursor coordinates onMouseScroll(this,pt); end
Destructor method
methods function kill(this) this.ax = []; this.fig = []; this.updateFcn = []; this.boundBoxFcn = []; this.axIsEq = []; this.freeBorder = []; delete(this); end end
end