Storing Drawings as Lists of Shape Objects
An off-screen canvas is a BufferedImage, a subclass of Image, that stores a picture in memory instead of showing it directly.
Why a Painted Picture Can Disappear
Imagine a program in which a user draws freehand curves and rectangles on a panel. The user's work appears on the screen, but the screen is not a permanent place to keep anything. If the window is resized, or the operating system asks the panel to repaint itself, the panel has to rebuild everything it shows. Anything that was painted directly onto the panel and not recorded somewhere else is gone.
So the program needs enough stored information to draw the picture again whenever it is asked to. This article looks at one way to hold that information: an image kept in memory, away from the screen, that the program can paint onto and later copy to the screen. As you read, keep one question in mind: where is the picture actually kept between one paint call and the next?
The Off-Screen Canvas as an Idea
An off-screen canvas is an image that lives in memory and is not displayed directly. The program draws into it the way it would draw onto a panel, but nothing appears on the screen until the finished image is copied there. The canvas is a working surface that the program owns, separate from the visible area.
What do you think happens?
If the panel is repainted and the picture is only in the canvas, what must happen before the user sees the picture again?
Reveal answer
The canvas must be copied onto the panel
The canvas holds the picture in memory. The paint method copies it to the panel with drawImage every time the panel paints, so the picture is rebuilt from the stored image rather than from the screen.
What a BufferedImage Is
A BufferedImage is an object of the class BufferedImage, which belongs to the package java.awt.image. It is a subclass of Image, and it is how Java represents an off-screen canvas.
Because BufferedImage is a subclass of Image, a BufferedImage can be passed to the drawImage method of a Graphics object exactly as any other image can. The image you copy onto the screen is the same kind of object you drew on, so no conversion step is needed between drawing and copying.
Creating the Canvas and Its Graphics Handle
A BufferedImage is created with a constructor that takes three arguments: a width, a height, and an image type. In the source program the width and height come from the panel's own size, so the canvas always matches the area it will be copied into. The constant BufferedImage.TYPE_INT_RGB selects the format used to store each pixel. Read the line as a sentence: create a new BufferedImage that is this wide, this tall, and stores its pixels in this format, and keep a reference to it in the variable OSC.
- The constructor receives width, height, and image type in that order
createGraphics()returns a Graphics handle whose drawing lands in the imagesetColorandfillRectwork on this handle exactly as they do on a panel- Filling the whole canvas makes it start out in the chosen color
Creating the canvas gives you storage but no drawing tools. The BufferedImage itself only holds pixels. To draw on it, you call createGraphics() on the image. That call returns a Graphics object whose operations, such as setColor and fillRect, land in the image instead of on the screen. The handle is the way into the canvas; the canvas is where the pixels live.
Tracing the Paint Method Step by Step
The painting program has a paint method that does three things in order. First it makes sure an off-screen canvas exists. Next it copies that canvas onto the panel. Finally it adds any shape the user is currently dragging out. The code below is a sketch of that method. The fillColor variable and the comment about the dragged shape are illustrative names added for this explanation; the source describes the steps but not these exact identifiers.
- The null check means the canvas is created only once
- The canvas is copied to the panel before any in-progress shape is added
- The dragged shape is drawn on the screen Graphics object g
Trace the three objects involved: the off-screen canvas, the Graphics handle used to draw on it, and the panel's own Graphics object g that paints to the screen. On the first paint call, the canvas and its handle are created and filled. On later paint calls, the canvas already exists, so the method goes straight to copying it. The canvas is the only one of the three that persists between calls; the handle and the screen output are used and then the screen is repainted from the canvas.
Copying the Canvas with drawImage
Once the canvas holds the picture, the paint method copies it to the panel with g.drawImage(OSC, 0, 0, null). The coordinates 0 and 0 place the top-left corner of the image at the top-left corner of the panel. The first argument is the image, and the fourth argument is an image observer. The observer is used to track images that are still loading. The source program passes null because the image is already fully available in memory.
What do you think happens?
After drawImage runs, which object holds the picture the user drew?
Reveal answer
The off-screen BufferedImage
drawImage copies the finished image to the screen, but the canvas keeps its pixels in memory. The copy on the screen is refreshed from the canvas each time paint runs, so the canvas remains the stored record of the picture.
Where the Picture Information Lives
The off-screen canvas stores the color of every individual pixel. It does not remember how the picture was made. A stroke, a rectangle, and a dot that produce the same colors are indistinguishable once they are in the image. This is a real design choice with consequences.
A program that keeps only pixels can redraw the picture by copying it, but it cannot easily move or delete one shape afterward. A program that keeps a list of shape objects can do those things, because each shape is a separate piece of information. The source contrasts the canvas approach with a sample program that records each drawn shape as an object.
Consider the user's freehand curve on the painting panel. In the canvas approach, once the curve is painted, the program no longer holds a separate record of it: only the pixels that form it remain. In the shape-list approach, the curve would be one object in a list, so the program could select it, move it, or remove it without touching the rest of the drawing.
Mistakes Beginners Make with the Canvas
Common mistake Expecting createGraphics to return a picture rather than a drawing handle
Why it failsThe BufferedImage holds the pixels.
createGraphicsreturns a Graphics object that draws into that image.Do this insteadKeep the image in a variable such as OSC, and use the Graphics object returned by createGraphics only to draw on it.
Common mistake Drawing on the panel and assuming the picture is saved
Why it failsAnything painted directly onto the panel and not recorded elsewhere is lost when the panel must repaint.
Do this insteadDraw the lasting picture onto the off-screen canvas, then copy the canvas to the panel with drawImage in paint.
Common mistake Forgetting that the canvas must match the panel's size
Why it failsThe source program takes width and height from the panel so that the canvas matches the area it is copied into.
Do this insteadPass getWidth() and getHeight() to the BufferedImage constructor, not arbitrary numbers.
Common mistake Assuming the canvas remembers which shape was drawn last
Why it failsThe canvas stores only pixel colors, so individual shapes cannot be found or changed afterward.
Do this insteadIf you need to move or delete shapes, store each shape as an object in a list and redraw from that list.
Practice Tracing the Canvas
A painting panel is 300 pixels wide and 200 pixels tall. Write the BufferedImage constructor call that creates its canvas using the panel's size and the TYPE_INT_RGB type, then write the call that gets a Graphics object for drawing on the canvas. Finally, state the coordinates you would pass to drawImage to place the canvas at the top-left corner of the panel.
Hints
- The width and height come from the panel, so you can use
getWidth()andgetHeight()rather than the numbers. createGraphics()is called on the BufferedImage, not on the panel.- The top-left corner of both the image and the panel is at coordinates 0 and 0.
A user draws three rectangles and then drags a fourth. Explain which of the three objects, the off-screen canvas, the canvas Graphics handle, or the panel Graphics g, holds the three finished rectangles between paint calls, and then explain how the program would need to change if it had to delete the second rectangle alone.
Hints
- Think about which object persists between paint calls.
- Consider what a pixel-only store can and cannot change after drawing.
Key Takeaways from the Canvas Approach
At a glance
- An off-screen canvas is a BufferedImage, a subclass of Image, that stores a picture in memory instead of showing it directly.
- A BufferedImage is created with a width, a height, and an image type such as BufferedImage.TYPE_INT_RGB, and
createGraphics()returns a Graphics handle that draws into it. drawImagecopies the finished canvas to the component's screen area, so the canvas is the stored record that lets the picture be redrawn on demand.- A canvas keeps the color of every pixel rather than a record of each shape, which makes redrawing easy but makes moving or deleting one shape harder than with a list of shape objects.