Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the "right" way to organize GUI code?

I'm working on a fairly sophisticated GUI program to be deployed with MATLAB Compiler. (There are good reasons MATLAB is being used to build this GUI, that is not the point of this question. I realize GUI-building is not a strong suit for this language.)

There are quite a few ways to share data between functions in a GUI, or even pass data between GUIs within an application:

  • setappdata/getappdata/_____appdata - associate arbitrary data to a handle
  • guidata - typically used with GUIDE; "store[s] or retrieve[s] GUI data" to a structure of handles
  • Apply a set/get operation to the UserData property of a handle object
  • Use nested functions within a main function; basically emulates "globally" scoping variables.
  • Pass the data back and forth among subfunctions

The structure for my code is not the prettiest. Right now I have the engine segregated from the front-end (good!) but the GUI code is pretty spaghetti-like. Here's a skeleton of an "activity", to borrow Android-speak:

function myGui

    fig = figure(...); 

    % h is a struct that contains handles to all the ui objects to be instantiated. My convention is to have the first field be the uicontrol type I'm instantiating. See draw_gui nested function

    h = struct([]);


    draw_gui;
    set_callbacks; % Basically a bunch of set(h.(...), 'Callback', @(src, event) callback) calls would occur here

    %% DRAW FUNCTIONS

    function draw_gui
        h.Panel.Panel1 = uipanel(...
            'Parent', fig, ...
            ...);

        h.Panel.Panel2 = uipanel(...
            'Parent', fig, ...
            ...);


        draw_panel1;
        draw_panel2;

        function draw_panel1
             h.Edit.Panel1.thing1 = uicontrol('Parent', h.Panel.Panel1, ...);
        end
        function draw_panel2
             h.Edit.Panel2.thing1 = uicontrol('Parent', h.Panel.Panel2, ...);
        end


    end

    %% CALLBACK FUNCTIONS
    % Setting/getting application data is done by set/getappdata(fig, 'Foo').
end

I have previously-written code where nothing is nested, so I ended up passing h back and forth everywhere (since stuff needed to be redrawn, updated, etc) and setappdata(fig) to store actual data. In any case, I've been keeping one "activity" in a single file, and I'm sure this is going to be a maintenance nightmare in the future. Callbacks are interacting with both application data and graphical handle objects, which I suppose is necessary, but that's preventing a complete segregation of the two "halves" of the code base.

So I'm looking for some organizational/GUI design help here. Namely:

  • Is there a directory structure I ought to be using to organize? (Callbacks vs drawing functions?)
  • What's the "right way" to interact with GUI data and keep it segregated from application data? (When I refer to GUI data I mean set/getting properties of handle objects).
  • How do I avoid putting all these drawing functions into one giant file of thousands of lines and still efficiently pass both application and GUI data back and forth? Is that possible?
  • Is there any performance penalty associated with constantly using set/getappdata?
  • Is there any structure my back-end code (3 object classes and a bunch of helper functions) should take to make it easier to maintain from a GUI perspective?

I'm not a software engineer by trade, I just know enough to be dangerous, so I'm sure these are fairly basic questions for seasoned GUI developers (in any language). I almost feel like the lack of a GUI design standard in MATLAB (does one exist?) is seriously interfering with my ability to complete this project. This is a MATLAB project that is much more massive than any I've ever undertaken, and I've never had to give much thought to complicated UIs with multiple figure windows, etc., before.

like image 355
Dang Khoa Avatar asked Nov 19 '13 06:11

Dang Khoa


4 Answers

As @SamRoberts explained, the Model–view–controller (MVC) pattern is well-suited as an architecture to design GUIs. I agree that there are not a lot of MATLAB examples out there to show such design...

Below is a complete yet simple example I wrote to demonstrate an MVC-based GUI in MATLAB.

  • The model represents a 1D function of some signal y(t) = sin(..t..). It is a handle-class object, that way we can pass the data around without creating unnecessary copies. It exposes observable properties, which allows other components to listen for change notifications.

  • The view presents the model as a line graphics object. The view also contains a slider to control one of the signal properties, and listens to model change notifications. I also included an interactive property which is specific to the view (not the model), where the line color can be controlled using the right-click context menu.

  • The controller is responsible of initializing everything and responding to events from the view and correctly updating the model accordingly.

Note that the view and controller are written as regular functions, but you could write classes if you prefer fully object-oriented code.

It is a little extra work compared to the usual way of designing GUIs, but one of the advantages of such architecture is the separation of the data from presentation layer. This makes for a cleaner and more readable code especially when working with complex GUIs, where code maintenance becomes more difficult.

This design is very flexible as it allows you to build multiple views of the same data. Even more you can have multiple simultaneous views, just instantiate more views instances in the controller and see how changes in one view are propagated to the other! This is especially interesting if your model can be visually presented in different ways.

In addition, if you prefer you can use the GUIDE editor to build interfaces instead of programmatically adding controls. In such a design we would only use GUIDE to build the GUI components using drag-and-drop, but we would not write any callback functions. So we'll only be interested in the .fig file produced, and just ignore the accompanying .m file. We would setup the callbacks in the view function/class. This is basically what I did in the View_FrequencyDomain view component, which loads the existing FIG-file built using GUIDE.

GUIDE generated FIG-file


Model.m

classdef Model < handle
    %MODEL  represents a signal composed of two components + white noise
    % with sampling frequency FS defined over t=[0,1] as:
    %   y(t) = a * sin(2pi * f*t) + sin(2pi * 2*f*t) + white_noise

    % observable properties, listeners are notified on change
    properties (SetObservable = true)
        f       % frequency components in Hz
        a       % amplitude
    end

    % read-only properties
    properties (SetAccess = private)
        fs      % sampling frequency (Hz)
        t       % time vector (seconds)
        noise   % noise component
    end

    % computable dependent property
    properties (Dependent = true, SetAccess = private)
        data    % signal values
    end

    methods
        function obj = Model(fs, f, a)
            % constructor
            if nargin < 3, a = 1.2; end
            if nargin < 2, f = 5; end
            if nargin < 1, fs = 100; end
            obj.fs = fs;
            obj.f = f;
            obj.a = a;

            % 1 time unit with 'fs' samples
            obj.t = 0 : 1/obj.fs : 1-(1/obj.fs);
            obj.noise = 0.2 * obj.a * rand(size(obj.t));
        end

        function y = get.data(obj)
            % signal data
            y = obj.a * sin(2*pi * obj.f*obj.t) + ...
                sin(2*pi * 2*obj.f*obj.t) + obj.noise;
        end
    end

    % business logic
    methods
        function [mx,freq] = computePowerSpectrum(obj)
            num = numel(obj.t);
            nfft = 2^(nextpow2(num));

            % frequencies vector (symmetric one-sided)
            numUniquePts = ceil((nfft+1)/2);
            freq = (0:numUniquePts-1)*obj.fs/nfft;

            % compute FFT
            fftx = fft(obj.data, nfft);

            % calculate magnitude
            mx = abs(fftx(1:numUniquePts)).^2 / num;
            if rem(nfft, 2)
                mx(2:end) = mx(2:end)*2;
            else
                mx(2:end -1) = mx(2:end -1)*2;
            end
        end
    end
end

View_TimeDomain.m

function handles = View_TimeDomain(m)
    %VIEW  a GUI representation of the signal model

    % build the GUI
    handles = initGUI();
    onChangedF(handles, m);    % populate with initial values

    % observe on model changes and update view accordingly
    % (tie listener to model object lifecycle)
    addlistener(m, 'f', 'PostSet', ...
        @(o,e) onChangedF(handles,e.AffectedObject));
end

function handles = initGUI()
    % initialize GUI controls
    hFig = figure('Menubar','none');
    hAx = axes('Parent',hFig, 'XLim',[0 1], 'YLim',[-2.5 2.5]);
    hSlid = uicontrol('Parent',hFig, 'Style','slider', ...
        'Min',1, 'Max',10, 'Value',5, 'Position',[20 20 200 20]);
    hLine = line('XData',NaN, 'YData',NaN, 'Parent',hAx, ...
        'Color','r', 'LineWidth',2);

    % define a color property specific to the view
    hMenu = uicontextmenu;
    hMenuItem = zeros(3,1);
    hMenuItem(1) = uimenu(hMenu, 'Label','r', 'Checked','on');
    hMenuItem(2) = uimenu(hMenu, 'Label','g');
    hMenuItem(3) = uimenu(hMenu, 'Label','b');
    set(hLine, 'uicontextmenu',hMenu);

    % customize
    xlabel(hAx, 'Time (sec)')
    ylabel(hAx, 'Amplitude')
    title(hAx, 'Signal in time-domain')

    % return a structure of GUI handles
    handles = struct('fig',hFig, 'ax',hAx, 'line',hLine, ...
        'slider',hSlid, 'menu',hMenuItem);
end

function onChangedF(handles,model)
    % respond to model changes by updating view
    if ~ishghandle(handles.fig), return, end
    set(handles.line, 'XData',model.t, 'YData',model.data)
    set(handles.slider, 'Value',model.f);
end

View_FrequencyDomain.m

function handles = View_FrequencyDomain(m)    
    handles = initGUI();
    onChangedF(handles, m);

    hl = event.proplistener(m, findprop(m,'f'), 'PostSet', ...
        @(o,e) onChangedF(handles,e.AffectedObject));
    setappdata(handles.fig, 'proplistener',hl);
end

function handles = initGUI()
    % load FIG file (its really a MAT-file)
    hFig = hgload('ViewGUIDE.fig');
    %S = load('ViewGUIDE.fig', '-mat');

    % extract handles to GUI components
    hAx = findobj(hFig, 'tag','axes1');
    hSlid = findobj(hFig, 'tag','slider1');
    hTxt = findobj(hFig, 'tag','fLabel');
    hMenu = findobj(hFig, 'tag','cmenu1');
    hMenuItem = findobj(hFig, 'type','uimenu');

    % initialize line and hook up context menu
    hLine = line('XData',NaN, 'YData',NaN, 'Parent',hAx, ...
        'Color','r', 'LineWidth',2);
    set(hLine, 'uicontextmenu',hMenu);

    % customize
    xlabel(hAx, 'Frequency (Hz)')
    ylabel(hAx, 'Power')
    title(hAx, 'Power spectrum in frequency-domain')

    % return a structure of GUI handles
    handles = struct('fig',hFig, 'ax',hAx, 'line',hLine, ...
        'slider',hSlid, 'menu',hMenuItem, 'txt',hTxt);
end

function onChangedF(handles,model)
    [mx,freq] = model.computePowerSpectrum();
    set(handles.line, 'XData',freq, 'YData',mx)
    set(handles.slider, 'Value',model.f)
    set(handles.txt, 'String',sprintf('%.1f Hz',model.f))
end

Controller.m

function [m,v1,v2] = Controller
    %CONTROLLER  main program

    % controller knows about model and view
    m = Model(100);           % model is independent
    v1 = View_TimeDomain(m);  % view has a reference of model

    % we can have multiple simultaneous views of the same data
    v2 = View_FrequencyDomain(m);

    % hook up and respond to views events
    set(v1.slider, 'Callback',{@onSlide,m})
    set(v2.slider, 'Callback',{@onSlide,m})
    set(v1.menu, 'Callback',{@onChangeColor,v1})
    set(v2.menu, 'Callback',{@onChangeColor,v2})

    % simulate some change
    pause(3)
    m.f = 10;
end

function onSlide(o,~,model)
    % update model (which in turn trigger event that updates view)
    model.f = get(o,'Value');
end

function onChangeColor(o,~,handles)
    % update view
    clr = get(o,'Label');
    set(handles.line, 'Color',clr)
    set(handles.menu, 'Checked','off')
    set(o, 'Checked','on')
end

MVC GUI1MVC GUI2

In the controller above, I instantiate two separate but synchronized views, both representing and responding to changes in the same underlying model. One view shows the time-domain of the signal, and another shows the frequency-domain representation using FFT.

like image 98
Amro Avatar answered Nov 07 '22 22:11

Amro


The UserData property is a useful, but legacy, property of MATLAB objects. The "AppData" suite of methods (i.e. setappdata, getappdata, rmappdata, isappdata, etc.) provide a great alternative to the comparatively more clumsy get/set(hFig,'UserData',dataStruct) approach, IMO. In fact, to manage GUI data, GUIDE employs the guidata function, which is just a wrapper for the setappdata/getappdata functions.

A couple of advantages of the AppData approach over the 'UserData' property that come to mind:

  • More natural interface for multiple heterogeneous properties.

    UserData is limited to a single variable, requiring you to devise another layer of data oranization (i.e. a struct). Say you want to store a string str = 'foo' and a numeric array v=[1 2]. With UserData, you would need to adopt a struct scheme such as s = struct('str','foo','v',[1 2]); and set/get the whole thing whenever you want either property (e.g. s.str = 'bar'; set(h,'UserData',s);). With setappdata, the process is more direct (and efficient): setappdata(h,'str','bar');.

  • Protected interface to the underlying storage space.

    While 'UserData' is just a regular handle graphics property, the property containing the application data is not visible, although it can be accessed by name ('ApplicationData', but don't do it!). You have to use setappdata to change any existing AppData properties, which prevents you from accidentally clobbering the entire contents of 'UserData' while trying to update a single field. Also, before setting or getting an AppData property, you can verify the existence of a named property with isappdata, which can help with exception handling (e.g. run a process callback before setting input values) and managing the state of the GUI or the tasks which it governs (e.g. infer state of a process by existence of certain properties and update GUI appropriately).

An important difference between the 'UserData' and 'ApplicationData' properties is that 'UserData' is by default [] (an empty array), while 'ApplicationData' is natively a struct. This difference, together with the fact that setappdata and getappdata have no M-file implementation (they are built-in), suggests that setting a named property with setappdata does not require rewriting the entire contents of the data struct. (Imagine a MEX function that performs a in-place modification of a struct field - an operation MATLAB is able to implement by maintaining a struct as the underlying data representation of the 'ApplicationData' handle graphics property.)


The guidata function is a wrapper to the AppData functions, but it is limited to a single variable, like 'UserData'. That means you have to overwrite the entire data structure containing all of your data fields to update a single field. A stated advantage is that you can access the data from a callback without needing the actual figure handle, but as far as I am concerned, this is not a big advantage if you are comfortable with the following statement:

hFig = ancestor(hObj,'Figure')

Also, as stated by MathWorks, there are efficiency issues:

Saving large amounts of data in the 'handles' structure can sometimes cause a considerable slowdown, especially if GUIDATA is often called within the various sub-functions of the GUI. For this reason, it is recommended to use the 'handles' structure only to store handles to graphics objects. For other kinds of data, SETAPPDATA and GETAPPDATA should be used to store it as application data.

This statement supports my assertion that the entire 'ApplicationData' is not rewritten when using setappdata to modify a single named property. (On the other hand, guidata crams the handles structure into a field of 'ApplicationData' called 'UsedByGUIData_m', so it is clear why guidata would need to rewrite all of the GUI data when one property is changed).


Nested functions require very little effort (no auxiliary structures or functions needed), but they obviously limit the scope of data to the GUI, making it impossible for other GUIs or functions to access that data without returning values to the base workspace or a common calling function. Obviously this prevents you from splitting sub-functions out into separate files, something that you can easily do with 'UserData' or AppData as long as you pass the figure handle.


In summary, if you choose to use handle properties to store and pass data, it is possible to use both guidata to manage graphics handles (not large data) and setappdata/getappdata for actual program data. They will not overwrite each other since guidata makes a special 'UsedByGUIData_m' field in ApplicationData for the handles structure (unless you make the mistake of using that property yourself!). Just to reiterate, do not directly access ApplicationData.

However, if you are comfortable with OOP, it may be cleaner to implement GUI functionality via a class, with handles and other data stored in member variables rather than handle properties, and callbacks in methods that can exist in separate files under the class or package folder. There is a nice example on MATLAB Central File Exchange. This submission demonstrates how passing data is simplified with a class since it is no longer necessary to constantly get and update guidata (members variables are always up to date). However there is the additional task of managing cleanup on exit, which the submission accomplishes by setting the figure's closerequestfcn, which then calls the delete function of the class. The submission nicely parallels the GUIDE example.

Those are the highlights as I see them, but many more details and different ideas are discussed by MathWorks. See also this official answer to UserData vs. guidata vs. setappdata/getappdata.

like image 38
chappjc Avatar answered Nov 08 '22 00:11

chappjc


I disagree that MATLAB is not good for implementing (even complex) GUIs - it's perfectly fine.

However, what is true is that:

  1. There are no examples in the MATLAB documentation of how to implement or organize a complex GUI application
  2. All the documentation examples of simple GUIs use patterns that do not scale well at all to complex GUIs
  3. In particular, GUIDE (the built-in tool for auto-generating GUI code) generates terrible code that is a dreadful example to follow if you're implementing something yourself.

Because of these things, most people are only exposed to either very simple or really horrible MATLAB GUIs, and they end up thinking MATLAB is not suitable for making GUIs.

In my experience the best way to implement a complex GUI in MATLAB is the same way as you would in another language - follow a well-used pattern such as MVC (model-view-controller).

However, this is an object-oriented pattern, so first you'll have to get comfortable with object-oriented programming in MATLAB, and particularly with the use of events. Using an object-oriented organization for your application should mean that all the nasty techniques you mention (setappdata, guidata, UserData, nested function scoping, and passing back and forth multiple data copies) are not necessary, as all the relevant things are available as class properties.

The best example I know of that MathWorks has published is in this article from MATLAB Digest. Even that example is very simple, but it gives you an idea of how to start off, and if you look into the MVC pattern it should become clear how to extend it.

In addition, I typically make heavy use of package folders to organize large codebases in MATLAB, to ensure there are no name clashes.

One final tip - use the GUI Layout Toolbox, from MATLAB Central. It makes many aspects of GUI development much easier, particularly implementing automatic resize behaviour, and gives you several additional UI elements to use.

Hope that helps!


Edit: In MATLAB R2016a MathWorks introduced AppDesigner, a new GUI-building framework intended to gradually replace GUIDE.

AppDesigner represents a major break with previous GUI-building approaches in MATLAB in several ways (most deeply, the underlying figure windows generated are based on an HTML canvas and JavaScript, rather than Java). It is another step along a road initiated by the introduction of Handle Graphics 2 in R2014b, and will doubtless evolve further over future releases.

But one impact of AppDesigner on the question asked is that it generates much better code than GUIDE did - it's pretty clean, object-oriented, and suitable to form the basis of an MVC pattern.

like image 42
Sam Roberts Avatar answered Nov 08 '22 00:11

Sam Roberts


I am very uncomfortable with the way GUIDE produces functions. (think about cases where you'd like to call one gui from another)

I would strongly suggest you write your code object oriented using handle classes. That way you can do fancy stuffs (e.g. this) and not get lost. For organizing code you have the + and @ directories.

like image 45
bdecaf Avatar answered Nov 07 '22 22:11

bdecaf