Подтвердить что ты не робот

Какой "правильный" способ организовать графический интерфейс?

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

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

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

Структура моего кода не самая красивая. Прямо сейчас у меня есть двигатель, отделенный от front-end (хорошо!), Но код 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 и сохраняет его отдельно от данных приложения? (Когда я ссылаюсь на данные GUI, я имею в виду set/get ting свойства объектов handle).
  • Как я могу исключить все эти функции рисования в один гигантский файл из тысяч строк и до сих пор эффективно передавать данные приложения и GUI взад и вперед? Возможно ли это?
  • Существует ли ограничение производительности, связанное с постоянным использованием set/getappdata?
  • Есть ли какая-либо структура моего внутреннего кода (3 класса объектов и куча вспомогательных функций), чтобы упростить поддержку с точки зрения графического интерфейса?

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

GUIDE generated FIG-file


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

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

Ответ 2

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

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


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

hFig = ancestor(hObj,'Figure')

Кроме того, как указано в MathWorks, есть проблемы с эффективностью:

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

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


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


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

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

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


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

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

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

Ответ 4

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

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

Ответ 5

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

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


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

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

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


Тем не менее, вы можете сами создавать GUI модульным способом, например. создавая определенные компоненты, просто передавая родительский контейнер:

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

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


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

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

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

Edit:

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

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

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

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