Drawing Graphics with JavaFX Canvas

Graphics drawing in JavaFX is completely different from AWT and Swing. Once you understand the different system, the rendering process itself is easy to perform. This section explains the basic Canvas control and the GraphicsContext class.

Canvas and GraphicsContext

In AWT and Swing, the basic idea of graphics drawing was the same. Components provided a paint method, or paintComponent, that was called when the display needed to be updated. By overriding it, drawing was executed automatically.

Each component contained an instance of Graphics or Graphics2D that managed rendering, and that instance was passed to the paint method. Drawing was done by calling drawing methods on this Graphics object.

JavaFX is quite different. First, controls do not have a method that is called when the display is updated. This may surprise Java programmers who are used to AWT and Swing. Since there is no such method, no Graphics object is passed either. Does that mean there is no way to draw?

No. It is the opposite. Instead of “you cannot draw unless you override the method called during display updates,” JavaFX lets you draw whenever and wherever you need. There is no restriction that says you must override one method and write rendering there.

To draw graphics, prepare a control named Canvas. AWT also had a Canvas class with the same name, but this is a completely different one. The JavaFX version is included in the javafx.scene.canvas package.

Canvas contains an instance of a class named GraphicsContext. This corresponds to the Graphics class in AWT/Swing. By obtaining this instance and calling its methods, you can draw shapes and other elements on the Canvas.

First, add a Canvas in FXML as follows.

<?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>

Prepare an Application class separately to load and display this FXML. You can also use the one created earlier.

This simply places a <Canvas> tag in the center of BorderPane. The <Canvas> tag includes width and height attributes, which set the size of the control.

Drawing with GraphicsContext

Now write the drawing process in the controller class. The following is a simple example.

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);
    }
}

When you run this, a circle with a blue outline is drawn over a red rectangular area.

Drawing on Canvas is performed in the order: get the GraphicsContext, configure drawing settings, and call drawing methods.

Getting GraphicsContext

variable = canvas.getGraphicsContext2D();

Use the getGraphicsContext2D method of a Canvas instance to obtain a GraphicsContext. Store the instance in a variable and use it.

Setting colors

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

Before drawing, set the details of the shape to draw. Here, color settings are prepared. setFill sets the fill contents, and setStroke sets the line drawing contents.

Both methods take a Paint instance. This represents the rendering style. For simple solid colors, it is common to use Color, a subclass of Paint.

Color provides standard color values as class variables. You can set colors easily by specifying them. Other colors can be created as follows.

new Color(red, green, blue)

This is similar to java.awt.Color, so it should be familiar.

Drawing Methods

Next, call drawing methods. This example calls methods for drawing a rectangle and an oval.

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

GraphicsContext includes many other drawing methods. Most drawing methods come in two forms: filling the inside of a shape and drawing the outline. Fill methods are named fillOO, and outline methods are named strokeOO. Here, the code performs “fill a rectangle” and “draw an oval,” so it calls fillRect and strokeOval.

Main Drawing Methods

What shape drawing methods are available? The important ones are summarized below.

Drawing a Line

"GraphicsContext".strokeLine(startX, startY, endX, endY);

Draws a straight line connecting two points. Specify the start and end points.

Drawing a Rectangle

"GraphicsContext".fillRectangle(x, y, width, height);
"GraphicsContext".strokeRect(x, y, width, height);

Draws a rectangle. fillRectangle draws a filled rectangle, and strokeRectangle draws only the outline. The arguments specify the position and size.

Drawing a Rounded Rectangle

"GraphicsContext".fillRoundRectangle (x, y, width, height, arc width, arc height);
"GraphicsContext".stokeRoundRectangle (x, y, width, height, arc width, arc height);

Draws a rectangle with rounded corners. In addition to position and size, specify the horizontal and vertical size of the corner arcs.

Drawing an Oval

"GraphicsContext".fillOval(x, y, width, height);
"GraphicsContext".strokeOval(x, y, width, height);

Draws an oval. As with rectangles, specify the position and width/height of the oval’s bounding area.

Drawing an Arc

"GraphicsContext".fillArc(x, y, radiusX, radiusY, start angle, length, type);
"GraphicsContext".stokeArc(x, y, radiusX, radiusY, start angle, length, type);

Draws an arc. In addition to position and size, specify the start angle, length, and type. The type controls the arc shape and is specified with an ArcType enum value.

  • CHORD: draws a shape where the arc ends are connected by a straight line.
  • OPEN: draws an open arc without closing the ends.
  • ROUND: draws a shape where both arc ends are connected to the center of the circle.

Drawing Polygons

"GraphicsContext".fillPolyline(x array, y array, number of points);
"GraphicsContext".stokePolyline(x array, y array, number of points);
"GraphicsContext".fillPolygon(x array, y array, number of points);
"GraphicsContext".stokePolygon(x array, y array, number of points);

Draws polygons that connect several vertices. The arguments are arrays of horizontal and vertical positions for each vertex, plus the number of vertices. OOPolyline draws an open shape, while OOPolygon draws a closed shape where the start and end points are connected.

Drawing Text

"GraphicsContext".fillText(text to draw, x, y);
"GraphicsContext".stokeText(text to draw, x, y);

Draws text at the specified position. Specify the text and drawing position.

Filling with Gradients

When drawing shapes, it is important to understand the fill method. Earlier we used setFill to set colors, but remember that this is not simply “setting a color.” It specifies the filling method, and the argument is not limited to Color; it is Paint, the superclass of Color.

Therefore, if it is a subclass of Paint, you can specify something other than Color. As an example, let’s use gradient classes.

LinearGradient Class

This class is used for linear gradients. It creates a gradual color change from one point to another. Create an instance as follows.

variable = new LinearGradient(startX, startY, endX, endY, boolean, cycle method, list);
  • startX, startY: the start point of the gradient.
  • endX, endY: the end point of the gradient.
  • boolean: whether to change colors uniformly.
  • cycle method: specified with a CycleMethod enum value.
  • list: color change information collected as an array of Stop instances.

RadialGradient Class

This creates a circular color change. It can represent color changing outward from a specified center position. Create an instance as follows.

variable = new RadialGradient(focus angle, focus distance, centerX, centerY, radius, boolean, cycle method, list);
  • focus angle: direction of the start position from the circle center.
  • focus distance: distance of the start position from the circle center.
  • centerX, centerY: center position of the circular gradient.
  • radius: radius of the gradient.
  • cycle method: repetition method specified by a CycleMethod enum.

The focus is used to offset the start position from the center of the circle. By specifying in which direction and how far it is offset from the center, you can adjust the start position of the gradient.

CycleMethod Enum

This specifies the repetition method. It includes the following values.

  • NO_CYCLE: does not repeat the color change.
  • REFRECT: repeats by moving back and forth between the start color and end color.
  • REPEAT: repeats the change between the start and end colors.

The following is an example of using these gradients.

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);
    }
}

Here, a rectangle is drawn with LinearGradient, and a circle is drawn inside it with RadialGradient. There are many arguments, so it may look difficult, but what it does is relatively simple.

Using Image Files

To render more complex graphics, it is often better to prepare the graphics as an image file, load it, and render it. Prepare an Image instance in advance and render it.

This Image is not the AWT Image. It is a class in the javafx.scene.image package, so it differs from AWT/Swing.

Creating an Image Instance

variable = new Image(path);

There are several ways to create an Image instance, but remember this one first. Specify the path of the image file to import as a String, and an Image instance that loads it is created.

Drawing an Image

GraphicsContext also provides methods for this. There are several depending on rendering style. The main methods are summarized below.

Draw an image at a specified position

"GraphicsContext".drawImage("Image", x, y);

Draw an image transformed into a specified area

"GraphicsContext".drawImage("Image", x, y, width, height);

Draw part of an image into a specific area of Canvas

"GraphicsContext".drawImage("Image", x1, y1, width1, height1, x2, y2, width2, height2);

The following example loads an image file named sample.jpg and displays it on a 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);
    }
}

Here, sample.jpg is loaded with new Image("sample.jpg");. In a Java program, the current directory is the location where the program file is placed, so prepare sample.jpg in the same location before running it.