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. (Use a JPanel for the checkerboard.) 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 panel is exactly 320 by 320 pixels, so that each square on the checkerboard is 40 by 40 pixels. Here is my checkerboard, shown at half-size, with the square in row 3, column 3 selected:
See the solution to Exercise 3.8 for a discussion of how to draw the checkerboard. In that exercise, the code for drawing the board was in a drawFrame() method. Now, we can use a JPanel as a drawing surface for the checkerboard, with the drawing code in the panel's paintComponent() method. The code for drawing the checkerboard is the same.
As always, there are many ways to organize the program. In this case, I decided to let the main panel class implement MouseListener. The panel listens for mouse events on itself.
To keep track of which square is selected, if any, the class contains instance 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, the paintComponent() method has to highlight the selected square, if there is one. I do this by drawing a 3-pixel cyan border around the inside of the selected square. This is the new code that is added to the checkerboard-drawing code:
if (selectedRow >= 0) { // Since there is a selected square, draw a cyan // border around it. g.setColor(Color.CYAN); y = selectedRow * 40; x = selectedCol * 40; g.drawRect(x, y, 39, 39); g.drawRect(x+1, y+1, 37, 37); g.drawRect(x+2, y+2, 35, 35); }
Since the squares are 40 pixels on each side, you might wonder why the first drawRect() command specifies a width and height of 39 instead of 40. In the fillRect() method that is used earlier in the paintComponent() method to fill in the square, a width and height of 40 is used. Remember that the drawRect() method actually draws a rectangle whose width and height are one more than the values specified in the parameters. (Remember the bit about the pen that hangs one pixel outside the rectangle on the right and bottom?)
To respond to user mouse clicks, the panel must implement the MouseListener interface. The constructor calls addMouseListener(this) to register the board to listen for mouse events on itself. (Remember that calling addMouseListener(this) is the same as calling this.addMouseListener(this).) Of the five methods specified in the MouseListener interface, only mousePressed has a non-empty definition. This method must figure out which square the user clicked and adjust the values of the instance variables selectedRow and selectedCol accordingly.
Let's say that the user clicked 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. Since the squares are 40 pixels wide, the row number of the clicked square is x/40. For values of x between 0 and 39, this gives a column number of 0, which is correct. For the next 40 pixels, from 40 to 79, x/40 is 1, which is the correct column number. For the next strip of pixels, from 80 to 119, the answer is 2. And so on. Similarly, y/40 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, repaint() 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 java.awt.*; import java.awt.event.*; import javax.swing.*; /** * This program draws a red-and-black checkerboard. * It is assumed that the size of the panel is 320 * by 320 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 JPanel implements MouseListener { /** * A main routine lets this class be run as an application. */ public static void main(String[] args) { JFrame window = new JFrame("Clickable Checkerboard"); ClickableCheckerboard content = new ClickableCheckerboard(); window.setContentPane(content); window.pack(); // Size the window to the preferred size of its content. window.setLocation(100,100); window.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); window.setResizable(false); // User can't change the window's size. window.setVisible(true); } //------------------------------------------------------------------- int selectedRow; // Row and column of selected square. If no 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 ClickableCheckerboard() { selectedRow = -1; addMouseListener(this); setPreferredSize( new Dimension(320,320) ); } /** * Draw the checkerboard and highlight selected square, if any. * (Note: super.paintComponent(g) is not necessary, since this * method already paints the entire surface of the object. * This assumes that the object is exactly 160-by-160 pixels. */ public void paintComponent(Graphics g) { int row; // Row number, from 0 to 7 int col; // Column number, from 0 to 7 int x,y; // Top-left corner of square for ( row = 0; row < 8; row++ ) { for ( col = 0; col < 8; col++) { x = col * 40; y = row * 40; if ( (row % 2) == (col % 2) ) g.setColor(Color.red); else g.setColor(Color.black); g.fillRect(x, y, 40, 40); } } // 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.setColor(Color.CYAN); y = selectedRow * 40; x = selectedCol * 40; g.drawRect(x, y, 39, 39); g.drawRect(x+1, y+1, 37, 37); g.drawRect(x+2, y+2, 35, 35); } } // end paint() /** * When the user clicks on the panel, figure out which * row and column the click was in and change the * selected square accordingly. */ public void mousePressed(MouseEvent evt) { int col = evt.getX() / 40; // Column where user clicked. int row = evt.getY() / 40; // 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; } repaint(); } // end mousePressed() public void mouseReleased(MouseEvent evt) { } public void mouseClicked(MouseEvent evt) { } public void mouseEntered(MouseEvent evt) { } public void mouseExited(MouseEvent evt) { } } // end class ClickableCheckerboard