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

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

问题描述

我正在开发一个相当复杂的 GUI 程序,该程序将与 MATLAB Compiler 一起部署.(使用 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 一起使用;存储 [s] 或检索 [s] GUI 数据"到句柄结构
  • 对句柄对象的UserData属性应用set/get操作
  • 在主函数中使用嵌套函数;基本上模拟全局"范围变量.
  • 在子函数之间来回传递数据
  • 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/getting 句柄对象的属性.
  • 如何避免将所有这些绘图功能放入一个包含数千行的巨大文件中,同时仍然有效地来回传递应用程序和 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 解释的那样,Model–view–controller (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

控制器.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天全站免登陆