JavaFX Sketch Pad: Custom Binding

Sketch pad programs are fun to build and fun to test. And writing a sketch pad program in JavaFX is no different, except that maybe it’s more fun. So, what constructs do we need to create a decent sketch pad program in JavaFX? At the minimum, we need to

  • Detect mouse events for mouse click, mouse drag, and mouse release
  • Draw line segments corresponding to where the user clicks and drags the mouse
  • Clear our sketch pad

It would also be nice to

  • Change the width of the line we’re drawing
  • Change the color of the line we’re drawing

So, let’s begin.

Version 1 JavaFX Sketch Pad Screenshot

Version 1 JavaFX Sketch Pad Screenshot

The Version 1 screenshot (including some test scribbles) has a Clear button, three RGB color sliders, a stroke width slider, and a sample line that shows both the current stroke width and color settings. The canvas is a rectangle with a light gray background.

You construct the lines (or scribbles) with the JavaFX Path class, a shape that holds path elements. Path elements include ArcTo, ClosePath, CubicCurveTo, HLineTo, LineTo, MoveTo, QuadCurveTo, and VLineTo. We’ll only need MoveTo (to position the cursor at the beginning of a drawing action) and LineTo (to draw the line corresponding to the mouse drag). As we build the path, we’ll add it to a Group called lineGroup.

First, let’s look at the JavaFX code that builds the lineGroup, sample line (a Line), and canvas (a Rectangle). Note that we set the canvas’ cursor to CROSSHAIR so a user knows when the drawing mouse is active. We set the canvas fill to LIGHTGRAY.

private Group lineGroup;       //  Class variable
. . .
// A group to hold all the drawn path elements
lineGroup = new Group();
// Build the sample line<
final Line sampleLine = new Line(0, 0, 140, 0);
// Build the canvas
final Rectangle canvas = new Rectangle(scene.getWidth() - 20,
            scene.getHeight() - 200);
canvas.setCursor(Cursor.CROSSHAIR);
canvas.setFill(Color.LIGHTGRAY);

Next, we build the path elements in the canvas’ mouse event handlers. Both the stroke width and stroke color are based on the values of the sample line. Let’s examine the mouse event handlers for mouse pressed, mouse released, and mouse dragged.

The mouse pressed handler defines a new path and makes it transparent to mouse clicks. This lets the user draw new lines on top of previously drawn lines. Since the path is “in front of” the rectangle, the path would otherwise consume mouse events. Next, we set the strokeWidth and stroke properties. We add the newly created path to lineGroup and add path element MoveTo as the first element of the path. The x and y coordinates of the MoveTo element are the scene coordinates passed to the mouse event handler.

private Path path;           // Class variable
. . .
canvas.setOnMousePressed(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent me) {
        path = new Path();
        path.setMouseTransparent(true);
        path.setStrokeWidth(sampleLine.getStrokeWidth());
        path.setStroke(sampleLine.getStroke());<
        lineGroup.getChildren().add(path);
        path.getElements().add(
              new MoveTo(me.getSceneX(), me.getSceneY()));
    }
});

A mouse dragged event is fired when the user drags the mouse. This can only happen after a mouse pressed event or a previous mouse dragged event. Here we check that the coordinates in the mouse event are within the canvas’ local coordinate space. If the coordinates are indeed within the canvas, we create a LineTo path element and add it to the path. This draws a line on the canvas from the previous mouse pressed or mouse dragged event location. The mouse dragged event handler is invoked multiple times as the user drags the mouse over the canvas.

canvas.setOnMouseDragged(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent me) {
        // keep lines within rectangle
        if (canvas.getBoundsInLocal().contains(
                             me.getX(), me.getY())) {
            path.getElements().add(
                 new LineTo(me.getSceneX(), me.getSceneY()));
        }
    }
});

When the user releases the mouse, the system invokes the mouse released event handler. Here, we simply set path to null. Now the user can optionally clear the canvas, change the stroke width, or change the stroke color.

canvas.setOnMouseReleased(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent me) {
        path = null;
    }
});

We clear the canvas with the Clear button. This event handler removes all the nodes contained in lineGroup.

Button btnClear = new Button();
btnClear.setText("Clear");
btnClear.setOnAction(new EventHandler<ActionEvent>() {
    public void handle(ActionEvent event) {
        lineGroup.getChildren().
             removeAll(lineGroup.getChildren());
    }
});

Recall that a slider controls the stroke width. Let’s see how this works. First, we define constants to set the slider’s minimum and maximum values (MINSTROKE and MAXSTROKE). Then, we set the starting stroke width to 3.0, which is a bit larger than the default of 1.0. We use these values to instantiate slider strokeSlider. As the user slides the handle right, the value increases up to the maximum. To track the changes, we bind the sampleLine‘s stroke width property to the value of the slider. This is straightforward, since both properties wrap Double values. As the user moves the slider, the sample line grows and shrinks to reflect its stroke width. Each time the user “draws” a line, the canvas’ mouse handler uses the new stroke width value.

private static final Double DEFAULTSTROKE = 3.0;
private static final Double MAXSTROKE = 30.0;
private static final Double MINSTROKE = 1.0;
. . .
Slider strokeSlider =<br
       new Slider(MINSTROKE, MAXSTROKE, DEFAULTSTROKE);
sampleLine.strokeWidthProperty().bind(
       strokeSlider.valueProperty());

Now let’s configure the line’s color. To control color, we use a triplet of sliders for the RGB color value. Each value is an integer from 0 to 255. Here are the three sliders and label controls.

private static final Integer DEFAULTRED = 0;
private static final Integer DEFAULTGREEN = 0;
private static final Integer DEFAULTBLUE = 255;
private static final Integer MAXRGB = 255;
private static final Integer MINRGB = 0;
. . .
// Build the RGB sliders and labels
final Slider redSlider =
        new Slider(MINRGB, MAXRGB, DEFAULTRED);
Label labelRed = new Label("R");
final Slider greenSlider =
        new Slider(MINRGB, MAXRGB, DEFAULTGREEN);
Label labelGreen = new Label("G");
final Slider blueSlider =
        new Slider(MINRGB, MAXRGB, DEFAULTBLUE);
Label labelBlue = new Label("B");

Here’s where things get a bit tricky. To set sampleLine‘s stroke property to a Color based on the combined values of the three RGB sliders, we use static Color method rgb(), as follows (which gets converted automatically to the required Paint object).

sampleLine.setStroke(Color.rgb(redSlider.valueProperty().intValue(),
                        greenSlider.valueProperty().intValue(),
                        blueSlider.valueProperty().intValue()));

However, just as we provided binding for sampleLine‘s stroke width property to the stroke width slider value, we also need to bind sampleLine‘s stroke property to the Color object made from the combined RGB slider values. When any of the three sliders change, sampleLine‘s stroke property must also change. In this case, there isn’t a built-in Paint property binding method that computes a new Paint object from an RGB triplet, so we’ll need to create our own custom binding object.

This ObjectBinding object will be built as an anonymous class. In the constructor we specify the properties that participate in the binding. This builds the necessary listeners that are notified when any of the slider’s value property changes. Next, we override method computeValue() to specify how a new Paint object is built from the three sliders. Once we build the custom binding object, we can specify the binding for sampleLine.

// Build a Binding object
// to compute a Paint object from the sliders
ObjectBinding<Paint> colorBinding = new ObjectBinding<Paint>() {
    {
        super.bind(redSlider.valueProperty(),
                greenSlider.valueProperty(),
                blueSlider.valueProperty());
    }
    @Override
    protected Paint computeValue() {
        return Color.rgb(redSlider.valueProperty().intValue(),
                greenSlider.valueProperty().intValue(),
                blueSlider.valueProperty().intValue());
    }
};
// Bind sampleLine to the Paint Binding object
sampleLine.strokeProperty().bind(colorBinding);

So there you have it. Download the Sketch Pad (Version 1) JavaFX source code here.

But, what about Version 2? Personally, manipulating three sliders to configure the draw color can be tedious. Version 2 of the Sketch Pad example uses a color picker instead of three sliders for the line color. Stay tuned.