open guide gui examples ejemplos create app 2014a matlab user-interface matlab-guide matlab-deployment

guide - open gui matlab



¿Cuál es la forma "correcta" de organizar el código GUI? (5)

Como se explicó @SamRoberts , el patrón Model–view–controller (MVC) es adecuado como una arquitectura para diseñar GUI. Estoy de acuerdo en que no hay muchos ejemplos de MATLAB para mostrar dicho diseño ...

A continuación se muestra un ejemplo completo pero simple que escribí para demostrar una GUI basada en MVC en MATLAB.

  • El modelo representa una función 1D de alguna señal y(t) = sin(..t..) . Es un objeto de clase manejable, de esa manera podemos pasar los datos sin crear copias innecesarias. Expone propiedades observables, lo que permite que otros componentes escuchen las notificaciones de cambio.

  • La vista presenta el modelo como un objeto de gráficos de líneas. La vista también contiene un control deslizante para controlar una de las propiedades de señal y escucha las notificaciones de cambio de modelo. También incluí una propiedad interactiva que es específica para la vista (no el modelo), donde el color de la línea se puede controlar usando el menú contextual del botón derecho.

  • El controlador es responsable de inicializar todo y responder a los eventos desde la vista y actualizar correctamente el modelo en consecuencia.

Tenga en cuenta que la vista y el controlador se escriben como funciones regulares, pero puede escribir clases si prefiere el código completamente orientado a objetos.

Es un poco más de trabajo en comparación con la forma habitual de diseñar GUI, pero una de las ventajas de dicha arquitectura es la separación de los datos de la capa de presentación. Esto hace que el código sea más limpio y más legible, especialmente cuando se trabaja con GUI complejas, donde el mantenimiento del código se vuelve más difícil.

Este diseño es muy flexible ya que le permite construir vistas múltiples de los mismos datos. Aún más, puede tener múltiples vistas simultáneas , solo instale más instancias de vistas en el controlador y vea cómo los cambios en una vista se propagan a la otra. Esto es especialmente interesante si su modelo se puede presentar visualmente de diferentes maneras.

Además, si lo prefiere, puede usar el editor GUIDE para construir interfaces en lugar de agregar controles mediante programación. En dicho diseño, solo usaríamos GUIDE para construir los componentes de la GUI mediante el método de arrastrar y soltar, pero no escribiríamos ninguna función de devolución de llamada. Así que solo nos interesará el archivo .fig producido, y simplemente ignoraremos el archivo .m acompaña. Configuraríamos las devoluciones de llamada en la función / clase de vista. Esto es básicamente lo que hice en el componente de vista View_FrequencyDomain , que carga el archivo FIG existente creado con 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

En el controlador anterior, instalé dos vistas separadas pero sincronizadas, que representan y responden a los cambios en el mismo modelo subyacente. Una vista muestra el dominio de tiempo de la señal, y otra muestra la representación de dominio de frecuencia usando FFT.

Estoy trabajando en un programa de GUI bastante sofisticado para implementar con el compilador MATLAB. (Hay buenas razones por las que MATLAB se está utilizando para construir esta GUI, ese no es el punto de esta pregunta. Me doy cuenta de que la construcción de GUI no es una opción fuerte para este lenguaje).

Hay bastantes formas de compartir datos entre funciones en una GUI, o incluso pasar datos entre GUI dentro de una aplicación:

  • setappdata/getappdata/_____appdata - asocia datos arbitrarios a un manejador
  • guidata : normalmente se usa con GUIDE; "almacenar [s] o recuperar [s] datos GUI" en una estructura de identificadores
  • Aplicar una operación set/get a la propiedad UserData de un objeto handle
  • Use funciones anidadas dentro de una función principal; básicamente emula las variables de alcance "globalmente".
  • Pasar los datos hacia adelante y hacia atrás entre las funciones secundarias

La estructura de mi código no es la más bonita. En este momento tengo el motor separado del front-end (¡bien!) Pero el código GUI es bastante similar a un espagueti. Aquí hay un esqueleto de una "actividad", para tomar prestado el habla de 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

He escrito código previamente donde nada está anidado, así que terminé pasando h ida y vuelta a todas partes (ya que las cosas necesitaban ser redibujadas, actualizadas, etc.) y setappdata(fig) para almacenar datos reales. En cualquier caso, he estado manteniendo una "actividad" en un solo archivo, y estoy seguro de que esto será una pesadilla de mantenimiento en el futuro. Las retrollamadas interactúan tanto con los datos de la aplicación como con los objetos gráficos, lo que supongo que es necesario, pero eso impide una segregación completa de las dos "mitades" de la base del código.

Así que estoy buscando ayuda para el diseño de la organización / GUI aquí. A saber:

  • ¿Hay una estructura de directorio que deba usar para organizar? (Callbacks vs funciones de dibujo?)
  • ¿Cuál es la "forma correcta" de interactuar con los datos de la GUI y mantenerlos segregados de los datos de las aplicaciones? (Cuando me refiero a los datos de la GUI me refiero a set/get propiedades de manejar objetos).
  • ¿Cómo evito poner todas estas funciones de dibujo en un único archivo gigante de miles de líneas y todavía pasar eficientemente tanto la información de la aplicación como la de la GUI? ¿Es eso posible?
  • ¿Hay alguna penalización de rendimiento asociada al uso constante de set/getappdata ?
  • ¿Hay alguna estructura que mi código de back-end (3 clases de objetos y un montón de funciones de ayuda) deba tomar para que sea más fácil de mantener desde una perspectiva de GUI?

No soy un ingeniero de software de oficio, solo sé lo suficiente como para ser peligroso, así que estoy seguro de que estas son preguntas bastante básicas para los desarrolladores de GUI experimentados (en cualquier idioma). Casi siento que la falta de un estándar de diseño de GUI en MATLAB (¿existe alguno?) Está interfiriendo seriamente con mi capacidad para completar este proyecto. Este es un proyecto de MATLAB que es mucho más masivo que cualquiera de los que he emprendido, y nunca antes había tenido que pensar mucho en IU complicadas con ventanas de figuras múltiples, etc.


La propiedad UserData es una propiedad útil pero heredada de objetos MATLAB. El conjunto de métodos "AppData" (es decir, setappdata , getappdata , rmappdata , isappdata , etc.) proporciona una excelente alternativa al enfoque get/set(hFig,''UserData'',dataStruct) comparativamente más torpe, IMO. De hecho, para administrar los datos de GUI, GUIDE emplea la función guidata , que es solo un contenedor para las funciones setappdata / getappdata .

''UserData'' vienen a la mente un par de ventajas del enfoque AppData sobre la propiedad ''UserData'' :

  • Interfaz más natural para múltiples propiedades heterogéneas.

    UserData está limitado a una sola variable , lo que requiere que desarrolles otra capa de organización de datos (es decir, una estructura). Supongamos que desea almacenar una cadena str = ''foo'' y una matriz numérica v=[1 2] . Con UserData , necesitaría adoptar un esquema de estructura como s = struct(''str'',''foo'',''v'',[1 2]); y set/get todo cuando desee cualquiera de las propiedades (por ejemplo, s.str = ''bar''; set(h,''UserData'',s); ). Con setappdata , el proceso es más directo (y eficiente): setappdata(h,''str'',''bar''); .

  • Interfaz protegida para el espacio de almacenamiento subyacente.

    Mientras que ''UserData'' es solo una propiedad de gráficos de control normal, la propiedad que contiene los datos de la aplicación no está visible, aunque se puede acceder por su nombre (''ApplicationData'', ¡pero no lo haga!). setappdata usar setappdata para cambiar cualquier propiedad de AppData existente, lo que le impide taclear accidentalmente todo el contenido de ''UserData'' mientras intenta actualizar un solo campo. Además, antes de configurar u obtener una propiedad AppData, puede verificar la existencia de una propiedad con nombre con isappdata , que puede ayudar con el manejo de excepciones (por ejemplo, ejecutar una devolución de llamada de proceso antes de establecer valores de entrada) y administrar el estado de la GUI o las tareas gobierna (por ejemplo, inferir el estado de un proceso por la existencia de ciertas propiedades y actualizar la GUI de manera apropiada).

Una diferencia importante entre las propiedades ''UserData'' y ''ApplicationData'' es que ''UserData'' es por defecto [] (una matriz vacía), mientras que ''ApplicationData'' es de forma nativa una estructura. Esta diferencia, junto con el hecho de que setappdata y getappdata no tienen implementación de M-file (están incorporados), sugiere que establecer una propiedad con nombre con setappdata no requiere reescribir todo el contenido de la estructura de datos . (Imagine una función MEX que realiza una modificación in situ de un campo de estructura, una operación que MATLAB puede implementar manteniendo una estructura como la representación de datos subyacente de la propiedad de gráficos del identificador ''ApplicationData'' ).

La función guidata es un contenedor de las funciones de AppData, pero está limitada a una sola variable, como ''UserData'' . Eso significa que debe sobrescribir toda la estructura de datos que contiene todos sus campos de datos para actualizar un solo campo. Una ventaja declarada es que puede acceder a los datos de una devolución de llamada sin necesitar el identificador real de la figura, pero en lo que a mí respecta, esta no es una gran ventaja si se siente cómodo con la siguiente afirmación:

hFig = ancestor(hObj,''Figure'')

Además, según lo declarado por MathWorks , hay problemas de eficiencia:

Guardar grandes cantidades de datos en la estructura ''maneja'' a veces puede causar una desaceleración considerable, especialmente si a menudo se llama a GUIDATA dentro de las diversas funciones secundarias de la GUI. Por este motivo, se recomienda utilizar la estructura ''handles'' solo para almacenar identificadores en objetos gráficos. Para otros tipos de datos, SETAPPDATA y GETAPPDATA se deben usar para almacenarlo como datos de la aplicación.

Esta afirmación respalda mi afirmación de que toda la ''ApplicationData'' no se reescribe cuando se usa setappdata para modificar una sola propiedad con nombre. (Por otro lado, guidata comprime la estructura de handles en un campo de ''ApplicationData'' llamado ''UsedByGUIData_m'' , por lo que está claro por qué guidata debería reescribir todos los datos de la GUI cuando se cambia una propiedad).

Las funciones anidadas requieren muy poco esfuerzo (no se necesitan estructuras auxiliares o funciones), pero obviamente limitan el alcance de los datos a la GUI, haciendo imposible que otras GUI o funciones accedan a esos datos sin devolver los valores al espacio de trabajo base o una llamada común función. Obviamente, esto evita que dividas las subfunciones en archivos separados, algo que puedes hacer fácilmente con ''UserData'' o con AppData siempre que pases el controlador de la figura.

En resumen, si elige usar las propiedades de manejo para almacenar y pasar datos, es posible usar ambos guidata para administrar los identificadores de gráficos (no datos grandes) y setappdata / getappdata para los datos reales del programa. No se sobreescribirán entre sí ya que guidata un campo especial ''UsedByGUIData_m'' en ApplicationData para la estructura de handles (¡a menos que cometa el error de usar esa propiedad usted mismo!). Solo para reiterar, no acceda directamente a ApplicationData .

Sin embargo, si se siente cómodo con OOP, puede ser más limpio implementar la funcionalidad de GUI a través de una clase , con identificadores y otros datos almacenados en variables miembro en lugar de manejar propiedades y callbacks en métodos que pueden existir en archivos separados en la clase o paquete carpeta . Hay un buen ejemplo en MATLAB Central File Exchange . Esta presentación demuestra cómo se simplifica la transferencia de datos con una clase, ya que ya no es necesario obtener y actualizar constantemente guidata (las variables de los miembros siempre están actualizadas). Sin embargo, existe la tarea adicional de administrar la limpieza al salir, que la sumisión logra estableciendo closerequestfcn la figura, que luego llama a la función de delete de la clase. La presentación es muy similar al ejemplo GUIDE.

Esos son los aspectos más destacados a medida que los veo, pero MathWorks analiza muchos más detalles e ideas diferentes. Ver también esta respuesta oficial a UserData vs. guidata vs. setappdata/getappdata .


Me siento muy incómodo con la manera en que GUIDE produce funciones. (piense en casos en los que le gustaría llamar a una GUI de otra)

Le sugiero encarecidamente que escriba su código objeto orientado utilizando clases de manejo. De esa forma puedes hacer cosas lujosas (por ejemplo, this ) y no perderte. Para organizar el código tiene los directorios + y @ .


No creo que la estructuración del código GUI sea fundamentalmente diferente de un código que no sea GUI.

Coloque las cosas que pertenecen juntas, juntas en algún lugar. Como funciones auxiliares que pueden ir a un directorio de util o helpers . Dependiendo del contenido, tal vez conviértalo en un paquete.

Personalmente, no me gusta la filosofía "one function one m-file" que tienen algunas personas MATLAB. Poniendo una función como:

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

en un archivo separado simplemente no tiene sentido, cuando no hay un escenario en absoluto lo llamará desde otro lugar y luego desde su propia GUI.

Sin embargo, puedes construir la GUI misma de forma modular, por ejemplo, creando ciertos componentes simplemente pasando el contenedor padre:

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

Esto también simplifica las pruebas de ciertos subcomponentes; puede simplemente llamar a createTable en una figura vacía y probar ciertas funcionalidades de la tabla sin cargar la aplicación completa.

Solo dos elementos adicionales que comencé a utilizar cuando mi aplicación se hizo cada vez más grande:

Utilice oyentes sobre devoluciones de llamadas, pueden simplificar significativamente la programación de GUI.

Si tiene datos muy grandes (como desde una base de datos, etc.) podría valer la pena implementar una clase de manejo con estos datos. Almacenar este controlador en algún lugar de la guidata / appdata mejora significativamente el rendimiento de get / setappdata.

Editar:

Oyentes sobre devoluciones de llamada:

Un pushbutton es un mal ejemplo. Presionar un botón por lo general solo se desencadena en ciertas acciones, aquí las devoluciones de llamadas están bien. Una ventaja principal en mi caso, por ejemplo, fue que el cambio programático de listas de texto / ventanas emergentes no activa las devoluciones de llamadas, mientras que los receptores en sus propiedades de String o Value se desencadenan.

Otro ejemplo:

Si hay una propiedad central (por ejemplo, como fuente de datos de entrada) de la que dependen múltiples componentes de la aplicación, entonces el uso de oyentes es muy conveniente para asegurar que se notifiquen todos los componentes si la propiedad cambia. Cada nuevo componente "interesado" en esta propiedad simplemente puede agregar su propio oyente, por lo que no es necesario modificar de forma centralizada la devolución de llamada. Esto permite un diseño mucho más modular de los componentes de la GUI y facilita la adición / eliminación de dichos componentes.


No estoy de acuerdo con que MATLAB no sea bueno para implementar GUI (incluso complejas), está perfectamente bien.

Sin embargo, lo que es cierto es que:

  1. No hay ejemplos en la documentación de MATLAB de cómo implementar u organizar una aplicación GUI compleja
  2. Todos los ejemplos de documentación de GUI simples usan patrones que no se adaptan bien a las GUI complejas
  3. En particular, GUIDE (la herramienta incorporada para generar automáticamente el código GUI) genera un código terrible que es un terrible ejemplo a seguir si usted está implementando algo usted mismo.

Debido a estas cosas, la mayoría de la gente solo está expuesta a GUIs de MATLAB muy simples o realmente horribles, y terminan pensando que MATLAB no es adecuado para hacer GUIs.

En mi experiencia, la mejor manera de implementar una GUI compleja en MATLAB es de la misma manera que lo haría en otro idioma: siga un patrón bien utilizado como MVC (modelo-vista-controlador).

Sin embargo, este es un patrón orientado a objetos, por lo que primero deberá sentirse cómodo con la programación orientada a objetos en MATLAB, y particularmente con el uso de eventos. El uso de una organización orientada a objetos para su aplicación debería significar que no son necesarias todas las técnicas desagradables que menciona ( setappdata , guidata , UserData , definición de funciones anidadas y pasar múltiples copias de datos hacia adelante y hacia atrás), ya que todas las cosas relevantes están disponibles como propiedades de clase.

El mejor ejemplo que conozco de que MathWorks ha publicado está en este artículo de MATLAB Digest. Incluso ese ejemplo es muy simple, pero te da una idea de cómo comenzar, y si observas el patrón MVC, debería quedar claro cómo extenderlo.

Además, normalmente hago un uso intensivo de las carpetas de paquetes para organizar grandes bases de código en MATLAB, para garantizar que no haya conflictos de nombres.

Un consejo final: use la GUI Layout Toolbox , de MATLAB Central. Hace que muchos aspectos del desarrollo de la GUI sean mucho más fáciles, en particular implementando el comportamiento de cambio de tamaño automático, y le da varios elementos de UI adicionales para usar.

¡Espero que ayude!

Editar: En MATLAB R2016a MathWorks introdujo AppDesigner, un nuevo marco de construcción de GUI destinado a reemplazar gradualmente a GUIDE.

AppDesigner representa una ruptura importante con los enfoques anteriores de construcción de GUI en MATLAB de varias maneras (más profundamente, las ventanas de figuras subyacentes generadas se basan en un lienzo HTML y JavaScript, en lugar de Java). Es un paso más a lo largo de un camino iniciado con la introducción de Handle Graphics 2 en R2014b, y sin duda evolucionará aún más en versiones futuras.

Pero uno de los impactos de AppDesigner en la pregunta es que genera un código mucho mejor que GUIDE, es bastante limpio, orientado a objetos y adecuado para formar la base de un patrón MVC.