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

时间:2023-02-15 12:20:08

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.)

我正在开发一个相当复杂的GUI程序,可以用MATLAB编译器进行部署。(有很好的理由使用MATLAB来构建这个GUI,这不是问题的重点。我意识到贵公司对这门语言不是很有一套。

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

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

  • setappdata/getappdata/_____appdata - associate arbitrary data to a handle
  • setappdata/getappdata/_____appdata -将任意数据关联到句柄。
  • guidata - typically used with GUIDE; "store[s] or retrieve[s] GUI data" to a structure of handles
  • guidata—通常与向导一起使用;“存储[s]或检索[s] GUI数据”到句柄结构。
  • Apply a set/get operation to the UserData property of a handle object
  • 将一个set/get操作应用到句柄对象的UserData属性。
  • 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:

我的代码结构不是最漂亮的。现在我的引擎与前端分离(很好!),但是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

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.

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

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

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

  • 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).
  • 与GUI数据交互的“正确方式”是什么,并将其与应用程序数据隔离开来?(当我引用GUI数据时,我是指设置/获取处理对象的属性)。
  • 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?
  • 如何避免将所有这些绘图功能放入数千行的巨型文件中,并且仍然有效地来回传递应用程序和GUI数据?这有可能吗?
  • Is there any performance penalty associated with constantly using set/getappdata?
  • 是否存在与经常使用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?
  • 我的后端代码(3个对象类和一组helper函数)是否应该使它更容易从GUI的角度进行维护?

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.

我不是一名软件工程师,我只知道有足够的危险,所以我确信这些对于经验丰富的GUI开发人员来说是相当基本的问题(在任何语言中)。我几乎感觉在MATLAB中缺少一个GUI设计标准(是否存在?)严重干扰了我完成这个项目的能力。这是一个MATLAB项目,它比我所做过的任何一个都要大得多,而且我从来没有考虑过复杂的UIs和多个图形窗口,等等。

5 个解决方案

#1


26  

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

正如@SamRoberts所解释的,模型-视图-控制器(MVC)模式非常适合作为设计gui的体系结构。我同意没有很多MATLAB的例子来展示这样的设计…

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

下面是一个完整而简单的例子,我在MATLAB中编写了一个基于mvc的GUI。

  • 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.

    该模型表示某信号y(t) = sin(. t..)的1D函数。它是一个手工类对象,这样我们就可以在不创建不必要的副本的情况下传递数据。它公开可观察的属性,允许其他组件侦听更改通知。

  • 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.

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

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.

此外,如果您希望您可以使用指南编辑器来构建接口,而不是通过编程方式添加控件。在这样的设计中,我们只使用向导来使用拖放来构建GUI组件,但是我们不会编写任何回调函数。因此,我们只对所生成的.fig文件感兴趣,而忽略附带的.m文件。我们将在视图函数/类中设置回调。这基本上就是我在view_encydomain视图组件中所做的,它加载了使用向导构建的现有图形文件。

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


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

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

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.

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

#2


10  

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.

UserData属性是一个有用的,但是遗留的,MATLAB对象的属性。方法的“AppData”套件(即setappdata、getappdata、rmappdata、isappdata等)为相对较笨拙的get/set(hFig,“UserData”,dataStruct)方法提供了一个很好的替代方法。实际上,为了管理GUI数据,GUIDE使用guidata函数,它只是setappdata/getappdata函数的包装器。

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

AppData方法对“UserData”属性的一些好处:

  • 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');.

    用户数据仅限于单个变量,要求您设计另一层数据或化(即结构)。假设您想存储一个字符串str = 'foo'和一个数字数组v=[1 2]。使用UserData,您需要采用结构方案,如s = struct('str','foo','v',[1 2]);无论什么时候你想要财产(例如,s),你都可以把整件事情做好。str = '酒吧';组(h,“用户数据”,年代);)。对于setappdata,这个过程更直接(也更有效):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).

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

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.)

“UserData”和“ApplicationData”属性之间的一个重要区别是,“UserData”是默认的[](一个空数组),而“ApplicationData”是原生的结构。这种差异,加上setappdata和getappdata没有M-file实现(它们是内置的),建议使用setappdata设置命名属性不需要重写数据结构的全部内容。(假设有一个MEX函数,它对struct字段进行就地修改——通过维护一个struct作为“ApplicationData”处理图形属性的底层数据表示,MATLAB可以实现这个功能。)


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:

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

hFig = ancestor(hObj,'Figure')

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

同样,正如MathWorks所述,存在效率问题:

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.

在“句柄”结构中保存大量数据有时会导致相当大的衰退,尤其是在GUI的各种子功能中经常调用GUIDATA时。出于这个原因,建议使用“句柄”结构来存储对图形对象的句柄。对于其他类型的数据,应该将SETAPPDATA和GETAPPDATA作为应用程序数据存储。

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).

这个语句支持我的断言,即当使用setappdata修改一个指定的属性时,整个“ApplicationData”不会被重写。(另一方面,guidata将处理结构转换为“ApplicationData”字段,称为“UsedByGUIData_m”,所以当一个属性发生更改时,GUI数据需要重写所有的GUI数据,这是很清楚的。


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.

嵌套函数需要很少的努力(不需要辅助结构或函数),但是它们显然限制了GUI的数据范围,使得其他GUI或函数无法访问该数据,而不返回基本工作区或普通的调用函数。很明显,这可以防止您将子函数分割成单独的文件,您可以很容易地使用“UserData”或AppData,只要您通过这个数字句柄即可。


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.

总之,如果您选择使用handle属性来存储和传递数据,那么可以使用guidata来管理图形处理(不是大数据)和setappdata/getappdata来处理实际的程序数据。它们不会相互覆盖,因为guidata在处理结构的ApplicationData中创建了一个特殊的“UsedByGUIData_m”字段(除非您自己错误地使用了该属性!)重申一下,不要直接访问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.

但是,如果您对OOP感到满意,那么可以通过一个类来实现GUI功能,它使用的是成员变量中存储的句柄和其他数据,而不是在类或包文件夹下的单独文件中存在的方法。在MATLAB*文件交换中有一个很好的例子。这个提交演示了如何将传递数据简化为一个类,因为它不再需要不断地获取和更新gui数据(成员变量总是最新的)。但是,还有一个额外的任务,就是在退出时管理清理,通过设置图的closerequestfcn来完成这个任务,然后调用类的delete函数。提交很好地与指南示例相比较。

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.

这些是我所看到的亮点,但是更多的细节和不同的想法是由MathWorks讨论的。还可以查看UserData与guidata与setappdata/getappdata的官方回答。

#3


7  

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

我不同意MATLAB对实现(甚至是复杂的)gui没有好处——它完全没问题。

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. 在MATLAB文档中没有关于如何实现或组织复杂GUI应用程序的示例。
  3. All the documentation examples of simple GUIs use patterns that do not scale well at all to complex GUIs
  4. 简单gui的所有文档示例都使用了不适合复杂gui的模式。
  5. 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.
  6. 特别是,向导(自动生成GUI代码的内置工具)会生成可怕的代码,如果您自己实现了这些代码,那么这将是一个可怕的示例。

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.

因为这些事情,大多数人只接触到非常简单或非常可怕的MATLAB gui,他们最后认为MATLAB不适合做gui。

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).

在我的经验中,在MATLAB中实现复杂GUI的最佳方法与在另一种语言中使用的方法是一样的——遵循一个使用良好的模式,比如MVC(模型-视图-控制器)。

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.

但是,这是一个面向对象的模式,所以首先您必须熟悉面向对象的MATLAB编程,特别是使用事件。在应用程序中使用面向对象的组织应该意味着您所提到的所有令人讨厌的技术(setappdata、guidata、UserData、嵌套的函数范围、来回传递多个数据副本)都是不必要的,因为所有相关的东西都可以作为类属性可用。

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.

我所知道的关于MathWorks的最好的例子是本文的MATLAB摘要。即使这个例子非常简单,但是它让您知道如何开始,如果您研究MVC模式,应该清楚如何扩展它。

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

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

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.

最后一个技巧——使用来自MATLAB中心的GUI布局工具箱。它使GUI开发的许多方面更加容易,特别是实现自动调整大小的行为,并为您提供了几个额外的UI元素。

Hope that helps!

希望会有帮助!


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

编辑:在MATLAB R2016a MathWorks中引入了AppDesigner,一种新的gui构建框架,旨在逐步取代指南。

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.

AppDesigner表示与以前的gui构建方法在MATLAB中有几种不同的方法(最深入的是,生成的底层图形窗口是基于HTML画布和JavaScript而不是Java)。这是在R2014b中引入处理图形2所开创的道路上的另一个步骤,并且毫无疑问将在未来的版本中进一步发展。

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.

但AppDesigner对这个问题的一个影响是,它生成的代码比指南的代码好得多——它非常干净、面向对象,并且适合构建MVC模式的基础。

#4


2  

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

我对指南的制作方式感到很不舒服。(考虑一下您想从另一个gui调用一个gui的情况)

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.

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

#5


1  

I don't think structuring GUI-code is fundamentally different from non-GUI code.

我不认为构建gui代码与非gui代码有本质的区别。

Put things that belong together, together at some location. Like helper-functions that might go into a util or helpers directory. Depending on the content, maybe make it a package.

把属于一起的东西放在一起,放在某个地方。比如可能进入util或helper目录的helper函数。根据内容不同,可以将其作为一个包。


Personally I don't like the "one function one m-file" philosophy some MATLAB people have. Putting a function like:

我个人不喜欢“一个函数一个m文件”的哲学,一些MATLAB的人有。把一个函数:

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

into a seperate file simply makes no sense, when there's no scenario whatsoever you'd call this from somewhere else then from your own GUI.

在没有任何场景的情况下,从你自己的GUI中调用这个文件,就没有任何意义。


You can however build the GUI itself in a modular way, by e.g. creating certain components by simply passing the parent container:

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

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

This also simplifies testing of certain sub-components - you could simply call createTable on an empty figure and test certain functionalities of the table without loading the full application.

这也简化了对某些子组件的测试——您可以简单地调用一个空图形上的createTable,并在不加载完整应用程序的情况下测试表的某些功能。


Just two additional items I started using when my application became increasingly larger:

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

Use listeners over callbacks, they can simplify GUI programming significantly.

在回调中使用侦听器,可以显著简化GUI编程。

If you have really large data (such as from a database, etc.) it might be worthwhile implementing a handle-class holding this data. Storing this handle somewhere in the guidata/appdata significantly improves get/setappdata performance.

如果您有非常大的数据(比如来自数据库等),那么实现这个数据的handleclass可能是值得的。将此句柄存储在guidata/appdata中,可以显著提高get/setappdata的性能。

Edit:

编辑:

Listeners over callbacks:

听众在回调函数:

A pushbutton is bad example. Pushing a button usually only triggers on certain action, here callbacks are fine imho. A main advantage in my case e.g. was that programmatically changing text/popup lists does not trigger callbacks, while listeners on their String or Value property are triggered.

按钮就是不好的例子。按一个按钮通常只会触发某些动作,这里的回调是很好的imho。在我的例子中,一个主要的优点是:通过编程方式改变文本/弹出列表不会触发回调,而在字符串或值属性上的侦听器被触发。

Another example:

另一个例子:

If there's some central property (e.g. like some source of inputdata) that multiple components in the application depend on, then using listeners is very convenient to assure that all components are notified if the property changes. Every new component "interested" in this property can simply add it's own listener, so there's no need to centrally modify the callback. This allows for a much more modular design of the GUI components and makes adding/removing of such components easier.

如果应用程序中的多个组件依赖于某个中心属性(例如某些inputdata的源代码),那么使用侦听器就非常方便,以确保如果属性发生变化,所有组件都会得到通知。这个属性中的每个新组件都可以简单地添加自己的侦听器,因此无需对回调进行集中修改。这使得GUI组件的设计更加模块化,并且使这些组件的添加/删除变得更加容易。

#1


26  

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

正如@SamRoberts所解释的,模型-视图-控制器(MVC)模式非常适合作为设计gui的体系结构。我同意没有很多MATLAB的例子来展示这样的设计…

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

下面是一个完整而简单的例子,我在MATLAB中编写了一个基于mvc的GUI。

  • 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.

    该模型表示某信号y(t) = sin(. t..)的1D函数。它是一个手工类对象,这样我们就可以在不创建不必要的副本的情况下传递数据。它公开可观察的属性,允许其他组件侦听更改通知。

  • 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.

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

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.

此外,如果您希望您可以使用指南编辑器来构建接口,而不是通过编程方式添加控件。在这样的设计中,我们只使用向导来使用拖放来构建GUI组件,但是我们不会编写任何回调函数。因此,我们只对所生成的.fig文件感兴趣,而忽略附带的.m文件。我们将在视图函数/类中设置回调。这基本上就是我在view_encydomain视图组件中所做的,它加载了使用向导构建的现有图形文件。

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


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

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

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.

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

#2


10  

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.

UserData属性是一个有用的,但是遗留的,MATLAB对象的属性。方法的“AppData”套件(即setappdata、getappdata、rmappdata、isappdata等)为相对较笨拙的get/set(hFig,“UserData”,dataStruct)方法提供了一个很好的替代方法。实际上,为了管理GUI数据,GUIDE使用guidata函数,它只是setappdata/getappdata函数的包装器。

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

AppData方法对“UserData”属性的一些好处:

  • 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');.

    用户数据仅限于单个变量,要求您设计另一层数据或化(即结构)。假设您想存储一个字符串str = 'foo'和一个数字数组v=[1 2]。使用UserData,您需要采用结构方案,如s = struct('str','foo','v',[1 2]);无论什么时候你想要财产(例如,s),你都可以把整件事情做好。str = '酒吧';组(h,“用户数据”,年代);)。对于setappdata,这个过程更直接(也更有效):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).

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

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.)

“UserData”和“ApplicationData”属性之间的一个重要区别是,“UserData”是默认的[](一个空数组),而“ApplicationData”是原生的结构。这种差异,加上setappdata和getappdata没有M-file实现(它们是内置的),建议使用setappdata设置命名属性不需要重写数据结构的全部内容。(假设有一个MEX函数,它对struct字段进行就地修改——通过维护一个struct作为“ApplicationData”处理图形属性的底层数据表示,MATLAB可以实现这个功能。)


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:

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

hFig = ancestor(hObj,'Figure')

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

同样,正如MathWorks所述,存在效率问题:

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.

在“句柄”结构中保存大量数据有时会导致相当大的衰退,尤其是在GUI的各种子功能中经常调用GUIDATA时。出于这个原因,建议使用“句柄”结构来存储对图形对象的句柄。对于其他类型的数据,应该将SETAPPDATA和GETAPPDATA作为应用程序数据存储。

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).

这个语句支持我的断言,即当使用setappdata修改一个指定的属性时,整个“ApplicationData”不会被重写。(另一方面,guidata将处理结构转换为“ApplicationData”字段,称为“UsedByGUIData_m”,所以当一个属性发生更改时,GUI数据需要重写所有的GUI数据,这是很清楚的。


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.

嵌套函数需要很少的努力(不需要辅助结构或函数),但是它们显然限制了GUI的数据范围,使得其他GUI或函数无法访问该数据,而不返回基本工作区或普通的调用函数。很明显,这可以防止您将子函数分割成单独的文件,您可以很容易地使用“UserData”或AppData,只要您通过这个数字句柄即可。


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.

总之,如果您选择使用handle属性来存储和传递数据,那么可以使用guidata来管理图形处理(不是大数据)和setappdata/getappdata来处理实际的程序数据。它们不会相互覆盖,因为guidata在处理结构的ApplicationData中创建了一个特殊的“UsedByGUIData_m”字段(除非您自己错误地使用了该属性!)重申一下,不要直接访问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.

但是,如果您对OOP感到满意,那么可以通过一个类来实现GUI功能,它使用的是成员变量中存储的句柄和其他数据,而不是在类或包文件夹下的单独文件中存在的方法。在MATLAB*文件交换中有一个很好的例子。这个提交演示了如何将传递数据简化为一个类,因为它不再需要不断地获取和更新gui数据(成员变量总是最新的)。但是,还有一个额外的任务,就是在退出时管理清理,通过设置图的closerequestfcn来完成这个任务,然后调用类的delete函数。提交很好地与指南示例相比较。

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.

这些是我所看到的亮点,但是更多的细节和不同的想法是由MathWorks讨论的。还可以查看UserData与guidata与setappdata/getappdata的官方回答。

#3


7  

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

我不同意MATLAB对实现(甚至是复杂的)gui没有好处——它完全没问题。

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. 在MATLAB文档中没有关于如何实现或组织复杂GUI应用程序的示例。
  3. All the documentation examples of simple GUIs use patterns that do not scale well at all to complex GUIs
  4. 简单gui的所有文档示例都使用了不适合复杂gui的模式。
  5. 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.
  6. 特别是,向导(自动生成GUI代码的内置工具)会生成可怕的代码,如果您自己实现了这些代码,那么这将是一个可怕的示例。

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.

因为这些事情,大多数人只接触到非常简单或非常可怕的MATLAB gui,他们最后认为MATLAB不适合做gui。

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).

在我的经验中,在MATLAB中实现复杂GUI的最佳方法与在另一种语言中使用的方法是一样的——遵循一个使用良好的模式,比如MVC(模型-视图-控制器)。

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.

但是,这是一个面向对象的模式,所以首先您必须熟悉面向对象的MATLAB编程,特别是使用事件。在应用程序中使用面向对象的组织应该意味着您所提到的所有令人讨厌的技术(setappdata、guidata、UserData、嵌套的函数范围、来回传递多个数据副本)都是不必要的,因为所有相关的东西都可以作为类属性可用。

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.

我所知道的关于MathWorks的最好的例子是本文的MATLAB摘要。即使这个例子非常简单,但是它让您知道如何开始,如果您研究MVC模式,应该清楚如何扩展它。

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

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

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.

最后一个技巧——使用来自MATLAB中心的GUI布局工具箱。它使GUI开发的许多方面更加容易,特别是实现自动调整大小的行为,并为您提供了几个额外的UI元素。

Hope that helps!

希望会有帮助!


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

编辑:在MATLAB R2016a MathWorks中引入了AppDesigner,一种新的gui构建框架,旨在逐步取代指南。

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.

AppDesigner表示与以前的gui构建方法在MATLAB中有几种不同的方法(最深入的是,生成的底层图形窗口是基于HTML画布和JavaScript而不是Java)。这是在R2014b中引入处理图形2所开创的道路上的另一个步骤,并且毫无疑问将在未来的版本中进一步发展。

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.

但AppDesigner对这个问题的一个影响是,它生成的代码比指南的代码好得多——它非常干净、面向对象,并且适合构建MVC模式的基础。

#4


2  

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

我对指南的制作方式感到很不舒服。(考虑一下您想从另一个gui调用一个gui的情况)

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.

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

#5


1  

I don't think structuring GUI-code is fundamentally different from non-GUI code.

我不认为构建gui代码与非gui代码有本质的区别。

Put things that belong together, together at some location. Like helper-functions that might go into a util or helpers directory. Depending on the content, maybe make it a package.

把属于一起的东西放在一起,放在某个地方。比如可能进入util或helper目录的helper函数。根据内容不同,可以将其作为一个包。


Personally I don't like the "one function one m-file" philosophy some MATLAB people have. Putting a function like:

我个人不喜欢“一个函数一个m文件”的哲学,一些MATLAB的人有。把一个函数:

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

into a seperate file simply makes no sense, when there's no scenario whatsoever you'd call this from somewhere else then from your own GUI.

在没有任何场景的情况下,从你自己的GUI中调用这个文件,就没有任何意义。


You can however build the GUI itself in a modular way, by e.g. creating certain components by simply passing the parent container:

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

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

This also simplifies testing of certain sub-components - you could simply call createTable on an empty figure and test certain functionalities of the table without loading the full application.

这也简化了对某些子组件的测试——您可以简单地调用一个空图形上的createTable,并在不加载完整应用程序的情况下测试表的某些功能。


Just two additional items I started using when my application became increasingly larger:

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

Use listeners over callbacks, they can simplify GUI programming significantly.

在回调中使用侦听器,可以显著简化GUI编程。

If you have really large data (such as from a database, etc.) it might be worthwhile implementing a handle-class holding this data. Storing this handle somewhere in the guidata/appdata significantly improves get/setappdata performance.

如果您有非常大的数据(比如来自数据库等),那么实现这个数据的handleclass可能是值得的。将此句柄存储在guidata/appdata中,可以显著提高get/setappdata的性能。

Edit:

编辑:

Listeners over callbacks:

听众在回调函数:

A pushbutton is bad example. Pushing a button usually only triggers on certain action, here callbacks are fine imho. A main advantage in my case e.g. was that programmatically changing text/popup lists does not trigger callbacks, while listeners on their String or Value property are triggered.

按钮就是不好的例子。按一个按钮通常只会触发某些动作,这里的回调是很好的imho。在我的例子中,一个主要的优点是:通过编程方式改变文本/弹出列表不会触发回调,而在字符串或值属性上的侦听器被触发。

Another example:

另一个例子:

If there's some central property (e.g. like some source of inputdata) that multiple components in the application depend on, then using listeners is very convenient to assure that all components are notified if the property changes. Every new component "interested" in this property can simply add it's own listener, so there's no need to centrally modify the callback. This allows for a much more modular design of the GUI components and makes adding/removing of such components easier.

如果应用程序中的多个组件依赖于某个中心属性(例如某些inputdata的源代码),那么使用侦听器就非常方便,以确保如果属性发生变化,所有组件都会得到通知。这个属性中的每个新组件都可以简单地添加自己的侦听器,因此无需对回调进行集中修改。这使得GUI组件的设计更加模块化,并且使这些组件的添加/删除变得更加容易。