JavaFXのウィンドウやSVG画像への図の描画

本記事はMuroran Institute of Technology Advent Calendar 2019 12/14の記事です.

adventar.org

概要

本記事では

  1. JavaFXウィンドウへの図の描画,
  2. SVG画像への図の描画,
  3. JavaFXウィンドウとSVG画像への同じ図の描画

について具体的なGradleプロジェクトの例を用いて説明します.

JavaFXJavaGUIライブラリの一つです. JavaFXはJava8で標準ライブラリに追加されてJDKの一部となりましたが,Java11からはJDKに同梱されなくなり,代わりにOpenJFXを利用することができるようになりました. 1.では,JavaFXCanvasクラスに図の描画を行うためにorg.jfreeのFXGraphics2DというライブラリのFXGraphics2Dクラスを利用します. FXGraphics2Dクラスは標準ライブラリのGraphics2Dクラスを継承しているため,Graphics2Dクラスと同じように扱うことができます.

SVG画像はベクタ画像(品質の劣化なくサイズを変更可能な形式の画像)の一つです. SVG画像の形式はXML形式で,仕様はW3Cによって勧告されています. 2.では,SVG画像に図の描画を行うためにorg.jfreeのJFreeSVGというライブラリのSVGGraphics2Dクラスを利用します. SVGGraphics2Dクラスも標準ライブラリのGraphics2Dクラスを継承しており,Graphics2Dクラスと同じように扱うことができます.

3.では,FXGraphics2DクラスとSVGGraphics2DクラスがGraphics2Dクラスを継承していることを生かたポリモーフィックなプログラムによって,JavaFXウィンドウとSVG画像への同じ図の描画を行います.

1.,2.,3.はOpenJFX,FXGraphics2D,JFreeSVGといった標準のJDKに含まれないライブラリを利用しますが,これらのライブラリの管理を含めたプロジェクトのビルドを行うために,本記事ではGradleというビルドツールを使用します. Gradleはbuild.gradleという設定ファイルに従って,コンパイル,ライブラリの用意,実行といった操作を自動的に行うソフトウェアです.

以下では,まず,本記事の実行環境について説明し,Graphics2Dクラスの基本的な使い方を示します. その後1.,2.,3.についてそれぞれ,Gradleプロジェクトの例を示します. 本記事ではJavaの環境としてJava13を利用しました.

実行環境

本記事のプログラムは,次のようにGitHubリポジトリからクローンして実行することができます.

git clone https://github.com/Jumpaku/AdventCalendar_2019-12-14.git
cd AdventCalendar_2019-12-14

# JavaFXウィンドウへの図の描画
cd example1
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

# SVG画像への図の描画
cd ../example2
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

# JavaFXウィンドウとSVG画像への同じ図の描画
cd ../example3
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

ただし,Java13とGitがインストールされていることが前提となります.

このリポジトリは以下のように3つのプロジェクトexample1,example2,example3で構成されています. f:id:Jumpaku:20191213230441p:plain 例えば,example1ディレクトリで./gradlew runを実行すると,Gradleがexample1/build.gradleに書かれたプロジェクトの設定に従ってプロジェクトをビルドし,起動します. プログラムが起動されるとexample1/src/main/java/example1/Main.javamainメソッドが呼ばれます. このmainメソッドはFXGraphics2Dクラスを利用します. FXGraphics2Dクラスはorg.jfreeのFXGraphics2Dというライブラリに含まれています. 必要となるライブラリをbuild.gradleに書いておくと,ビルドする時にGradleによってダウンロードされてライブラリを利用できるようになります.

Graphics2Dの基本的な使い方

Graphics2Dの基本的な使い方として

  • 基本図形のストローク描画とフィル描画
  • パス操作
  • 領域操作
  • アフィン変換

を示します.

基本図形のストローク描画とフィル描画

    public static void exampleDrawAndFill(Graphics2D g) {
        // 楕円を表現するEllipse2Dオブジェクトを生成する
        // 基本図形は他にLine2D(線分),Rectangle2D(長方形),Arc2D(弧)などもある
        Ellipse2D ellipse = new Ellipse2D.Double(0.0, 25.0, 100.0, 50.0);
        // Ellipseオブジェクトの内部の塗りつぶし色を設定する
        g.setColor(new Color(255, 75, 0));
        // Ellipseオブジェクトの内部を塗りつぶす
        g.fill(ellipse);
        // Ellipseオブジェクトの輪郭線の色を黒に設定する
        g.setColor(Color.BLACK);
        // Ellipseオブジェクトの輪郭線を太さ1の線に設定する
        g.setStroke(new BasicStroke(1f));
        g.draw(ellipse);
    }

パス操作

    public static void examplePath(Graphics2D g) {
        // 複雑なパス図形を表現できるPath2Dオブジェクトを生成する
        Path2D curve = new Path2D.Double();
        // Path2Dオブジェクトの開始点を設定する
        curve.moveTo(125.0, 25.0);
        // Path2Dオブジェクトの開始点に,3次Bezier曲線を接続する
        curve.curveTo(200.0, 25.0, 100.0, 75.0, 175.0, 75.0);
        // Path2Dオブジェクトの輪郭線を描画する
        g.setColor(new Color(3, 175, 122));
        g.setStroke(new BasicStroke(1f));
        g.draw(curve);
        // Path2Dオブジェクトを生成する
        Path2D polyline = new Path2D.Double();
        // Path2Dオブジェクトの開始点を設定する
        polyline.moveTo(125.0, 25.0);
        // Path2Dオブジェクトの開始点に,線分を接続する
        polyline.lineTo(200.0, 25.0);
        // Path2Dオブジェクトに,さらに線分を接続する
        polyline.lineTo(100.0, 75.0);
        // Path2Dオブジェクトに,さらに線分を接続する
        polyline.lineTo(175.0, 75.0);
        // Path2Dオブジェクトの輪郭線を黒い太さ1線に設定し,描画する
        // BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUNDは線分の端点や接続部分を丸くするための設定
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(polyline);
    }

領域操作

    public static void exampleArea(Graphics2D g) {
        // 3個のEllipse2Dオブジェクトを生成する
        Ellipse2D ellipse0 = new Ellipse2D.Double(0.0, 100.0, 100.0, 50.0);
        Ellipse2D ellipse1 = new Ellipse2D.Double(0.0, 100.0, 50.0, 100.0);
        Ellipse2D ellipse2 = new Ellipse2D.Double(0.0, 100.0, 50.0, 50.0);
        // 3個のEllipse2Dオブジェクトの和集合領域を表現するAreaオブジェクトを生成する
        Area union = new Area(ellipse0);
        union.add(new Area(ellipse1));
        union.add(new Area(ellipse2));
        // 3個のEllipse2Dオブジェクトの共通集合領域を表現するAreaオブジェクトを生成する
        Area intersection = new Area(ellipse0);
        intersection.intersect(new Area(ellipse1));
        intersection.intersect(new Area(ellipse2));
        // 和集合領域と共通集合領域を塗りつぶす
        g.setColor(new Color(77, 196, 255));
        g.fill(union);
        g.setColor(new Color(0, 90, 255));
        g.fill(intersection);
        // 和集合領域と共通集合領域の輪郭線を描画する
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(union);
        g.draw(intersection);
    }

アフィン変換

    public static void exampleTransform(Graphics2D g) {
        // 弧を表現するArc2Dオブジェクトを生成する
        Arc2D arc = new Arc2D.Double(-1.0, -1.0, 2.0, 2.0, -45.0, 270.0, Arc2D.PIE);
        // アフィン変換を表現するAffineTransformオブジェクトを生成する
        AffineTransform t = new AffineTransform();
        t.translate(150.0, 150.0); // 平行移動
        t.rotate(Math.PI / 2); // 回転
        t.scale(50.0, 50.0); // 拡大
        // Arc2Dオブジェクトにアフィン変換した図形オブジェクトを生成する
        Shape transformed = t.createTransformedShape(arc);
        // アフィン変換した図形オブジェクトを塗りつぶす
        g.setColor(new Color(255, 241, 0));
        g.fill(transformed);
        // アフィン変換した図形オブジェクトの輪郭線を描く
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(transformed);
    }

JavaFXウィンドウへの図の描画

JavaFXウィンドウへの図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
    // JavaFXを利用するためにOpenJFXのプラグインを追加する
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// JavaFXに関する設定をする
javafx {
    // 使用するJavaFXのバージョンを指定する
    version = "13.0.1"
    // JavaFXで使用するモジュールを指定する(モジュールはJava9の機能)
    modules = ['javafx.controls', 'javafx.swing']
}
// example2.Mainクラスをメインクラスとして指定する
mainClassName = 'example1.Main'
// FXGraphics2Dライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8'
}

Main.java

package example1;
/*import...*/
public class Main {
    /**
     * JavaFXウィンドウに図を描画する
     * さらに描画した図をpng画像としてファイルに書き出す
     * @param args
     */
    public static void main(String[] args) {
        // Appクラスを指定してアプリケーションを起動する
        Application.launch(App.class, args);
    }
    public static class App extends Application {
        /**
         * アプリケーション起動時に呼び出される
         * @param primaryStage
         */
        @Override
        public void start(Stage primaryStage) throws IOException {
            // Canvasオブジェクトを用意する
            int width = 320;
            int height = 240;
            Canvas canvas = new Canvas(width, height);
            // CanvasオブジェクトのGraphicsContext2DオブジェクトをFXGraphics2Dクラスのコンストラクタに渡す
            GraphicsContext ctx = canvas.getGraphicsContext2D();
            Graphics2D g = new FXGraphics2D(ctx);
            // Graphics2Dクラスを通してCanvasオブジェクトに図を描画する
            exampleDrawAndFill(g);
            examplePath(g);
            exampleArea(g);
            exampleTransform(g);
            // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する
            primaryStage.setScene(new Scene(new Pane(canvas)));
            primaryStage.show();
            // CanvasオブジェクトのPNG画像を書き出す
            SnapshotParameters params = new SnapshotParameters();
            WritableImage img = canvas.snapshot(params, new WritableImage(width, height));
            File file = new File("example.png");
            ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file);
        }
    }
    public static void exampleDrawAndFill(Graphics2D g) {/*...*/}
    public static void examplePath(Graphics2D g) {/*...*/}
    public static void exampleArea(Graphics2D g) {/*...*/}
    public static void exampleTransform(Graphics2D g) {/*...*/}
}

実行結果

example1ディレクトリにおいて,./gradlew runを実行して表示されたウィンドウを以下に示します.

f:id:Jumpaku:20191212024459p:plain

また,書き出されたPNG画像(example1/example.png)を以下に示します.

f:id:Jumpaku:20191212024023p:plain

SVG画像への図の描画

SVG画像への図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// example2.Mainクラスをメインクラスとして指定する
mainClassName = 'example2.Main'
// JFreeSVGライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4'
}

Main.java

package example2;
/*import...*/
public class Main {
    /**
     * SVG画像に図を描画しファイルに書き出す
     * @param args
     */
    public static void main(String[] args) {
        // SVGGraphics2Dオブジェクトを用意する
        SVGGraphics2D g = new SVGGraphics2D(320, 240);
        // SVGGraphics2Dオブジェクトに図を描画する
        exampleDrawAndFill(g);
        examplePath(g);
        exampleArea(g);
        exampleTransform(g);
        // SVGGraphics2DオブジェクトからSVG画像の文字列を取得しファイルに書き出す
        String svg = g.getSVGDocument();
        try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) {
            writer.print(svg);
        } catch (FileNotFoundException exp) {
            exp.printStackTrace();
        }
    }
    public static void exampleDrawAndFill(Graphics2D g) {/*...*/}
    public static void examplePath(Graphics2D g) {/*...*/}
    public static void exampleArea(Graphics2D g) {/*...*/}
    public static void exampleTransform(Graphics2D g) {/*...*/}
}

実行結果

example2ディレクトリにおいて,./gradlew runを実行して書き出されたSVG画像(example2/example.svg)を以下に示します.

JavaFXウィンドウとSVG画像への同じ図の描画

JavaFXウィンドウとSVG画像への同じ図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
    // JavaFXを利用するためにOpenJFXのプラグインを追加する
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// JavaFXに関する設定をする
javafx {
    // 使用するJavaFXのバージョンを指定する
    version = "13.0.1"
    // JavaFXで使用するモジュールを指定する
    modules = ['javafx.controls', 'javafx.swing']
}
// example3.Mainクラスをメインクラスとして指定する
mainClassName = 'example3.Main'
// FXGraphics2DライブラリとJFreeSVGライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8'
    implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4'
}

Main.java

package example3;
/*import...*/
public class Main {
    /**
     * JavaFXウィンドウ上にドラッグ軌跡を描画するPNG画像に書き出す
     * JavaFXウィンドウに描画されたものと同じドラッグ軌跡をSVG画像にも書き出す
     */
    public static void main(String[] args) {
        // Appクラスを指定してアプリケーションを起動する
        Application.launch(App.class, args);
    }
    public static class App extends Application {
        /** 直前のドラッグ位置 */
        Point2D previousPoint;
        /**
         * アプリケーション起動時に呼び出される
         * @param primaryStage
         */
        @Override
        public void start(Stage primaryStage) {
            int width = 640;
            int height = 480;
            // Canvasオブジェクトを生成する
            Canvas canvas = new Canvas(width, height);
            // FXGraphics2Dオブジェクトを生成する
            FXGraphics2D fxGraphics2D = new FXGraphics2D(canvas.getGraphicsContext2D());
            // SVGGraphics2Dオブジェクトを生成する
            SVGGraphics2D svgGraphics2D = new SVGGraphics2D(width, height);
            // FXGraphics2DオブジェクトとSVGGraphics2Dオブジェクトをまとめたリストを生成する
            List<Graphics2D> graphics2DList = Arrays.asList(fxGraphics2D, svgGraphics2D);
            // Canvasオブジェクトにマウスプレス時のイベントハンドラを設定する
            canvas.setOnMousePressed(e -> {
                // マウスプレス時には図をクリアして現在のカーソル位置を保存する
                graphics2DList.forEach(g -> {
                    g.setBackground(Color.WHITE);
                    g.clearRect(0, 0, width, height);
                });
                previousPoint = new Point2D.Double(e.getX(), e.getY());
            });
            // Canvasオブジェクトにマウスドラッグ時のイベントハンドラを設定する
            canvas.setOnMouseDragged(e -> {
                // マウスドラッグ時には直前のカーソル位置と現在のカーソル位置の間に線分を描画して現在のカーソル位置を保存する
                Point2D.Double currentPoint = new Point2D.Double(e.getX(), e.getY());
                graphics2DList.forEach(g -> g.draw(new Line2D.Double(previousPoint, currentPoint)));
                previousPoint = currentPoint;
            });
            // Canvasオブジェクトにマウスリリース時のイベントハンドラを設定する
            canvas.setOnMouseReleased(e -> {
                // マウスリリース時にはSVG画像とPNG画像の書き出しを行う
                // PNG画像を書き出す
                SnapshotParameters params = new SnapshotParameters();
                WritableImage img = canvas.snapshot(params, new WritableImage(width, height));
                try {
                    File file = new File("example.png");
                    ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                // SVG画像を書き出す
                String svg = svgGraphics2D.getSVGDocument();
                try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) {
                    writer.print(svg);
                } catch (FileNotFoundException exp) {
                    exp.printStackTrace();
                }
            });
            // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する
            primaryStage.setScene(new Scene(new Pane(canvas)));
            primaryStage.show();
        }
    }
}

実行結果

example3ディレクトリにおいて,./gradlew runを実行しました. 表示されたウィンドウにドラッグ軌跡が描画される様子を以下に示します.

書き出された(example3/example.png)を以下に示します. f:id:Jumpaku:20191213233934p:plain 書き出されたSVG画像(example3/example.svg)を以下に示します.

まとめ

  • Graphic2Dクラスの基本的な使い方として,基本図形のストローク描画とフィル描画,パス操作,領域操作,アフィン変換を示した.
  • JavaFXのウィンドウやSVG画像に図を描画する例を示した.
  • FXGraphics2DやSVGGraphics2Dを利用しGraphic2Dクラスを通して統一的なプログラムで図の描画を行う例を示した.
  • Java13で,JavaFX,FXGraphics2D,JFreeSVGといったライブラリを利用するGradleプロジェクトの設定例を示した.
  • JavaFXJDKに同梱されなくなって,準備が面倒だと思った.