web-dev-qa-db-ja.com

GUIコードを整理する「正しい」方法は何ですか?

私は、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設計の助けを探しています。つまり:

  • 整理に使用するディレクトリ構造はありますか? (コールバックvs描画関数?)
  • GUIデータを操作し、アプリケーションデータから分離するための「正しい方法」は何ですか? (GUIデータとは、ハンドルオブジェクトの_set/get_ tingプロパティを意味します)。
  • これらすべての描画関数を数千行の1つの巨大なファイルに入れずに、アプリケーションとGUIデータの両方を効率的にやり取りするにはどうすればよいですか?それは可能ですか?
  • _set/getappdata_を常に使用すると、パフォーマンスが低下しますか?
  • GUIの観点から保守を容易にするために、バックエンドコード(3つのオブジェクトクラスと一連のヘルパー関数)に必要な構造はありますか?

私は貿易ではソフトウェアエンジニアではありません。危険なことは十分知っているので、これらは熟練したGUI開発者(任意の言語)にとってかなり基本的な質問だと思います。私は、MATLABにGUI設計標準がない(存在するか?)ために、このプロジェクトを完了する能力に深刻な影響を与えているように感じます。これは、私がこれまで行ったどのプロジェクトよりもはるかに大規模なMATLABプロジェクトであり、以前に複数のFigureウィンドウなどを備えた複雑なUIをあまり考慮する必要がありませんでした。

27
Dang Khoa

@ 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ビューコンポーネントで行ったものです。

GUIDE generated FIG-file


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

MVC GUI1MVC GUI2

上記のコントローラーでは、2つの個別の同期されたビューをインスタンス化します。どちらも、同じ基本モデルの変更を表し、それに対応しています。 1つのビューは信号の時間領域を示し、もう1つのビューはFFTを使用した周波数領域表現を示します。

26
Amro

UserData property は、MATLABオブジェクトの便利ですが、従来のプロパティです。 「AppData」メソッドスイート(つまり、setappdatagetappdatarmappdataisappdataなど)は、比較的扱いにくい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'_は構造体です。この違いは、setappdatagetappdataにMファイルの実装がない(組み込みである)ことと合わせて、setappdataで名前付きプロパティを設定すると、しない )データ構造の内容全体を書き換える必要があります。 (構造体フィールドのインプレース変更を実行するMEX関数を想像してください。MATLABは、構造体を_'ApplicationData'_ハンドルグラフィックスプロパティの基本的なデータ表現として維持することで実装できる操作です。)


guidata関数はAppData関数のラッパーですが、_'UserData'_のような単一の変数に制限されています。つまり、単一のフィールドを更新するには、すべてのデータフィールドを含むデータ構造全体を上書きする必要があります。述べられている利点は、実際のFigureハンドルを必要とせずにコールバックからデータにアクセスできることですが、私に関する限り、次のステートメントに慣れている場合、これは大きな利点ではありません。

_hFig = ancestor(hObj,'Figure')
_

また、 MathWorksで述べられているように には、効率の問題があります。

大量のデータを「ハンドル」構造に保存すると、特にGUIDATAがGUIのさまざまなサブ関数内で頻繁に呼び出される場合に、かなりの速度低下が発生することがあります。このため、グラフィックスオブジェクトへのハンドルを格納するためだけに 'handles'構造体を使用することをお勧めします。他の種類のデータの場合、SETAPPDATAおよびGETAPPDATAを使用して、アプリケーションデータとして格納する必要があります。

このステートメントは、setappdataを使用して単一の名前付きプロパティを変更する場合、_'ApplicationData'_全体が書き換えられないという私の主張をサポートしています。 (一方、guidatahandles構造体を_'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_。

10
chappjc

MATLABが(複雑な)GUIの実装に適していないことに同意しません-完全に問題ありません。

ただし、本当のことは次のとおりです。

  1. 複雑なGUIアプリケーションを実装または編成する方法のMATLABドキュメントには例がありません
  2. 単純なGUIのすべてのドキュメンテーションの例は、複雑なGUIにはまったく拡張できないパターンを使用しています
  3. 特に、GUIDE(GUIコードを自動生成するための組み込みツール)は、自分で何かを実装する場合に従うべき恐ろしい例である恐ろしいコードを生成します。

これらの理由により、ほとんどの人は非常に単純な、または本当に恐ろしいMATLAB GUIにしか触れておらず、MATLABはGUIの作成には適していないと考えています。

私の経験では、MATLABで複雑なGUIを実装する最良の方法は、他の言語で行う場合と同じです。MVC(model-view-controller)などのよく使われるパターンに従ってください。

ただし、これはオブジェクト指向のパターンであるため、最初に、MATLABでのオブジェクト指向プログラミング、特にイベントの使用に慣れる必要があります。アプリケーションでオブジェクト指向の組織を使用するということは、あなたが言及する厄介なテクニック(setappdataguidataUserData、ネストされた関数のスコープ、および複数のやり取りをやり取りすること)データコピー)は必要ありません。関連するものはすべてクラスプロパティとして使用できるためです。

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パターンの基礎。

7
Sam Roberts

GUIDEが関数を生成する方法に非常に不快です。 (あるGUIを別のGUIから呼び出したい場合について考えてください)

ハンドルクラスを使用してコードオブジェクト指向を記述することを強くお勧めします。そうすれば、空想的なこと(例 this )を実行して、迷子にならないようにすることができます。コードを整理するには、+および@ディレクトリ。

2
bdecaf

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コンポーネントをよりモジュール化した設計が可能になり、そのようなコンポーネントの追加/削除が容易になります。

1
sebastian