CS 161 Lab J - Painter

Due Fri Nov 22 at 9:00am

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 is hard :p)

This lab will be completed in pairs. Be sure to review the pair programming guidelines. You should also try to work with a different partner than you have before!

Objectives

Necessary Files

You will need to download and extract the BlueJ project from the LabJ.zip file. This project will supply you with a PainterFrame class. This class will get you started with the frame and buttons for your Painting program (you should know how to do this, but the provided class will help keep the lab managable). You will not need to modify this class.

There is also a provided tester if you'd like it.

Details

For this lab you will be making one new class: a Painter class that extends JPanel. The Painter class will represent and display the drawing you have created.

  1. Start by creating the new Painter class. This class should extend JPanel. It will also need a couple of specific methods, described a ways further down the page (you can also see what methods are required by the PainterFrame). 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.
    • Reminder: you will need to import the necessary packages (java.awt.*, javax.swing.*, and java.awt.event.*) for building a GUI!
    • You'll also want to call setPreferredSize() to specify how big your canvas will be (800x600 is nice--maybe make a CANVAS_WIDTH and CANVAS_HEIGHT constant?)
      • Helpful hint: JPanels already have WIDTH and HEIGHT constants. However, these don't quite do what you think they'd do. If you want to make particular constants, you'll want to create new variables (with different names), rather than trying to override the inherited constants!
  2. You will need to override the 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 should be a familiar step from other graphical programs we've made.
    • You might try using the fillRect() method to draw a quick rectangle on the canvas, just to make sure things work!
  3. There is a problem though: we 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 we will need to store the drawing the user creates. (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. Remember to import the class (you can look up its package in the documentation)! Be sure and initialize the object in your Painter's constructor. If you don't remember how to do this, look back at old labs (like the PhotoShopper, where we last used the BufferedImage).
      • 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 we 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 (which is 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: we 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
    • 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.
  4. Excellent! The next step is to add in the Mouse interaction. To do this, you will need to have your Painter implement the MouseListener interface so that you can 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 windowing system, we need to implement the interface which means having all the methods!
    • 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. 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 MouseEvents: 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!
  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 we fix this?
    • The cleanest way is to think about instead of drawing a dot at each MouseEvent, we're going to draw a line between the location of succesive MouseEvents. This will let us (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. You'll need to store this location as instance variable(s).
    • 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.
    • This can get tricky, but isn't too complicated. 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 is easy: simply draw a big white rectangle that fills you Graphics2D object, thereby painting over everything that used to be there. Be sure to call repaint() to make the new blank canvas show up!
      • Remember to set the drawing color back to black!! (You can also 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, we're going to 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. You might notice that 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: just pass in a number (a float in our case) that specifies how many pixels thick you'd like the stroke to be.
      • To be perfectly clear: your setBrushSize() method should create a new BasicStroke object, passing the given width as the parameter for the BasicStroke 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 :)

Submitting

Once you are sure that your program works (remember to test your code), make sure that both your and your partner's names are in the class comment at the top of your Painter class. Then upload the entire project directory to the LabJ submission folder on hedwig. 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 morning after lab.

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

Extensions

Grading

This assignment will be graded on approximately the following criteria: