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