CS 261 Lab B - Painter

Due Wed Sep 10 at 3:00pm

Overview

In this lab you will create a simple graphical "painting" program, similar to Microsoft Paint. With this program, the user should be able to drag the mouse and draw lines on a window in order to sketch a pretty picture :) The user will also be able to change the color of the "brush" (what color line they are drawing), and the thickness of the brush stroke. (Drawing lines without a mouse exacerbates my already poor handwriting :p)

This lab will be completed in pairs. Partner assignments for this lab can be found on Moodle

Objectives

Necessary Files

You will need to download the copy of the zipped source files. and import them into Eclipse (see previous lab for instructions).

This file includes a PainterFrame class, which will get you started with the frams and buttons for your painting program. (You should know how to put these together, but the provided class will help keep the lab managable). Note that this class PainterFrame contains the main() method.

Lab Details

For this lab, you'll be making a new class: a Painter which is a GUI element that will represent and display the drawing you have created.

  1. Start by creating the new Painter class. This is going to be a GUI element, so you should extend the existing JPanel class to automatically give it basic display functionality (e.g., allow it to be shown on the screen). You will also need a couple of specific methods, described a ways further down the page (you can also see what methods are required to get the provided PainterFrame to compile). First fill in the "skeleton" of your program by writing in the method signatures, so that everything can compile and you can test your code.
    • Remember to import the necessary packages (java.awt.*, javax.swing.*, and java.awt.event.*) for building a GUI! You can do this easily using the "Source > Organize Imports" menu option in Eclipse.
    • You'll want to call the inherited setPreferredSize() method to specify how big your canvas will be. The size should be stored as constants for easy modification--don't use "magic numbers"!
      • Helpful hint: JPanels already have WIDTH and HEIGHT constants, which get inherited. But you can't override inherited constants (because static variables/methods are not polymorphic!), so you can't change the variables the way you want. Instead, you'll need to create new variables (with different names)--for example, CANVAS_WIDTH and CANVAS_HEIGHT.
  2. You will need to override the inherited public void paintComponent(Graphics g) method, so that you can have the Painter be drawn differently than the normal parent JPanel---that is, with something on it! This method will be automatically called by Java whenever you need to "refresh" what is shown on the screen, such as when you decide to draw something new.
    • Be sure and call super.paintComponent(g) to make sure any secret setup performed by the parent method occurs.
    • You should try using the fillRect() method to draw a quick rectangle on the canvas, just to make sure things work!
  3. There is a problem with this basic setup though: you want to be able to update the display to show whatever the user has drawn. However, everytime the panel refreshes and paintComponent() is called, any previous display gets erased and forgotten! So you will need to store the drawing the user creates somehow. (Take a moment and think about this problem---do you understand what the issue is?)
    • To do this, we are going to be slightly tricky: rather than having the user draw directly onto the JPanel's Graphics object, we're instead going to have the user draw onto a separate image object: in particular a BufferedImage (this will act as a "buffer" between the user and the display). We will then draw that BufferedImage onto the JPanel's Graphics object, thereby transferring the user's drawing onto the screen!
    • You will need to make a BufferedImage instance variable. Be sure and initialize the object in your Painter's constructor.
      • To save you some time: image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) is the basic template you want.
    • Here's the sneaky part: the BufferedImage does have a Graphics object that you can draw on just like any other Graphics object! We can access this by calling the createGraphics() method on the BufferedImage object we've created. This method returns a Graphics2D object (a class that extends Graphics with extra methods; our normal paintbrush object). Call this method in your Painter's constructor and store this Graphics2D object as an instance variable.
    • Now whenever you want to draw on a graphics context, just draw on that Graphics2D instance variable. For example, you might start by painting a giant white rectangle on that object to act as a background, as well as a smaller colored oval to test things.
    • Last step: you need to make sure that the BufferedImage (along with its associated Graphics2D object) is being drawn on the JPanel! Inside the paintComponent(), call the drawImage() method on the parameter g, passing in the BufferedImage instance variable. For example:
        g.drawImage(image,0,0,null); //image is the name of the BufferedImage. null is the "imageobserver" we're not worrying about.
    • This is like drawing on a large sheet of butcher paper (the BufferedImage), and then hanging that paper in the window (drawing it in paintComponent).
    • Be sure and test that this works! Your colored oval should show up on the JFrame when you run the program. Now you have a basic double-buffered graphics program!
  4. The next step is to add in the user interaction (via the mouse). This will use event-driven programming--when an event happens (e.g., the user moves the mouse), you will respond with some code to run. To set this up, you will need to have your Painter implement the MouseListener interface, which will let you respond to Mouse events. However, the MouseListener only handles when the mouse buttons are pressed and when the mouse enters or leaves the JPanel--it doesn't track when the mouse moves around the JPanel. To do that, you'll need to also implement the MouseMotionListener interface.
    • There will be a lot of methods you'll need to add--but you won't necessary use them all (many of them can be empty). However, in order to work with the Java's event system, you need to use these particular interfaces which means having all the methods. In effect, you're agreeing to fulfill the contract, but you fulfill that contract by doing nothing.
    • Remember the three steps when implementing a listener:
      1. Modify the class declaration so you implement the interfaces. Be sure to define the required methods (yes there are a lot, but you won't need to fill in code for all of them, I promise!).
      2. Register the listener, so that the events know which listener object will respond to them. You'll need to call addMouseListener() and addMouseMotionListener() in your constructor. Think about what object you are calling these methods on (hint: it's the Painter you're creating!) and what object is the listener that you are adding (hint: it's the Painter you're creating!)
      3. Fill in the required methods. You might start by just printing out a message like "mouse was pressed!" or "mouse was dragged!" Also try printing out the MouseEvent objects themselves: what kinds of information are you seeing?
  5. Now you need to make it so that you draw on the Graphics2D instance variable whenever the mouse is pressed and/or dragged. Start by just drawing a tiny oval (diameter 1 or 2) at the position where the MouseEvent occured. Since lots of events should occur, you'll get lots of tiny circles that when connected together will form lines!
    • You can use the getX() and getY() methods of the MouseEvent object to get the x- and y-coordinates of the event's location.
    • Remember to call repaint() whenever you've updated the drawing, so that Java knows it should call the paintComponent() method!
  6. You might notice that the dots get scattered if you move the mouse too fast: this is because MouseEvents don't generate fast enough to keep up with your lightning hands! So how do you fix this?
    • The cleanest way is to think about instead of drawing a dot at each MouseEvent, you're going to draw a line between the location of succesive MouseEvents. This will let you (literally) connect the dots!
    • To do this, your program will need to remember the location (i.e., the x and y variables) of the previous MouseEvent.
    • Then whenever a MouseEvent occurs where you'd want to draw, you'll draw a line from the previous location to the location of the current event, and then store the location of current event as the previous location!
      • You'll need to be careful about whether there IS a previous location (i.e., when the mouse is first pressed, you can't draw a line from nowhere). Similarly, when the mouse button is released, you'll want to "clear" your previous event. Storing values of -1 for locations are a good way to "flag" or mark that a location has not been set, or you could use a separate sentinel variable.
    • This can get tricky, but gets to the heart of event-driven programming: rather than having code simply run when you start a program, you have bits of code run every time an event occurs--even if you don't know what those events will be!
      • Be sure and test your code! Remember to "play computer" and/or use print statements to figure out what is going on during different methods.
  7. At this point, you should be able to draw nice black lines all over your program! Now you can add some finishing touches to enable interesting drawing in response to the PainterFrame's buttons being pressed. The methods you need to add and their functions are described below:
    • public void eraseAll()
      • This method should clear the current drawing. Since there is no "erase" method for a Graphics object, you should instead just white everything out. Be sure to call repaint() to make the new blank canvas show up!
      • You should store the previously selected color in a local variable, and then change the paint color to that after you've whited things out.
    • public void setPaintColor(Color c)
      • This method will let you change the color of the "brush" the user is currently drawing with. This method is easy to fill in: just set the color or paint of the Graphics2D instance variable!
    • public void setBrushSize(float width)
      • This method will let you change the thickness of the "brush" the user is currently drawing with. Again, you can use built-in Java classes to do this easily. The Graphics2D has a setStroke() method that lets you change the thickness of the stroke. This method takes as a parameter a Stroke object. But Stroke is just an interface, so instead you'll want to use a BasicStroke object (whew!) But don't worry, a BasicStroke is simple to create: check the documentation for how to call its constructor.
      • This is only one or two lines of code: do not overthink it!
  8. And that should about do it! Be sure and test your code (and all the buttons) thoroughly, and be sure to draw lots of pretty pictures :)

Submission

Make sure both your names are on all the classes you've modified, and upload your source code to the LabB submission folder on vhedwig (see the previous lab if you need help). Only one partner needs to upload the code. Make sure you upload your work to the correct folder! The lab is due at the start of class the day after lab.

After you have submitted your solution, log onto Moodle and submit the Lab B Partner Evaluation. Both partners need to submit evaluations.

Extensions

Grading

This assignment will be graded out of 15 points: