GO BACK

Cocoa-Java 教室 その3

「ベジエによるグラフィックの描画」



■グラフィックはベジエ曲線が基本

 とりあえずコントロールの基本的な使い方がわかったところで、次にグラフィックの描画の基本について説明しましょう。コントロールは、使い方さえわかれば後はリファレンスを見て調べながら使えそうですが、グラフィックは基本の考え方がわからないとなんともやりようがないのです。なぜなら、Cocoaではグラフィックは単純に「このメソッドを実行」というだけでは済まないからです。

 Cocoaでは、グラフィックというのは「ベジエ曲線」として用意するのが基本です。Mac OS Xでは「Quartz」というグラフィックエンジンが使われている、というのは聞いたことがありますね? これは、AdobeのPostScriptをベースとしています。このPostScriptというのは、要するにさまざまな曲線データを元に描画するやつで、その基本となっているのがベジエ曲線である、というわけです。まるで「風が吹けば桶屋が儲かる」みたいですが、そういうことで「Cocoaのグラフィックはベジエ曲線が基本である」と考えて下さい。

 グラフィックを描画するというのは、「描画のメソッドを呼び出す」という発想でした。JavaのAWTも、Graphicsインスタンスに用意されている「円を描く」とか「四角形を描く」といったメソッドを呼び出せば、それが表示されました。が、Cocoaではこの考えは根本的に改める必要があります。Cocoaでは、グラフィックを描くというのは「図形のパスを作成する」「それを元に画面に描く」という手順をとります。要するに「図形の作成」と「描画」が分かれているのです。

 また、JavaのAWTでは「描くコンポーネント内の描画メソッドを呼び出す」という発想でした。あるコンポーネントに描画したければ、そのコンポーネントのGraphicsインスタンスを取得し、その中にあるメソッドを呼び出せばそこに描かれたわけですね。——ところがCocoaでは違います。コントロールごとに描画用のオブジェクトは「ない」のです。それぞれのコントロールには、描画対象として設定する機能があり、ベジエ曲線のオブジェクトに描くメソッドがあります。そして「お前が描画対象になれ」「お前の図形を描け」というようにして描画をします。

 ——まぁ、これだけじゃわからんでしょう。そこで、JavaとCocoaの描画の基本的な流れを簡単に整理してみましょう。

※Java(AWT)の場合

  1. 描画するコンポーネントの描画用オブジェクト(Graphics)を取得する。
  2. そのオブジェクト内にある描画用メソッドを呼び出す。(ここで描画される)
  3. 描画が終ったら、必要に応じてオブジェクトの破棄などを行なって、おしまい。

※Cocoaの場合

  1. 描画するコントロールに「お前が描画対象になれ!」と命令する。
  2. 描画する図形のベジエ曲線を作成する。
  3. その他、必要なオブジェクト(色など)を用意する。
  4. 描画用オブジェクトやベジエ曲線で、描画のメソッドを呼び出す。(ここで描画される)
  5. 描画が終ったら、描画するコントロールを描画対象から解放して、おしまい。

 ざっとですが、違いがわかってきたでしょうか。ちょっとCocoaの方が面倒ですね。なにしろ「描く図形」と「描画」がわかれているので、複雑な図形を描こうとするとけっこう大変そうです。——が、逆に「1つの図形を使い回せる」という利点もあります。つまり、図形を作成したら、それを元にして移動したり拡大縮小したりして描画すれば、かなり高度なグラフィックも簡単に描ける(場合もある)のです。


■基本図形を描いてみる

 では、基本的な図形を描画してみましょう。とりあえず簡単なプロジェクトとして、「ボタンと描画用コントロールの2つだけあるアプリケーション」を作ってみます。

1.新規プロジェクト(Cocoa-Java Application)を作成したら、MainMenu.nibをInterface Builderで開きます。

2.デザインウィンドウに「NSImageView」と「NSButton」を1つずつ配置します。NSImageViewというのは、ツールパレットの左から3番目のアイコンをクリックすると現れます。左上の、中に絵が描かれている部品です。これらを配置して、位置や大きさを整えて下さい。


NSImageViewの配置


3.インターフェイスが出来上がったら、Javaのクラスを作成します。以下の手順に従ってMyObjectクラスとインスタンスを作成して下さい。
  1. Nibファイルウィンドウで「Classes」タブを選択し、「java.lang.Object」を選択する。そして「Subclass java.lang.Object」メニューで「MyObject」クラスを作成する。
  2. Infoウィンドウで、言語が「Java」になっていることを確認する。
  3. アウトレットを作成する。名前:「img1」、種類:「NSImageView」という項目を1つ作成する。
  4. アクションを作成する。名前:「btn1Click()」という項目を1つ作成する。
  5. 以上が終ったら、MyObjectをファイルに保存する。
  6. 作成したMyObjectを選択して「Instantiate MyObject」メニューでインスタンスを1つ作成する。

4.接続を行ないます。まずMyObjectインスタンスからNSImageViewまでCtrlキー+ドラッグ&ドロップし、「img1」に接続します。次にNSButtonからMyObjectインスタンスまでCtrlキー+ドラッグ&ドロップし、「btn1Click()」に接続します。

 これで、Interface Builderでの作業は完了しました。後は描画用のbtn1Clickメソッドを記述すれば完成です。Project Builderに戻り、MyObject.javaで以下のようにメソッドを作成して下さい。


public void btn1Click(Object sender) { /* IBAction */
img1.lockFocus();

NSRect r1 = new NSRect(10,10,70,70);
NSBezierPath p1 = NSBezierPath.bezierPathWithRect(r1);
NSColor c1 = NSColor.redColor();
c1.set();
p1.stroke();

NSRect r2 = new NSRect(50,50,70,70);
NSBezierPath p2 = NSBezierPath.bezierPathWithOvalInRect(r2);
NSColor c2 = NSColor.blueColor();
c2.set();
p2.fill();

img1.unlockFocus();
}


 これでプロジェクトは完成です。できあがったら、ビルド&実行してみましょう。そして現れたウィンドウで、ボタンをクリックしてみて下さい。NSImageViewに円と四角形が描かれます。


プロジェクトの実行画面


 いかがです? 単純な図形にしてはちょっと面倒くさそうですが、ともかく描画自体は行なえそうですね。今回は「四角形と円の2つのベジエ曲線作成」「2つの色の設定」の両方をやってるので少し長めですが、単に1つの図形を描くだけならもっとシンプルに行なえます。


■図形描画の基本手順

 では、順番にソースコードを見ていきましょう。まずは、描画先をNSImageView(img1)に設定しています。この部分ですね。
img1.lockFocus();
 この「lockFocus」というメソッドが、描画先を設定する(フォーカスをロックする)メソッドです。これはCocoaのコントロールにはすべて用意されているもので、これでロックすると、以後の描画命令はすべてロックされたコントロール上で実行されるようになるのです。

 ロックしたフォーカスは、メソッド最後にある「unlockFocus」というメソッドで解除します。このようにCocoaでは「描画先をロックする」「終ったらロック解除する」という作業が描画の最初と最後に必ず必要となります。これは忘れないで下さい。

 その後の部分が、図形を描画しているところです。まず赤い四角形を描いている部分からです。
NSRect r1 = new NSRect(10,10,70,70);
 最初に「NSRect」というオブジェクトを作っています。これは、Cocoaで領域を示すのに用いるものです。AWTの「Rectangle」と同じものと考えればよいでしょう。JavaとCocoaでは、ほぼ同じ働きをするものでも、オブジェクトの構造が違うため、別々にクラスが用意されていることが多いのです。このRectangleとNSRectもそうですし、位置を示すPointとNSPointというのもあります。この後に登場するNSColorも、AWTのColorと同様のものです。

 このNSRectは、「右位置」「上位置」「横幅」「縦幅」の4つの値を引数に指定します。この点はRectangleと同じですが、ただし値はintではなくfloatになるので注意して下さい。このNSRectに限らず、Cocoaでは位置や幅などを指定する値はfloatを使うのが基本です。

 また、Cocoaの場合「縦軸は画面の一番下がゼロになり、上にいくほど値が大きくなる」のです。普通、パソコンでは左上がゼロ地点で下に行くほど値は大きくなりますが、Cocoaでは縦軸が逆向きになっているのですね。PostScriptがそういう仕様になっているからなのでしょうが、慣れない内はよく勘違いすることがありますので注意して下さい。
NSBezierPath p1 = NSBezierPath.bezierPathWithRect(r1);
 次の行が、ベジエ曲線(のインスタンス)を作成している部分です。より正確にいえば「ベジエ曲線の『パス』」でしょうか。要するに、図形の線分情報を示すオブジェクトなわけですね。このベジエ曲線(のパス)は、「NSBezierPath」というクラスとして用意されています。これはnewで新たに作成する方法もあるんですが、ここでは「bezierPathWithRect」というメソッドを使ってインスタンスを作成しています。

 このメソッドは、引数に指定したNSRectを使い、その領域の四角形のベジエ曲線インスタンスを生成します。ベジエ曲線で図形を作るのはけっこう面倒なので、四角形とか円といった基本図形は、そのベジエ曲線を作る専用メソッドを用意してあるのですね。
NSColor c1 = NSColor.redColor();
 次に、色を示すオブジェクト「NSColor」を作成しています。これは、先も触れましたがAWTのColorのCocoa版といったものです。RGBを指定してインスタンスを作ったりすることもできますが、ここでは「redColor」という赤い色を示すインスタンスを返すメソッドでインスタンスを取得しています。このようにNSColorには、主な色のインスタンスを返すメソッドが用意されており、それらを使って簡単に色のインスタンスが得られます。主な色生成のメソッドとしては、以下のようなものがあります。
blackColor() , dargGrayColor() , lightGrayColor , whiteColor() , grayColor() , redColor() , greenColor() ,cyanColor() , yellowColor() , magentaColor() , orangeColor() , purpleColor() , brownColor() , clearColor()
 とりあえず、これらのメソッドがわかれば、ちょっとした色変更はできるでしょう。どれも「《色名》Color」という名前ですから、覚えるのにそう苦労はしませんね。

 さて、以上で描画のために必要なオブジェクト類はすべて揃いました。いよいよ実際の描画に入ります。
c1.set();
 まず、描画する色を設定します。色を設定する場合、JavaのAWTでは「色設定のメソッドを呼び出す」という感じでしたが、Cocoaでは「色のインスタンスに『お前を使う』と指示する」のです。この「set」メソッドがそのためのもので、これを実行すると、そのNSColorインスタンスが描画色として設定されます。
p1.stroke();
 次に、ベジエ曲線を描画します。図形の描画も、AWTのように「描画のメソッドを呼び出す」という感じではなく、描く図形のベジエ曲線インスタンスに「お前を描画しろ」と指示するわけです。この「stroke」メソッドは、そのベジエ曲線インスタンスを線描画(内部を塗りつぶさない)するメソッドです。

 これで、赤い四角形の描画は終りました。次に、青い円を描く部分です。——これも、下準備の部分は基本的に似ています。まず描画する領域を示すNSRectを作成し、それを元にベジエ曲線インスタンスを作成します。
NSBezierPath p2 = NSBezierPath.bezierPathWithOvalInRect(r2);
 ここでは先ほどとちょっと違うメソッドを呼び出していますね。「bezierPathWithOvalInRect」は、引数に指定したNSRectの領域内にぴったりおさまる円のベジエ曲線インスタンスを返すメソッドです。先ほどの四角形作成メソッドとペアで覚えておくとよいでしょう。

 後は、buleColorメソッドで青のNSColorを作成し、それをsetしてから、円を描画します。ただし、今回は線描画ではなく、面描画(内部を塗りつぶす)をさせてみました。
p2.fill();
 この「fill」が、塗りつぶして図形を描画するメソッドです。これで青い円も描画できました。——最後にunlockFocusでフォーカスロックを解除し、作業終了というわけです。

 とりあえずざっと説明しましたが、だいたいの流れはわかったでしょうか。実際には、この他にも「線の太さはどうするんだ?」「ウィンドウをリサイズすると図形が消えてしまうがどうするんだ?」「図形の回転や拡大ができるといったがどうするんだ?」等々、ざっと思い付くだけでも二億三千万ぐらいの疑問が湧きそうですが、とりあえず「基本はこんな手順になるんだ」ということだけしっかり覚えて下さい。


■複雑なベジエ曲線を描く

 では、四角形や円以外の、もっと複雑な図形を描くにはどうすればいいんでしょうか。それには、図形のベジエ曲線を自分できちんと作成していかなければいけません。——では、これも簡単な例を挙げましょう。先ほどのbtn1Clickメソッドを、以下のように修正して実行してみて下さい。


public void btn1Click(Object sender) { /* IBAction */
img1.lockFocus();

NSBezierPath p1 = new NSBezierPath();
p1.moveToPoint(new NSPoint(20,20));
p1.lineToPoint(new NSPoint(150,150));
p1.lineToPoint(new NSPoint(20,150));
p1.curveToPoint(new NSPoint(20,80), new NSPoint(150,100),
new NSPoint(150,20));
NSColor c1 = NSColor.redColor();
c1.set();
p1.fill();

img1.unlockFocus();
}

複雑な図形の描画


 今度は、このようになんともへんてこりんな図形が描画されます。ま、「複雑な図形」ではあるでしょう。——ここでは、lockFocusした後、まず最初にからっぽ(?)のベジエ曲線インスタンスを作るところから始まります。
NSBezierPath p1 = new NSBezierPath();
 このように、newでNSBezierPathを作成すると、何の図形情報も持たないインスタンスが作成されます。ここに、自分で図形の情報を追加していくのです。

 図形の描画は、主に「描画地点を移動する」「指定した地点まで直線を描く」「指定した地点まで曲線を描く」の3つの処理を組み合わせて行ないます。ここでは、まず描画を開始する地点を移動しています。
p1.moveToPoint(new NSPoint(20,20));
 この「moveToPoint」というメソッドが、描画地点を移動するメソッドです。引数には「NSPoint」というインスタンスを指定します。これは、AWTの「Point」に相当するもので、位置を示すオブジェクトです。これはnewで横・縦の位置のfloat値を引数に指定してインスタンスを作成します。
p1.lineToPoint(new NSPoint(150,150));
p1.lineToPoint(new NSPoint(20,150));
 次に、その地点から直線を作成します。「lineToPoint」というのがそれで、現在の描画地点から引数に指定した地点までの直線をベジエ曲線インスタンスに追加します。描画地点はlineToPointした場所に移動するので、連続して呼び出すことで、次々に直線を描き足していけるわけです。
p1.curveToPoint(new NSPoint(20,80), new NSPoint(150,100),
new NSPoint(150,20));
 次に実行しているのが曲線を描くメソッドです。「curveToPoint」は、描画地点から引数に指定した位置情報を元に作成した曲線をベジエ曲線インスタンスに追加します。これは3つの引数がありますが、これはそれぞれ以下のような役割をします。
 ベジエ曲線の曲線(ああややこしい)というのは、基本的に4つの点によって作成されます。開始地点・終了地点・そして両地点からそれぞれ延びる2つのコントロールポイントです。まぁ、これは実際に各点をいろいろと指定して描画してみると、どういうものかわかってくることと思います。

 このようにして、moveToPoint・lineToPoint・curveToPointを組み合わせていけば、どんな図形でも作成することができます。図形ができたら、例によってfillやstrokeを呼び出せば、その図形が描画されるというわけです。

 とりあえず、これで簡単な描画ぐらいはできるようになりました。まだまだ初歩の初歩ですが、ともかく「自分でさまざまな表示ができる」というだけでも十分楽しいプログラムが作れるようになるはずですよ。



GO NEXT


GO HOME