私は、MATLAB Compilerで配布されるかなり洗練されたGUIプログラムに取り組んでいます。 (MATLABがこのGUIの構築に使用されていることには十分な理由があります。それがこの質問の要点ではありません。GUIの構築はこの言語にはあまり適していません。)
GUI内の関数間でデータを共有する方法、またはアプリケーション内のGUI間でデータを渡す方法はいくつかあります。
setappdata/getappdata/_____appdata
_-任意のデータをハンドルに関連付けますguidata
-通常GUIDEで使用されます。ハンドルの構造体に「GUIデータを保存または取得する」UserData
プロパティに_set/get
_操作を適用する私のコードの構造は最もきれいではありません。現在、エンジンはフロントエンドから分離されていますが(良い!)、GUIコードはかなりスパゲッティのようです。 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
_
私は以前に何もネストされていないコードを書いたので、どこにでもh
をやり取りしました(再描画、更新などが必要なため)、実際のデータを保存するためにsetappdata(fig)
。いずれにせよ、私は1つの「アクティビティ」を1つのファイルに保存してきましたが、これは将来のメンテナンスの悪夢になると確信しています。コールバックは、アプリケーションデータとグラフィカルハンドルオブジェクトの両方と相互作用しますが、これらは必要だと思いますが、これにより、コードベースの2つの「半分」を完全に分離できなくなります。
だから私はここでいくつかの組織的/ GUI設計の助けを探しています。つまり:
set/get
_ tingプロパティを意味します)。set/getappdata
_を常に使用すると、パフォーマンスが低下しますか?私は貿易ではソフトウェアエンジニアではありません。危険なことは十分知っているので、これらは熟練したGUI開発者(任意の言語)にとってかなり基本的な質問だと思います。私は、MATLABにGUI設計標準がない(存在するか?)ために、このプロジェクトを完了する能力に深刻な影響を与えているように感じます。これは、私がこれまで行ったどのプロジェクトよりもはるかに大規模なMATLABプロジェクトであり、以前に複数のFigureウィンドウなどを備えた複雑なUIをあまり考慮する必要がありませんでした。
@ SamRoberts で説明したように、 Model–view–controller (MVC)パターンは、GUIを設計するためのアーキテクチャとして適しています。そのような設計を示すMATLABの例はそれほど多くないことに同意します...
以下は、MATLABでMVCベースのGUIをデモンストレーションするために私が作成した、完全かつシンプルな例です。
modelは、いくつかの信号の1D関数y(t) = sin(..t..)
を表します。これはハンドルクラスのオブジェクトなので、不要なコピーを作成せずにデータを渡すことができます。他のコンポーネントが変更通知をリッスンできるようにする監視可能なプロパティを公開します。
viewは、モデルをライングラフィックスオブジェクトとして表示します。ビューには、信号プロパティの1つを制御するスライダーも含まれており、モデル変更通知をリッスンします。右クリックのコンテキストメニューを使用して線の色を制御できる、ビュー(モデルではない)に固有のインタラクティブプロパティも含めました。
controllerは、すべてを初期化し、ビューからのイベントに応答し、それに応じてモデルを正しく更新します。
ビューとコントローラーは通常の関数として記述されていますが、完全にオブジェクト指向のコードが必要な場合はクラスを記述できます。
これは、通常のGUIの設計方法に比べて少し余分な作業ですが、そのようなアーキテクチャの利点の1つは、データをプレゼンテーションレイヤーから分離することです。これにより、特にコードのメンテナンスが困難になる複雑なGUIを操作する場合に、コードがよりクリーンで読みやすくなります。
この設計では、同じデータの複数のビューを作成できるため、非常に柔軟です。さらに、複数の同時ビューを使用できます。コントローラーでより多くのビューインスタンスをインスタンス化し、1つのビューの変更が他のビューにどのように伝達されるかを確認してください!これは、モデルをさまざまな方法で視覚的に表示できる場合に特に興味深いものです。
さらに、プログラムでコントロールを追加する代わりに、GUIDEエディターを使用してインターフェイスを構築することもできます。このような設計では、ドラッグアンドドロップを使用してGUIコンポーネントを構築するためにGUIDEのみを使用しますが、コールバック関数は記述しません。したがって、生成される.fig
ファイルのみに関心があり、付随する.m
ファイルは無視します。ビューの関数/クラスにコールバックを設定します。これは基本的に、GUIDEを使用して構築された既存のFIGファイルをロードするView_FrequencyDomain
ビューコンポーネントで行ったものです。
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
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
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
上記のコントローラーでは、2つの個別の同期されたビューをインスタンス化します。どちらも、同じ基本モデルの変更を表し、それに対応しています。 1つのビューは信号の時間領域を示し、もう1つのビューはFFTを使用した周波数領域表現を示します。
UserData
property は、MATLABオブジェクトの便利ですが、従来のプロパティです。 「AppData」メソッドスイート(つまり、setappdata
、getappdata
、rmappdata
、isappdata
など)は、比較的扱いにくいget/set(hFig,'UserData',dataStruct)
アプローチ(IMO)の優れた代替手段を提供します。実際、GUIDEはGUIデータを管理するためにthe guidata
関数を使用しています。これは、setappdata
/getappdata
関数のラッパーにすぎませんです。
思い浮かぶ_'UserData'
_プロパティに対するAppDataアプローチのいくつかの利点:
複数の異種プロパティのより自然なインターフェース。
UserData
は1つの変数に制限されます。データの別の層(つまり、構造体)を工夫する必要があります。文字列_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'
_のような単一の変数に制限されています。つまり、単一のフィールドを更新するには、すべてのデータフィールドを含むデータ構造全体を上書きする必要があります。述べられている利点は、実際のFigureハンドルを必要とせずにコールバックからデータにアクセスできることですが、私に関する限り、次のステートメントに慣れている場合、これは大きな利点ではありません。
_hFig = ancestor(hObj,'Figure')
_
また、 MathWorksで述べられているように には、効率の問題があります。
大量のデータを「ハンドル」構造に保存すると、特にGUIDATAがGUIのさまざまなサブ関数内で頻繁に呼び出される場合に、かなりの速度低下が発生することがあります。このため、グラフィックスオブジェクトへのハンドルを格納するためだけに 'handles'構造体を使用することをお勧めします。他の種類のデータの場合、SETAPPDATAおよびGETAPPDATAを使用して、アプリケーションデータとして格納する必要があります。
このステートメントは、setappdata
を使用して単一の名前付きプロパティを変更する場合、_'ApplicationData'
_全体が書き換えられないという私の主張をサポートしています。 (一方、guidata
はhandles
構造体を_'ApplicationData'
_と呼ばれる_'UsedByGUIData_m'
_のフィールドに詰め込むため、1つのプロパティが変更されたときにguidata
がすべてのGUIデータを再書き込みする必要がある理由は明らかです)。
入れ子関数はほとんど労力を必要としません(補助構造や関数は不要)が、データの範囲をGUIに限定しているため、他のGUIまたは関数がベースワークスペースまたは一般的な呼び出しに値を返さずにそのデータにアクセスすることは不可能です関数。これは明らかに、サブ関数を別々のファイルに分割することを防ぎます。これは、図のハンドルを渡せば、_'UserData'
_またはAppDataで簡単に実行できます。
要約すると、ハンドルプロパティを使用してデータを格納および渡すことを選択した場合、guidata
を使用してグラフィックハンドル(大きなデータではない)とsetappdata
/getappdata
の両方を管理することができます。プログラムデータ。 これらは互いに上書きしませんguidata
は、ApplicationData
構造体に対してhandles
に特別な_'UsedByGUIData_m'
_フィールドを作成するため(自分でプロパティを誤って使用しない限り!)繰り返しますが、ApplicationData
に直接アクセスしないでください。
ただし、OOPに慣れている場合は、クラスを介してGUI機能を実装する方がきれいかもしれません、ハンドルプロパティではなくメンバー変数に格納されているハンドルとその他のデータ、およびに存在できるメソッドのコールバック クラスまたはパッケージフォルダーの下の個別のファイル 。 MATLAB Central File Exchangeの良い例 があります。この送信では、guidata
を常に取得および更新する必要がないため、データの受け渡しがクラスによってどのように簡略化されるかを示します(メンバー変数は常に最新です)。ただし、終了時にクリーンアップを管理する追加のタスクがあります。これは、サブミットが図のcloserequestfcn
を設定することによって達成し、次にクラスのdelete
関数を呼び出します。この提出はGUIDEの例によく似ています。
これらは私が見る限りのハイライトですが、さらに多くの詳細とさまざまなアイデアが MathWorksで議論されています です。参照 この公式回答 to UserData
vs. guidata
vs. _setappdata/getappdata
_。
MATLABが(複雑な)GUIの実装に適していないことに同意しません-完全に問題ありません。
ただし、本当のことは次のとおりです。
これらの理由により、ほとんどの人は非常に単純な、または本当に恐ろしいMATLAB GUIにしか触れておらず、MATLABはGUIの作成には適していないと考えています。
私の経験では、MATLABで複雑なGUIを実装する最良の方法は、他の言語で行う場合と同じです。MVC(model-view-controller)などのよく使われるパターンに従ってください。
ただし、これはオブジェクト指向のパターンであるため、最初に、MATLABでのオブジェクト指向プログラミング、特にイベントの使用に慣れる必要があります。アプリケーションでオブジェクト指向の組織を使用するということは、あなたが言及する厄介なテクニック(setappdata
、guidata
、UserData
、ネストされた関数のスコープ、および複数のやり取りをやり取りすること)データコピー)は必要ありません。関連するものはすべてクラスプロパティとして使用できるためです。
MathWorksが公開したことを知っている最良の例は、MATLAB Digestの この記事 にあります。その例でも非常に単純ですが、開始方法のアイデアが得られます。MVCパターンを調べると、それを拡張する方法が明らかになります。
さらに、私は通常、MATLABで大規模なコードベースを整理するためにパッケージフォルダーを多用し、名前の衝突がないことを確認します。
最後のヒント-MATLAB Centralの GUIレイアウトツールボックス を使用します。特に自動サイズ変更動作の実装など、GUI開発の多くの側面がはるかに簡単になり、使用するいくつかの追加のUI要素が提供されます。
お役に立てば幸いです。
編集:MATLAB R2016aで、MathWorksはAppDesignerを導入しました。これは、GUIDEを徐々に置き換えることを目的とした新しいGUI構築フレームワークです。
AppDesignerは、MATLABの以前のGUI構築アプローチのいくつかの点で大きな一歩を踏み出しています(最も深くは、生成される基になるFigureウィンドウは、JavaではなくHTMLキャンバスとJavaScriptに基づいています)。これは、R2014bでのHandle Graphics 2の導入によって開始された道に沿った別のステップであり、将来のリリースでさらに進化することは間違いありません。
しかし、質問に対するAppDesignerの影響の1つは、GUIDEよりもmuch優れたコードを生成することです。これは、かなりクリーンでオブジェクト指向であり、フォームに適しています。 MVCパターンの基礎。
GUIDEが関数を生成する方法に非常に不快です。 (あるGUIを別のGUIから呼び出したい場合について考えてください)
ハンドルクラスを使用してコードオブジェクト指向を記述することを強くお勧めします。そうすれば、空想的なこと(例 this )を実行して、迷子にならないようにすることができます。コードを整理するには、+
および@
ディレクトリ。
GUIコードの構造化は、非GUIコードと根本的に異なるとは思いません。
属しているものをどこかにまとめます。 util
またはhelpers
ディレクトリに入る可能性のあるヘルパー関数のように。内容によっては、パッケージにすることもあります。
個人的には、MATLABの一部の人が持っている「1つの関数が1つのmファイル」という哲学が好きではありません。次のような関数を置く:
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
を呼び出すだけで、アプリケーション全体をロードせずにテーブルの特定の機能をテストできます。
アプリケーションが次第に大きくなったときに使用し始めた2つの追加アイテム:
コールバックではなくリスナーを使用すると、GUIプログラミングを大幅に簡略化できます。
(データベースなどからの)本当に大きなデータがある場合、このデータを保持するハンドルクラスを実装する価値があるかもしれません。このハンドルをguidata/appdataのどこかに格納すると、get/setappdataのパフォーマンスが大幅に向上します。
編集:
コールバックのリスナー:
pushbutton
は悪い例です。ボタンを押すと、通常、特定のアクションでのみトリガーされます。ここでは、コールバックは問題ありません。私の場合の主な利点、例えばプログラムでテキスト/ポップアップリストを変更してもコールバックはトリガーされませんが、String
またはValue
プロパティのリスナーがトリガーされます。
もう一つの例:
アプリケーションの複数のコンポーネントが依存するいくつかの中心的なプロパティ(たとえば、inputdataのソースなど)がある場合、リスナーを使用すると、プロパティが変更された場合にすべてのコンポーネントに確実に通知されます。このプロパティに「関係する」すべての新しいコンポーネントは、独自のリスナーを追加するだけでよいので、コールバックを一元的に変更する必要はありません。これにより、GUIコンポーネントをよりモジュール化した設計が可能になり、そのようなコンポーネントの追加/削除が容易になります。