JavaFX Canvasグラフィック描画

JavaFXのグラフィック描画は、AWTやSwingとはまったく異なる。その仕組みを理解すれば、レンダリング処理自体は簡単に行える。ここでは基本となるCanvasコントロールとGraphicsContextクラスの使い方を説明する。

CanvasとGraphicsContext

グラフィック描画は、AWTでもSwingでも基本的な考え方は同じだった。コンポーネントには表示を更新するときに呼び出されるpaintメソッド、またはpaintComponentが用意されており、このメソッドをオーバーライドすると自動的に呼び出されて描画が実行された。

それぞれのコンポーネントには、レンダリング処理を管理するGraphicsまたはGraphics2Dクラスのインスタンスが含まれており、それがpaintメソッドに引数として渡される。このGraphicsにある描画メソッドを呼び出して描画できた。

しかしJavaFXはかなり事情が異なる。まず、コントロールには表示を更新するときに呼び出されるメソッドがない。この時点で「ではどうするのか」と戸惑うJavaプログラマーも多いだろう。メソッドがないので、当然Graphicsも渡されない。では描画手段がないのだろうか。

そうではない。むしろ逆である。「表示更新時に呼び出されるメソッドをオーバーライドしなければ描画できない」のではなく、「必要なときに、いつでもどこでも描画できる」ようになったのである。

グラフィック描画を行うには、Canvasというコントロールを用意する。AWTにも同じ名前のCanvasクラスがあったが、それとはまったく別のものである。JavaFX用のものはjavafx.scene.canvasパッケージに含まれている。

CanvasにはGraphicsContextというクラスのインスタンスが含まれている。これがAWT/SwingのGraphicsクラスに相当する。このインスタンスを取得し、そこにあるメソッドを呼び出すことで、Canvasに図形などを描画できる。

まずFXMLでCanvasを追加してみよう。次のようにFXMLファイルを作成して記述する。

<?xml version="1.0" encoding="UTF-8"?>
 
<?import java.lang.*?>
<?import javafx.scene.canvas.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx"
    xmlns:fx="http://javafx.com/fxml"
    fx:controller="com.devkuma.javafx.AppController">
    <center>
        <Canvas fx:id="canvas" width="300" height="300" />
    </center>
</BorderPane>

このFXMLをロードして表示するApplicationクラスは別途用意しよう。これまで作成したものをそのまま使ってもよい。

BorderPanecenterに単純に<Canvas>タグを配置しているだけである。<Canvas>にはwidthheight属性が追加されている。これによりコントロールのサイズが設定される。

GraphicsContextで描画する

では、コントローラクラスに描画処理を書いてみよう。次のような簡単な例を作成する。

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
 
 
public class AppController implements Initializable {
    @FXML Canvas canvas;
    GraphicsContext gc;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        draw();
    }
     
    void draw(){
        gc.setFill(Color.RED);
        gc.setStroke(Color.BLUE);
        gc.fillRect(50, 50, 150, 150);
        gc.strokeOval(100, 100, 50, 50);
    }
}

これを実行すると、赤い四角形領域の上に青い線で円が描かれる。

Canvasの描画は「GraphicsContextを取得する」「描画設定を行う」「描画メソッドを呼び出す」という流れで行う。

GraphicsContextを取得する

変数 = canvas.getGraphicsContext2D();

GraphicsContextの取得は、CanvasインスタンスのgetGraphicsContext2Dメソッドで行う。これでインスタンスを取得できるので、変数などに保存して使用する。

色を設定する

graphicsContext.setFill("Color");
graphicsContext.setStroke("Color");

描画を行う前に、描く図形に関する詳細な設定を行う必要がある。ここでは色に関する設定だけを用意している。setFillは塗りつぶしの内容を設定し、setStrokeは線描画の内容を設定する。

どちらも引数にはPaintインスタンスを指定する。これはレンダリング方式を表すために使われる。決まった色で塗る場合は、PaintのサブクラスであるColorを使うのが一般的である。

Colorは基本色の値をクラス変数として利用できる。それらを指定すれば簡単に色を設定できる。その他の色は次のようにインスタンスを作成できる。

new Color(, , )

このあたりはjava.awt.Colorとほぼ同じなので、理解しやすいだろう。

描画メソッド

次は描画メソッドを呼び出して描画する。今回は四角形と楕円を描画するメソッドを呼び出している。

gc.fillRect(50, 50, 150, 150);
gc.strokeOval(100, 100, 50, 50);

GraphicsContextには、これ以外にも多数の描画メソッドが含まれている。多くの描画メソッドには「図形の内部を塗りつぶす」「図形の輪郭線を描く」という2種類が用意されている。塗りつぶしはfillOO、輪郭線描画はstrokeOOという名前になっている。

主な描画メソッド

図形描画メソッドにはどのようなものがあるのか。重要なものを整理しておく。

直線を描く

"GraphicsContext".strokeLine(開始横位置, 開始縦位置, 終了横位置, 終了縦位置);

2つの点を結ぶ直線を描く。引数には線の開始点と終了点を指定する。

四角形を描く

"GraphicsContext".fillRectangle(横位置, 縦位置, , 高さ);
"GraphicsContext".strokeRect(横位置, 縦位置, , 高さ);

四角形を描く。fillRectangleは塗りつぶした四角形、strokeRectangleは輪郭線だけを描く。引数には位置とサイズを指定する。

角丸四角形を描く

"GraphicsContext".fillRoundRectangle (横位置, 縦位置, , 高さ, 角の幅, 角の高さ);
"GraphicsContext".stokeRoundRectangle (横位置, 縦位置, , 高さ, 角の幅, 角の高さ);

角が丸くなった四角形を描く。位置とサイズに加えて、角部分の横幅と縦幅を指定する。

楕円を描く

"GraphicsContext".fillOval(横位置, 縦位置, , 高さ);
"GraphicsContext".strokeOval(横位置, 縦位置, , 高さ);

楕円を描く。基本的には四角形描画と同じで、描画する楕円の位置と横縦の幅を指定する。

円弧を描く

"GraphicsContext".fillArc(横位置, 縦位置, 横半径, 縦半径, 開始角度, 長さ, 種類);
"GraphicsContext".stokeArc(横位置, 縦位置, 横半径, 縦半径, 開始角度, 長さ, 種類);

円弧を描く。位置とサイズのほか、その円弧の開始位置、長さ、種類を指定する。種類は描かれる円弧の形に関するもので、ArcTypeという列挙型の値を使って指定する。

  • CHORD: 円弧の端を直線で結んだ形の図形を描く。
  • OPEN: 弧の端を結ばず、開いた形で描く。
  • ROUND: 弧の両端と円の中心を直線で結んだ形の図形を描く。

多角形を描く

"GraphicsContext".fillPolyline(横配列, 縦配列, 頂点数);
"GraphicsContext".stokePolyline(横配列, 縦配列, 頂点数);
"GraphicsContext".fillPolygon(横配列, 縦配列, 頂点数);
"GraphicsContext".stokePolygon(横配列, 縦配列, 頂点数);

複数の頂点を結んだ多角形を描く。引数には、各頂点の横位置と縦位置をそれぞれdouble配列にまとめたものと、頂点数を指定する。OOPolylineは閉じていない図形、OOPolygonは開始点と終了点が接続された閉じた図形を描く。

テキストを描く

"GraphicsContext".fillText(描画テキスト, 横位置, 縦位置);
"GraphicsContext".stokeText(描画テキスト, 横位置, 縦位置);

指定した位置にテキストを描く。描画するテキストと描画位置を指定する。

グラデーションで塗りつぶす

図形を描くときに理解しておきたいのは、塗りつぶし方法である。前にsetFillなどで色を設定したが、これは単に「色を指定する」ものではない。「塗りつぶし方式」を指定するものであり、引数もColorではなく、ColorのスーパークラスであるPaintを指定する。

つまりPaintのサブクラスであれば、Color以外も指定できる。その例として、グラデーションのクラスを指定してみよう。

LinearGradientクラス

これは直線的なグラデーションを利用するためのクラスである。ある地点から別の地点へ、徐々に色が変化していく表現を作れる。次のようにインスタンスを作成する。

変数 = new LinearGradient (開始横, 開始縦, 終了横, 終了縦, boolean, 繰り返し方式, リスト);
  • 開始横、開始縦: グラデーションの開始地点。
  • 終了横、終了縦: グラデーションの終了地点。
  • boolean: 色を均一に変化させていくかどうか。
  • 繰り返し方式: CycleMethodという列挙型の値で指定する。
  • リスト: 色の変化情報を配列にまとめたもの。Stopクラスのインスタンスを用意する。

RadialGradientクラス

これは円形に色が変化していくものである。指定した中心位置から円形に色が変化していく表現ができる。次のようにインスタンスを作成する。

変数 = new RadialGradient (フォーカス方向, フォーカス距離, 中心横, 中心縦, 半径, boolean, 繰り返し方式, リスト);
  • フォーカス方向: 円の中心から見た開始位置の方向。
  • フォーカス距離: 円の中心から見た開始位置の距離。
  • 中心横、中心縦: グラデーションの円の中心位置。
  • 半径: グラデーションの半径。
  • 繰り返し方式: CycleMethod列挙型による繰り返し方式。

「フォーカス」は開始位置を円の中心からずらすためのものである。これにより、円の中心からどの方向へどれだけずらすかを指定し、グラデーションの開始位置を調整できる。

CycleMethod列挙型

繰り返し方式を指定する。次の値が含まれており、その中から1つを指定する。

  • NO_CYCLE: 色の変化を繰り返さない。
  • REFRECT: 開始地点の色と終了地点の色を往復して繰り返し変化させる。
  • REPEAT: 開始地点の色と終了地点の色への変化を繰り返す。

これらのグラデーションの使用例は次のとおりである。

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;

public class AppController implements Initializable {
    @FXML Canvas canvas;
    GraphicsContext gc;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        draw();
    }
     
    void draw(){
        Stop[] stops1 = new Stop[] {
            new Stop(0, Color.RED),
            new Stop(1, Color.YELLOW)
        };
        LinearGradient gradient1 = new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, stops1);
        gc.setFill(gradient1);
        gc.fillRect(50, 50, 150, 150);
        Stop[] stops2 = new Stop[] {
            new Stop(0, Color.BLUE), 
            new Stop(0.5, Color.CYAN)
        };
        RadialGradient gradient2 = new RadialGradient(0, 0, 125, 125, 100, false, CycleMethod.NO_CYCLE, stops2);
        gc.setFill(gradient2);
        gc.fillOval(75, 75, 100, 100);
    }
}

ここではLinearGradientで四角形を描き、その中にRadialGradientで円を描いている。引数が多いため難しく見えるが、行っていること自体は比較的単純である。

画像ファイルを使う

より複雑なグラフィックをレンダリングするには、あらかじめレンダリングするグラフィックを画像ファイルとして用意し、それをロードして描画するとよい。あらかじめImageインスタンスを用意し、それをレンダリングする。

この場合のImageはAWTのImageではない。これはjavafx.scene.imageパッケージに含まれるクラスである。そのため、AWT/Swingの場合とは異なる。

Imageインスタンスを作成する

変数 = new Image(パス);

Imageインスタンスの作成方法はいくつかあるが、まずはこの作成方法を覚えておこう。引数に読み込む画像ファイルのパスをStringで指定すると、それをロードしたImageインスタンスが作成される。

Imageを描画する

これもGraphicsContextクラスにメソッドが用意されている。レンダリング方法に応じていくつかある。主な方法をまとめると次のようになる。

指定位置に画像を描く

"GraphicsContext".drawImage("Image", 横位置, 縦位置);

指定領域に画像を変形して描く

"GraphicsContext".drawImage("Image", 横位置, 縦位置, , 高さ);

画像の一部をCanvasの特定領域に描く

"GraphicsContext".drawImage("Image", 横位置1, 縦位置1, 幅1, 高さ1, 横位置2, 縦位置2, 幅2, 高さ2);

では実際に画像ファイルを描いてみよう。たとえばsample.jpgという画像ファイルを読み込み、Canvasに表示する例は次のとおりである。

package com.devkuma.javafx;
 
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;

public class AppController implements Initializable {
    @FXML Canvas canvas;
    GraphicsContext gc;
     
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        gc = canvas.getGraphicsContext2D();
        draw();
    }
     
    void draw(){
        Image image = new Image("sample.jpg");
        gc.drawImage(image, 0, 0);
    }
}

ここではnew Image("sample.jpg");としてsample.jpgをロードしている。Javaプログラムでは、プログラムファイルが配置される場所がカレントディレクトリになるため、同じ場所にsample.jpgを用意して実行する必要がある。