Quelle est la “bonne” façon d'organiser le code de la GUI?
Je suis en train de travailler sur un assez sophistiqué programme GUI être déployé avec MATLAB Compilateur. (Il y a de bonnes raisons de MATLAB est utilisé pour construire cette interface graphique, qui n'est pas le point de cette question. Je me rends compte GUI-bâtiment n'est pas un point fort pour cette langue.)
Il y a assez peu de moyens pour partager des données entre les fonctions dans une interface graphique, ou même de transmettre les données entre les Interfaces graphiques au sein d'une application:
setappdata/getappdata/_____appdata
- associer des données arbitraires à une poignée deguidata
- généralement utilisé avec GUIDE; "store[s] ou de récupérer[s] GUI de données" à une structure de poignées- Appliquer un
set/get
fonctionnement de laUserData
propriété d'un objet descripteur - Utiliser les fonctions imbriquées à l'intérieur d'une fonction principale; fondamentalement, émule "globalement" la portée des variables.
- Transmettre les données entre les subfunctions
La structure de mon code n'est pas la plus jolie. Droit maintenant, j'ai le moteur séparé du front-end (bon!) mais le code de la GUI est joli comme des spaghettis. Voici un squelette d'une "activité", pour emprunter Android-parler:
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
J'ai déjà écrit de code où rien n'est imbriqué, j'ai donc fini par passer h
en arrière et en avant partout (depuis choses nécessaires à la refonte, mise à jour, etc) et setappdata(fig)
pour stocker des données réelles. En tout cas, j'ai été une "activité" dans un seul fichier, et je suis sûr que cela va être un entretien de cauchemar dans l'avenir. Les rappels sont en interaction à la fois avec les données de l'application graphique et de manipuler des objets, qui, je suppose, est nécessaire, mais c'est la prévention d'une ségrégation complète des deux "moitiés" de la base de code.
Donc je suis à la recherche pour certains d'organisation/GUI conception de l'aide ici. À savoir:
- Est-il une structure de répertoire que je devrais utiliser pour l'organiser? (Rappels vs fonctions de dessin?)
- Quelle est la "bonne façon" d'interagir avec l'interface graphique de données et de les garder séparés à partir de données de l'application? (Quand je parle de GUI de données je veux dire
set/get
ting propriétés de manipuler des objets). - Comment puis-je éviter de mettre toutes ces fonctions de dessin dans un gigantesque fichier de milliers de lignes et toujours efficace de transmettre à la fois l'application et de l'interface de données d'avant en arrière? Est-ce possible?
- Est-il des performances de la pénalité associée avec en permanence à l'aide de
set/getappdata
? - Est-il de toute la structure de mon code back-end (3 classes d'objet et un tas de fonctions d'assistance) devrait prendre pour rendre plus facile à maintenir à partir d'une interface graphique point de vue?
Je ne suis pas un ingénieur logiciel par le commerce, je sais juste assez pour être dangereux, donc je suis sûr que ce sont plutôt des questions de base les plus aguerris GUI développeurs (dans toutes les langues). J'ai presque envie de l'absence d'une interface graphique de conception standard dans MATLAB (n'existe?) est sérieusement interférer avec la capacité de réaliser ce projet. C'est un projet MATLAB qui est beaucoup plus massif que ce que j'ai jamais entrepris, et je n'ai jamais eu à donner beaucoup de pensée complexes de l'Isu avec de multiples figure de windows, etc., avant de.
- certains sont liés à la question: stackoverflow.com/q/335535, stackoverflow.com/q/3490481. Aussi: stackoverflow.com/q/7699788, stackoverflow.com/q/1115703, stackoverflow.com/q/16170475, stackoverflow.com/q/7708308
- comme c'est très utile, ainsi que votre réponse, vous pouvez inclure ce lien suggestions dans votre réponse, plutôt qu'ici dans les commentaires. C'est un bon début pour plus de lecture et un peu perdu dans les commentaires 😉
Vous devez vous connecter pour publier un commentaire.
Comme @SamRoberts expliqué, le Modèle–vue–contrôleur (MVC) est bien adapté comme une architecture, à la conception d'Interfaces graphiques. Je suis d'accord qu'il n'y a pas beaucoup de MATLAB exemples pour montrer le design...
Ci-dessous est un complet mais simple exemple, j'ai écrit pour démontrer un MVC à base de GUI MATLAB.
La modèle représente un 1D fonction de certains signaux
y(t) = sin(..t..)
. C'est un handle de la classe de l'objet, de cette façon, nous pouvons transmettre les données sans créer d'inutiles copies. Il expose les propriétés observables, ce qui permet à d'autres composants pour écouter les notifications de changement.La vue présente le modèle comme une ligne d'objet graphique. La vue contient également un curseur de contrôle de l'une des propriétés de signal, et à l'écoute de changement de modèle de notifications. J'ai aussi inclus un cours interactif de la propriété, qui est spécifique à la vue (pas le modèle), où la couleur de la ligne peut être contrôlé à l'aide du menu contextuel.
La contrôleur est responsable de l'initialisation de tout et de répondre à des événements à partir de la vue et correctement mise à jour le modèle en conséquence.
Notez que la vue et le contrôleur sont écrites comme des fonctions classiques, mais vous pouvez écrire des classes si vous préférez entièrement le code orienté objet.
C'est un peu de travail supplémentaire par rapport à la manière habituelle de concevoir des Interfaces graphiques, mais l'un des avantages de ce type d'architecture est la séparation des données de couche de présentation. Ceci en fait un nettoyant et un code plus lisible, surtout lorsque l'on travaille avec des Interfaces graphiques complexes, où le code de la maintenance devient de plus en plus difficile.
Cette conception est très flexible car il permet de construire des points de vue multiples des mêmes données. Encore plus, vous pouvez avoir plusieurs simultanée vues, il vous suffit d'instancier plus vues les instances du contrôleur et de voir comment les changements dans un point de vue sont propagées à l'autre! Ceci est particulièrement intéressant si votre modèle peut être visuellement présenté de différentes façons.
En outre, si vous préférez, vous pouvez utiliser le GUIDE de l'éditeur de construire des interfaces au lieu de les ajouter par programme des contrôles. Dans une telle conception, nous ne utiliser le GUIDE pour construire des composants de l'interface utilisateur à l'aide de glisser-déposer, mais nous n'aurions pas d'écrire des fonctions de rappel. Nous allons donc nous être intéressés à la
.fig
fichier produit, et il suffit de les ignorer l'accompagnement.m
fichier. Nous setup les rappels dans la fonction de visualisation/classe. C'est en gros ce que j'ai fait dans leView_FrequencyDomain
vue de composant, qui charge l'existant FIG-fichier construit à l'aide de GUIDE.Modèle.m
View_TimeDomain.m
View_FrequencyDomain.m
Contrôleur.m
Dans le contrôleur ci-dessus, j'instancie deux distincts, mais vues synchronisées, à la fois de représenter et de s'adapter aux changements sur le même modèle sous-jacent. D'un point de vue montre le domaine temporel du signal, et l'autre montre la représentation du domaine fréquentiel à l'aide de la FFT.
hgload
, en tant que GUIDE est un outil utile même si vous décidez de ne pas utiliser les rappels qu'il génère.uipanel
conteneur au lieu de cela, de cette façon, plusieurs points de vue peuvent être combinées en une seule figure. La boîte à outils d'interface utilisateur, qui est un excellent package, pourraient être utilisées aussi bien en combinaison avec le modèle MVChandles
struct. Si à la place vous utilisé des classes plutôt que des fonctions, ne serait-ce pas une violation de la Loi de Déméter? Il est plus facile d'accéder à lahandles
struct directement, mais il serait sans doute mieux (et ajouter de manière plus travail) pour fairehandles
privé et d'exposer les méthodes pour manipuler leuicontrol
s?La
UserData
de la propriété est un utile, mais l'héritage, la propriété de MATLAB objets. Le "AppData", une gamme de méthodes (c'est à diresetappdata
,getappdata
,rmappdata
,isappdata
, etc.) fournir une excellente alternative à la comparaison, plus maladroitget/set(hFig,'UserData',dataStruct)
approche, de l'OMI. En fait, pour gérer l'interface graphique de données, GUIDE emploie laguidata
fonction, qui est juste un wrapper pour lesetappdata
/getappdata
fonctions.Quelques avantages de la AppData approche plus de la
'UserData'
propriété qui viennent à l'esprit:Plus naturel interface pour de multiples propriétés hétérogènes.
UserData
est limitée à une seule variable, vous obligeant à concevoir une autre couche de données oranization (c'est à dire une structure (struct). Dites que vous voulez stocker une chaînestr = 'foo'
et d'un tableau numériquev=[1 2]
. AvecUserData
, vous devez adopter une structure régime tel ques = struct('str','foo','v',[1 2]);
etset/get
le tout chaque fois que vous voulez, soit de propriété (par exemple,s.str = 'bar'; set(h,'UserData',s);
). Avecsetappdata
, le processus est plus directe (et efficace):setappdata(h,'str','bar');
.Interface protégée sous-jacente de l'espace de stockage.
Tout
'UserData'
est juste une simple poignée de graphiques de propriété, la propriété contenant les données de l'application n'est pas visible, mais il peut être accessible par un nom ('ApplicationData", mais ne le faites pas!). Vous devez utilisersetappdata
à modifier les AppData propriétés, ce qui vous évite de casser la totalité du contenu de'UserData'
tout en essayant de mettre à jour un champ unique. Aussi, avant de fixer ou obtenir un AppData propriété, vous pouvez vérifier l'existence d'une propriété nommée avecisappdata
, ce qui peut aider avec la gestion des exceptions (par exemple, exécuter un processus de rappel avant de définir les valeurs d'entrée) et de la gestion de l'état de l'interface utilisateur ou les tâches qu ' il régit (par exemple, en déduire l'état d'un processus par l'existence de certaines propriétés et de mise à jour de GUI de façon appropriée).Une différence importante entre les
'UserData'
et'ApplicationData'
propriétés, c'est que'UserData'
est par défaut[]
(un tableau vide), tandis que'ApplicationData'
est nativement une struct. Cette différence, en collaboration avec le fait quesetappdata
etgetappdata
ont pas de M-fichier de mise en œuvre (ils sont intégrés), suggère que définition d'une propriété nommée avecsetappdata
ne pas nécessitent une réécriture de l'ensemble du contenu des données struct. (Imaginez un MEX fonction qui effectue une en place, la modification d'une structure de champ - une opération MATLAB est capable de mettre en œuvre par le maintien d'une structure de sous-jacent à la représentation des données de la'ApplicationData'
poignée de propriété graphics.)La
guidata
fonction est un wrapper pour la AppData fonctions, mais il est limité à une seule variable, comme'UserData'
. Cela signifie que vous devez remplacer l'ensemble de la structure de données contenant tous les champs de données pour mettre à jour un champ unique. Un avantage est que vous pouvez accéder aux données à partir d'un rappel sans avoir le chiffre réel de la poignée, mais pour autant que je suis concerné, ce n'est pas un gros avantage si vous êtes à l'aise avec l'énoncé suivant:Aussi, comme indiqué par MathWorks, il y a des problèmes d'efficacité:
Cette instruction prend en charge mon affirmation que l'ensemble de la
'ApplicationData'
n'est pas réécrit lors de l'utilisation desetappdata
à la modification d'une propriété nommée. (D'un autre côté,guidata
crams lahandles
structure dans un champ de'ApplicationData'
appelé'UsedByGUIData_m'
, donc il est clair pourquoiguidata
aurait besoin de réécrire l'ensemble de l'interface graphique de données lorsqu'une propriété est modifiée).Fonctions imbriquées nécessitent très peu d'effort (pas d'auxiliaire de structures ou de fonctions nécessaires), mais ils ont évidemment de limiter la portée de données pour l'interface graphique, ce qui rend impossible pour d'autres Interfaces ou des fonctions pour accéder à ces données sans avoir à retourner les valeurs à la base de l'espace de travail ou un commun d'appeler la fonction. Évidemment, cela vous empêche de fractionner les sous-fonctions dans des fichiers séparés, quelque chose que vous pouvez facilement le faire avec
'UserData'
ou AppData aussi longtemps que vous passez à la figure de la poignée.En résumé, si vous choisissez d'utiliser la poignée de propriétés pour stocker et transmettre des données, il est possible d'utiliser les deux
guidata
à gérer les graphiques poignées (pas de données de grande taille) etsetappdata
/getappdata
pour le programme réel des données. Ils ne remplacent pas les uns les autres depuisguidata
fait un'UsedByGUIData_m'
champ dansApplicationData
pour lahandles
structure (sauf si vous faites l'erreur d'utiliser cette propriété vous-même!). Juste pour rappeler, ne sont pas directement accèsApplicationData
.Toutefois, si vous êtes à l'aise avec la programmation orientée objet, il peut être plus propre à mettre en œuvre GUI fonctionnalité via une classe, avec des poignées et autres données stockées dans des variables de membre plutôt que de gérer les propriétés, et rappels sur les méthodes qui peuvent exister dans fichiers distincts en vertu de la classe ou le dossier du package. Il y a un bel exemple sur MATLAB Central d'Échange de Fichiers. Cette présentation montre comment la transmission des données est simplifiée grâce à une classe car il n'est plus nécessaire de constamment obtenir et mettre à jour
guidata
(membres des variables sont toujours à jour). Cependant, il est la tâche supplémentaire de la gestion de l'assainissement de la sortie, où la soumission accomplit par la définition de la figure de l'closerequestfcn
, qui appelle ladelete
fonction de la classe. La présentation bien parallèle à celui de l'exemple.Ceux qui en sont les points que je les vois, mais beaucoup plus de détails et d'idées différentes sont discuté par MathWorks. Voir aussi cette réponse officielle à
UserData
vsguidata
vssetappdata/getappdata
.ApplicationData
plutôt que dansUserData
. Donc, la performance sage, ils devraient être assez similaires.getappdata
/setappdata
a de meilleures performances queguidata
implique que la totalité de la structure dansApplicationData
besoin d'être réécrite. Aussi, si vous neget(h,'ApplicationData')
vous voyez que c' est nativement une structure, alors queget(h,'UserData')
est[]
, vous obligeant à stocker une structure dans la propriété. Le fait qu'il n'existe pas de M-fichier implémentations degetappdata
/setappdata
suggère un accès plus efficace méthode pour laApplicationData
structure de propriété.ApplicationData
ainsi? Je suppose qu'il n'a pas vraiment beaucoup de différence. Juste voulez définir une convention et le bâton avec elleguidata
pour stocker des graphiques poignées (pas les données de l'utilisateur) ETset/getappdata
pour les données réelles. Ils ne remplacent pas les uns les autres depuisguidata
utilise le spécial'UsedByGUIData_m'
propriété deApplicationData
(sauf si vous faites l'erreur d'utiliser cette propriété vous-même!). À partir de ici: "à l'Aide de la getappdata, setappdata, et rmappdata fonctions n'affecte pas l'interface graphique de données." Et juste pour rappeler, ne sont pas directement accès'ApplicationData'
.Je suis en désaccord que MATLAB n'est pas bon pour la mise en œuvre (même complexe) d'Interfaces utilisateur - il est parfaitement bien.
Cependant, ce qui est vrai, c'est que:
À cause de ces choses, la plupart des gens ne sont exposées soit très simple ou vraiment horrible MATLAB des Interfaces graphiques, et ils finissent par croire MATLAB n'est pas adapté pour faire des Interfaces graphiques.
Dans mon expérience, la meilleure façon de mettre en œuvre un complexe GUI MATLAB est de la même manière que vous le feriez dans une autre langue - suivre un bon modèle tels que MVC (modèle-vue-contrôleur).
Cependant, c'est un des modèles orientés objets, donc tout d'abord vous aurez à obtenir à l'aise avec la programmation orientée objet en MATLAB, et en particulier avec l'utilisation d'événements. L'utilisation d'un objet organisation axée sur les résultats pour votre application devrait signifier que toutes les sales techniques que vous mentionnez (
setappdata
,guidata
,UserData
, fonction imbriquée de délimitation de l'étendue, et de passer à l'arrière-et-vient multiples copies de données) ne sont pas nécessaires, comme toutes les choses sont disponibles comme des propriétés de la classe.Le meilleur exemple que je connaisse qui MathWorks a publié est en cet article de MATLAB Digérer. Même que l'exemple est très simple, mais il vous donne une idée de la façon de commencer, et si vous regardez dans le modèle MVC, il devrait être clair comment l'étendre.
En plus, j'ai généralement de faire un usage intensif de l'emballage des dossiers pour organiser de grandes bases de code en MATLAB, il n'y a pas de nom affrontements.
Une dernière astuce - pour utiliser le Boîte à outils d'interface utilisateur, à partir de MATLAB Central. Il rend de nombreux aspects de développement du GUI beaucoup plus facile, en particulier la mise en œuvre de redimensionnement automatique de comportement, et vous donne plusieurs autres éléments de l'INTERFACE utilisateur à utiliser.
Espère que ça aide!
Edit: Dans MATLAB R2016a MathWorks introduit AppDesigner, une nouvelle interface graphique-cadre pour le renforcement des destiné à remplacer progressivement GUIDE.
AppDesigner représente une rupture majeure avec les précédents GUI de renforcement des approches dans MATLAB de plusieurs façons (le plus profondément, le sous-jacent figure fenêtres générées sont basées sur un canevas HTML et JavaScript, plutôt que de Java). C'est une autre étape le long d'une route initiée par l'introduction de la Poignée Graphique 2 dans R2014b, et sera sans doute encore évoluer au fil des versions futures.
Mais un impact de AppDesigner sur la question posée, c'est qu'il génère beaucoup un code de meilleure qualité que le GUIDE a fait - c'est assez propre, orienté objet, et adapté pour former la base d'un modèle MVC.
TabPanel
). Je vais lire dans le modèle de conception MVC.Je suis très mal à l'aise avec la façon dont le GUIDE produit de fonctions. (pensez au sujet des cas où vous souhaitez appeler une interface graphique à partir d'un autre)
Je suggère fortement que vous écrivez votre code orienté objet à l'aide de la poignée de classes. De cette façon, vous pouvez faire de fantaisie pour animaux (par exemple,cette) et ne pas se perdre. Pour l'organisation du code, vous avez la
+
et@
répertoires.Je ne pense pas que la structuration de GUI-code est fondamentalement différente de la non-GUI code.
Mettre les choses qui vont ensemble, ensemble, à un certain endroit.
Comme aide-fonctions susceptibles d'aller dans un
util
ouhelpers
répertoire. Selon le contenu, peut-être en faire un paquet.Personnellement je n'aime pas le "a une fonction d'un m-file" de la philosophie de certains MATLAB gens ont.
Mettre une fonction comme:
dans un autre fichier fait tout simplement pas de sens, quand il n'y a pas de scénario que vous souhaitez appeler cela de quelque part d'autre, puis, à partir de votre propre interface utilisateur.
Cependant, vous pouvez construire l'interface graphique de façon modulaire, en créant par exemple de certains composants, en passant dans le conteneur parent:
Cela simplifie également les tests de certains sous-composants - il vous suffit d'appeler
createTable
sur une zone vide de la figure et de tester certaines fonctionnalités du tableau sans charger la totalité de la demande.Juste deux autres articles que j'ai commencé à utiliser lors de mon application est devenue de plus en plus grande:
Utiliser les auditeurs sur les rappels, ils peuvent simplifier la programmation GUI de manière significative.
Si vous avez vraiment de données de grande taille (comme à partir d'une base de données, etc.) il pourrait être utile de mettre en œuvre une poignée de classe la tenue de ces données.
Le stockage de cette poignée quelque part dans le guidata/appdata améliore de manière significative obtenir/setappdata performance.
Edit:
Auditeurs sur les rappels:
Un
pushbutton
est mauvais exemple. En poussant un bouton est généralement déclenche seulement sur certaines d'action, ici rappels sont beaux à mon humble avis.Un principal avantage dans mon cas, par exemple, était de programme pour le remplacement de texte/listes déroulantes ne déclenche pas de rappels, tandis que les auditeurs sur leur
String
ouValue
propriété sont déclenchées.Un autre exemple:
S'il y a une centrale de propriété (par exemple, comme une source de inputdata) que plusieurs composants de l'application dépendent, puis à l'aide d'écouteurs est très pratique pour s'assurer que tous les composants sont notifié si la modification de la propriété.
Chaque nouveau composant "intéressé" dans cette propriété peut simplement ajouter, il est propre à l'écoute, donc il n'y a pas de nécessité de centraliser modifier la fonction de rappel.
Cela permet beaucoup plus de la conception modulaire des composants de l'interface utilisateur et fait de l'ajout/retrait de composants plus facile.
pushbutton
objet d'être pressé?