[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 6.2


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


Exercise 6.2:

Write a program that shows a small red square and a small blue square. The user should be able to drag either square with the mouse. (You'll need an instance variable to remember which square the user is dragging.) The user can drag the square out of the window if she wants, and it will disappear. To allow the user to get the squares back into the window, add a KeyPressed event handler that will restore the squares to their original positions when the user presses the Escape key. The key code for the Escape key is KeyCode.ESCAPE.


Discussion

Since the squares can move, the program needs global variables to keep track of the current location of each square. I do that with instance variables x1 and y1 for the upper left corner of the red square and x2 and y2 for the upper left corner of the blue square. These variables are of type double since mouse positions are given as real numbers in JavaFX. The canvas is also a global variable. I wrote a method, draw(), to completely redraw the content of the canvas. It is called in the start() method and whenever one of the squares is moved. The draw() method can get the graphics context that it needs to do its drawing from the canvas:

private void draw() {
    GraphicsContext g = canvas.getGraphicsContext2D();
    g.setFill(Color.rgb(230,255,230)); // light green
    g.fillRect(0,0,canvas.getWidth(),canvas.getHeight());
    g.setFill(Color.RED);
    g.fillRect(x1, y1, 30, 30);
    g.setFill(Color.BLUE);
    g.fillRect(x2, y2, 30, 30);
}

To implement dragging, the program needs event handlers for MousePressed, MouseDragged, and (maybe) MouseReleased events. The handlers need to be added to the canvas that fills the window. My event handlers simply call methods such as mousePressed(evt) that are defined elsewhere in the program.

For this simple application of dragging, the program doesn't need to keep track of the previous or starting positions of the mouse. But as usual for dragging, I use a boolean variable, dragging, to keep track of whether or not a drag operation is in progress. Not every mouse press starts a drag operation. If the user clicks the canvas outside of the squares, there is nothing to drag, and so dragging is set to false in the mousePressed method. If the user clicks one of the squares, dragging is set to true. Since there are two squares to be dragged, we have to keep track of which is being dragged. I use a boolean variable, dragRedSquare, which is true if the red square is being dragged and is false if the blue square is being dragged. (A boolean variable is actually not the best choice in this case. It would be a problem if we wanted to add another square. A boolean variable only has two possible values, so an integer variable would probably be a better choice. Better yet, the rectangles could be represented by objects, and a the global variable would refer to the object that is being dragged, or would be null when no object is being dragged.)

There is one little problem. The mouse location is a single (x,y) point. A square occupies a whole bunch of points. When we move the square to follow the mouse, where exactly should we put the square? One possibility is to put the upper left corner of the square at the mouse location. If we did this, the mouseDragged routine would look like:

public void mouseDragged(MouseEvent evt) { 
    if (dragging == false)  
      return;
    double x = evt.getX();  // Get mouse position.
    double y = evt.getY();
    if (dragRedSquare) {  // Move the red square.
       x1 = x;  // Put top-left corner at mouse position.
       y1 = y;
    }
    else {   // Move the blue square.
       x2 = x;  // Put top-left corner at mouse position.
       y2 = y;
    }
    draw();  // redraw canvas with square in new location
}

This works, but it is not very aesthetic. When the user starts dragging a square, no matter where in the square the user clicks, the square will jump so that its top-left corner is at the mouse position. This is not what a user typically expects. If I grab a square by clicking its center, then I want the center to stay under the mouse cursor as I move it. If I grab the lower right corner, I want the lower right corner to follow the mouse, not the upper left corner. There is a solution to this, and it's one that is often needed for dragging operations. We need to record the original position of the mouse relative to the upper left corner of the square. This tells us where in the square the user clicked. This is done in the mousePressed() method by assigning appropriate values to two new instance variables offsetX and offsetY:

double x = evt.getX();  // Location where user clicked.
double y = evt.getY();

if (x >= x2 && x < x2+30 && y >= y2 && y < y2+30) {
      // It's the blue square (which should be checked first,
      // since it's drawn on top of the red square.)
   dragging = true;
   dragRedSquare = false;
   offsetX = x - x2;  // Distance from corner of square to (x,y).
   offsetY = y - y2;
}
else if (x >= x1 && x < x1+30 && y >= y1 && y < y1+30) {
      // It's the red square.
   dragging = true;
   dragRedSquare = true;
   offsetX = x - x1;  // Distance from corner of square to (x,y).
   offsetY = y - y1;
}

In mouseDragged(), when the mouse moves to a new (x,y) point, we move the square so that the vertical and horizontal distances between the mouse location and the top left corner of the square remain the same, as given by offsetX and offsetY:

if (dragRedSquare) {  // Move the red square.
   x1 = x - offsetX;  // Offset corner from mouse location.
   y1 = y - offsetY;
}
else {   // Move the blue square.
   x2 = x - offsetX;  // Offset corner from mouse location.
   y2 = y - offsetY;
}

An alternative to using offsetX and offsetY would be to keep track of prevX and prevY, the previous values of x and y. Then, in mouseDragged(), you can use the current and previous values of x and y to determine how far the mouse has moved. Then, you can simply move the square by the same amount. This idea can be implemented in mouseDragged() with the following code:

int x = evt.getX();  // Current location of the mouse.
int y = evt.getY();

int dx = x - prevX;   // amount by which the mouse has moved.
int dy = y - prevY;

if (dragRedSquare) {
   x1 = x1 + dx;  // move the red square by the same amount
   y1 = y1 + dy;
}
else {
   x2 = x2 + dx;  // move the blue square by the same amount
   y2 = y2 + dy;
}

prevX = x;   // new values of prevX,prevY for next call to mouseDragged()
prevY = y;

By the way, if you wanted to stop the user from dragging the square outside the window, you would just have to add code to the mouseDragged routine to "clamp" the variables x1, y1, x2, and y2 so that they lie in the acceptable range. Here is a modified routine that keeps the square entirely within the canvas:

public void mouseDragged(MouseEvent evt) { 
    if (dragging == false)  
      return;
    double x = evt.getX();
    double y = evt.getY();
    if (dragRedSquare) {  // Move the red square.
       x1 = x - offsetX;
       y1 = y - offsetY;
       if (x1 < 0)  // Clamp (x1,y1) so the square lies in the canvas.
          x1 = 0;
       else if (x1 >= canvas.getWidth() - 30)
          x1 = canvas.getWidth() - 30;
       if (y1 < 0)
          y1 = 0;
       else if (y1 >= canvas.getHeight() - 30)
          y1 = canvas.getHeight() - 30;
    }
    else {   // Move the blue square.
       x2 = x - offsetX;
       y2 = y - offsetY;
       if (x2 < 0)  // Clamp (x2,y2) so the square lies in the canvas.
          x2 = 0;
       else if (x2 >= canvas.getWidth() - 30)
          x2 = canvas.getWidth() - 30;
       if (y2 < 0)
          y2 = 0;
       else if (y2 >= canvas.getHeight() - 30)
          y2 = canvas.getHeight() - 30;
    }
    draw();
}

Finally, we need to program a response to the Escape key. As discussed in Subsection 6.3.4, we can register the KeyPressed event handler with the scene. The handler simply has to reset the variables x1, y1, x2, and y2 to their original values and redraw the canvas. I define the handler in the start() method after creating the scene:

Scene scene = new Scene(root);
scene.setOnKeyPressed( e -> {
       // If user pressed ESCAPE, move squares
       // back to starting positions, and redraw.
    if ( e.getCode() == KeyCode.ESCAPE ) {
        x1 = 10;
        y1 = 10;
        x2 = 50;
        y2 = 10;
        draw();
    }
});

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.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseEvent;

/**
 * A program that shows a red square and a blue square that the user
 * can drag with the mouse.   The user can drag the squares off
 * the canvas and drop them.  Pressing the escape key will restore
 * both squares to their original positions.
 */
public class DragTwoSquares extends Application {

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

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


    private double x1 = 10, y1 = 10;   // Coords of top-left corner of the red square.
    private double x2 = 50, y2 = 10;   // Coords of top-left corner of the blue square.

    private Canvas canvas;  // The canvas where the sqaures are drawn.


    /**
     *  The start method sets up the GUI.  It adds mouse event handlers to
     *  the canvas to implement dragging.  It adds a key pressed handler
     *  to the scene that will restore the squares to their original 
     *  positions when the user presses the escape key.
     */
    public void start(Stage stage) {

        canvas = new Canvas(300,250);
        draw(); // show squares in original positions
        
        canvas.setOnMousePressed( e -> mousePressed(e) );
        canvas.setOnMouseDragged( e -> mouseDragged(e) );
        canvas.setOnMouseReleased( e -> mouseReleased(e) );
        
        Pane root = new Pane(canvas);
        
        Scene scene = new Scene(root);
        
        scene.setOnKeyPressed( e -> {
               // If user pressed ESCAPE, move squares
               // back to starting positions, and redraw.
            if ( e.getCode() == KeyCode.ESCAPE ) {
                x1 = 10;
                y1 = 10;
                x2 = 50;
                y2 = 10;
                draw();
            }
        });
        
        stage.setScene(scene);
        stage.setTitle("Drag the squares!");
        stage.setResizable(false);
        stage.show();
    } 


    /**
     * Draw the canvas, showing the squares in their current positions.
     */
    private void draw() {
        GraphicsContext g = canvas.getGraphicsContext2D();
        g.setFill(Color.rgb(230,255,230)); // light green
        g.fillRect(0,0,canvas.getWidth(),canvas.getHeight());
        g.setFill(Color.RED);
        g.fillRect(x1, y1, 30, 30);
        g.setFill(Color.BLUE);
        g.fillRect(x2, y2, 30, 30);
    }

    
    //-----------------  Variables and methods for responding to drags -----------

    private boolean dragging;      // Set to true when a drag is in progress.

    private boolean dragRedSquare; // True if red square is being dragged, false
                                   //    if blue square is being dragged.

    private double offsetX, offsetY;  // Offset of mouse-click coordinates from the
                                      //   top-left corner of the square that was
                                      //   clicked.

    /**
     * Respond when the user presses the mouse on the canvas.
     * Check which square the user clicked, if any, and start
     * dragging that square.
     */
    public void mousePressed(MouseEvent evt) { 

        if (dragging)  // Exit if a drag is already in progress.
            return;

        double x = evt.getX();  // Location where user clicked.
        double y = evt.getY();

        if (x >= x2 && x < x2+30 && y >= y2 && y < y2+30) {
                // It's the blue square (which should be checked first,
                // since it's drawn on top of the red square.)
            dragging = true;
            dragRedSquare = false;
            offsetX = x - x2;  // Distance from corner of square to (x,y).
            offsetY = y - y2;
        }
        else if (x >= x1 && x < x1+30 && y >= y1 && y < y1+30) {
                // It's the red square.
            dragging = true;
            dragRedSquare = true;
            offsetX = x - x1;  // Distance from corner of square to (x,y).
            offsetY = y - y1;
        }

    }

    /**
     * Dragging stops when user releases the mouse button.
     */
    public void mouseReleased(MouseEvent evt) { 
        dragging = false;
    }

    /**
     * Respond when the user drags the mouse.  If a square is 
     * not being dragged, then exit. Otherwise, change the position
     * of the square that is being dragged to match the position
     * of the mouse.  Note that the corner of the square is placed
     * in the same relative position with respect to the mouse that it
     * had when the user started dragging it.
     */
    public void mouseDragged(MouseEvent evt) { 
        if (dragging == false)  
            return;
        double x = evt.getX();
        double y = evt.getY();
        if (dragRedSquare) {  // Move the red square.
            x1 = x - offsetX;
            y1 = y - offsetY;
        }
        else {   // Move the blue square.
            x2 = x - offsetX;
            y2 = y - offsetY;
        }
        draw();  // (Calls the draw() to show squares in new positions.)
    }

} // end class DragTwoSquares


[ Exercises | Chapter Index | Main Index ]