4-7 A Line Drawing App
Let’s apply what we’ve learned about Swing components, drawing shapes, layout managers, and mouse event handling to create a simple mouse-controlled line drawing application.
Using a BorderLayout, we’ll include a status label in the NORTH region of the main JFrame showing the current mouse coordinates. In the CENTER region we’ll design a custom JPanel that will handle mouse events to enable the drawing of Line objects.
This time I’ll start with the test code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import javax.swing.JFrame; import javax.swing.JLabel; import java.awt.BorderLayout; public class LinePainter { public static void main( String[] args ) { JFrame appWindow = new JFrame( "Line Painter App" ); // Put statusLabel in NORTH to display mouse coordinates JLabel statusLabel = new JLabel(); appWindow.add( statusLabel, BorderLayout.NORTH ); // Put DrawPanel in CENTER and pass reference to statusLabel for updates appWindow.add( new DrawPanel( statusLabel ), BorderLayout.CENTER ); appWindow.setSize( 320, 320 ); appWindow.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); appWindow.setVisible( true ); } } |
To keep the DrawPanel code as clear as possible, we will use a (regular) inner class called MouseEventListener to handle mouse events. This class will inherit from MouseAdapter such that we only need to override the mouse event methods that we need to. Let’s take a look at the DrawPanel code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
import java.awt.Color; import java.awt.Graphics; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JPanel; import javax.swing.JLabel; public class DrawPanel extends JPanel { private JLabel statusBar; private int freeIndex = 0; private Line currentLine = null; private Line[] lines = new Line[1000]; // Constructor instantiates an array of 10 Random Line objects public DrawPanel( JLabel statusLabel ) { statusBar = statusLabel; setBackground( Color.WHITE ); // Create and register listener for mouse and mouse motion events MouseEventListener drawPanelListener = new MouseEventListener(); addMouseListener( drawPanelListener ); addMouseMotionListener( drawPanelListener ); } // Inner class to handle mouse events class MouseEventListener extends MouseAdapter { // Mouse press indicates a new line has been started @Override public void mousePressed( MouseEvent event ) { currentLine = new Line( event.getX(), event.getY(), event.getX(), event.getY(), Color.RED ); // Tell JVM to call paintComponent( g ) repaint(); } // Mouse release indicates the new line is finished @Override public void mouseReleased( MouseEvent event ) { // Update ending coordinates and switch color to BLACK currentLine.setX2( event.getX() ); currentLine.setY2( event.getY() ); currentLine.setColor( Color.BLACK ); // If there's space, add the new line to our array if ( freeIndex < lines.length ) { lines[freeIndex] = currentLine; freeIndex++; } // Get ready for the next line to be drawn currentLine = null; repaint(); } // As mouse is dragged, update ending coordinates of currentLine and statusBar @Override public void mouseDragged( MouseEvent event ) { currentLine.setX2( event.getX() ); currentLine.setY2( event.getY() ); statusBar.setText( String.format( "Mouse at (%d, %d)", event.getX(), event.getY() ) ); repaint(); } // As mouse is moved, just update the statusBar @Override public void mouseMoved( MouseEvent event ) { statusBar.setText( String.format( "Mouse at (%d, %d)", event.getX(), event.getY() ) ); } } // This method is called automatically by the JVM when the window needs to be (re)drawn. @Override public void paintComponent( Graphics g ) { super.paintComponent( g ); // Call the draw() method for each Line object in the array for ( int i = 0; i < freeIndex; i++ ) lines[i].draw( g ); // If a line is in progress, draw it on top of all others if ( currentLine != null ) currentLine.draw( g ); } } |
You’ll note on line 12 that I am using an array of 1000 Line objects to store the lines as they are drawn — I’ll leave it as an exercise for you to use a dynamic data structure instead. In 4-2 Q1 you designed a complete class hierarchy for Line, Rectangle, and Oval objects. To test the code above, use your Line class.
On lines 15 and 16 the DrawPanel constructor receives a reference to the statusLabel created in main(). A copy of this reference is kept so that it can be updated by the event handling methods in the DrawPanel class. There are four mouse events that we are interested in.
When the mouse is pressed we instantiate a new Line object and set its starting and ending coordinates to where the mouse button was pressed. This currentLine is not considered complete until the user releases the mouse, so we will show it in Color.RED to indicate this to the user. Since we do not have a reference to the Graphics object g, and therefore cannot directly call the paintComponent() method, the repaint(); statement tells the JVM to call paintComponent() for us (more on this method below).
When the mouse is released, we use the mouse coordinates to update the (x2, y2) coordinate of the currentLine and switch it to Color.BLACK to indicate to the user that it is not currently being drawn. As long as there is space in the lines array we add the currentLine to the array. Again a call to repaint() will redraw all of the lines so that we see this latest update.
When the mouse is dragged (i.e., button down) it means that a currentLine has been started by mousePressed(), so we simply update the end coordinates of the currentLine, update the mouse location in the statusLabel, and call repaint() to reflect all of these changes.
When the mouse is moved (i.e., button up) we simply update the mouse location in the statusLabel. No new lines have been created so there is no need to call repaint() to update our JPanel.
Now consider the paintComponent() method. This method is called automatically by the JVM whenever the window is resized, but we can force it to be executed by calling the repaint() method. The paintComponent() method simply calls the draw() method for each Line object in the array. We have to be careful not to index past the point where Line objects are found or we will get a NullPointerException when calling draw(). Lastly, if a currentLine is in the process of being drawn then we draw the currentLine as well, on top of those from the lines array.
That’s all there is to it! You can see that the bulk of the logic for this program happens in the mouse event handling methods.
You Try!
- Try changing the array size to 10 to see how the Line Drawing App behaves. Enhance the DrawPanel to use an ArrayList instead of an array.
- Change the code such that it draws Rectangle or Oval objects instead.