Что такое "правильный" способ организовать код 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.
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
В вышеприведенном контроллере я создаю два отдельных, но синхронизированных представления, которые представляют изменения в одной базовой модели и реагируют на них. Один вид показывает временную область сигнала, а другой - представление частотной области с использованием БПФ.
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 не подходит для реализации (даже сложных) графических интерфейсов - это совершенно нормально.
Тем не менее, это правда, что:
- В документации MATLAB нет примеров того, как реализовать или организовать сложное приложение с графическим интерфейсом.
- Все примеры документации простых графических интерфейсов используют шаблоны, которые плохо масштабируются для сложных графических интерфейсов.
- В частности, 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
собственность сработала.
Другой пример:
Если есть какое-то центральное свойство (например, какой-то источник входных данных), от которого зависят несколько компонентов в приложении, тогда использование слушателей очень удобно, чтобы гарантировать, что все компоненты будут уведомлены в случае изменения свойства. Каждый новый компонент, "заинтересованный" в этом свойстве, может просто добавить своего собственного слушателя, поэтому нет необходимости централизованно изменять обратный вызов. Это обеспечивает гораздо более модульную конструкцию компонентов графического интерфейса и облегчает добавление / удаление таких компонентов.