Solution for
Programming Exercise 7.8
THIS PAGE DISCUSSES ONE POSSIBLE SOLUTION to
the following exercise from this on-line
Java textbook.
Exercise 7.8:
Turn your applet from the previous exercise into a stand-alone
application that runs as a JFrame. (If you didn't do the
previous exercise, you can do this exercise with the original
SimplePaint3.java.)
To make the exercise
more interesting, remove the JButtons and JComboBoxes
and replace them with a menubar at the top of the frame. You can
design the menus any way you like, but you should have at least the same
functionality as in the original program.
As an improvement, you might add an "Undo" command.
When the user clicks on the "Undo" button, the previous drawing operation will
be undone. This just means returning to the image as it was before the
drawing operation took place. This is easy to implement, as long
as we allow just one operation to be undone. When the off-screen canvas,
OSI, is created, make a second off-screen canvas, undoBuffer,
of the same size. Before starting any drawing operation,
copy the image from OSI to undoBuffer. You can do this with
the commands
Graphics undoGr = undoBuffer.getGraphics();
undoGr.drawImage(OSI, 0, 0, null);
When the user clicks "Undo", just swap the values of OSI
and undoBuffer and repaint. The previous image will appear
on the screen. Clicking on "Undo" again will "undo the undo".
As another improvement, you could make it possible for the user
to select a drawing color using a JColorChooser dialog box.
Here is a button that opens my program in its own window. (You don't have to
write an applet to launch your frame. Just create the frame in the
program's main() routine.)
Discussion
Let's start by discussing the Undo operation, since there is not
much more to say about it beyond what is said in the exercise.
The off-screen canvas for the undo operation, undoBuffer, is created in
the setupOSI() method where OSI is created.
It is filled with the background color, just like OSI.
(Ordinarily, the user won't see the initial contents of the
undo buffer. But if the user clicks the Undo button
before drawing anything, the undo buffer will be swapped in and
displayed. So, it should have some well-defined content. Since,
in fact, both OSI and undoBuffer are initially
filled with the background color, the user won't see any difference
when they are swapped.)
In the actionPerformed method,
in response to the "Undo" command, the values of OSI
and undoBuffer are swapped with the statements
Image temp = OSI;
OSI = undoBuffer;
undoBuffer = temp;
repaint();
The values stored in the variables OSI and undoBuffer
are swapped. But remember that the values are only pointers to the
Image objects. The Images themselves are not
copied. After the swap, the Images themselves have not
moved, but OSI and undoBuffer are pointing to
different images. When the screen is repainted, and OSI is
drawn to the screen, it's the former undo buffer that appears on the
screen. The former OSI is now the new undo buffer.
In the mousePressed() routine, before a drawing operation
starts, the current image is saved in the undo buffer, using the
commands given in the exercise.
In the source code below, all changes having to do with the
Undo command are shown in red.
In changing the program from an applet to a frame, the first line,
which was
public class KaleidaPaint extends JApplet {
is changed to
public class KaleidaPaintFrame extends JFrame {
The init() method of the applet is replace
by a constructor for the frame. Since the frame will use
a menu bar in place of the buttons and JComboBoxes
used in the applet, the constructor is very different from
the applet's init() method. Its main purpose is
to create the menu bar and the menus that it contains.
The design of the menu bars could be very different from
the ones that I used, but techniques for setting up the
menus are pretty standard and straightforward. (They
do require a lot of typing, though!) The techniques
are the same as those used in the ShapeDrawFrame
example from Section 7. I won't
discuss the set up of the menus here.
In my applet, the "Color", "Shape", and "Symmetry" menus control
what type of drawing is done when the user drags the mouse.
Each menu contains a group of JRadioButtonMenuItems.
Java, unfortunately, doesn't have a good way of checking a group
of radio buttons to see which one is selected. You have to
go through and check each button. To make this possible in my
applet, I have instance variables to represent each of the
JRadioButtonMenuItems, and I wrote methods to check
for the currently selected color, shape, and symmetry type.
For example:
private int getSelectedSymmetry() {
// Check the "Symmetry" menu and return the code
// for the type of symmetry that is currently selected.
if (noSymmetry.isSelected())
return NO_SYMMETRY;
else if (twoWay.isSelected())
return SYMMETRY_2;
else if (fourWay.isSelected())
return SYMMETRY_4;
else
return SYMMETRY_8;
}
The method checks the buttons in the "Symmetry" menu and returns a constant
that represents the selected item in the menu. This method,
along with the very similar getSelectedColor() and
getSelectedShape(), are called in mousePressed()
when the user begins drawing. The values they return are used
to determine what to draw when the user drags the mouse.
The other menu, "Control", contains commands that will be carried
out by the actionPerformed() method of the Display
class. Most of this is straightforward. In order to implement
custom colors, an instance variable, customColor, in the
Display class holds the currently selected custom color.
Initially, the color is gray. (It has to have some initial value,
in case the user uses the custom color without first selecting one.)
The value of this variable is changed if the user selects a new
custom color with the "Select Custom Color..." command. The
custom color is used for the background color by the "Fill with Custom"
command in the "Control" menu, and it is used for the drawing color
if "Custom" is selected in the "Color" menu. In my original solution,
the "Select Custom Color..." command did nothing but change the
customColor variable. To draw with this color, the
user also had to select "Custom" from the color menu. After using
the program, I found that this behavior didn't feel right. I wanted
to be able to draw with the new color immediately after using
the "Select Custom Color..." command. So, I changed the program to
select "Custom" in the "Color" menu automatically. This might
surprise the user, but I found it more surprising when the color
wasn't changed. (There is something called the
"Principle of Least Surprise" which is a valuable guideline
for user interface design.)
In addition to setting up the menus, the constructor does some
initialization that any frame needs, either in its constructor
or elsewhere:
pack();
setLocation(75,50);
setResizable(false);
setDefaultCloseOperation(EXIT_ON_CLOSE);
show();
The pack() command sets the frame to its preferred size.
Since I use the pack() command to set the size of the frame,
all the components in the frame must define a reasonable preferred
size. My original display class did not do this, so I added a call to
setPreferredSize() method to the constructor of the display class.
The default size is 450-by-450. Symmetry looks better on a square drawing
area. The frame will be sized so that this is the exact size of the
drawing area. As an alternative to using pack(),
I could have set the size of the frame by calling its setSize()
or setBounds() method.
The setResizable(false) command prevents the user from changing
the size of the window. This is not usually desirable, but I decided to do
it here since changing the window size will erase the current picture,
and I thought that might be too surprising for the user.
The default close operation tells what happens when the user clicks
the close box of the window. For a stand-alone application with a single
window, it's appropriate to call System.exit(), which is what
I do here.
The show() command is important, since without it the frame
would never appear on the screen. The show() command does
not have to occur in the constructor. If the constructor does not
show the frame, then the frame remains invisible until some other
routine calls the frame's show() method. Sometimes it's
good to be able to show and hide a frame at will.
The frame class has a main() routine that makes it possible
to run the frame as a standalone program. The main() routine
just needs to say "new KaleidaPaintFrame()" to create the
frame:
public static void main(String[] args) {
new KaleidaPaintFrame();
}
Note that the main() routine ends immediately after it
opens the frame. It does not wait for the window to close. The main()
routine ends even while the frame continues to exist. The main()
routine is, in fact, a separate thread from the user interface thread
that runs the frame. The main() routine could even go on to
perform other tasks independently of the frame, such as interacting
with the user via TextIO.
The only other change I made was to remove the getInsets()
method from the original program. The frame has its own borders anyway.
In general, there is no reason to use insets on a frame
The Solution
Changes related to the Undo operation are shown in red.
Other significant changes related are shown in blue.
/*
A nifty program where the user can sketch curves and shapes in a
variety of colors on a variety of background colors. The user can
draw symmetric pictures in which shapes are reflected horizontally,
vertically, and diagonally. The user selects the type of symmetry
from a "Symmetry" menu at the top of the frame. The user can
select a color to be used for drawing from a "Color" menu.
The user selects the shape to draw from a "Shape" menu at the
top of the frame. The user can draw free-hand curves, straight
lines, and one of six different types of shapes.
A "Control" menu contains commands for filling the drawing
area with various colors. It also contains commands: "Clear", "Undo",
and "Quit". The "Clear" command will clear the frame to the current
background color. The user can undo only the most recent operation.
The user can close the window by chosing the "Quit" command or
by clicking the Window's close box. Finally, there is a
"Select Custom Color..." command that lets the user set a custom
color for drawing or for the background.
The user's drawing is saved in an off-screen image, which is
used to refresh the screen when repainting. The frame is made
non-resizable, since the way the program is written, the
picture would be lost if the drawing area were to change size.
This file defines two classes, KaleidaPaintFrame.class, and
class, KaleidaPaintFrame$Display.class.
*/
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class KaleidaPaintFrame extends JFrame {
public static void main(String[] args) {
// A main routine that allows this class to be run
// as a stand-alone application. It just opens a frame.
new KaleidaPaintFrame();
}
JRadioButtonMenuItem black, red, green, blue, cyan, magenta,
yellow, white, custom;
// Items for the "Color" menu, which controls the drawing color.
// They form a group in which only one item can be selected.
// When the user starts drawing, the color is determined by
// checking to see which of the items is selected.
JRadioButtonMenuItem curve, straightLine, rectangle, oval,
roundRect, filledRectangle, filledOval, filledRoundRect;
// Items for the "Shape" menu, which determine the shape to be drawn.
JRadioButtonMenuItem noSymmetry, twoWay, fourWay, eightWay;
// Items for the "Symmetry" menu, which determine which
// reflections of the basic figure should be drawn.
public boolean standAlone = true;
// If a frame is created by an applet, the applet should
// set this variable to false. Otherwise, an error will
// be generated when the user selects the "Quit" command,
// since that command will call System.exit() if standalone
// is true. The applet should also call the frame's
// setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE).
public KaleidaPaintFrame() { // replaces init() method.
// Constructor creates a drawing area and uses it as its
// content pane. It also sets up the menu bar.
super("KaleidaPaint"); // Set a title for the window.
Display canvas = new Display(); // The drawing area.
setContentPane(canvas);
// Create menu bar and menus.
JMenuBar menubar = new JMenuBar();
JMenu controlMenu = new JMenu("Control",true);
menubar.add(controlMenu);
JMenu colorMenu = new JMenu("Color",true);
menubar.add(colorMenu);
JMenu shapeMenu = new JMenu("Shape",true);
menubar.add(shapeMenu);
JMenu symmetryMenu = new JMenu("Symmetry",true);
menubar.add(symmetryMenu);
setJMenuBar(menubar);
// Set up the "Control" menu, and set the canvas to respond
// to commands from this menu. Add accelerators for some
// of the commands.
controlMenu.add("Fill with Black").addActionListener(canvas);
controlMenu.add("Fill with Red").addActionListener(canvas);
controlMenu.add("Fill with Green").addActionListener(canvas);
controlMenu.add("Fill with Blue").addActionListener(canvas);
controlMenu.add("Fill with Cyan").addActionListener(canvas);
controlMenu.add("Fill with Magenta").addActionListener(canvas);
controlMenu.add("Fill with Yellow").addActionListener(canvas);
controlMenu.add("Fill with White").addActionListener(canvas);
controlMenu.add("Fill with Custom").addActionListener(canvas);
controlMenu.addSeparator();
JMenuItem customItem = new JMenuItem("Set Custom Color...");
customItem.addActionListener(canvas);
customItem.setAccelerator( KeyStroke.getKeyStroke("ctrl T") );
controlMenu.add(customItem);
JMenuItem clearItem = new JMenuItem("Clear");
clearItem.addActionListener(canvas);
clearItem.setAccelerator( KeyStroke.getKeyStroke("ctrl K") );
controlMenu.add(clearItem);
JMenuItem undoItem = new JMenuItem("Undo");
undoItem.addActionListener(canvas);
undoItem.setAccelerator( KeyStroke.getKeyStroke("ctrl Z") );
controlMenu.add(undoItem);
JMenuItem quitItem = new JMenuItem("Quit");
quitItem.setAccelerator( KeyStroke.getKeyStroke("ctrl Q") );
quitItem.addActionListener(canvas);
controlMenu.add(quitItem);
// Set up the "Color" menu, with all the items in a button group.
ButtonGroup colorGroup = new ButtonGroup();
black = new JRadioButtonMenuItem("Black");
colorGroup.add(black);
colorMenu.add(black);
red = new JRadioButtonMenuItem("Red");
colorGroup.add(red);
colorMenu.add(red);
green = new JRadioButtonMenuItem("Green");
colorGroup.add(green);
colorMenu.add(green);
blue = new JRadioButtonMenuItem("Blue");
colorGroup.add(blue);
colorMenu.add(blue);
cyan = new JRadioButtonMenuItem("Cyan");
colorGroup.add(cyan);
colorMenu.add(cyan);
magenta = new JRadioButtonMenuItem("Magenta");
colorGroup.add(magenta);
colorMenu.add(magenta);
yellow = new JRadioButtonMenuItem("Yellow");
colorGroup.add(yellow);
colorMenu.add(yellow);
white = new JRadioButtonMenuItem("White");
colorGroup.add(white);
colorMenu.add(white);
custom = new JRadioButtonMenuItem("Custom Color");
colorGroup.add(custom);
colorMenu.add(custom);
black.setSelected(true);
// Set up the "Shape" menu.
ButtonGroup shapeGroup = new ButtonGroup();
curve = new JRadioButtonMenuItem("Curve");
shapeGroup.add(curve);
shapeMenu.add(curve);
straightLine = new JRadioButtonMenuItem("Straight Line");
shapeGroup.add(straightLine);
shapeMenu.add(straightLine);
rectangle = new JRadioButtonMenuItem("Rectangle");
shapeGroup.add(rectangle);
shapeMenu.add(rectangle);
oval = new JRadioButtonMenuItem("Oval");
shapeGroup.add(oval);
shapeMenu.add(oval);
roundRect = new JRadioButtonMenuItem("RoundRect");
shapeGroup.add(roundRect);
shapeMenu.add(roundRect);
filledRectangle = new JRadioButtonMenuItem("Filled Rectangle");
shapeGroup.add(filledRectangle);
shapeMenu.add(filledRectangle);
filledOval = new JRadioButtonMenuItem("Filled Oval");
shapeGroup.add(filledOval);
shapeMenu.add(filledOval);
filledRoundRect = new JRadioButtonMenuItem("Filled RoundRect");
shapeGroup.add(filledRoundRect);
shapeMenu.add(filledRoundRect);
curve.setSelected(true);
// Set up the "Symmetry" menu.
ButtonGroup symmetryGroup = new ButtonGroup();
noSymmetry = new JRadioButtonMenuItem("None");
noSymmetry.setAccelerator( KeyStroke.getKeyStroke("ctrl 0") );
symmetryGroup.add(noSymmetry);
symmetryMenu.add(noSymmetry);
twoWay = new JRadioButtonMenuItem("Two-way");
twoWay.setAccelerator( KeyStroke.getKeyStroke("ctrl 2") );
symmetryGroup.add(twoWay);
symmetryMenu.add(twoWay);
fourWay = new JRadioButtonMenuItem("Four-way");
fourWay.setAccelerator( KeyStroke.getKeyStroke("ctrl 4") );
symmetryGroup.add(fourWay);
symmetryMenu.add(fourWay);
eightWay = new JRadioButtonMenuItem("Eight-way");
eightWay.setAccelerator( KeyStroke.getKeyStroke("ctrl 8") );
symmetryGroup.add(eightWay);
symmetryMenu.add(eightWay);
noSymmetry.setSelected(true);
// Set size, etc., of frame and make it visible.
pack();
setLocation(75,50);
setResizable(false);
setDefaultCloseOperation(EXIT_ON_CLOSE);
show();
} // end constructor
private class Display extends JPanel
implements MouseListener, MouseMotionListener, ActionListener {
// Nested class Display represents the drawing surface of the
// applet. It lets the user use the mouse to draw colored curves
// and shapes. The current color is specified by the pop-up menu
// colorChoice. The current shape is specified by another pop-up menu,
// figureChoice. (These are instance variables in the main class.)
// The panel also listens for action events from buttons
// named "Clear" and "Set Background". The "Clear" button fills
// the panel with the current background color. The "Set Background"
// button sets the background color to the current drawing color and
// then clears. These buttons are set up in the main class.
private final static int
CURVE = 0,
LINE = 1,
RECT = 2, // Some constants that code
OVAL = 3, // for the different types of
ROUNDRECT = 4, // figure the program can draw.
FILLED_RECT = 5,
FILLED_OVAL = 6,
FILLED_ROUNDRECT = 7;
private final static int
NO_SYMMETRY = 0, // Some constants that code for
SYMMETRY_2 = 1, // the different symmetry styles.
SYMMETRY_4 = 2,
SYMMETRY_8 = 3;
Color customColor = Color.gray; // The custom color that is used
// when the user selects "Custom Color"
// as the drawing color or "Fill with Custom"
// from the "Control" menu. This color
// is changed when the user selects the
// "Set Custom Color..." command.
/* Some variables used for backing up the contents of the panel. */
Image OSI; // The off-screen image (created in checkOSI()).
int widthOfOSI, heightOfOSI; // Current width and height of OSI. These
// are checked against the size of the applet,
// to detect any change in the panel's size.
// If the size has changed, a new OSI is created.
// The picture in the off-screen image is lost
// when that happens.
Image undoBuffer; // An off-screen image that is used to implement
// the undo operation. When the user begins
// a drawing operation, the OSI is copied to
// undoBuffer. If the user selects the "Undo"
// command, the OSI and the undoBuffer are swapped
// and the panel is repainted to show the previous image.
/* The following variables are used when the user is sketching a
curve while dragging a mouse. */
private int mouseX, mouseY; // The location of the mouse.
private int prevX, prevY; // The previous location of the mouse.
private int startX, startY; // The starting position of the mouse.
// (Not used for drawing curves.)
private boolean dragging; // This is set to true when the user is drawing.
private int figure; // What type of figure is being drawn. This is
// specified by the figureChoice menu.
private int symmetry; // What type of symmetry style is being used. This is
// specified by the symmetryChoice menu.
private Graphics dragGraphics; // A graphics context for the off-screen image,
// to be used while a drag is in progress.
private Color dragColor; // The color that is used for the figure that is
// being drawn.
Display() {
// Constructor. When this component is first created, it is set to
// listen for mouse events and mouse motion events from
// itself. The initial background color is white.
addMouseListener(this);
addMouseMotionListener(this);
setBackground(Color.white);
setPreferredSize( new Dimension(450,450) );
}
private Color getSelectedColor() {
// Check the "Color" menu and return the color
// that is currently selected.
if (black.isSelected())
return Color.black;
else if (red.isSelected())
return Color.red;
else if (green.isSelected())
return Color.green;
else if (blue.isSelected())
return Color.blue;
else if (cyan.isSelected())
return Color.cyan;
else if (magenta.isSelected())
return Color.magenta;
else if (yellow.isSelected())
return Color.yellow;
else if (white.isSelected())
return Color.white;
else
return customColor;
}
private int getSelectedShape() {
// Check the "Shape" menu and return the code
// for the shape that is currently selected.
if (curve.isSelected())
return CURVE;
else if (straightLine.isSelected())
return LINE;
else if (rectangle.isSelected())
return RECT;
else if (oval.isSelected())
return OVAL;
else if (roundRect.isSelected())
return ROUNDRECT;
else if (filledRectangle.isSelected())
return FILLED_RECT;
else if (filledOval.isSelected())
return FILLED_OVAL;
else
return FILLED_ROUNDRECT;
}
private int getSelectedSymmetry() {
// Check the "Symmetry" menu and return the code
// for the type of symmetry that is currently selected.
if (noSymmetry.isSelected())
return NO_SYMMETRY;
else if (twoWay.isSelected())
return SYMMETRY_2;
else if (fourWay.isSelected())
return SYMMETRY_4;
else
return SYMMETRY_8;
}
private void drawFigure(Graphics g, int shape, int x1, int y1, int x2, int y2) {
// This method is called to do ALL drawing in this applet!
// Draws a shape in the graphics context g.
// The shape parameter tells what kind of shape to draw. This
// can be LINE, RECT, OVAL, ROUNTRECT, FILLED_RECT,
// FILLED_OVAL, or FILLED_ROUNDRECT. (Note that a CURVE is
// drawn by drawing multiple LINES, so the shape parameter is
// never equal to CURVE.) For a LINE, a line is drawn from
// the point (x1,y1) to (x2,y2). For other shapes, the
// points (x1,y1) and (x2,y2) give two corners of the shape
// (or of a rectangle that contains the shape).
if (shape == LINE) {
// For a line, just draw the line between the two points.
g.drawLine(x1,y1,x2,y2);
return;
}
int x, y; // Top left corner of rectangle that contains the figure.
int w, h; // Width and height of rectangle that contains the figure.
if (x1 >= x2) { // x2 is left edge
x = x2;
w = x1 - x2;
}
else { // x1 is left edge
x = x1;
w = x2 - x1;
}
if (y1 >= y2) { // y2 is top edge
y = y2;
h = y1 - y2;
}
else { // y1 is top edge.
y = y1;
h = y2 - y1;
}
switch (shape) { // Draw the appropriate figure.
case RECT:
g.drawRect(x, y, w, h);
break;
case OVAL:
g.drawOval(x, y, w, h);
break;
case ROUNDRECT:
g.drawRoundRect(x, y, w, h, 20, 20);
break;
case FILLED_RECT:
g.fillRect(x, y, w, h);
break;
case FILLED_OVAL:
g.fillOval(x, y, w, h);
break;
case FILLED_ROUNDRECT:
g.fillRoundRect(x, y, w, h, 20, 20);
break;
}
}
private void putMultiFigure(Graphics g, int shape, int x1, int y1, int x2, int y2) {
// Draws the shape and possibly some of its reflections.
// The reflections that are drawn depend on the selected
// item in symmetryChoice. The shapes are drawn by calling
// the drawFigure method.
int width = getWidth();
int height = getHeight();
drawFigure(g,shape,x1,y1,x2,y2); // Draw the basic figure
if (symmetry >= SYMMETRY_2) { // Draw the horizontal reflection.
drawFigure(g, shape, width - x1, y1, width - x2, y2);
}
if (symmetry >= SYMMETRY_4) { // Draw the two vertical reflections.
drawFigure(g, shape, x1, height - y1, x2, height - y2);
drawFigure(g, shape, width - x1, height - y1, width - x2, height - y2);
}
if (symmetry == SYMMETRY_8) { // Draw the four diagonal reflections.
int a1 = (int)( ((double)y1 / height) * width );
int b1 = (int)( ((double)x1 / width) * height );
int a2 = (int)( ((double)y2 / height) * width );
int b2 = (int)( ((double)x2 / width) * height );
drawFigure(g, shape, a1, b1, a2, b2);
drawFigure(g, shape, width - a1, b1, width - a2, b2);
drawFigure(g, shape, a1, height - b1, a2, height - b2);
drawFigure(g, shape, width - a1, height - b1, width - a2, height - b2);
}
}
private void repaintRect(int x1, int y1, int x2, int y2) {
// Call repaint on a rectangle that contains the points (x1,y1)
// and (x2,y2). (Add a 1-pixel border along right and bottom
// edges to allow for the pen overhang when drawing a line.)
int x, y; // top left corner of rectangle that contains the figure
int w, h; // width and height of rectangle that contains the figure
if (x2 >= x1) { // x1 is left edge
x = x1;
w = x2 - x1;
}
else { // x2 is left edge
x = x2;
w = x1 - x2;
}
if (y2 >= y1) { // y1 is top edge
y = y1;
h = y2 - y1;
}
else { // y2 is top edge.
y = y2;
h = y1 - y2;
}
repaint(x,y,w+1,h+1);
}
private void repaintMultiRect(int x1, int y1, int x2, int y2) {
// Call repaint on a rectangle that contains the points (x1,y1)
// and (x2,y2). Also call repaint on reflections of this
// rectangle, depending on the type of symmetry. The
// rects are repainted by calling repaintRect().
int width = getWidth();
int height = getHeight();
repaintRect(x1,y1,x2,y2); // repaint the original rect
if (symmetry >= SYMMETRY_2) { // repaint the horizontal reflection.
repaintRect(width - x1, y1, width - x2, y2);
}
if (symmetry >= SYMMETRY_4) { // repaint the two vertical reflections.
repaintRect(x1, height - y1, x2, height - y2);
repaintRect(width - x1, height - y1, width - x2, height - y2);
}
if (symmetry == SYMMETRY_8) { // repaint the four diagonal reflections.
int a1 = (int)( ((double)y1 / height) * width );
int b1 = (int)( ((double)x1 / width) * height );
int a2 = (int)( ((double)y2 / height) * width );
int b2 = (int)( ((double)x2 / width) * height );
repaintRect(a1, b1, a2, b2);
repaintRect(width - a1, b1, width - a2, b2);
repaintRect(a1, height - b1, a2, height - b2);
repaintRect(width - a1, height - b1, width - a2, height - b2);
}
}
private void checkOSI() {
// This method is responsible for creating the off-screen image.
// It should be called before using the OSI. It will make a new OSI if
// the size of the panel changes.
if (OSI == null || widthOfOSI != getSize().width || heightOfOSI != getSize().height) {
// Create the OSI, or make a new one if panel size has changed.
OSI = null; // (If OSI already exists, this frees up the memory.)
undoBuffer = null; // (Free memory.)
widthOfOSI = getWidth();
heightOfOSI = getHeight();
OSI = createImage(widthOfOSI,heightOfOSI);
Graphics OSG = OSI.getGraphics(); // Graphics context for drawing to OSI.
OSG.setColor(getBackground());
OSG.fillRect(0, 0, widthOfOSI, heightOfOSI);
OSG.dispose();
undoBuffer = createImage(widthOfOSI,heightOfOSI);
OSG = undoBuffer.getGraphics(); // Graphics context for drawing to undoBuffer
OSG.setColor(getBackground());
OSG.fillRect(0, 0, widthOfOSI, heightOfOSI);
OSG.dispose();
}
}
public void paintComponent(Graphics g) {
// Copy the off-screen image to the screen,
// after checking to make sure it exists. Then,
// if a shape other than CURVE is being drawn,
// draw it on top of the image from the OSI.
checkOSI();
g.drawImage(OSI, 0, 0, this);
if (dragging && figure != CURVE) {
g.setColor(dragColor);
putMultiFigure(g,figure,startX,startY,mouseX,mouseY);
}
}
public void actionPerformed(ActionEvent evt) {
// Respond when the user selects an item from the "Control" menu.
String command = evt.getActionCommand();
checkOSI();
if (command.equals("Fill with Black"))
clear(Color.black);
else if (command.equals("Fill with Red"))
clear(Color.red);
else if (command.equals("Fill with Green"))
clear(Color.green);
else if (command.equals("Fill with Blue"))
clear(Color.blue);
else if (command.equals("Fill with Cyan"))
clear(Color.cyan);
else if (command.equals("Fill with Magenta"))
clear(Color.magenta);
else if (command.equals("Fill with Yellow"))
clear(Color.yellow);
else if (command.equals("Fill with White"))
clear(Color.white);
else if (command.equals("Fill with Custom"))
clear(customColor);
else if (command.equals("Set Custom Color...")) {
Color c = JColorChooser.showDialog(this,"Select Custom Color",customColor);
if (c != null) {
// Change the custom color and select it for use as
// the drawing color.
customColor = c;
custom.setSelected(true);
}
}
else if (command.equals("Clear")) {
// Clear to current background color.
Graphics g = OSI.getGraphics();
g.setColor(getBackground());
g.fillRect(0,0,getSize().width,getSize().height);
g.dispose();
repaint();
}
else if (command.equals("Undo")) {
// Undo the most recent drawing operation
// by swapping OSI with undoBuffer.
Image temp = OSI;
OSI = undoBuffer;
undoBuffer = temp;
repaint();
}
else if (command.equals("Quit")) {
// Close the window and exit. Note: The
// exit command will cause an error when
// the frame is opened from an applet.
// An applet should set the frame's standAlone
// variable to false after creating the frame.
dispose();
if (standAlone)
System.exit(0);
}
}
private void clear(Color background) {
// Fill with the specified color. If the
// color is equal to the current drawing color, then
// the current drawing color is changed, so that
// drawing operations will not be invisible.
setBackground(background);
if (background.equals(getSelectedColor())) {
if (background.equals(Color.black))
white.setSelected(true); // On a black background, draw in white.
else
black.setSelected(true); // On other backgrounds, use black.
}
Graphics g = OSI.getGraphics();
g.setColor(getBackground());
g.fillRect(0,0,getSize().width,getSize().height);
g.dispose();
repaint();
}
public void mousePressed(MouseEvent evt) {
// This is called when the user presses the mouse on the
// panel. This begins a draw operation in which the user
// sketches a curve or draws a shape. (Note that curves
// are handled differently from other shapes. For CURVE,
// a new segment of the curve is drawn each time the user
// moves the mouse. For the other shapes, a "rubber band
// cursor" is used. That is, the figure is drawn between
// the starting point and the current mouse location.)
if (dragging == true) // Ignore mouse presses that occur
return; // when user is already drawing a curve.
// (This can happen if the user presses
// two mouse buttons at the same time.)
prevX = startX = evt.getX(); // Save mouse coordinates.
prevY = startY = evt.getY();
figure = getSelectedShape(); // Get data from menus for drawing.
symmetry = getSelectedSymmetry();
dragColor = getSelectedColor();
checkOSI();
Graphics undoGraphics = undoBuffer.getGraphics();
undoGraphics.drawImage(OSI,0,0,null); // Remember the current image,
// for "Undo" operations,
// before changing the image.
undoGraphics.dispose();
dragGraphics = OSI.getGraphics();
dragGraphics.setColor(dragColor);
dragging = true; // Start drawing.
} // end mousePressed()
public void mouseReleased(MouseEvent evt) {
// Called whenever the user releases the mouse button.
// If the user was drawing a shape, we make the shape
// permanent by drawing it to the off-screen image.
if (dragging == false)
return; // Nothing to do because the user isn't drawing.
dragging = false;
mouseX = evt.getX();
mouseY = evt.getY();
if (figure == CURVE) {
// A CURVE is drawn as a series of LINEs
putMultiFigure(dragGraphics,LINE,prevX,prevY,mouseX,mouseY);
repaintMultiRect(prevX,prevY,mouseX,mouseY);
}
else if (figure == LINE) {
repaintMultiRect(startX,startY,prevX,prevY);
if (mouseX != startX || mouseY != startY) {
// Draw the line only if it has non-zero length.
putMultiFigure(dragGraphics,figure,startX,startY,mouseX,mouseY);
repaintMultiRect(startX,startY,mouseX,mouseY);
}
}
else {
repaintMultiRect(startX,startY,prevX,prevY);
if (mouseX != startX && mouseY != startY) {
// Draw the shape only if both its height
// and width are both non-zero.
putMultiFigure(dragGraphics,figure,startX,startY,mouseX,mouseY);
repaintMultiRect(startX,startY,mouseX,mouseY);
}
}
dragGraphics.dispose();
dragGraphics = null;
}
public void mouseDragged(MouseEvent evt) {
// Called whenever the user moves the mouse while a mouse button
// is down. If the user is drawing a curve, draw a segment of
// the curve on the off-screen image, and repaint the part
// of the panel that contains the new line segment. Otherwise,
// just call repaint and let paintComponent() draw the shape on
// top of the picture in the off-screen image.
if (dragging == false)
return; // Nothing to do because the user isn't drawing.
mouseX = evt.getX(); // x-coordinate of mouse.
mouseY = evt.getY(); // y=coordinate of mouse.
if (figure == CURVE) {
// A CURVE is drawn as a series of LINEs.
putMultiFigure(dragGraphics,LINE,prevX,prevY,mouseX,mouseY);
repaintMultiRect(prevX,prevY,mouseX,mouseY);
}
else {
// Repaint two rectangles: The one that contains the previous
// version of the figure, and the one that will contain the
// new version. The first repaint is necessary to restore
// the picture from the off-screen image in that rectangle.
repaintMultiRect(startX,startY,prevX,prevY);
repaintMultiRect(startX,startY,mouseX,mouseY);
}
prevX = mouseX; // Save coords for the next call to mouseDragged or mouseReleased.
prevY = mouseY;
} // end mouseDragged.
public void mouseEntered(MouseEvent evt) { } // Some empty routines.
public void mouseExited(MouseEvent evt) { } // (Required by the MouseListener
public void mouseClicked(MouseEvent evt) { } // and MouseMotionListener
public void mouseMoved(MouseEvent evt) { } // interfaces).
} // end nested class Display
} // end class KaleidaPaint
[ Exercises
| Chapter Index
| Main Index
]