Solution for Programming Exercise 6.5
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 6.5:
In Exercise 3.8, you drew a checkerboard. For this exercise, write a program where the user can select a square by clicking on it. Highlight the selected square by drawing a colored border around it. When the program starts, no square is selected. When the user clicks on a square that is not currently selected, it becomes selected, and the previously selected square, if any, is unselected. If the user clicks the square that is selected, it becomes unselected. Assume that the size of the checkerboard is exactly 400 by 400 pixels, so that each square on the checkerboard is 50 by 50 pixels. Here is my checkerboard, with the square in row 3, column 3 selected, shown at reduced size:
See the solution to Exercise 3.8 for a discussion of how to draw the checkerboard. For the new version, we need to add a MousePressed event handler to the Canvas on which the checkerboard is drawn. We also need to add instance variables to keep track of which square, if any, is selected. I use two integer variables selectedRow and selectedCol. When no square is selected, selectedRow is -1 (and I don't care what selectedCol is). When a square is selected, selectedRow is the number of the row that contains that square and selectedCol is the number of the column that contains the selected square. Remember that rows and columns are numbered from 0 to 7. This makes some of the calculations easier than numbering them from 1 to 8.
After drawing the checkerboard, draw(), the method that draws the canvas, has to highlight the selected square, if there is one. I do this by drawing a cyan border around the inside of the selected square. This is the new code that is added to the checkerboard-drawing code, after the checkerboard has been drawn:
if (selectedRow >= 0) { // Since there is a selected square, draw a cyan // border around it. (If selectedRow < 0, then // no square is selected and no border is drawn.) g.setStroke(Color.CYAN); g.setLineWidth(3); // the border will be 3 pixels wide y = selectedRow * 50; // y-coord of top-left corner of selected square x = selectedCol * 50; // x-coord of top-left corner of selected square g.strokeRect(x+1.5, y+1.5, 47, 47); }
The border of the selected square is three pixels wide. To make the border lie entirely inside the square, we need to stroke a rectangle that is inset from the edges of the square by half of the line width, that is, by 1.5 pixels. This explains the parameters to g.strokeRect(). Remember that when a shape is stroked, the center of the pen is dragged along the border of the shape.
Now, let's say that the user clicks at the point (x,y). The problem is to determine which square on the checkerboard contains that point. The column number of the square is obtained by dividing the x coordinate by the width of the squares. The column number is an integer, while x is of type double. Since the squares are 50 pixels wide, the row number of the clicked square is (int)(x/50). For values of x between 0 and 49, x/50 is greater than or equal to 0 and less than 1, so taking its integer part gives a column number of 0, which is correct. For the next 50 pixels, from 50 to 99, (int)(x/50) is 1, which is the correct column number. For the next strip of pixels, from 100 to 149, the answer is 2. And so on. Similarly, (int)(y/50) gives the row number of the square where the user clicked. (I often get rows and columns mixed up—remember that the x coordinate corresponds to columns and the y coordinate corresponds to rows.)
Once we know the row and column where the user clicked, we can compare them to selectedRow and selectedCol. If the values are the same, then the user clicked in a square that was already selected. We want to remove the highlighting. That can be done by setting selectedRow = -1, the value that indicates that no square is selected. Otherwise, the values of selectedRow and selectedCol are set to the row and column that the user clicked. In the end, draw() is called so that the change will be reflected in what is shown on the screen.
All this explains the reasoning behind the mousePressed() routine, which you can see below.
import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.scene.layout.Pane; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.input.MouseEvent; /** * This program draws a red-and-black checkerboard. * It is assumed that the size of the canvas is 400 * by 400 pixels. When the user clicks a square, that * square is selected, unless it is already selected. * When the user clicks the selected square, it is * unselected. If there is a selected square, it is * highlighted with a cyan border. */ public class ClickableCheckerboard extends Application { public static void main(String[] args) { launch(args); } //------------------------------------------------------------------- private Canvas canvas; // Where the checkerboard is drawn private int selectedRow; // Row and column of selected square. If no private int selectedCol; // square is selected, selectedRow is -1. /** * Constructor. Set selectedRow to -1 to indicate that * no square is selected. And set the board object * to listen for mouse events on itself. */ public void start(Stage stage) { selectedRow = -1; // To start, no square is selected! canvas = new Canvas(400,400); draw(); canvas.setOnMousePressed(e -> mousePressed(e)); Pane root = new Pane(canvas); Scene scene = new Scene(root); stage.setScene(scene); stage.setResizable(false); stage.setTitle("Click Me!"); stage.show(); } /** * Draw the checkerboard and highlight selected square, if any. */ private void draw() { int row; // Row number, from 0 to 7 int col; // Column number, from 0 to 7 int x,y; // Top-left corner of square GraphicsContext g = canvas.getGraphicsContext2D(); for ( row = 0; row < 8; row++ ) { for ( col = 0; col < 8; col++) { x = col * 50; y = row * 50; if ( (row % 2) == (col % 2) ) g.setFill(Color.RED); else g.setFill(Color.BLACK); g.fillRect(x, y, 50, 50); } } // end for row if (selectedRow >= 0) { // Since there is a selected square, draw a cyan // border around it. (If selectedRow < 0, then // no square is selected and no border is drawn.) g.setStroke(Color.CYAN); g.setLineWidth(3); y = selectedRow * 50; x = selectedCol * 50; g.strokeRect(x+1.5, y+1.5, 47, 47); } } // end paint() /** * When the user clicks on the canvas, figure out which * row and column the click was in and change the * selected square accordingly. */ private void mousePressed(MouseEvent evt) { int col = (int)(evt.getX() / 50); // Column where user clicked. int row = (int)(evt.getY() / 50); // Row where user clicked. if (selectedRow == row && selectedCol == col) { // User clicked on the currently selected square. // Turn off the selection by setting selectedRow to -1. selectedRow = -1; } else { // Change the selection to the square the user clicked on. selectedRow = row; selectedCol = col; } draw(); } // end mousePressed() } // end ClickableCheckerboard