【MATLAB】たった一行でホワイトボードが作れる!一行コード縛りで学ぶグラフィックス機能


この記事は?

MATLAB のグラフィックス機能の神髄を学ぶべく、一行のコードだけで複雑なグラフィックス処理を作ろう、という突発企画です。
具体的には、こんなかんじのホワイトボード的なものを作ります。

クリックしたところに文字を入力でき、ドラッグで文字を移動できます。これを一行縛りで作成します。

グラフィックス機能で遊びながら、

・グラフィックスハンドル
・コールバック関数
・無名関数
・Text, Axes のプロパティ

あたりの機能を勉強できるという一石二鳥な内容になっている……はず。

注意事項

この記事のコードは基本的に実用性皆無です。あくまでグラフィックス機能について勉強するために「一行」という縛りを付けてます。
コード書くときは普通に複数行で書きましょう。そっちの方が後から読んだときわかりやすいです。

ちなみに、内容的にはある程度グラフィックスオブジェクトの扱いに慣れている人向けになっています。
「グラフィックスオブジェクトよくわからないよー」という方は

グラフィックスオブジェクト
コールバック

あたりの私の記事が参考になるかと思います。

ルール

  1. 使えるコードは 1 行だけ
  2. セミコロンやカンマで区切って 複数のコマンドを 1 行で並列的に実行するのは禁止(関数の中で関数を呼ぶのは OK)
  3. 表示する座標軸の範囲は [0 10] x [0 10] に設定し、縦横の比率は同じ にする(見栄え重視)
  4. gcf, gca, findobj は使わない(これらを使うと難易度が下がるので)

本題

せっかくなんでクイズ形式で行こうと思います。皆さんも考えてみてね!

Q1:ルール 3 を満たす座標軸の作り方

そもそもルール 3

「表示する座標軸の範囲は [0 10] x [0 10] に設定し、縦横の比率は同じ にする」

を満たす座標軸、皆さん 1 行で作れますか?
座標を作るのは axes 関数ですが、座標範囲と縦横比を変更するにはどうすればよいでしょうか?

答えはこちら
>> axes('XLim',[0 10],'YLim',[0 10],'DataAspectRatio',[1 1 1]);

座標軸の範囲は XLim, YLim で、縦横比を変えるには DataAspectRatio を変更します。DataAspectRatio は x, y, z の表示上の比率を制御するプロパティで、特に画像をゆがませずに表示したい場合などに使う場面が多いです。ちなみに、普通は以下のように 2 つに分けて書きます。

axis equal;
axis([0 10 0 10]);

axis 関数は座標軸の範囲だけでなく、軸の向きや縦横比を簡単に変えられるので便利です。ただ、axis equal とか axis image とかで縦横比を変えるときは、内部で DataAspectRatio が自動で適切な値に変更されているんです。この問題の答えを見ると、axis 関数のありがたみがわかりますね。

Q2:文字列「MATLAB」を左端が座標軸上の (1, 1) に来るように配置

次は座標軸上に文字列を配置します。「文字列の配置なんて Text 関数で余裕!」と思ったら大間違いです。
ルール 3 がありますので、座標軸の設定も同時にやらなければなりません。座標軸の設定と文字列の配置を 1 行でやるにはどうすればいいでしょうか?

答えはこちら
>> text(1,1,'MATLAB','Parent', axes('XLim',[0 10],'YLim',[0 10],'DataAspectRatio',[1 1 1]));

グラフィックスオブジェクトの Parent プロパティに Axes のハンドルを設定すると、そのオブジェクトはその Axes 上に配置されます。これを利用すれば、座標軸を設定しつつそこに文字列を配置することも可能です。

念のためここでも書きますが、実用性は皆無です。素直に Axes と Text を分けて書きましょう。

Q3:自由に入力可能な文字列を左端が座標軸上の (1, 1) に来るように配置

決められた文字列を配置してもつまらないので、入力可能な文字列を配置しましょう。座標軸の部分は Q2 と一緒でいいとして、入力可能な文字列ってどんな関数を使えば配置できるでしょうか?

答えはこちら
>> text(1,1,'','Editing','On','Parent', axes('XLim',[0 10],'YLim',[0 10],'DataAspectRatio',[1 1 1]));

Text の Editing プロパティが On のとき、文字列は編集モードになります。そのため、これを Text 作成時に On に設定しておくことで、自由に入力可能な文字列を作成できます。ちなみに編集終了は ESC キーです。
だんだんホワイトボードっぽくなってきました。

Q4:自由に入力可能な文字列を左端が座標軸上でクリックしたところに来るように配置

さらにホワイトボードに近づけるため、クリックしたところに文字列を置くようにしましょう。クリックを検知するには Axes のコールバックの一つである ButtonDownFcn を使えばよさそうですが、一行縛りのせいで別途コールバック関数を定義することは不可能。どうすればコールバックを設定できるでしょうか?

答えはこちら
>> axes('XLim',[0 10],'YLim',[0 10],'DataAspectRatio',[1 1 1],'ButtonDownFcn',@(ax,~) text(ax.CurrentPoint(1,1),ax.CurrentPoint(1,2),'','Editing','on'));

だいぶコードも長くなってきました。
ポイントはコールバックに無名関数を使うことです。わかりやすくするために、無名関数のところだけ抜き出してみましょう。

% 無名関数
@(ax,~) text(ax.CurrentPoint(1,1),ax.CurrentPoint(1,2),'','Editing','on')

ButtonDownFcn は「クリックされたオブジェクトのハンドル」と「イベントデータ」の 2 つを入力引数とする関数を設定する必要があります。今回はイベントデータは使わないので、無名関数は第一引数を ax、第二引数は ~ に設定します。
関数の処理では、text 関数で文字列を作成しています。ただし、クリックした点を取得する必要がありますので、「クリックされたオブジェクトのハンドル」である ax の CurrentPoint プロパティにアクセスしてクリックされた位置を取得します。

これで、クリックされた場所に文字列を配置する関数ができたので、axes の ButtonDownFcn にそれを設定すれば完成。
ちなみに、無名関数はわざわざ関数を定義しなくてもいいので結構便利ですが、これぐらいの処理になると別途関数を作ってしまったほうがわかりやすい気がします。

ほぼホワイトボードの完成です。

Q5:自由に入力可能な文字列を左端が座標軸上でクリックしたところに来るように配置し、ドラッグで移動できるようにする

最後のクイズです。ついに問題文が一行じゃなくなりました笑。
今回はドラッグでの移動。ドラッグ処理にはコールバックが 3 つ必要になりますが、これを 1 行で書くなんてできるんでしょうか?

答えはこちら
>> axes('XLim',[0 10],'YLim',[0 10],'DataAspectRatio',[1 1 1],'ButtonDownFcn',@(ax,~) text(ax.CurrentPoint(1,1),ax.CurrentPoint(1,2),'','Editing','on','ButtonDownFcn',@(tx,~) set(ax.Parent,'WindowButtonMotionFcn',@(f,~) set(tx,'Position',[ax.CurrentPoint(1,1) ax.CurrentPoint(1,2)]), 'WindowButtonUpFcn',@(f,~) set(f,'WindowButtonMotionFcn','','WindowButtonUpFcn',''))));

ゴリ押し万歳!(白目)

長すぎてまともに読めないので、順を追って説明します。

ドラッグでの移動処理のために設定しなくてはならないコールバックは以下の 3 つです。
・Text の ButtonDownFcn:マウスが動いたときおよび離れたときの処理を有効にする
・Figure の WindowButtonMotionFcn:マウスが動いたとき、同じ位置に Text を移動する
・Figure の WindowButtonUpFcn:マウスが離れたあとに Text が動かないように、マウスが動いたときおよび離れたときの処理を無効にする

Text は以下のようなものを Axes の ButtonDownFcn 内で作っています。

text(ax.CurrentPoint(1,1),ax.CurrentPoint(1,2),'','Editing','on','ButtonDownFcn',<Text  BDF 関数>)

Editing までは Q4 と同じです。今回は新たにドラッグ処理のために ButtonDownFcn が設定されてます。その中身はというと、

% Text の BDF 関数
@(tx,~) set(ax.Parent,'WindowButtonMotionFcn',<Figure  WBMF 関数>, 'WindowButtonUpFcn'.<Figure  WBUF 関数>)

こうなってます。ドラッグ処理のためには Figure のコールバックである WindowButtonMotionFcn および WindowButtonUpFcn の設定が必要になります。
そのため、ax の Parent プロパティにアクセスすることで、Figure ウィンドウのハンドルを取得し、コールバックを設定します。

じゃあそのコールバックはどうなってるかというと、

% Figure の WBMF 関数
@(f,~) set(tx,'Position',[ax.CurrentPoint(1,1) ax.CurrentPoint(1,2)])
% Figure の WBUF 関数
@(f,~) set(f,'WindowButtonMotionFcn','','WindowButtonUpFcn','')

WBMF の方は Text の位置をマウスの位置に移動する処理、WBUF の方は Figure の 2 つのコールバックをリセットする処理を行ってます。

以上をまとめると、次のような処理になります。

・座標軸上をクリックすると入力可能な文字列が配置される
・入力した文字列をクリックすると、Figure の WBMF と WBUF が有効になる
・マウスを動かすと、Figure の WBMF によって文字列がマウスと同じ位置に移動する
・マウスを離すと、Figure の WBUF によって WBMF と WBUF が無効になる

言い換えると、ドラッグで移動できる入力可能な文字列が座標軸上に配置される、となります。

実用性は皆無です。複数行でやりましょう。

まとめ

一行縛りでホワイトボード的なものを作ってみました。
一行縛りをすると、各オブジェクトのプロパティやコールバック関数、無名関数の正しい知識が必要になってくるので、勉強がてらいろいろ作ってみるのも面白いかなと思います。

コード自体の実用性は皆無ですが、勉強会などでちょっとした一発ネタとして使えるかもしれません。

みんなもグラフィックスオブジェクトでよい MATLAB ライフを!

 
※よければ MATLAB でゲーム制作シリーズも読んでみてください
- 0 章:グラフィックスオブジェクト
- 1 章:コールバック
- 2 章:メインループ
- 3 章:アクションゲームでジャンプ

「いいね」が増えると次回の記事が豪華になるかも