LoginSignup
2
1

More than 1 year has passed since last update.

【MATLAB/AppDesigner】 マウスを使ってオブジェクトを動かす その2

Last updated at Posted at 2022-05-14

概要

MATLABにはAppDesignerという簡単にGUIアプリを作れる優れた機能がある.
今回はAppDesignerを使って,マウスでオブジェクトを動かすプログラムを紹介する.

本記事はその2として,AppDesigner上で複数のpatchオブジェクトをそれぞれマウスで動かす方法を紹介する.
単に動かすだけでは面白くないため,3つの四角形オブジェクトを結ぶ三角形を描画し,その内接円をリアルタイムに表示するプログラムを作成した.

方針

前回の記事では,動かすオブジェクトをプライベートプロパティとして確保していた.

今回は四角形をpatchオブジェクトで描画する.
そこでグラフィックオブジェクト用のプロパティとしてsquare1として用意し,patchオブジェクトをここに格納することを考える.
これにより,マウス操作時にコールバックする関数からもpatchオブジェクトにアクセスできるようにしている.

properties (Access = private)
   square1
end

動かしたいオブジェクトが2個や3個の場合は,プロパティをその分だけ確保しておけば,プログラム上は問題ない.
ただ,必要なオブジェクトが10個やそれ以上必要な場合,いちいちその分だけ確保するのは現実的ではないと思われる.
そこで,今回はオブジェクト用のプロパティを用意せずにプログラムしてみる.

どうすればよいか

MATLABのグラフィックスオブジェクトは階層構造にまとめられている.
そこで,これを利用してオブジェクトにアクセスする方法を考えた.

参考:グラフィックス オブジェクトの階層

グラフィックス オブジェクトが階層構造を成しているのは、オブジェクトが他のオブジェクトに含まれているためです。各オブジェクトはグラフィックスの表示で特定の役割を果たします。

たとえば、関数 plot を使用して線グラフを作成する場合を考えます。axes オブジェクトはデータを表すラインに対して参照のフレームを定義します。Figure はグラフを表示するウィンドウです。この Figure には座標軸が含まれ、さらに座標軸にはライン、テキスト、凡例などグラフを表すために使用されるオブジェクトが含まれます。

引用のとおり,座標軸オブジェクトが分かれば,その子オブジェクトであるpatchやlineなどのグラフィックオブジェクトにアクセスできる.
AppDesignarの場合,座標軸オブジェクトはapp.UIAxesだから,その子オブジェクトはapp.UIAxes.Childrenで表される.
複数のオブジェクトが描画された状態でワークスペースからapp.UIAxes.Childrenを見てみると,
work1.PNG

とあり,描画されているオブジェクトが表示されている.
たとえば,h = app.UIAxes.Children(1)とすると,一番上にあるオブジェクトをhに代入し取り出すことも可能だ.
こうすれば,多数のオブジェクトがある場合でも,自由に任意のオブジェクトにアクセスすることができる.

それだけでよいか

残念ながらこれだけでは充分でない.というのも,どのオブジェクトがどの順番で割り当てられているかわからないためである.
(ちなみに,オブジェクトの表示の順番は,Childrenベクトルの順番で決まっているそうです.最初の要素が最後に表示されたオブジェクトになっている.)
そこで,グラフィックスオブジェクトに予め割り当てられているプロパティ値'Tag'を利用することを考える.
'Tag'には任意の文字ベクトルを設定することができ,これを用いてオブジェクトを検索する関数も用意されている.
たとえば,h.Tag = 'S1'としたオブジェクトをappDesigner上で検索するには

 Obj = findall(app.UIAxes,'Tag','S1');

とすればよい.
これを用いて,実際にプログラムを書いてみる.

実装

AppDesignerの設定はその1同様とする.
マウス操作の関数についても,基本的には同様である.
ただし今回は,関数の引数に動かす対象のオブジェクトを追加することで,クリックしたオブジェクトのみを動かすように工夫する.

コードの例は以下のとおり.

    methods (Access = private)
        function leftButtonDown(app,obj)
            disableDefaultInteractivity(app.UIAxes)
            seltype = app.UIFigure.SelectionType;
            if strcmp(seltype,'normal')
                app.UIFigure.Pointer = 'circle';
                app.UIFigure.WindowButtonMotionFcn = @(~,~)windowButtonMove(app,obj);
                app.UIFigure.WindowButtonUpFcn = @(~,~)leftButtonUp(app);
            end
        end

        function windowButtonMove(app,obj)
            x = app.UIAxes.CurrentPoint(1,1);
            y = app.UIAxes.CurrentPoint(1,2);
            obj.XData = [x-1/2 x+1/2 x+1/2 x-1/2];
            obj.YData = [y-1/2 y-1/2 y+1/2 y+1/2];
        end

        function leftButtonUp(app)
            app.UIFigure.Pointer = 'arrow';
            app.UIFigure.WindowButtonMotionFcn = '';
            app.UIFigure.WindowButtonUpFcn = '';
            enableDefaultInteractivity(app.UIAxes)
        end
    end

    % Callbacks that handle component events
    methods (Access = private)
        % Code that executes after component creation
        function startupFcn(app)
            pointNum = 10; %描画する四角の個数
            initialCoord = randi([-10 10],pointNum,2);

            for ii = 1:pointNum 
                tmpSq = [initialCoord(ii,1)-1/2 initialCoord(ii,1)+1/2 initialCoord(ii,1)+1/2 initialCoord(ii,1)-1/2; ...
                    initialCoord(ii,2)-1/2 initialCoord(ii,2)-1/2 initialCoord(ii,2)+1/2 initialCoord(ii,2)+1/2;];
                patch(app.UIAxes,tmpSq(1,:),tmpSq(2,:),'red','Tag',['S' num2str(ii)]);
                squareObj = findall(app.UIAxes,'Tag',['S' num2str(ii)]);
                set(squareObj,'ButtonDownFcn',@(~,~)leftButtonDown(app,squareObj))
            end

            xlim(app.UIAxes,([-15 15])), ylim(app.UIAxes,([-15 15]))
            daspect(app.UIAxes,[1 1 1])
            grid(app.UIAxes,'on')
        end
    end

ポイントは,leftButtonDown(app,obj),windowButtonMove(app,obj)にグラフィックスオブジェクトの引数を追加したことと,startupFcn(app)中の

patch(app.UIAxes,tmpSq(1,:),tmpSq(2,:),'red','Tag',['S' num2str(ii)]);
squareObj = findall(app.UIAxes,'Tag',['S' num2str(ii)]);
set(squareObj,'ButtonDownFcn',@(~,~)leftButtonDown(app,squareObj))

の部分.ここでは,作成した四角形に'S1','S2',...,'S10'の識別子をつけ,それをもとにfindall関数で作成したオブジェクトを検索している.
これにより'ButtonDownFcn'プロパティに,オブジェクトを指定したleftButtonDown関数を渡すことができている.

結果

実行結果は以下のとおり.
Animation1.gif
任意の四角形を選んで,ドラッグ&ドロップで動かせていることが確認できる.

三角形に内接する円を描画してみる

単に点を動かせるだけでは面白くないので,点と点を結んで多角形をつくってみる.
今回は3点から三角形を作り,その内接円を描画する.
結果はこのようになる.(コードは記事の末尾)
Animation5.gif

また,三角形だけでなくN角形も作ることができる.
Animation3.gif

おわりに

2回にわたって,AppDesigner上でマウス操作を実現する手法を紹介した.
matlabでGUIを作成するときの参考にしていただけると大変ありがたいです.
また,今回の手法はAppDesignerに限らず,通常のMATLABのFigureでも適用可能です.

本家の方による解説記事もありました.
【MATLAB】カーソルを追いかけるプロットの作り方
MATLABでお絵描きツールを作ってみた (マウス操作イベントに応じたプログラム例)

付録 コード

properties (Access = private)
    sqs %squareSize
    pointNum
    circle
end

methods (Access = private)

    function leftButtonDownFnc(app,obj)
        disableDefaultInteractivity(app.UIAxes)
        seltype = app.UIFigure.SelectionType;
        if strcmp(seltype,'normal')
            app.UIFigure.Pointer = 'circle';
            app.UIFigure.WindowButtonMotionFcn = @(~,~)windowButtonMoveFnc(app,obj);
            app.UIFigure.WindowButtonUpFcn = @(~,~)leftButtonUpFnc(app);
        end
    end

    function windowButtonMoveFnc(app,obj)
        objNum = obj.Tag;
        objNum = str2double(objNum(2:end));
        x = app.UIAxes.CurrentPoint(1,1);
        y = app.UIAxes.CurrentPoint(1,2);
        obj.XData = [x-app.sqs/2 x+app.sqs/2 x+app.sqs/2 x-app.sqs/2];
        obj.YData = [y-app.sqs/2 y-app.sqs/2 y+app.sqs/2 y+app.sqs/2];
        plotLine(app,objNum)
        plotCircle(app)
    end

    function leftButtonUpFnc(app)
        app.UIFigure.Pointer = 'arrow';
        app.UIFigure.WindowButtonMotionFcn = '';
        app.UIFigure.WindowButtonUpFcn = '';
        enableDefaultInteractivity(app.UIAxes)
    end

    function plotLine(app,objNum)
        if objNum == 1
            objNum0 = app.pointNum;
            objNum1 = 1;
            objNum2 = 2;
        elseif objNum == app.pointNum
            objNum0 = app.pointNum-1;
            objNum1 = app.pointNum;
            objNum2 = 1;
        else
            objNum0 = objNum-1;
            objNum1 = objNum;
            objNum2 = objNum+1;
        end

        line1 = findall(app.UIAxes,'Tag',['L' num2str(objNum0)]);
        line2 = findall(app.UIAxes,'Tag',['L' num2str(objNum1)]);
        sqr0 = findall(app.UIAxes,'Tag',['S' num2str(objNum0)]);
        sqr1 = findall(app.UIAxes,'Tag',['S' num2str(objNum1)]);
        sqr2 = findall(app.UIAxes,'Tag',['S' num2str(objNum2)]);

        line1.XData = [sqr0.XData(1)+app.sqs/2, sqr1.XData(1)+app.sqs/2];
        line1.YData = [sqr0.YData(1)+app.sqs/2, sqr1.YData(1)+app.sqs/2];
        line2.XData = [sqr1.XData(1)+app.sqs/2, sqr2.XData(1)+app.sqs/2];
        line2.YData = [sqr1.YData(1)+app.sqs/2, sqr2.YData(1)+app.sqs/2];
    end

    function plotCircle(app)
        sqr1 = findall(app.UIAxes,'Tag','S1');
        sqr2 = findall(app.UIAxes,'Tag','S2');
        sqr3 = findall(app.UIAxes,'Tag','S3');
        sqrCoord1 = [sqr1.XData(1)+app.sqs/2, sqr1.YData(1)+app.sqs/2];
        sqrCoord2 = [sqr2.XData(1)+app.sqs/2, sqr2.YData(1)+app.sqs/2];
        sqrCoord3 = [sqr3.XData(1)+app.sqs/2, sqr3.YData(1)+app.sqs/2];
        a = norm(sqrCoord2-sqrCoord3); b = norm(sqrCoord3-sqrCoord1); c = norm(sqrCoord1-sqrCoord2);
        ss = (a+b+c)/2;
        S = sqrt(ss*(ss-a)*(ss-b)*(ss-c)); %三角形の面積(ヘロンの公式)
        r = 2*S/(a+b+c); %円の半径
        I = [a*sqrCoord1(1)+b*sqrCoord2(1)+c*sqrCoord3(1) a*sqrCoord1(2)+b*sqrCoord2(2)+c*sqrCoord3(2)]/(a+b+c); %内心の座標
        app.circle.Position = [I(1)-r I(2)-r 2*r 2*r];
    end
end


    % Callbacks that handle component events
    methods (Access = private)

        % Code that executes after component creation
        function startupFcn(app)
        app.UIAxes.SortMethod = 'childorder';
        app.sqs = 0.5;
        app.pointNum = 3;
        initialCoord = randi([-5 5],app.pointNum,2);
        
        app.circle = rectangle(app.UIAxes,'Position',[0 0 0 0],'Curvature',1,'EdgeColor','none','FaceColor','blue'); %内接円

        for ii = 1:app.pointNum %線分作成
            tmpLine = circshift(initialCoord,-(ii-1),1);
            v = [tmpLine(1,1), tmpLine(1,2); tmpLine(2,1), tmpLine(2,2)];
            f = [1 2];
            patch(app.UIAxes,'Faces',f,'Vertices',v,'EdgeColor','k','FaceColor','none','LineWidth',1.5,'Tag', ['L' num2str(ii)]);
        end

        for ii = 1:app.pointNum %頂点作成
            tmpSq = [initialCoord(ii,1)-app.sqs/2 initialCoord(ii,1)+app.sqs/2 initialCoord(ii,1)+app.sqs/2 initialCoord(ii,1)-app.sqs/2; ...
                initialCoord(ii,2)-app.sqs/2 initialCoord(ii,2)-app.sqs/2 initialCoord(ii,2)+app.sqs/2 initialCoord(ii,2)+app.sqs/2;];
            patch(app.UIAxes,tmpSq(1,:),tmpSq(2,:),'red','Tag',['S' num2str(ii)]);
            squareObj = findall(app.UIAxes,'Tag',['S' num2str(ii)]);
            set(squareObj,'ButtonDownFcn',@(~,~)leftButtonDownFnc(app,squareObj))
        end

        plotCircle(app)

        xlim(app.UIAxes,([-10 10])), ylim(app.UIAxes,([-10 10]))
        daspect(app.UIAxes,[1 1 1])
        grid(app.UIAxes,'on')
        end
    end
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1