JavaFXのウィンドウやSVG画像への図の描画
本記事はMuroran Institute of Technology Advent Calendar 2019 12/14の記事です.
概要
本記事では
について具体的なGradleプロジェクトの例を用いて説明します.
JavaFXはJavaのGUIライブラリの一つです. JavaFXはJava8で標準ライブラリに追加されてJDKの一部となりましたが,Java11からはJDKに同梱されなくなり,代わりにOpenJFXを利用することができるようになりました. 1.では,JavaFXのCanvasクラスに図の描画を行うために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で構成されています.
例えば,example1
ディレクトリで./gradlew run
を実行すると,Gradleがexample1/build.gradle
に書かれたプロジェクトの設定に従ってプロジェクトをビルドし,起動します.
プログラムが起動されるとexample1/src/main/java/example1/Main.java
のmain
メソッドが呼ばれます.
この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
を実行して表示されたウィンドウを以下に示します.
また,書き出されたPNG画像(example1/example.png)を以下に示します.
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)を以下に示します. 書き出されたSVG画像(example3/example.svg)を以下に示します.