Solution for Programming Exercise 7.4
This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.
Exercise 7.4:
In Exercise 6.2, you wrote a program DragTwoSquares that allows the user to drag a red square and a blue square around on a canvas. Write a much improved version where the user can add squares to a canvas and drag them around. In particular: If the user shift-clicks or right-clicks the canvas, then the user is trying to drag a square; find the square that contains the mouse position, if any, and move it as the user drags the mouse. Other clicks should add squares. You can place the center of the new square at the current mouse position. To make the picture more visually appealing, give each square a random color, and when you draw the squares, draw a black outline around each square. (My program also gives the square a random alpha value between 0.5 and 1.0).
Write a class to represent the data needed for drawing one square, and use an ArrayList to store the data for all the squares in the picture. If the user drags a square completely off the canvas, delete it from the list.
We need three pieces of information in order to draw a square: its x-coordinate, its y-coordinate, and its color. We can pack the three pieces of information into a class:
/** * A class to hold to hold the data for one square. */ private static class SquareData { double x,y; // The coordinates of the center of the square. Color color; // The color of the square. }
The data for the whole collection of squares is then stored using an instance variable named squares of type ArrayList<SquareData>. By using an ArrayList, we allow for an unlimited number of squares. (Note that since the xy coordinates will change when a square is dragged, we cannot use a record class here.)
Given this data structure, it's easy to write a paintComponent() method that completely redraws the contents of the canvas. It can use a for-each loop to go through the ArrayList. The loop control variable, squareData, is of type SquareData, and the data needed to draw the square is given by its three instance variables squareData.x, squareData.y, and squareData.color. To draw a rectangle, we need the coordinates of the top-left corner of the square. Since squareData.x and squareData.y are the coordinates of the center of the square, we have to subtract half of the square size from them to get the coordinates for the top-left corner. In my program, the square size is 100, so the drawing method can be written as follows:
public void paintComponent(Graphics g) { super.paintComponent(g); // Fill with background color. for ( SquareData squareData: squares ) { g.setColor( squareData.color ); g.fillRect( squareData.x - 50, squareData.y - 50, 100, 100); g.setColor( Color.BLACK ); g.drawRect( squareData.x - 50, squareData.y - 50, 100, 100); } }
This method is called every time the mouse is moved while the user is dragging a square. It's not sufficient to draw the square in its new position—it must be removed from its previous position, and the area that it previously occupied has to be redrawn because it might contain other squares. It is easiest, though not most efficient, just to redraw the entire picture. When the user adds a new square, it would be enough to just draw the new square, but in fact my program just calls repaint() in that case as well.
Another interesting point is in the mousePressed() routine, which is called when the user clicks the mouse on the drawing area. If the user shift-clicks or right-clicks, we have to figure out which square contains the mouse position, so that we know which square to drag. To do that, we have to search the ArrayList of squares to find one that meets the criterion that it contains the mouse position. (This is an example of linear search, as defined in Subsection 7.5.1.) There is a twist, since if there are two squares under the mouse, we want the one that is on top, and that will be the one that comes later in the array. So, we should search the list starting at the end and working towards the beginning. Then the first square that we encounter that contains the mouse position is the one that we want:
x = evt.getX(); // (x,y) is the point where the mouse was pressed y = evt.getY(); for (int i = squares.size() - 1; i >= 0; i--) { SquareData squareData = squares.get(i); double cx = squareData.x; // (cx,cy) is the center of the square double cy = squareData.y; if ( x >= cx - 50 && x <= cx + 50 && y >= cy - 50 && y <= cy + 50) { dragging = true; draggedSquare = squareData; offsetX = x - cx; offsetY = y - cy; break; // stop as soon as we find square containing (x,y) } }
You can check out the sample solution below for other details.
import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.ArrayList; /** * A program that lets the user add squares to a canvas by clicking. * The center of a square is placed at the point where the user clicked. * Squares all have the same size (100-by-100). They have random * colors with up to 50% transparency. If the user shift-clicks * or right-clicks a square, the user can drag the square. If * the user drags a square off the canvas, it is deleted from the * list of squares. */ public class DragLotsOfSquares extends JPanel { /** * A main routine allows this class to be run as an application. */ public static void main(String[] args) { JFrame window = new JFrame("Click to add; Shift-click to drag!"); DragLotsOfSquares content = new DragLotsOfSquares(); window.setContentPane(content); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); window.setLocation(120,70); window.setSize(600,450); window.setVisible(true); } //--------------------------------------------------------------------- /** * An object of type SquareData contains the data necessary to * draw one square, that is, the color of the square and the * coordinates of its center. */ private static class SquareData { int x,y; // Location of center of square. The size is always 100-by-100. Color color; // The color of the square } private ArrayList<SquareData> squares; // Info for all squares in the picture. /** * The constructor places the two squares in their initial positions and * sets up listening for mouse events and mouse motion events. */ public DragLotsOfSquares() { setBackground(new Color(200,255,200)); // Set up appearance of the panel setBorder(BorderFactory.createLineBorder(Color.BLACK, 2) ); Dragger listener = new Dragger(); // Listening object, belonging to a nested // class that is defined below. addMouseListener(listener); // Set up listening. addMouseMotionListener(listener); squares = new ArrayList<SquareData>(); } /** * paintComponent just draws all the squares in their current positions. */ public void paintComponent(Graphics g) { super.paintComponent(g); // Fill with background color. for ( SquareData squareData: squares ) { g.setColor( squareData.color ); g.fillRect( squareData.x - 50, squareData.y - 50, 100, 100); g.setColor( Color.BLACK ); g.drawRect( squareData.x - 50, squareData.y - 50, 100, 100); } } /** * This private class is used to define the listener that listens * for mouse events and mouse motion events on the panel. */ private class Dragger implements MouseListener, MouseMotionListener { /* Some variables used during dragging */ boolean dragging; // Set to true when a drag is in progress. SquareData draggedSquare; // When a drag is in progress, this is // the square that is being dragged. int offsetX, offsetY; // Offset of mouse-click coordinates from the // center of the square that is being dragged. /** * Respond when the user presses the mouse on the panel. * A shift-click or right-click starts dragging the square * under the mouse, if any. Other clicks will add a new * square with its center at the mouse position. A drag * operation is begun only if the user shift-clicks or * right-clicks a square. */ public void mousePressed(MouseEvent evt) { if (dragging) // Exit if a drag is already in progress. return; int x = evt.getX(); // Location where user clicked. int y = evt.getY(); if (evt.isShiftDown() || evt.getButton() == MouseEvent.BUTTON3) { // If user shift-clicked or right-clicked a square, start dragging it. /* Find the square, if any, that contains (x,y). If several squares * contain (x,y), we want the one on top, which is the LAST one in * the list that contains (x,y) -- so consider the squares in the * reverse of their order in the list. */ for (int i = squares.size() - 1; i >= 0; i--) { SquareData squareData = squares.get(i); int cx = squareData.x; // (cx,cy) is the center of the square int cy = squareData.y; if ( x >= cx - 50 && x <= cx + 50 && y >= cy - 50 && y <= cy + 50) { dragging = true; draggedSquare = squareData; offsetX = x - cx; offsetY = y - cy; break; // stop as soon as we find square containing (x,y) } } } else { // Add a new square with center at (x,y) SquareData squareData = new SquareData(); squareData.x = x; squareData.y = y; int red = (int)(256*Math.random()); int green = (int)(256*Math.random()); int blue = (int)(256*Math.random()); int alpha = 255 - (int)(128*Math.random()); squareData.color = new Color(red,green,blue,alpha); squares.add( squareData ); repaint(); // Redraw the whole picture to show the new square. // (Could have just drawn it here instead!) } } /** * Dragging stops when user releases the mouse button. If the user * has dragged the square completely off the canvas, then it is deleted * from the list of squares. (That will have no visible effect on the * picture, so the canvas is not redrawn.) */ public void mouseReleased(MouseEvent evt) { if ( ! dragging ) return; if (draggedSquare.x > getWidth() + 50 || draggedSquare.x < -50 || draggedSquare.y > getHeight() + 50 || draggedSquare.y < -50) { // Square is completely off the canvas, so remove it! squares.remove(draggedSquare); // For testing, to make sure square is actually deleted: System.out.println("Removed square; list size = " + squares.size()); } dragging = false; // drag operation has ended. draggedSquare = null; } /** * 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 center 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 ) return; int x = evt.getX(); int y = evt.getY(); draggedSquare.x = x - offsetX; draggedSquare.y = y - offsetY; repaint(); // Redraw picture to show square in new positions. } public void mouseMoved(MouseEvent evt) { } // empty methods required by interfaces. public void mouseClicked(MouseEvent evt) { } public void mouseEntered(MouseEvent evt) { } public void mouseExited(MouseEvent evt) { } } // end nested class Dragger } // end class DragTwoSquaresPanel