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

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

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

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

Структура моего кода не самая красивая. Прямо сейчас у меня есть движок, отделенный от внешнего интерфейса (хорошо!), Но код GUI довольно похож на спагетти. Вот скелет "активности", позаимствовать Android-говорящий:

function myGui

    fig = figure(...); 

    % h is a struct that contains handles to all the ui objects to be instantiated. My convention is to have the first field be the uicontrol type I'm instantiating. See draw_gui nested function

    h = struct([]);


    draw_gui;
    set_callbacks; % Basically a bunch of set(h.(...), 'Callback', @(src, event) callback) calls would occur here

    %% DRAW FUNCTIONS

    function draw_gui
        h.Panel.Panel1 = uipanel(...
            'Parent', fig, ...
            ...);

        h.Panel.Panel2 = uipanel(...
            'Parent', fig, ...
            ...);


        draw_panel1;
        draw_panel2;

        function draw_panel1
             h.Edit.Panel1.thing1 = uicontrol('Parent', h.Panel.Panel1, ...);
        end
        function draw_panel2
             h.Edit.Panel2.thing1 = uicontrol('Parent', h.Panel.Panel2, ...);
        end


    end

    %% CALLBACK FUNCTIONS
    % Setting/getting application data is done by set/getappdata(fig, 'Foo').
end

У меня есть ранее написанный код, в котором ничего не вложено h туда и обратно везде (поскольку вещи нужно перерисовывать, обновлять и т. д.) и setappdata(fig) хранить актуальные данные. В любом случае, я сохранил одно "действие" в одном файле, и я уверен, что в будущем это будет кошмар обслуживания. Обратные вызовы взаимодействуют как с данными приложения, так и с объектами графического дескриптора, что, я полагаю, необходимо, но это предотвращает полное разделение двух "половинок" базы кода.

Поэтому мне нужна помощь в разработке организационного / графического интерфейса. А именно:

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

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

5 ответов

Решение

Как объяснил @SamRoberts, шаблон Model-view-controller(MVC) хорошо подходит в качестве архитектуры для проектирования графических интерфейсов. Я согласен, что примеров MATLAB не так много, чтобы показать такой дизайн...

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

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

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

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

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

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

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

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

GUIDE сгенерированный FIG-файл


Model.m

classdef Model < handle
    %MODEL  represents a signal composed of two components + white noise
    % with sampling frequency FS defined over t=[0,1] as:
    %   y(t) = a * sin(2pi * f*t) + sin(2pi * 2*f*t) + white_noise

    % observable properties, listeners are notified on change
    properties (SetObservable = true)
        f       % frequency components in Hz
        a       % amplitude
    end

    % read-only properties
    properties (SetAccess = private)
        fs      % sampling frequency (Hz)
        t       % time vector (seconds)
        noise   % noise component
    end

    % computable dependent property
    properties (Dependent = true, SetAccess = private)
        data    % signal values
    end

    methods
        function obj = Model(fs, f, a)
            % constructor
            if nargin < 3, a = 1.2; end
            if nargin < 2, f = 5; end
            if nargin < 1, fs = 100; end
            obj.fs = fs;
            obj.f = f;
            obj.a = a;

            % 1 time unit with 'fs' samples
            obj.t = 0 : 1/obj.fs : 1-(1/obj.fs);
            obj.noise = 0.2 * obj.a * rand(size(obj.t));
        end

        function y = get.data(obj)
            % signal data
            y = obj.a * sin(2*pi * obj.f*obj.t) + ...
                sin(2*pi * 2*obj.f*obj.t) + obj.noise;
        end
    end

    % business logic
    methods
        function [mx,freq] = computePowerSpectrum(obj)
            num = numel(obj.t);
            nfft = 2^(nextpow2(num));

            % frequencies vector (symmetric one-sided)
            numUniquePts = ceil((nfft+1)/2);
            freq = (0:numUniquePts-1)*obj.fs/nfft;

            % compute FFT
            fftx = fft(obj.data, nfft);

            % calculate magnitude
            mx = abs(fftx(1:numUniquePts)).^2 / num;
            if rem(nfft, 2)
                mx(2:end) = mx(2:end)*2;
            else
                mx(2:end -1) = mx(2:end -1)*2;
            end
        end
    end
end

View_TimeDomain.m

function handles = View_TimeDomain(m)
    %VIEW  a GUI representation of the signal model

    % build the GUI
    handles = initGUI();
    onChangedF(handles, m);    % populate with initial values

    % observe on model changes and update view accordingly
    % (tie listener to model object lifecycle)
    addlistener(m, 'f', 'PostSet', ...
        @(o,e) onChangedF(handles,e.AffectedObject));
end

function handles = initGUI()
    % initialize GUI controls
    hFig = figure('Menubar','none');
    hAx = axes('Parent',hFig, 'XLim',[0 1], 'YLim',[-2.5 2.5]);
    hSlid = uicontrol('Parent',hFig, 'Style','slider', ...
        'Min',1, 'Max',10, 'Value',5, 'Position',[20 20 200 20]);
    hLine = line('XData',NaN, 'YData',NaN, 'Parent',hAx, ...
        'Color','r', 'LineWidth',2);

    % define a color property specific to the view
    hMenu = uicontextmenu;
    hMenuItem = zeros(3,1);
    hMenuItem(1) = uimenu(hMenu, 'Label','r', 'Checked','on');
    hMenuItem(2) = uimenu(hMenu, 'Label','g');
    hMenuItem(3) = uimenu(hMenu, 'Label','b');
    set(hLine, 'uicontextmenu',hMenu);

    % customize
    xlabel(hAx, 'Time (sec)')
    ylabel(hAx, 'Amplitude')
    title(hAx, 'Signal in time-domain')

    % return a structure of GUI handles
    handles = struct('fig',hFig, 'ax',hAx, 'line',hLine, ...
        'slider',hSlid, 'menu',hMenuItem);
end

function onChangedF(handles,model)
    % respond to model changes by updating view
    if ~ishghandle(handles.fig), return, end
    set(handles.line, 'XData',model.t, 'YData',model.data)
    set(handles.slider, 'Value',model.f);
end

View_FrequencyDomain.m

function handles = View_FrequencyDomain(m)    
    handles = initGUI();
    onChangedF(handles, m);

    hl = event.proplistener(m, findprop(m,'f'), 'PostSet', ...
        @(o,e) onChangedF(handles,e.AffectedObject));
    setappdata(handles.fig, 'proplistener',hl);
end

function handles = initGUI()
    % load FIG file (its really a MAT-file)
    hFig = hgload('ViewGUIDE.fig');
    %S = load('ViewGUIDE.fig', '-mat');

    % extract handles to GUI components
    hAx = findobj(hFig, 'tag','axes1');
    hSlid = findobj(hFig, 'tag','slider1');
    hTxt = findobj(hFig, 'tag','fLabel');
    hMenu = findobj(hFig, 'tag','cmenu1');
    hMenuItem = findobj(hFig, 'type','uimenu');

    % initialize line and hook up context menu
    hLine = line('XData',NaN, 'YData',NaN, 'Parent',hAx, ...
        'Color','r', 'LineWidth',2);
    set(hLine, 'uicontextmenu',hMenu);

    % customize
    xlabel(hAx, 'Frequency (Hz)')
    ylabel(hAx, 'Power')
    title(hAx, 'Power spectrum in frequency-domain')

    % return a structure of GUI handles
    handles = struct('fig',hFig, 'ax',hAx, 'line',hLine, ...
        'slider',hSlid, 'menu',hMenuItem, 'txt',hTxt);
end

function onChangedF(handles,model)
    [mx,freq] = model.computePowerSpectrum();
    set(handles.line, 'XData',freq, 'YData',mx)
    set(handles.slider, 'Value',model.f)
    set(handles.txt, 'String',sprintf('%.1f Hz',model.f))
end

Controller.m

function [m,v1,v2] = Controller
    %CONTROLLER  main program

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

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

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

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

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

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

MVC GUI1MVC GUI2

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

UserData Свойство является полезным, но унаследованным свойством объектов MATLAB. Набор методов AppData (т.е. setappdata, getappdata, rmappdata, isappdata и т. д.) предоставляют отличную альтернативу сравнительно более неуклюжим get/set(hFig,'UserData',dataStruct) подход, ИМО. Фактически, для управления данными графического интерфейса, GUIDE использует 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, который может помочь с обработкой исключений (например, запустить обратный вызов процесса перед установкой входных значений) и управлять состоянием графического интерфейса пользователя или задачами, которыми он управляет (например, выводить состояние процесса по наличию определенных свойств и соответствующим образом обновлять графический интерфейс пользователя).

Важное различие между '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 структура в поле 'ApplicationData' называется 'UsedByGUIData_m' так понятно почему guidata потребуется переписать все данные графического интерфейса при изменении одного свойства).


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


Таким образом, если вы решите использовать свойства дескриптора для хранения и передачи данных, можно использовать оба guidata управлять графическими дескрипторами (не большими данными) и setappdata / getappdata для фактических данных программы. Они не будут перезаписывать друг друга, так как guidata делает специальный 'UsedByGUIData_m' поле в ApplicationData для handles структура (если вы не допустите ошибку, используя это свойство самостоятельно!). Просто повторить, не получить прямой доступ ApplicationData,

Однако, если вам удобно с ООП, возможно, будет удобнее реализовать функциональность GUI через класс, с дескрипторами и другими данными, хранящимися в переменных-членах, а не со свойствами, и с обратными вызовами в методах, которые могут существовать в отдельных файлах в классе или пакете папка На MATLAB Central File Exchange есть хороший пример. Это представление демонстрирует, как передача данных упрощается с помощью класса, так как больше нет необходимости постоянно получать и обновлять guidata (переменные членов всегда актуальны). Однако есть дополнительная задача управления очисткой при выходе, которая выполняется путем установки значения фигуры. closerequestfcn, который затем вызывает delete функция класса. Представление прекрасно соответствует примеру GUIDE.

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

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

Тем не менее, это правда, что:

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

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

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

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

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

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

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

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


Изменить: В MATLAB R2016a MathWorks представила AppDesigner, новую среду построения GUI, предназначенную для постепенной замены GUIDE.

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

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

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

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

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

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


Лично мне не нравится философия "одна функция - один файл", которую придерживаются некоторые люди из MATLAB. Помещение функции вроде:

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

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


Однако вы можете построить сам графический интерфейс модульным способом, например, создавая определенные компоненты, просто передавая родительский контейнер:

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

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


Просто два дополнительных элемента я начал использовать, когда мое приложение становилось все больше и больше:

Используйте слушателей через обратные вызовы, они могут значительно упростить программирование GUI.

Если у вас действительно большие данные (например, из базы данных и т. Д.), Возможно, стоит реализовать класс-дескриптор, содержащий эти данные. Хранение этого дескриптора где-то в guidata / appdata значительно улучшает производительность get / setappdata.

Редактировать:

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

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

Другой пример:

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

Другие вопросы по тегам