Каков "правильный" способ организации кода GUI?

Я работаю над довольно сложной программой GUI для развертывания с компилятором MATLAB. (Есть веские причины, по которым MATLAB используется для создания этого GUI, это не суть этого вопроса. Я понимаю, что GUI-building не подходит для этого языка.)

существует довольно много способов обмена данными между функциями в GUI или даже передачи данных между GUI в приложении:

  • setappdata/getappdata/_____appdata - связать произвольные данные с a ручка
  • guidata - обычно используется с руководством; "хранить[s] или извлекать [S] данные GUI" в структуру дескрипторов
  • применить set/get операции UserData свойство объекта дескриптора
  • использовать вложенные функции в основной функции; в основном эмулирует" глобально " переменные области видимости.
  • передайте данные взад и вперед между подфункциями

структура для моего кода не самая красивая. Сейчас у меня двигатель отделенный от передней части (хорошо!) но код GUI довольно похож на спагетти. Вот скелет "активности", чтобы заимствовать 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) для хранения фактических данных. В любом случае, я держал одну "активность" в одном файле, и я уверен, что это будет кошмар обслуживания в будущее. Обратные вызовы взаимодействуют как с данными приложения, так и с графическими объектами дескриптора, что, я полагаю, необходимо, но это предотвращает полное разделение двух "половин" базы кода.

поэтому я ищу некоторую организационную/GUI-помощь здесь. А именно:

  • есть ли структура каталогов, которую я должен использовать для организации? (Обратные вызовы против функций рисования?)
  • каков "правильный способ" взаимодействия с данными GUI и сохранить его отдельно от данных приложения? (Когда я ссылаюсь на данные GUI, я имею в виду set/getТинг свойства обработки объектов).
  • как мне избежать размещения всех этих функций рисования в один гигантский файл из тысяч строк и по-прежнему эффективно передавать данные приложения и GUI туда и обратно? Это возможно?
  • есть ли какой-либо штраф за производительность, связанный с постоянным использованием set/getappdata?
  • есть ли какая-либо структура моего внутреннего кода (3 класса объектов и куча вспомогательные функции) должны принять, чтобы сделать его легче поддерживать с точки зрения GUI?

Я не инженер-программист по профессии, я просто знаю достаточно, чтобы быть опасным, поэтому я уверен, что это довольно основные вопросы для опытных разработчиков GUI (на любом языке). Я почти чувствую, что отсутствие стандарта дизайна GUI в MATLAB (существует ли он?) серьезно мешает моей способности завершить этот проект. Это проект MATLAB, который намного массивнее, чем любой, который я когда-либо предпринято, и мне никогда не приходилось много думать о сложных UIs с несколькими фигурными окнами и т. д., до.

5 ответов


As @SamRoberts пояснил, что модель–представление–контроллер (MVC) шаблон хорошо подходит в качестве архитектуры для проектирования GUIs. Я согласен, что существует не так много примеров MATLAB, чтобы показать такой дизайн...

ниже приведен полный, но простой пример, который я написал, чтобы продемонстрировать графический интерфейс на основе MVC в MATLAB.

  • The модель представляет собой 1D функцию некоторого сигнала y(t) = sin(..t..). Это объект класса дескриптора, таким образом, мы можем передавать данные без создания ненужных копий. Он предоставляет наблюдаемые свойства, которые позволяют другим компонентам прослушивать уведомления об изменениях.

  • на посмотреть представляет модель как объект линейной графики. Представление также содержит ползунок для управления одним из свойств сигнала и прослушивает уведомления об изменении модели. Я также включил интерактивное свойство, которое специфично для представления (а не для модели), где цвет линий можно управлять с помощью контекстного меню.

  • The контроллер отвечает за инициализацию всего и реагирование на события из представления и корректное обновление модели соответственно.

обратите внимание, что представление и контроллер записываются как обычные функции, но вы можете писать классы, если предпочитаете полностью объектно-ориентированный код.

это немного дополнительная работа по сравнению с обычный способ проектирования GUIs, но одним из преимуществ такой архитектуры является отделение данных от уровня представления. Это делает более чистый и читаемый код особенно при работе со сложными GUIs, где обслуживание кода становится более сложным.

этот дизайн очень гибкий, поскольку он позволяет создавать несколько представлений одних и тех же данных. Еще больше вы можете иметь несколько одновременно вид, просто инстанцировать больше просмотров экземпляров контроллер и посмотреть, как изменения в одном представлении распространяются на другие! Это особенно интересно, если ваша модель может быть визуально представлена по-разному.

кроме того, если вы предпочитаете, вы можете использовать редактор руководства для создания интерфейсов вместо программного добавления элементов управления. В таком дизайне мы будем использовать только руководство для создания компонентов GUI с помощью перетаскивания, но мы не будем писать никаких функций обратного вызова. Так что нас интересует только произведено, и просто игнорируйте сопровождающие . Мы бы настроили обратные вызовы в функции/классе view. Это в основном то, что я сделал в View_FrequencyDomain view компонент, который загружает существующий FIG-файл, построенный с помощью руководства.

GUIDE generated FIG-file


модель.м

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

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

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
function [m,v1,v2] = Controller
    %CONTROLLER  main program

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

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

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

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

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

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

MVC GUI1MVC GUI2

в контроллер выше, я создаю два отдельных, но синхронизированных представления, представляющих и реагирующих на изменения в той же базовой модели. Одно представление показывает временную область сигнала, а другое-представление частотной области с использованием FFT.


на UserData свойства является полезным, но устаревшим свойством объектов MATLAB. Набор методов" AppData " (т. е. setappdata, getappdata, rmappdata, isappdata, etc.) обеспечить отличную альтернативу сравнительно более неуклюжим get/set(hFig,'UserData',dataStruct) подход, ИМО. На самом деле, для управления данными GUI, руководство использует the guidata функция, которая является просто оболочкой для setappdata/getappdata функции.

несколько преимуществ AppData подходите к 'UserData' свойство, которое приходит на ум:

  • более естественный интерфейс для нескольких гетерогенных свойств.

    UserData ограничивается одной переменной, требуя от вас разработки другого уровня оранизации данных (т. е. структуры). Скажем, вы хотите сохранить строку str = 'foo' и числовой массив v=[1 2]. С UserData, вам нужно будет принять схему структуры, такую как s = struct('str','foo','v',[1 2]); и set/get все это, когда вы хотите либо свойство (например s.str = 'bar'; set(h,'UserData',s);). С setappdata, процесс более прямой (и эффективный):setappdata(h,'str','bar');.

  • защищенный интерфейс к основному пространству хранения.

    пока 'UserData' является обычным графическим свойством дескриптора, свойство, содержащее данные приложения, не отображается, хотя к нему можно получить доступ по имени ("ApplicationData", но не делайте этого!). Вы должны использовать setappdata для изменения любых существующих свойств AppData, что предотвращает вы от случайного забивания всего содержимого 'UserData' при попытке обновить одно поле. Кроме того, перед установкой или получением свойства AppData можно проверить наличие именованного свойства с помощью isappdata, который может помочь с обработкой исключений (например, запустить обратный вызов процесса перед установкой входных значений) и управлять состоянием GUI или задачами, которые он управляет (например, вывести состояние процесса по наличию определенных свойств и обновить GUI соответственно.)

важное различие между 'UserData' и 'ApplicationData' свойства заключается в том, что 'UserData' по умолчанию [] (пустой массив), в то время как 'ApplicationData' встроена структура. Эта разница, вместе с тем, что setappdata и getappdata не имеют реализации M-файла (они встроены), предполагает, что установка именованного свойства с помощью setappdata тут не требуется переписать все содержимое структуры данных. (Представьте себе функцию MEX, которая выполняет модификацию поля структуры на месте-операция MATLAB может быть реализована путем поддержания структуры в качестве базового представления данных 'ApplicationData' обрабатывать графику собственность.)


на guidata функция является оболочкой для функций AppData, но она ограничена одной переменной, например 'UserData'. Это означает, что вы должны перезаписать всю структуру данных, содержащую все ваши поля данных, чтобы обновить одно поле. Заявленное преимущество заключается в том, что вы можете получить доступ к данным из обратного вызова без необходимости фактического дескриптора фигуры, но, насколько мне известно, это не большое преимущество, если вам удобно со следующим утверждением:

hFig = ancestor(hObj,'Figure')

и как указано в MathWorks, есть вопросы эффективности:

сохранение больших объемов данных в структуре "дескрипторов" иногда может вызвать значительное замедление, особенно если GUIDATA часто вызывается в различных подфункциях GUI. По этой причине рекомендуется использовать структуру "дескрипторы" только для хранения дескрипторов графических объектов. Для других типов данных SETAPPDATA и GETAPPDATA следует использовать для хранения их в качестве данных приложения.

это утверждение подтверждает мое утверждение, что весь 'ApplicationData' не переписывается при использовании setappdata для изменения одного именованного свойства. (С другой стороны, guidata вставляет handles структура в a поле 'ApplicationData' под названием 'UsedByGUIData_m', поэтому понятно, почему guidata потребуется переписать все данные GUI при изменении одного свойства).


вложенные функции требуют очень мало усилий (никаких вспомогательных структур или функций не требуется), но они, очевидно, ограничивают область данных GUI, что делает невозможным для других GUI или функций доступ к этим данным без возврата значений в базовую рабочую область или общую вызывающую функцию. Очевидно, это мешает вам разделение подфункций на отдельные файлы, то, что вы можете легко сделать с 'UserData' или AppData, пока вы передаете дескриптор фигуры.


в общем, если вы решите использовать свойства дескриптора для хранения и передачи данных, можно использовать оба guidata для управления графическими дескрипторами (не большими данными) и setappdata/getappdata для фактических данных программы. они не будут переписывать друг друга С guidata специально


Я не согласен с тем, что MATLAB не подходит для реализации (даже сложных) GUIs - это прекрасно.

однако, что верно, так это то, что:

  1. в документации MATLAB нет примеров того, как реализовать или организовать сложное приложение GUI
  2. все примеры документации простых GUIs используют шаблоны, которые не масштабируются хорошо на всех сложных GUIs
  3. в частности, руководство (встроенный инструмент для автоматической генерации кода GUI) генерирует ужасный код, который является ужасным примером для подражания, если вы что-то реализуете сами.

из-за этих вещей большинство людей подвергаются только очень простым или действительно ужасным ГИС MATLAB, и они в конечном итоге думают, что MATLAB не подходит для создания ГИС.

по моему опыту лучший способ реализовать сложный графический интерфейс в MATLAB так же, как и на другом языке - следуйте хорошо используемому шаблону, такому как MVC (Модель-Вид-Контроллер).

однако это объектно-ориентированный шаблон, поэтому сначала вам придется освоиться с объектно-ориентированным программированием в MATLAB, и особенно с использованием событий. Использование объектно-ориентированной организации для вашего приложения должно означать, что все неприятные методы, которые вы упоминаете (setappdata, guidata, UserData, область вложенных функций и передача взад и вперед нескольких копий данных) не нужны, так как все соответствующие вещи доступны как класс свойства.

лучший пример, который я знаю о том, что MathWorks опубликовал в в этой статье из Matlab Digest. Даже этот пример очень прост, но он дает вам представление о том, как начать, и если вы посмотрите на шаблон MVC, должно стать ясно, как его расширить.

кроме того, я обычно интенсивно использую папки пакетов для организации больших кодовых баз в MATLAB, чтобы убедиться, что нет столкновений имен.

один последний совет - используйте GUI Layout Toolbox, из MATLAB Central. Это упрощает многие аспекты разработки GUI, особенно реализацию автоматического изменения размера, и дает вам несколько дополнительных элементов UI для использования.

надеюсь, что это поможет!


Edit: в MATLAB R2016a MathWorks представлен AppDesigner, новая структура построения GUI, предназначенная для постепенной замены руководства.

AppDesigner представляет собой серьезный разрыв с предыдущими подходами к построению GUI в MATLAB несколькими способами (наиболее глубоко, базовые фигурные окна, созданные на основе HTML-холста и JavaScript, а не Java). Это еще один шаг по пути, инициированному введением Handle Graphics 2 в R2014b, и, несомненно, будет развиваться дальше в будущих выпусках.

но одно влияние AppDesigner на заданный вопрос заключается в том, что он генерирует много лучший код, чем руководство - это довольно чистый, объектно-ориентированный и подходящий для формирования основы шаблона MVC.


мне очень неудобно с тем, как руководство производит функции. (подумайте о случаях, когда вы хотели бы вызвать один gui из другого)

я настоятельно рекомендую вам написать свой объектно-ориентированный код, используя классы дескрипторов. Таким образом, вы можете делать причудливые вещи (например,этой) и не заблудиться. Для организации кода у вас есть + и @ справочники.


Я не думаю, что структурирование GUI-кода принципиально отличается от кода без GUI.

положите вещи, которые принадлежат вместе, вместе в каком-то месте. Как вспомогательные функции, которые могут входить в util или . В зависимости от содержания, возможно, сделать его пакетом.


лично мне не нравится философия "one function one m-file", которую имеют некоторые люди MATLAB. Установка функции типа:

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 спектакль.

Edit:

слушатели по обратным вызовам:

A pushbutton это плохой пример. Нажатие кнопки обычно срабатывает только при определенных действиях, здесь обратные вызовы прекрасны imho. Основным преимуществом в моем случае, например, было то, что программно изменяющиеся текстовые / всплывающие списки не вызывают обратные вызовы, в то время как слушатели на их String или Value собственность срабатывает.

еще пример:

если есть какое-то центральное свойство (например, как некоторый источник inputdata), от которого зависят несколько компонентов в приложении, тогда использование прослушивателей очень удобно, чтобы гарантировать, что все компоненты будут уведомлены, если свойство изменится. Каждый новый компонент, "заинтересованный" в этом свойстве, может просто добавить собственный прослушиватель, поэтому нет необходимости централизованно изменять обратный вызов. Это позволяет гораздо более модульное проектирование компонентов GUI и делает добавление / удаление таких компонентов проще.