正如@SamRoberts 解释的那样,Model–view–controller (MVC) 模式非常适合作为设计 GUI 的架构。我同意没有很多 MATLAB 示例可以展示这种设计......
以下是我编写的一个完整而简单的示例,用于在 MATLAB 中演示基于 MVC 的 GUI。
模型表示某个信号y(t) = sin(..t..)的一维函数。它是一个句柄类对象,这样我们就可以在不创建不必要的副本的情况下传递数据。它公开了可观察的属性,允许其他组件侦听更改通知。
视图 将模型显示为线图形对象。该视图还包含一个滑块来控制信号属性之一,并监听模型更改通知。我还包括了一个特定于视图(不是模型)的交互式属性,可以使用右键单击上下文菜单来控制线条颜色。
控制器负责初始化所有内容并响应视图中的事件并相应地正确更新模型。
请注意,视图和控制器是作为常规函数编写的,但如果您更喜欢完全面向对象的代码,您可以编写类。
与设计 GUI 的通常方式相比,这有点额外的工作,但这种架构的优点之一是数据与表示层的分离。这使得代码更清晰、更易读,尤其是在使用复杂的 GUI 时,代码维护变得更加困难。
这种设计非常灵活,因为它允许您构建相同数据的多个视图。您甚至可以拥有多个同时视图,只需在控制器中实例化更多视图实例,然后查看一个视图中的更改如何传播到另一个视图!如果您的模型可以以不同的方式呈现,这将特别有趣。
此外,如果您愿意,可以使用 GUIDE 编辑器来构建界面,而不是以编程方式添加控件。在这样的设计中,我们只会使用 GUIDE 通过拖放来构建 GUI 组件,但我们不会编写任何回调函数。所以我们只对生成的.fig 文件感兴趣,而忽略随附的.m 文件。我们将在视图函数/类中设置回调。这基本上是我在 View_FrequencyDomain 视图组件中所做的,它加载了使用 GUIDE 构建的现有 FIG 文件。
模型.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
控制器.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
在上面的控制器中,我实例化了两个独立但同步的视图,它们都表示和响应同一底层模型中的更改。一个视图显示信号的时域,另一个显示使用 FFT 的频域表示。