什么是“正确"? GUI代码的组织方式? [英] What's the "right" way to organize GUI code?

查看:83
本文介绍了什么是“正确"? GUI代码的组织方式?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在研究要与MATLAB Compiler一起部署的相当复杂的GUI程序. (有充分的理由使用MATLAB来构建此GUI,但这不是这个问题的重点.我意识到GUI的构建并不适合这种语言.)

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的功能之间共享数据,甚至可以在应用程序的GUI之间传递数据:

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-将任意数据关联到句柄
  • guidata-通常与GUIDE一起使用; 存储或检索GUI数据"到句柄结构
  • set/get操作应用于句柄对象的UserData属性
  • 在主函数中使用嵌套函数;基本上模拟全局"作用域变量.
  • 在子功能之间来回传递数据
  • 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

我的代码的结构不是最漂亮的.现在,我已将引擎与前端隔离开(很好!),但GUI代码非常类似于意大利面条.这是借用Android说话的活动"的骨架:

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

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

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.

因此,我在这里寻求一些组织/GUI设计方面的帮助.即:

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

  • 是否应该使用目录结构进行组织? (回调与绘图功能?)
  • 与GUI数据进行交互并将其与应用程序数据隔离的正确方法"是什么? (当我指的是GUI数据时,我指的是set/get处理对象的属性).
  • 如何避免将所有这些绘图功能放入一个包含数千行的巨型文件中,并且仍然有效地来回传递应用程序和GUI数据?有可能吗?
  • 经常使用set/getappdata是否会降低性能?
  • 我的后端代码(3个对象类和一堆帮助函数)是否应该采用任何结构,以便从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).
  • 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?

我不是行业软件工程师,我只知道足够危险,所以对于那些经验丰富的GUI开发人员(无论使用哪种语言),我相信这些都是相当基本的问题.我几乎觉得MATLAB中缺少GUI设计标准(是否存在?)严重干扰了我完成此项目的能力.这是一个MATLAB项目,比我以前从事的任何项目都要庞大得多,而且我以前从未考虑过具有多个图形窗口等复杂UI的问题.

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.

推荐答案

正如 @SamRoberts 所述,模型-视图-控制器(MVC)模式很好-适合作为设计GUI的体系结构.我同意没有太多的MATLAB示例可以显示这种设计...

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

下面是我编写的一个完整而简单的示例,用于在MATLAB中演示基于MVC的GUI.

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

  • 模型表示某些信号y(t) = sin(..t..)的一维功能.它是一个句柄类对象,因此我们可以传递数据而无需创建不必要的副本.它公开了可观察的属性,该属性允许其他组件侦听更改通知.

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

与设计GUI的常规方法相比,这是一项额外的工作,但是这种体系结构的优点之一是将数据与表示层分离.这使得代码更清晰,可读性更好,尤其是在使用复杂的GUI时,代码维护变得更加困难.

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.

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

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.

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

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

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.

这篇关于什么是“正确"? GUI代码的组织方式?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆