Qual è il modo “giusto” per organizzare il codice GUI?

Sto lavorando a un programma GUI abbastanza sofisticato da distribuire con MATLAB Compiler. (Ci sono buone ragioni per cui MATLAB è stato usato per build questa GUI, non è questo il punto di questa domanda. Mi rendo conto che la costruzione di GUI non è un valido esempio di questo linguaggio.)

Esistono diversi modi per condividere i dati tra le funzioni di una GUI o anche per passare i dati tra le GUI all’interno di un’applicazione:

  • setappdata/getappdata/_____appdata : associa dati arbitrari a un handle
  • guidata – tipicamente utilizzato con GUIDE; “Memorizza [s] o recupera [s] i dati della GUI” in una struttura di handle
  • Applicare un’operazione set/get alla proprietà UserData di un object handle
  • Utilizzare funzioni annidate all’interno di una funzione principale; fondamentalmente emula variabili di ambito “globali”.
  • Passa i dati avanti e indietro tra le sottofunzioni

La struttura per il mio codice non è la più bella. In questo momento ho il motore segregato dal front-end (buono!) Ma il codice GUI è piuttosto simile agli spaghetti. Ecco uno scheletro di “attività”, per prendere in prestito 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 

Ho un codice scritto in precedenza in cui nulla è annidato, quindi ho finito per passare avanti e indietro ovunque (dal momento che le cose dovevano essere ridisegnate, aggiornate, ecc.) E setappdata(fig) per memorizzare i dati reali. In ogni caso, ho mantenuto una “attività” in un singolo file, e sono sicuro che questo sarà un incubo di manutenzione in futuro. Le callback interagiscono sia con i dati dell’applicazione che con gli oggetti grafici di handle, che suppongo siano necessari, ma ciò impedisce una completa separazione delle due “metà” del code base.

Quindi sto cercando un aiuto di progettazione organizzativa / GUI qui. Vale a dire:

  • C’è una struttura di directory che dovrei usare per organizzare? (Callbacks vs funzioni di disegno?)
  • Qual è il “modo giusto” per interagire con i dati della GUI e tenerlo separato dai dati dell’applicazione? (Quando mi riferisco ai dati della GUI, intendo le proprietà di set/get di oggetti handle).
  • Come evitare di inserire tutte queste funzioni di disegno in un file gigante di migliaia di righe e comunque passare in modo efficiente sia i dati dell’applicazione che quelli della GUI avanti e indietro? È ansible?
  • Esiste una penalità legata alle prestazioni associata all’uso costante di set/getappdata ?
  • C’è qualche struttura che il mio codice di back-end (3 classi di oggetti e un mucchio di funzioni di supporto) dovrebbe prendere per rendere più facile il mantenimento da una prospettiva GUI?

Non sono un ingegnere del software per mestiere, ne so abbastanza per essere pericoloso, quindi sono sicuro che queste sono domande di base per gli sviluppatori di GUI esperti (in qualsiasi lingua). Mi sento quasi come se la mancanza di uno standard di progettazione della GUI in MATLAB (ne esiste uno?) Interferisca seriamente con la mia capacità di completare questo progetto. Questo è un progetto MATLAB che è molto più grande di qualsiasi altro che abbia mai intrapreso, e non ho mai dovuto preoccuparmi di complicate interfacce utente con windows a figure multiple, ecc.

Come spiegato da @SamRoberts , il pattern Model-view-controller (MVC) è adatto come architettura per progettare interfacce grafiche . Sono d’accordo sul fatto che non ci sono molti esempi di MATLAB là fuori per mostrare tale design …

Di seguito è riportato un esempio completo ma semplice che ho scritto per dimostrare una GUI basata su MVC in MATLAB.

  • Il modello rappresenta una funzione 1D di alcuni segnali y(t) = sin(..t..) . È un object di class handle, in questo modo possiamo passare i dati senza creare copie inutili. Espone le proprietà osservabili, che consente ad altri componenti di ascoltare le notifiche di modifica.

  • La vista presenta il modello come un object grafico a linee. La vista contiene anche un cursore per controllare una delle proprietà del segnale e ascolta le notifiche di modifica del modello. Ho anche incluso una proprietà intertriggers che è specifica per la vista (non il modello), in cui il colore della linea può essere controllato utilizzando il menu contestuale del tasto destro.

  • Il controller è responsabile di inizializzare tutto e rispondere agli eventi dalla vista e aggiornare correttamente il modello di conseguenza.

Si noti che la vista e il controller sono scritti come funzioni regolari, ma è ansible scrivere classi se si preferisce il codice completamente orientato agli oggetti.

È un piccolo lavoro in più rispetto al solito modo di progettare GUI, ma uno dei vantaggi di tale architettura è la separazione dei dati dal livello di presentazione. Questo rende un codice più pulito e più leggibile, specialmente quando si lavora con interfacce grafiche complesse, dove la manutenzione del codice diventa più difficile.

Questo design è molto flessibile in quanto consente di creare più viste degli stessi dati. Ancora di più è ansible avere più viste simultanee , basta istanziare più istanze di visualizzazioni nel controller e vedere come le modifiche in una vista vengono propagate all’altra! Questo è particolarmente interessante se il tuo modello può essere presentato visivamente in modi diversi.

Inoltre, se si preferisce, è ansible utilizzare l’editor GUIDE per creare interfacce invece di aggiungere i controlli a livello di codice. In un tale progetto useremmo GUIDE per build i componenti della GUI usando il drag-and-drop, ma non scriveremo alcuna funzione di callback. Quindi saremo interessati solo al file .fig prodotto e ignoreremo semplicemente il file .m allegato. Dovremmo impostare i callback nella funzione / class di visualizzazione. Questo è fondamentalmente ciò che ho fatto nel componente View_FrequencyDomain , che carica il file FIG esistente creato usando GUIDE.

FIG-file generato da 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) mf = 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

Nel controller precedente, istanzio due viste separate ma sincronizzate, che rappresentano e rispondono alle modifiche nello stesso modello sottostante. Una vista mostra il dominio temporale del segnale e un altro mostra la rappresentazione del dominio della frequenza usando FFT.

La proprietà UserData è una proprietà utile, ma legacy, degli oggetti MATLAB. La suite di metodi “AppData” (ad esempio setappdata , getappdata , rmappdata , isappdata , ecc.) Fornisce un’ottima alternativa get/set(hFig,'UserData',dataStruct) relativamente più goffo get/set(hFig,'UserData',dataStruct) , IMO. Infatti, per gestire i dati GUI, GUIDE utilizza la funzione guidata , che è solo un wrapper per le funzioni setappdata / getappdata .

Un paio di vantaggi dell’approccio AppData rispetto alla proprietà 'UserData' che vengono in mente:

  • Interfaccia più naturale per molteplici proprietà eterogenee.

    UserData è limitato a una singola variabile , che richiede di escogitare un altro livello di oranizzazione dei dati (cioè una struttura). Supponi di voler memorizzare una stringa str = 'foo' e un array numerico v=[1 2] . Con UserData , avresti bisogno di adottare uno schema struct come s = struct('str','foo','v',[1 2]); e set/get il tutto quando vuoi o la proprietà (es. s.str = 'bar'; set(h,'UserData',s); ). Con setappdata , il processo è più diretto (ed efficiente): setappdata(h,'str','bar'); .

  • Interfaccia protetta per lo spazio di archiviazione sottostante.

    Mentre 'UserData' è solo una normale proprietà grafica di handle, la proprietà contenente i dati dell’applicazione non è visibile, sebbene sia accessibile per nome (‘ApplicationData’, ma non farlo!). Devi usare setappdata per modificare qualsiasi proprietà AppData esistente, che ti impedisce di rovinare accidentalmente l’intero contenuto di 'UserData' durante il tentativo di aggiornare un singolo campo. Inoltre, prima di impostare o ottenere una proprietà AppData, è ansible verificare l’esistenza di una proprietà denominata con isappdata , che può aiutare con la gestione delle eccezioni (ad esempio, eseguire una richiamata del processo prima di impostare i valori di input) e gestire lo stato della GUI o le attività che governa (ad esempio, inferire lo stato di un processo in base all’esistenza di determinate proprietà e aggiornare la GUI in modo appropriato).

Una differenza importante tra le proprietà 'UserData' e 'ApplicationData' è che 'UserData' è di default [] (una matrice vuota), mentre 'ApplicationData' è nativamente una struct. Questa differenza, insieme al fatto che setappdata e getappdata non hanno getappdata M-file (sono integrati), suggerisce che l’ impostazione di una proprietà denominata con setappdata non richiede la riscrittura dell’intero contenuto della struttura di dati . (Immaginate una funzione MEX che esegue una modifica sul posto di un campo struct – un’operazione che MATLAB è in grado di implementare mantenendo una struct come la rappresentazione dei dati sottostanti di 'ApplicationData' gestisce la proprietà grafica.)


La funzione guidata è un wrapper per le funzioni AppData, ma è limitata a una singola variabile, come 'UserData' . Ciò significa che devi sovrascrivere l’intera struttura dati contenente tutti i tuoi campi di dati per aggiornare un singolo campo. Un vantaggio dichiarato è che è ansible accedere ai dati da una richiamata senza dover utilizzare l’handle della figura reale, ma per quanto mi riguarda, questo non è un grande vantaggio se si ha familiarità con la seguente affermazione:

 hFig = ancestor(hObj,'Figure') 

Inoltre, come affermato da MathWorks , ci sono problemi di efficienza:

Il salvataggio di grandi quantità di dati nella struttura ‘maniglie’ può a volte causare un notevole rallentamento, specialmente se la GUIDATA viene spesso chiamata all’interno delle varie sotto-funzioni della GUI. Per questo motivo, si consiglia di utilizzare la struttura ‘maniglie’ solo per archiviare gli handle sugli oggetti grafici. Per altri tipi di dati, è necessario utilizzare SETAPPDATA e GETAPPDATA per archiviarli come dati dell’applicazione.

Questa affermazione supporta la mia affermazione che l’intero 'ApplicationData' non viene riscritto quando si usa setappdata per modificare una singola proprietà con nome. (D’altra parte, guidata crama la struttura delle handles in un campo di 'ApplicationData' chiamato 'UsedByGUIData_m' , quindi è chiaro perché guidata dovrebbe riscrivere tutti i dati della GUI quando viene modificata una proprietà).


Le funzioni annidate richiedono uno sforzo minimo (non sono necessarie strutture o funzioni ausiliarie), ma limitano ovviamente l’ambito dei dati alla GUI, rendendo imansible ad altre GUI o funzioni accedere a tali dati senza restituire valori all’area di lavoro di base o una chiamata comune funzione. Ovviamente questo ti impedisce di dividere le sub funzioni in file separati, cosa che puoi facilmente fare con 'UserData' o AppData purché passi l’handle della figura.


In sintesi, se si sceglie di utilizzare le proprietà handle per archiviare e trasmettere i dati, è ansible utilizzare sia guidata per gestire gli handle grafici (non i dati di grandi dimensioni) e setappdata / getappdata per i dati del programma effettivo. Non si sovrascriveranno a vicenda poiché la guida rende uno speciale campo 'UsedByGUIData_m' in ApplicationData per la struttura degli handles (a meno che non si commetta l’errore di usare quella proprietà da soli!). Giusto per ripetere, non accedere direttamente ad ApplicationData .

Tuttavia, se sei a tuo agio con OOP, potrebbe essere più semplice implementare le funzionalità della GUI tramite una class , con maniglie e altri dati memorizzati nelle variabili membro anziché gestire le proprietà e callback in metodi che possono esistere in file separati sotto la class o il pacchetto cartella . C’è un bell’esempio su MATLAB Central File Exchange . Questa sottomissione dimostra come i dati di passaggio siano semplificati con una class in quanto non è più necessario ottenere e aggiornare costantemente (le variabili dei membri sono sempre aggiornate). Tuttavia, vi è l’ulteriore compito di gestire la pulizia all’uscita, che l’invio si ottiene impostando la funzione closerequestfcn , che quindi chiama la funzione di delete della class. La sottomissione si avvicina piacevolmente all’esempio GUIDE.

Questi sono i punti salienti mentre li vedo, ma molti altri dettagli e idee diverse sono discussi da MathWorks . Vedi anche questa risposta ufficiale a setappdata/getappdata vs. setappdata/getappdata vs. setappdata/getappdata .

Non sono d’accordo sul fatto che MATLAB non sia adatto all’implementazione di GUI (anche complesse) – è perfettamente soddisfacente.

Tuttavia, ciò che è vero è che:

  1. Non ci sono esempi nella documentazione MATLAB su come implementare o organizzare un’applicazione complessa per la GUI
  2. Tutti gli esempi di documentazione di semplici GUI utilizzano schemi che non si adattano affatto a complesse interfacce grafiche
  3. In particolare, GUIDE (lo strumento integrato per la generazione automatica del codice della GUI) genera un codice terribile che è un terribile esempio da seguire se stai implementando qualcosa tu stesso.

A causa di queste cose, molte persone sono esposte solo a GUI MATLAB molto semplici o davvero orribili e finiscono per pensare che MATLAB non sia adatto per creare GUI.

Nella mia esperienza, il modo migliore per implementare una GUI complessa in MATLAB è la stessa che faresti in un’altra lingua: segui uno schema ben utilizzato come MVC (model-view-controller).

Tuttavia, questo è un modello orientato agli oggetti, quindi per prima cosa dovrete familiarizzare con la programmazione orientata agli oggetti in MATLAB e, in particolare, con l’uso degli eventi. L’uso di un’organizzazione orientata agli oggetti per la tua applicazione dovrebbe significare che tutte le tecniche brutte che tu menzioni ( setappdata , guidata , setappdata , setappdata nidificato e passaggio di più copie di dati) non sono necessarie, poiché tutte le cose rilevanti sono disponibili come proprietà di class.

Il miglior esempio che conosco che MathWorks abbia pubblicato è in questo articolo di MATLAB Digest. Anche quell’esempio è molto semplice, ma ti dà un’idea di come iniziare, e se guardi il pattern MVC dovrebbe essere chiaro come estenderlo.

Inoltre, in genere utilizzo pesantemente le cartelle dei pacchetti per organizzare codebase di grandi dimensioni in MATLAB, per garantire che non vi siano conflitti di nomi.

Un ultimo consiglio: usa la GUI Layout Toolbox , da MATLAB Central. Rende molto più semplici gli aspetti dello sviluppo della GUI, in particolare implementando il comportamento di ridimensionamento automatico e fornisce numerosi elementi aggiuntivi per l’interfaccia utente da utilizzare.

Spero possa aiutare!


Modifica: in MATLAB R2016a MathWorks ha introdotto AppDesigner, un nuovo framework per la costruzione di GUI destinato a sostituire gradualmente GUIDE.

AppDesigner rappresenta un’importante interruzione con i precedenti approcci di costruzione di GUI in MATLAB in diversi modi (in particolare, le windows di figura sottostanti generate sono basate su una canvas HTML e JavaScript, piuttosto che Java). Si tratta di un altro passo lungo una strada avviata dall’introduzione di Handle Graphics 2 in R2014b, e senza dubbio si evolverà ulteriormente rispetto alle versioni future.

Ma un impatto di AppDesigner sulla domanda posta è che genera un codice molto migliore di quello fatto da GUIDE: è abbastanza pulito, orientato agli oggetti e adatto a formare la base di un pattern MVC.

Sono molto a disagio nel modo in cui GUIDE produce funzioni. (pensa ai casi in cui ti piacerebbe chiamare una gui da un’altra)

Suggerisco caldamente di scrivere il codice object orientato usando le classi handle. In questo modo puoi fare cose di fantasia (ad esempio questo ) e non perderti. Per organizzare il codice hai le directory + e @ .

Non penso che strutturare il codice GUI sia fondamentalmente diverso dal codice non-GUI.

Metti le cose che appartengono insieme, insieme in qualche luogo. Come le funzioni di aiuto che potrebbero essere inserite in una directory di util o di helpers . A seconda del contenuto, forse renderlo un pacchetto.


Personalmente non mi piace la filosofia “una funzione un m-file” che alcune persone di MATLAB hanno. Mettendo una funzione come:

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

in un file separato semplicemente non ha senso, quando non c’è nessuno scenario che si possa chiamare da qualche altra parte allora dalla propria GUI.


È comunque ansible creare la GUI stessa in modo modulare, ad esempio creando alcuni componenti semplicemente passando il contenitore principale:

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

Ciò semplifica anche il testing di alcuni sottocomponenti: è sufficiente chiamare createTable su una figura vuota e testare determinate funzionalità della tabella senza caricare l’intera applicazione.


Solo due elementi aggiuntivi che ho iniziato a utilizzare quando la mia applicazione è diventata sempre più grande:

Utilizza gli ascoltatori tramite i callback, possono semplificare in modo significativo la programmazione della GUI.

Se si dispone di dati veramente grandi (ad esempio da un database, ecc.) Potrebbe essere utile implementare una class handle che trattiene questi dati. La memorizzazione di questa maniglia da qualche parte in guidata / appdata migliora in modo significativo le prestazioni get / setappdata.

Modificare:

Ascoltatori oltre i callback:

Un pushbutton è un cattivo esempio. Premendo un pulsante di solito si innesca solo su determinate azioni, qui le callback vanno bene. Un vantaggio principale nel mio caso, ad esempio, è che gli elenchi di testo / popup che cambiano a livello di codice non triggersno i callback, mentre gli ascoltatori con la loro proprietà String o Value vengono triggersti.

Un altro esempio:

Se esiste una proprietà centrale (ad es. Come una fonte di dati di input) da cui dipendono più componenti nell’applicazione, l’uso di listener è molto comodo per garantire che tutti i componenti vengano notificati se la proprietà cambia. Ogni nuovo componente “interessato” a questa proprietà può semplicemente aggiungere il proprio listener, quindi non è necessario modificare centralmente il callback. Ciò consente un design molto più modulare dei componenti della GUI e rende più facile l’aggiunta / rimozione di tali componenti.