CS 161 Lab K - Painter
Due Thurs Apr 25 at 11:59pm
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 also must work with a different partner than you have before!
Objectives
- To practice using a MouseListener
- To learn to "buffer" graphical images using... a BufferedImage (surprise!)
- To practice with drawing through event-driven programming
- To learn about a couple more helpful GUI and graphics classes
Necessary Files
You will need to download and extract the BlueJ project from the
LabK.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 need 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. You will also be adding a couple of small pieces to the PainterFrame
class.
- Start by creating the new
Painter
class. This class should extendJPanel
. 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.*
, andjava.awt.event.*
) for building a GUI! -
Remember to start your constructor by calling
super
's constructor. You'll also want to callsetPreferredSize()
to specify how big your canvas will be (800x600 is nice--maybe make aCANVAS_WIDTH
andCANVAS_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!
-
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!- You might try using the
fillRect()
method to draw a quick rectangle on the canvas, just to make sure things work!
- You might try using the
-
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!)- To save you some time:
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
is the basic template you want.
- To save you some time:
-
Here's the tricky 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 you should recall is a child of the Graphics object, and so has all the same methods). Call this method in your Painter's constructor and store thisGraphics2D
object as an instance variable. - Now whenever you want to draw on a graphics context, just draw on the 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 thedrawImage()
method on the parameterg
, 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.
-
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
-
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 theMouseMotionListener
interface.-
Remember the three steps when implementing a listener:
- 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!).
- Register the listener. You'll need to call
addMouseListener()
andaddMouseMotionListener()
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!) - 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?
-
Remember the three steps when implementing a listener:
-
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 (radius 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()
andgetY()
methods of theMouseEvent
object to get the location of the event. - Remember to call
repaint()
whenever you've updated the drawing!
- You can use the
-
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
andy
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.
- 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
- 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
-
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).
- 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
-
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 asetStroke()
method that lets you change the thickness of the stroke. This method takes as a parameter aStroke
object. You might notice thatStroke
is just an interface, so instead you'll want to use aBasicStroke
object (whew!) But don't worry, aBasicStroke
is simple to create: just pass in a number (afloat
in our case) that specifies how many pixels thick you'd like the stroke to me. - 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!
-
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
-
- 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, make sure that both your and your partner's names are in a class comment at the top of the Painter class. Then submit your program to the submissions folder.
- Upload your entire BlueJ project folder to the LabK folder on the submission folder on hedwig. Make sure you upload your work to the correct folder! The lab is due at midnight on the day of the lab.
- One last time: please fill out the lab partner evaluation on Moodle?
Extensions
- Add in a button that lets you save the drawn image to a file. You can use the file saving code from the Photoshopper lab--just put that code in your Painter and save the BufferedImage you've been drawing on!
- Add the ability to go into "eraser" mode. This is basically drawing thick lines that are the same color as the background!
-
For extra complexity, try adding the ability to draw simple shapes (i.e., rectangles and circles). The user might click somewhere to start the shape, then drag to size how big the rectangle should be. Note that you can use the built-in
Rectangle
andEllipse2D.Double
classes to temporarily store the shape that is being created, so you can easily erase/modify it as the position of the mouse changes.
Grading
This assignment will be graded on approximately the following criteria:
- Implements a JPanel that shows up in the provided frame and displays an image [10%]
- Uses a BufferedImage to store the drawing, and draws that Image on the panel [15%]
- Successfully implements the MouseListener and MouseMotionListener interfaces [10%]
- Draws on the screen in response to MouseEvents [15%]
- Draws solid lines on the screen (without skipping) [15%]
- Implements the eraseAll() method [5%]
- Implements the setPaintColor() [5%]
- Implements the setBrushStroke() method using BasicStroke [10%]
- You have used good programming style and documentation [10%]
- You completed your lab partner evaluation [5%]