组织GUI代码的“正确”方法是什么?

时间:2013-11-19 06:23:34

标签: matlab user-interface matlab-guide matlab-deployment

我正在开发一个相当复杂的GUI程序,可以使用MATLAB Compiler进行部署。 (有很好的理由使用MATLAB来构建这个GUI,这不是这个问题的重点。我意识到GUI构建不适合这种语言。)

有很多方法可以在GUI中的函数之间共享数据,甚至可以在应用程序中的GUI之间传递数据:

  • setappdata/getappdata/_____appdata - 将任意数据与句柄相关联
  • guidata - 通常与GUIDE一起使用; “存储[s]或检索[s] GUI数据”到句柄结构
  • set/get操作应用于句柄对象的UserData属性
  • 在主函数中使用嵌套函数;基本上模拟“全局”范围变量。
  • 在子功能之间来回传递数据

我的代码的结构并不是最漂亮的。现在我将引擎与前端隔离开来(好!)但GUI代码很像意大利面条。这是一个“活动”的骨架,借用Android说话:

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

我之前编写的代码中没有嵌套的东西,所以我最终在任何地方来回传递h(因为需要重新绘制,更新等等)和setappdata(fig)来存储实际数据。无论如何,我一直在一个文件中保留一个“活动”,我相信这将成为未来的维护噩梦。回调与应用程序数据和图形句柄对象进行交互,我认为这是必要的,但这阻止了对代码库的两个“一半”的完全隔离。

所以我在这里寻找一些组织/ GUI设计帮助。即:

  • 我应该使用目录结构来组织吗? (回调与绘图功能?)
  • 与GUI数据交互并将其与应用程序数据隔离的“正确方法”是什么? (当我提到GUI数据时,我的意思是set/get处理对象的属性。)
  • 如何避免将所有这些绘图函数放入一个包含数千行的巨型文件中,并且仍能有效地来回传递应用程序和GUI数据?那可能吗?
  • 持续使用set/getappdata
  • 是否会导致性能下降?
  • 我的后端代码(3个对象类和一堆辅助函数)是否应该采用任何结构,以便从GUI角度更容易维护?

我不是交易软件工程师,我只知道危险,所以我确信这些对于经验丰富的GUI开发人员(使用任何语言)都是相当基本的问题。我几乎觉得MATLAB中缺少GUI设计标准(确实存在吗?)严重干扰了我完成这个项目的能力。这是一个比我曾经做过的任何规模都要大得多的MATLAB项目,而且我以前从未考虑过具有多个数字窗口等的复杂UI。

5 个答案:

答案 0 :(得分:26)

正如@SamRoberts所解释的那样,Model–view–controller(MVC)模式非常适合作为设计GUI的架构。我同意没有很多MATLAB例子来展示这样的设计......

下面是我用来演示MATLAB中基于MVC的GUI的一个完整而简单的例子。

  • 模型表示某些信号y(t) = sin(..t..)的1D函数。它是一个句柄类对象,这样我们就可以传递数据而不会创建不必要的副本。它公开了可观察的属性,允许其他组件监听变更通知。

  • 视图将模型显示为线条图形对象。该视图还包含一个滑块,用于控制其中一个信号属性,并侦听模型更改通知。我还包括一个特定于视图(而不是模型)的交互式属性,其中可以使用右键单击上下文菜单控制线条颜色。

  • 控制器负责初始化所有内容并响应视图中的事件并相应地正确更新模型。

请注意,视图和控制器是作为常规函数编写的,但如果您更喜欢完全面向对象的代码,则可以编写类。

与通常的GUI设计方法相比,这是一项额外的工作,但这种架构的一个优点是数据与表示层的分离。这使得代码更清晰,更易读,尤其是在使用复杂的GUI时,代码维护变得更加困难。

这种设计非常灵活,因为它允许您构建相同数据的多个视图。您可以拥有多个并发视图,只需在控制器中实例化更多视图实例,并查看一个视图中的更改如何传播到另一个视图中!如果您的模型可以通过不同的方式直观呈现,那么这一点尤为有趣。

此外,如果您愿意,可以使用GUIDE编辑器构建界面,而不是以编程方式添加控件。在这样的设计中,我们只使用GUIDE来使用拖放来构建GUI组件,但是我们不会编写任何回调函数。因此,我们只对所生成的.fig文件感兴趣,并忽略随附的.m文件。我们将在view function / class中设置回调。这基本上就是我在View_FrequencyDomain视图组件中所做的,它加载了使用GUIDE构建的现有FIG文件。

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 GUI1 MVC GUI2

在上面的控制器中,我实例化两个独立但同步的视图,它们都表示并响应同一底层模型中的更改。一个视图显示信号的时域,另一个视图显示使用FFT的频域表示。

答案 1 :(得分:10)

UserData property是MATLAB对象的有用但遗留的属性。 &#34; AppData&#34;一套方法(即setappdatagetappdatarmappdataisappdata等)为相对更笨拙的get/set(hFig,'UserData',dataStruct)方法IMO提供了一个很好的选择。实际上,为了管理GUI数据,GUIDE使用 guidata函数,它只是setappdata / getappdata函数的包装器。

AppData方法优于'UserData'属性的几个优点:

  • 多个异构属性的更自然界面。

    UserData仅限于单个变量,要求您设计另一层数据oranization(即结构)。假设您要存储字符串str = 'foo'和数字数组v=[1 2]。使用UserData时,只要您需要任何属性(例如s = struct('str','foo','v',[1 2]);),就需要采用结构方案,例如set/gets.str = 'bar'; set(h,'UserData',s);。使用setappdata,流程更直接(也更有效):setappdata(h,'str','bar');

  • 基础存储空间的受保护接口。

    虽然'UserData'只是一个常规的句柄图形属性,但包含应用程序数据的属性不可见,虽然它可以通过名称访问(&#39; ApplicationData&#39;,但不要做到这一点!)。您必须使用setappdata来更改任何现有的AppData属性,这样可以防止您在尝试更新单个字段时意外地破坏'UserData'的全部内容。此外,在设置或获取AppData属性之前,您可以使用isappdata验证命名属性是否存在,这有助于异常处理(例如,在设置输入值之前运行进程回调)和管理GUI的状态或者它所管理的任务(例如,通过存在某些属性来推断过程的状态并适当地更新GUI)。

'UserData''ApplicationData'属性之间的一个重要区别是'UserData'默认为[](空数组),而'ApplicationData'本身就是结构。这种差异以及setappdatagetappdata没有M文件实现(它们是内置的)的事实表明设置带setappdata的命名属性< em> not 需要重写数据结构的全部内容。 (想象一下MEX函数执行结构字段的就地修改 - 一个操作MATLAB能够通过将结构维护为'ApplicationData'句柄图形属性的基础数据表示来实现。)


guidata函数是AppData函数的包装器,但它仅限于单个变量,如'UserData' 。这意味着您必须覆盖包含所有数据字段的整个数据结构才能更新单个字段。一个明确的优点是您可以从回调中访问数据而无需实际的数字句柄,但就我而言,如果您对以下语句感到满意,这不是一个很大的优势:

hFig = ancestor(hObj,'Figure')

此外,as stated by MathWorks还存在效率问题:

  

在&#39;句柄中保存大量数据。结构有时会导致相当大的减速,特别是如果GUIDATA经常在GUI的各个子功能中调用。出于这个原因,建议使用&#39;手柄&#39;结构只用于存储图形对象的句柄。对于其他类型的数据,应使用SETAPPDATA和GETAPPDATA将其存储为应用程序数据。

此语句支持我断言在使用'ApplicationData'修改单个命名属性时不会重写整个setappdata。 (另一方面,guidatahandles结构压缩到名为'ApplicationData'的{​​{1}}字段中,因此很清楚为什么'UsedByGUIData_m'需要重写所有guidata更改一个属性时的GUI数据。)


嵌套函数需要很少的工作量(不需要辅助结构或函数),但它们显然将数据范围限制在GUI,使得其他GUI或函数无法在不将值返回到基础工作区的情况下访问该数据或一个共同的呼叫功能。显然,这会阻止您将子功能拆分为单独的文件,只要您传递图形句柄,就可以轻松地使用'UserData'或AppData。


总之,如果您选择使用句柄属性来存储和传递数据,则可以使用guidata来管理图形句柄(不是大数据) {{1 } / setappdata用于实际的程序数据。 他们不会互相覆盖,因为getappdataguidata 'UsedByGUIData_m'结构中为ApplicationData字段创建了一个特殊handles字段(除非您犯了使用错误)那个属性你自己!)。重申一下,不要直接访问ApplicationData

但是,如果您对OOP感到满意,通过类实现GUI功能可能更清晰,句柄和其他数据存储在成员变量中而不是处理属性,并且方法中的回调可以存在于separate files under the class or package folder中。有一个nice example on MATLAB Central File Exchange。此提交演示了如何使用类简化传递数据,因为不再需要不断地获取和更新guidata(成员变量始终是最新的)。但是,还有一个额外的任务是在退出时管理清理,通过设置数字closerequestfcn来完成提交,然后调用该类的delete函数。提交很好地与GUIDE示例相似。

这些是我看到的亮点,但更多细节和不同的想法是discussed by MathWorks。另请参阅this official answerUserDataguidatasetappdata/getappdata的对比。

答案 2 :(得分:7)

我不同意MATLAB不适合实现(甚至是复杂的)GUI - 它非常好。

然而,真实的是:

  1. MATLAB文档中没有关于如何实现或组织复杂GUI应用程序的示例
  2. 简单GUI的所有文档示例都使用的模式根本无法很好地扩展到复杂的GUI
  3. 特别是,GUIDE(用于自动生成GUI代码的内置工具)会生成可怕的代码,如果您自己实现某些内容,这是一个可怕的示例。
  4. 由于这些原因,大多数人只接触到非常简单或非常糟糕的MATLAB GUI,他们最终认为MATLAB不适合制作GUI。

    根据我的经验,在MATLAB中实现复杂GUI的最佳方法与在另一种语言中实现的方式相同 - 遵循一个常用的模式,如MVC(模型 - 视图 - 控制器)。

    然而,这是一个面向对象的模式,所以首先你必须熟悉MATLAB中的面向对象编程,尤其是事件的使用。为您的应用程序使用面向对象的组织应该意味着您提到的所有讨厌的技术(setappdataguidataUserData,嵌套函数作用域,以及来回传递多个数据副本)没有必要,因为所有相关的东西都可以作为类属性。

    我所知道MathWorks发布的最好的例子是在MATLAB Digest的this article中。即使这个例子也很简单,但是它让你知道如何开始,如果你研究MVC模式,它应该变得清晰,如何扩展它。

    此外,我通常会大量使用包文件夹来组织MATLAB中的大型代码库,以确保没有名称冲突。

    最后一个提示 - 使用MATLAB Central中的GUI Layout Toolbox。它使GUI开发的许多方面变得更加容易,特别是实现自动调整大小行为,并为您提供了几个额外的UI元素。

    希望有所帮助!


    编辑:在MATLAB R2016a中MathWorks推出了AppDesigner,这是一个新的GUI构建框架,旨在逐步取代GUIDE。

    AppDesigner以几种方式代表了MATLAB中以前的GUI构建方法的一个重大突破(最深层的,生成的基础图窗口基于HTML画布和JavaScript,而不是Java)。这是在R2014b中引入Handle Graphics 2引发的另一条道路,无疑将在未来版本中进一步发展。

    但AppDesigner对问题的一个影响是它产生的比GUIDE更好的代码 - 它非常干净,面向对象,并且适合形成一个基础MVC模式。

答案 3 :(得分:2)

我对GUIDE生成函数的方式感到非常不舒服。 (想想你想从另一个人那里打电话的情况)

我强烈建议您使用句柄类编写面向代码的对象。这样你就可以做一些花哨的东西(例如this)而不会迷路。对于组织代码,您有+@目录。

答案 4 :(得分:1)

我认为构建GUI代码与非GUI代码根本不同。

将属于一起的东西放在一起。 就像可能进入utilhelpers目录的辅助函数一样。根据内容,可能会成为一个包。


我个人不喜欢MATLAB人员所拥有的“一个功能一个m文件”的哲学。 设置如下功能:

function pushbutton17_callback(hObject,evt, handles)
    some_text = someOtherFunction();
    set(handles.text45, 'String', some_text);
end

进入一个单独的文件是没有意义的,当没有任何情况你从其他地方然后从你自己的GUI中调用它。


然而,您可以通过模块化方式构建GUI本身,例如通过简单地传递父容器来创建某些组件:

 handles.panel17 = uipanel(...);
 createTable(handles.panel17); % creates a table in the specified panel

这也简化了某些子组件的测试 - 你可以简单地在空图上调用createTable并测试表的某些功能而不加载完整的应用程序。


当我的应用程序变得越来越大时,我开始使用的另外两项:

使用侦听器而不是回调,它们可以显着简化GUI编程。

如果你有非常大的数据(例如来自数据库等),那么实现一个包含这些数据的句柄类可能是值得的。 在guidata / appdata中的某个位置存储此句柄可显着提高get / setappdata的性能。

修改

回调听众:

pushbutton是不好的例子。按下按钮通常只会触发某些动作,这里回调很好。 在我的情况下的主要优点,例如是以编程方式更改文本/弹出列表不会触发回调,而StringValue属性上的侦听器被触发。

另一个例子:

如果应用程序中的多个组件依赖于某些中心属性(例如某些inputdata源),则使用侦听器非常方便,以确保在属性更改时通知所有组件。 在这个属性中“感兴趣”的每个新组件都可以简单地添加它自己的监听器,因此不需要集中修改回调。 这样可以实现GUI组件的更加模块化设计,并且可以更轻松地添加/删除这些组件。