[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 6.1


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 6.1:

In Subsection 6.3.2, the following code was given as an example. It installs a MousePressed event handler on a canvas. The handler lets the user draw a red rectangle at the point where the user clicks the mouse, or, by holding the shift key down, a blue oval:

canvas.setOnMousePressed( evt -> {
    GraphicsContext g = canvas.getGraphicsContext2D();
    if ( evt.isShiftDown() ) {
        g.setFill( Color.BLUE );
        g.fillOval( evt.getX() - 30, evt.getY() - 15, 60, 30 )
    }
    else {
        g.setFill( Color.RED );
        g.fillRect( evt.getX() - 30, evt.getY() - 15, 60, 30 );
    }
} );

Write a complete program that does the same, but in addition, the program will continue to draw figures if the user drags the mouse. That is, the mouse will leave a trail of figures as the user drags. However, if the user right-clicks the canvas, then the canvas should simply be cleared and no figures should be drawn even if the user drags the mouse after right-clicking. See the discussion of dragging in Subsection 6.3.3. Here is a picture of my solution:

the program showing trails of ovals and rects

Note that a black border has been added around each shape to make them more distinct.

To make the problem a little more challenging, when drawing shapes during a drag operation, make sure that the shapes that are drawn are at least, say, 5 pixels apart. To implement this, you have to keep track of the position where the previous shape was drawn.


Discussion

In order to implement dragging, we need to add a handler for MouseDragged events, in addition to the handler for MousePressed events. Since the code for handling the events is rather long, I wrote separate methods to handle the events, and the event handlers that I register with the canvas simply call those methods:

canvas.setOnMousePressed( e -> mousePressed(e) );
canvas.setOnMouseDragged( e -> mouseDragged(e) );

I also decided to use a global variable, canvasGraphics, of type GraphicsContext to represent the graphics context for drawing on the canvas. I get the graphics context in the start() method when I create the canvas, and I fill the canvas with white:

Canvas canvas = new Canvas(500,380);
canvasGraphics = canvas.getGraphicsContext2D();
canvasGraphics.setFill(Color.WHITE);
canvasGraphics.fillRect(0,0,500,380);
canvasGraphics.setStroke(Color.BLACK); // stroke color never changes

I wanted to have a black border around the canvas (more for the screenshot than for anything else). I could have drawn the border on the canvas itself, but I decided to apply the border to the pane that is used as the root of the scene graph. This led me to use a BorderPane for the root rather than a simple Pane. When a BorderPane computes its preferred size, it allows space for any border or padding that have been applied to the pane, and the content is correctly placed inside the border and padding. A Pane would not take care of that for me. (When I first wrote the program, using a Pane, the canvas covered part of the border.) So, the root pane is created with

BorderPane root = new BorderPane(canvas);
root.setStyle("-fx-border-color: black; -fx-border-width: 2px");

The mousePressed() method can use the code given in the exercise, except that the graphics context is named canvasGraphics instead of g, and code should be added to stroke the shapes with black after filling them. However, before drawing anything, the method must check whether the user right-clicked the canvas; if so, the canvas should be cleared and nothing should be drawn. The test that is needed is if evt.getButton() == MouseButton.SECONDARY.

Furthermore, we need to add some global variables to the program to keep track of what is happening as the user drags the mouse. First of all, mouseDragged() does not always draw shapes; if the user used the right mouse button, no shapes are drawn. We can implement that with the typical boolean instance variable, dragging. This variable is set to false in mousePressed() if the user right-clicked, and it is set to true otherwise. If dragging is false in mouseDragged(), then that method returns without drawing anything.

In addition, even when dragging is true, mouseDragged() should not draw anything unless the mouse has moved at least 5 pixels since the previous shape was drawn. We need two more global variables, of type double, to keep track of the mouse position where a shape was most recently drawn. In my program, the variables are prevShapeX and prevShapeY. The variables' values should be set every time a shape is drawn, in either mousePressed() or mouseDragged(), and mouseDragged() should compare their values to the current mouse location to decide whether the mouse has moved far enough to draw the next shape. It is not completely clear what "move at least 5 pixels" means, but I take it to mean "move at least 5 pixels vertically or at least 5 pixels horizontally". So, mouseDragged() starts off like this:

public void mouseDragged(MouseEvent evt) {
    if ( dragging == false ) { 
        return;
    }
    
    double x = evt.getX();  // x-coordinate where user clicked.
    double y = evt.getY();  // y-coordinate where user clicked.
    
    if ( Math.abs(x - prevShapeX) < 5 && Math.abs(y - prevShapeY) < 5 ) {
            // The mouse has not moved at least 5 pixels horizontally
            // or vertically, so don't draw another shape yet.
        return;
    }
    
    prevShapeX = x;  // Save coords where the next shape is being drawn.
    prevShapeY = y;

The Solution


import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.layout.BorderPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;


/**
 * A simple demonstration of MouseEvents.  Shapes are drawn
 * on a white background when the user clicks the canvas.  If
 * the user right-clicks, the canvas is cleared.  If the user
 * shift-clicks the canvas, a blue oval is drawn.  Otherwise,
 * when the user clicks, a red rectangle is drawn.  If the user
 * did not right-click, then ovals or rects continue to be 
 * drawn as the user drags the mouse.
 */
public class SimpleStamperWithDrag extends Application {

    public static void main(String[] args) {
        launch();
    }

    // ----------------------------------------------------------------------

    /**
     * This variable is set to true during a drag operation, unless the
     * user was holding down the shift key when the mouse was first
     * pressed (since in that case, the mouse gesture simply clears the
     * canvas and no figures should be drawn if the user drags the mouse).
     */
    private boolean dragging;
    
    /**
     * While dragging, prevShapeX and prevShapeY are the coordinates
     * at which the previous shape was drawn.  They are used to avoid 
     * drawing the next shape until the mouse has moved at least 5 pixels
     * horizontally or vertically.
     */
    private double prevShapeX, prevShapeY;
    
    /**
     * A graphics context for drawing on the canvas that fills the screen.
     */
    private GraphicsContext canvasGraphics;

    
    /**
     * This start() method sets up the GUI to show a canvas where the shapes
     * are drawn, and it installs mouse handlers on the canvas to draw shapes
     * as the user presses and drags the mouse.
     */
    public void start(Stage stage) {
        
        Canvas canvas = new Canvas(500,380);
        canvasGraphics = canvas.getGraphicsContext2D();
        canvasGraphics.setFill(Color.WHITE);
        canvasGraphics.fillRect(0,0,500,380);
        canvasGraphics.setStroke(Color.BLACK); // stroke color never changes
        
        canvas.setOnMousePressed( e -> mousePressed(e) );
        canvas.setOnMouseDragged( e -> mouseDragged(e) );
        
        BorderPane root = new BorderPane(canvas);
        root.setStyle("-fx-border-color: black; -fx-border-width: 2px");
        
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.setTitle("Mouse Drag Demo");
        stage.setResizable(false);
        stage.show();
        
    } // end start()


    /**
     *  This method will be called when the user clicks the mouse on the canvas.
     *  If the user right-clicked, it clears the canvas.  Otherwise, it draws
     *  a shape and starts a drag operation, but only if the mouse has moved
     *  sufficiently far since drawing the previous shape.
     */
    public void mousePressed(MouseEvent evt) {

        if ( evt.getButton() == MouseButton.SECONDARY ) {
                // The user right-clicked the canvas.  Fill the canvas with white
                // to erase its current contents.
            dragging = false;
            canvasGraphics.setFill(Color.WHITE);
            canvasGraphics.fillRect(0,0,500,380);
            return;
        }

        dragging = true;

        double x = evt.getX();  // x-coordinate where user clicked.
        double y = evt.getY();  // y-coordinate where user clicked.
        
        prevShapeX = x;  // Save coordinates where first shape is drawn.
        prevShapeY = y;

        if ( evt.isShiftDown() ) {
                // User was holding down the shift key. Draw a blue oval centered 
                // at the point (x,y). (A black outline around the oval will make it 
                // more distinct when shapes overlap.)
            canvasGraphics.setFill(Color.BLUE);  // Blue interior.
            canvasGraphics.fillOval( x - 30, y - 15, 60, 30 );
            canvasGraphics.strokeOval( x - 30, y - 15, 60, 30 );
        }
        else {
                // Draw a red rectangle centered at (x,y).
            canvasGraphics.setFill(Color.RED);   // Red interior.
            canvasGraphics.fillRect( x - 30, y - 15, 60, 30 );
            canvasGraphics.strokeRect( x - 30, y - 15, 60, 30 );
        }

    } // end mousePressed();


    /**
     *  This method is called when the user drags the mouse.  If a the value of the
     *  instance variable dragging is true, it will draw a rect or oval at the
     *  current mouse position.
     */
    public void mouseDragged(MouseEvent evt) {
        if ( dragging == false ) { 
            return;
        }
        
        double x = evt.getX();  // x-coordinate where user clicked.
        double y = evt.getY();  // y-coordinate where user clicked.
        
        if ( Math.abs(x - prevShapeX) < 5 && Math.abs(y - prevShapeY) < 5 ) {
                // The mouse has not moved at least 5 pixels horizontally
                // or vertically, so don't draw another shape yet.
            return;
        }
        
        prevShapeX = x;  // Save coords where the next shape is being drawn.
        prevShapeY = y;

        if (evt.isShiftDown() ) {
                // User was holding down the shift key. Draw a blue oval centered 
                // at the point (x,y). (A black outline around the oval will make it 
                // more distinct when shapes overlap.)
            canvasGraphics.setFill(Color.BLUE);  // Blue interior.
            canvasGraphics.fillOval( x - 30, y - 15, 60, 30 );
            canvasGraphics.strokeOval( x - 30, y - 15, 60, 30 );
        }
        else {
                // Draw a red rectangle centered at (x,y).
            canvasGraphics.setFill(Color.RED);   // Red interior.
            canvasGraphics.fillRect( x - 30, y - 15, 60, 30 );
            canvasGraphics.strokeRect( x - 30, y - 15, 60, 30 );
        }
        
    } // end mouseDragged();


} // end class SimpleStamperWithDrag


[ Exercises | Chapter Index | Main Index ]