概要
MATLABにはAppDesignerという簡単にGUIアプリを作れる優れた機能がある.
今回はAppDesignerを使って,マウスでオブジェクトを動かすプログラムを紹介する.
- その1:四角形のpatchオブジェクトを動かすプログラム
- その2:上記を発展させて3点からなる三角形の内接円を描画するプログラム ← この記事
本記事はその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
を見てみると,
とあり,描画されているオブジェクトが表示されている.
たとえば,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
関数を渡すことができている.
結果
実行結果は以下のとおり.
任意の四角形を選んで,ドラッグ&ドロップで動かせていることが確認できる.
三角形に内接する円を描画してみる
単に点を動かせるだけでは面白くないので,点と点を結んで多角形をつくってみる.
今回は3点から三角形を作り,その内接円を描画する.
結果はこのようになる.(コードは記事の末尾)
おわりに
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