Chapter 15. Graphics and Events

CONTENTS
  •  A Quick Graphics Tour
  •  A Quick Tour of Common Events
  •  Putting Things Together: A Drawing Program
  •  The Complete Shapes and DrawShapes Modules
  •  Summary

Terms in This Chapter

  • Coupling

  • Encapsulation

  • Event-driven programming

  • Event model

  • Graphics object

  • Hit testing

  • Input focus

  • Modularity

  • Polyline

  • Scaffolding code

  • Static versus interactive mode

  • Unit level test

In this chapter, we'll learn the basics of events and graphics. These are the things you'll need to know when you write your own graphical components. In particular, you'll need to understand the event model and some rudimentary graphics programming. In a later chapter, we'll see how to package your component in a Java bean so you can distribute it to the unsuspecting world. We're just scratching the surface of graphics in this book.

A Quick Graphics Tour

Every AWT component has a method called getGraphics(), which returns the instance of the graphics object associated with it. The graphics object allows you to draw on the component. With the java.awt.Graphics object you can draw arcs, images, lines, ovals, polylines, and such. We can't cover all of the possibilities, only enough to whet your appetite. Later we'll build a drawing package that works with text, rectangles, circles, and ovals. For now, let's do an interactive session that introduces drawing on a component.

Import a frame, and create an instance of it. We'll use it to do our drawing.

>>> from javax.swing import JFrame, JPanel >>> frame = JFrame("Drawing", size=(400,400), visible=1) >>> graphics = frame.contentPane.graphics

Get the graphics instance from the frame, and draw a line, but first make sure that no other window is obscuring the frame. You may have to reduce the size of the window that contains the Jython prompt for this.

Draw a line with starting coordinates of 50,50 and ending coordinates of 200,200 (in pixels).

>>> graphics = frame.getGraphics()() >>> graphics.drawLine(50, 50, 200,200)

Draw a circle (actually an oval with equal width and height).

>>> graphics.drawOval(50,50,300,300)

To put text on the frame, pass the starting coordinates and the string you want to draw to the graphic's drawString() method. The string will be in the component's current font. Draw a string with starting coordinates of 50,50.

>>> graphics.drawString("Hello World", 50,50)

Draw a string with starting coordinates of 75,75.

>>> graphics.drawString("I am late again", 75,75)

Draw a string with starting coordinates of 200,200.

>>> graphics.drawString("Hello Cruel World", 200, 200)

We can draw filled shapes as well as outlines. We can also change color by setting the graphics color property. Draw an oval.

>>> graphics.fillOval(150,150, 50, 100)

Draw a blue oval.

>>> from java.awt import Color >>> graphics.color = Color.blue >>> graphics.fillOval(150,150, 100, 50)

Dispose of the graphics resource. You must always do this when you're done with your drawing.

>>> graphics.dispose()

Your frame should look like Figure 15-1. Notice that, if you resize or obscure the frame with another window, the image is erased. This simply won't do. We need something a little less transitive, which gets us back to event-driven programming. Enter the paint() method, which will tell us when the window needs to be redrawn so that we can take the necessary action.

Figure 15-1. Outlined and Filled Shapes

graphics/15fig01.gif

A component (window, frame, panel, etc.) is notified that its window needs to be redrawn via the paint() method. paint() is passed a Graphics class instance that refers to the current component. With paint(), you don't dispose of this instance when you're done.

Here's an example of the paint() method (class DrawPicture from draw2.py) showing how it redraws the image we created in our first session. This time we'll be able to resize and obscure the window, and it will always redraw the picture properly.

from javax.swing import JFrame from java.awt import Color class DrawPicture(JFrame):        def __init__(self):              JFrame.__init__(self,"Drawing", size=(400,400), visible=1)              def closing(event):                     from java.lang import System                     System.exit(0)              self.windowClosing = closing        def paint(self,graphics):                     #Draw a line and a circle.              graphics.drawLine(50, 50, 200,200)              graphics.drawOval(50,50,300,300)                     #Draw some strings              graphics.drawString("Hello World", 50,50)              graphics.drawString("I am late again", 75,75)              graphics.drawString("Hello Cruel World", 200, 200)                     #Draw an Oval              graphics.fillOval(150,150, 50, 100)                     #Draw a Blue Oval             graphics.color = Color.blue             graphics.fillOval(150, 150, 100, 50) if __name__ == '__main__':        d = DrawPicture()

The frame should now look like Figure 15-2. Notice that you can resize it and obscure it with other windows. Every time you return to the picture, it will be just like it was when you left it.

Figure 15-2. Figure 15-1 Redrawn with the paint() Method

graphics/15fig02.gif

That was a painless introduction to graphics programming. Look up java.awt.Graphics in the Java API documentation; you'll see that we've covered only a fraction of what you can do with it. You may also want to learn about the 2D and 3D APIs.

A Quick Tour of Common Events

Table 15-1 lists common events and their corresponding event properties. Table 15-2 lists common events and the first component in the class hierarchy to which each event maps. The first thing you may notice in Table 15-2 is that some components are missing, such as JFrame and, for that matter, Frame. Remember, though, that JFrame is a subclass of Frame, and that Frame is a subclass of Window. Thus, JFrame publishes window events through the Window's interface, WindowListener.

Table 15-1. Common Graphics Events and Their Event Properties
Event Listener Interface Property
ActionEvent ActionListener actionPerformed
AdjustmentEvent AdjustmentListener adjustmentValueChanged
ComponentEvent ComponentListener

componentHidden

componentMoved

componentResized

componentShown

ContainerEvent ContainerListener

componentAdded

componentRemoved

FocusEvent FocusListener

focusGained

focusLost

ItemEvent ItemListener itemStateChanged
KeyEvent KeyListener

keyPressed

keyReleased

keyTyped

MouseEvent MouseListener

mouseClicked

mouseEntered

mouseExited

mousePressed

mouseReleased

MouseEvent MouseMotionListener

mouseDragged

mouseMoved

TextEvent TextListener textValueChanged
WindowEvent WindowListener

windowActivated

windowClosed

windowClosing

windowDeactivated

windowDeiconified

windowIconified

windowOpened

 

Table 15-2. Common Graphic Events and Their Class Hierarchy Mappings
Component Event Published through Listener Meaning
AbstractButton ActionEvent, ActionListener User clicked button
JButton ActionEvent, ActionListener User clicked button
JCheckBox ItemEvent, ItemListener User selected or unselected checkbox
JCheckBoxMenuItem ItemEvent, ItemListener User selected or unselected CheckboxMenuItem
Component ComponentEvent, ComponentListener Component moved, resized shown, or hidden
  FocusEvent, FocusListener Component lost or got focus
  MouseEvent, MouseListener User clicked, pressed, and released mouse, and mouse cursor entered or exited component
  MouseEvent, MouseMotionListener User moved mouse in component, and dragged mouse, i.e., pressed button while moving mouse cursor
Container ContainerEvent, ContainerListener Component added or removed
JComboBox ActionEvent, ActionListener Item double-clicked
  ItemEvent, ItemListener Item selected or unselected
JList ListSelectionEvent, ListSelectionListener Item double-clicked
  ItemEvent, ItemListener Item selected or unselected
JMenuItem ActionEvent, ActionListener Menu item selected
JScrollBar AdjustmentEvent, AdjustmentListener User moved scrollbar
JTextField ActionEvent, ActionListener User pressed Enter in text box
Window WindowEvent, WindowListener Window was opened, closed, iconified, restored, or a close was requested

It doesn't stop there. Window is a subclass of Container, so JFrame publishes container events through the ContainerListener interface as well. And, like other components, JFrame is a subclass of Component, so it publishes component events through the ComponentListener interface, focus events through the FocusListener interface, and mouse events through the MouseListener and MouseMotionListener interfaces (Container is a direct subclass of Component). The point is that a component doesn't just publish the events it defines; it publishes all events from its hierarchy of superclasses.

Another example of this is JButton, which is a subclass of AbstractButton, which is a subclass of JComponent, which is a subclass of Container, which is a subclass of Component. As such, it publishes all of the events that its superclasses define.

An Event Frame

Let's create a frame that handles every possible event that is, essentially, one frame that handles every event mentioned in the last section that Frame supports. We'll use the Frame class in java.awt (from which Swing gets much of its behavior and design). Examine the source code closely (it's from FrameEvents.py). Later we'll break it down.

from java.awt import Frame, List, Panel, Button, FlowLayout, BorderLayout class EventFrame(Frame):       def __init__(self, event_viewer):             Frame.__init__(self, "Event Frame")             self.event_viewer = event_viewer             self.layout = FlowLayout()                   #ComponentEvent publishes to ComponentListener             self.componentHidden = self.__handle_event             self.componentMoved = self.__handle_event             self.componentResized = self.__handle_event             self.componentShown = self.__handle_event                   #ContainerEvent publishes to ContainerListener             self.componentAdded = self.__handle_event             self.componentRemoved = self.__handle_event                   #FocusEvent publishes to FocusListener             self.focusGained = self.__handle_event             self.focusLost = self.__handle_event                   #KeyEvent publishes to KeyListener             self.keyPressed = self.__handle_event             self.keyReleased = self.__handle_event             self.keyTyped = self.__handle_event                   #MouseEvent publishes to MouseListener             self.mouseClicked = self.__handle_event             self.mouseEntered = self.__handle_event             self.mouseExited = self.__handle_event             self.mousePressed = self.__handle_event             self.mouseReleased = self.__handle_event                   # MouseEvent publishes to MouseMotionListener             self.mouseDragged = self.__handle_event             self.mouseMoved = self.__handle_event                   # WindowEvent publishes to WindowListener             self.windowActivated = self.__handle_event             self.windowClosed = self.__handle_event             self.windowClosing = self.__handle_event             self.windowDeactivated = self.__handle_event             self.windowDeiconified = self.__handle_event             self.windowIconified = self.__handle_event             self.windowOpened = self.__handle_event                self.size=(100,100)             self.visible=1             #handles all of the events in this demo       def __handle_event(self, event):             self.event_viewer.addEvent(event) . . . . . . class EventViewer(Frame):       def __init__(self, close):             Frame.__init__(self,"Event Viewer")                   # Create a list to display events             self.__list = List()             self.add(self.__list, BorderLayout.CENTER)                   # Set up toolbar to add and remove components from frame             add = Button("Add")             add.actionPerformed = self.__handle_add             remove = Button("Remove")             remove.actionPerformed = self.__handle_remove             tool_pane = Panel()             tool_pane.add(add)             tool_pane.add(remove)             self.add(tool_pane, BorderLayout.NORTH)             self.size=(500,400)             self.visible=1             self.location=(300,300)             . . .       def addEvent(self, event):             self.__list.add(`event`,0)             #add a component to the frame       def __handle_add(self, event):             global frame             frame.add(Button(`frame.componentCount`))             frame.invalidate();frame.validate()             #removes a component from the frame       def __handle_remove(self, event):             if (frame.componentCount > 0):                   frame.remove(frame.componentCount-1)                   frame.invalidate();frame.validate() if __name__ == '__main__':       viewer = EventViewer(close)       frame = EventFrame(viewer)

Window Events with EventFrame

The code for our example is pretty simple. In the constructor of the EventFrame class, we register for every event that Frame publishes. All of the events are registered to the same event handler method, __handle_event().

       #handles all of the events in this demo def __handle_event(self, event):        self.event_viewer.addEvent(event)

The event handler calls self.event_viewer.addEvent, which adds the event to a listbox so we can view it. addEvent() forces the event to the top of the list, using back quotes to get its string representation.

def addEvent(self, event):        self.__list.add(`event`,0)

Now let's use EventFrame to examine all of the events that the frame handles:

  • windowActivated

  • windowClosed

  • windowClosing

  • windowDeactivated

  • windowDeiconified

  • windowIconified

  • windowOpened

To use WindowActivated, all we need to do is start EventFrames.py. When the window comes up, we get the following events:

  • java.awt.event.WindowEvent[WINDOW_ACTIVATED] on frame0

  • java.awt.event.FocusEvent[FOCUS_GAINED,permanent] on frame0

  • java.awt.event.ComponentEvent[COMPONENT_SHOWN] on frame0

  • java.awt.event.WindowEvent[WINDOW_OPENED] on frame0

WindowEvent, with its ID set to WINDOW_ACTIVATED, corresponds to the windowActivated event property of the frame. This pattern repeats itself for all event properties and event IDs. Notice that, in addition to windowOpened and windowActivated, Frame publishes focusGained and componentShown.

When the focus changes to another window, you get the following windowDeactivated events. Click on the caption of the event viewer.

  • java.awt.event.WindowEvent[WINDOW_DEACTIVATED] on frame0

  • java.awt.event.FocusEvent[FOCUS_LOST,temporary] on frame0

To get a windowIconified event, we have to minimize the window by clicking the Minimize button on the right side of the window's caption. The following events result:

  • java.awt.event.FocusEvent[FOCUS_LOST,temporary] on frame0

  • java.awt.event.WindowEvent[WINDOW_ICONIFIED] on frame0

  • java.awt.event.WindowEvent[WINDOW_DEACTIVATED] on frame0

Now we want to restore the frame, so we go to the taskbar and click the Frames icon. These are the events we get:

  • java.awt.event.WindowEvent[WINDOW_ACTIVATED] on frame0

  • java.awt.event.WindowEvent[WINDOW_DEICONIFIED] on frame0

  • java.awt.event.FocusEvent[FOCUS_GAINED,permanent] on frame0

Then we click the window's Close button to get the windowClosing event:

  • java.awt.event.WindowEvent[WINDOW_CLOSING] on frame0

The event is a request to close, not the actual closing. For that we have to restart the application in the interactive interpreter and type

>>> frame.visible = 0 java.awt.event.ComponentEvent[COMPONENT_HIDDEN] on frame0 >>> frame.dispose() java.awt.event.WindowEvent[WINDOW_CLOSED] on frame0

Hiding the frame doesn't close it. The dispose() function does that.

Container Events with EventFrame

On the event viewer window are the Add and Remove buttons, which add components to and remove them from the frame. If you hit Add twice and Remove twice, you'll get the following events:

  • java.awt.event.ContainerEvent[COMPONENT_ADDED,child=button0] on frame0

  • java.awt.event.ContainerEvent[COMPONENT_ADDED,child=button1] on frame0

  • java.awt.event.ContainerEvent[COMPONENT_REMOVED,child=button1] on frame0

  • java.awt.event.ContainerEvent[COMPONENT_REMOVED,child=button0] on frame0

Key Events with EventFrame

If you make the frame the active window and press the A key, you should get the following events:

  • java.awt.event.KeyEvent[KEY_PRESSED,keyCode=65,keyChar='a'] on frame0

  • java.awt.event.KeyEvent[KEY_TYPED,keyCode=0,keyChar='a'] on frame0

  • java.awt.event.KeyEvent[KEY_RELEASED,keyCode=65,keyChar='a'] on frame0

If you hold the key down, you'll get keyPressed and keyTyped, but you won't get keyReleased.

As an exercise, look up KeyListener in the Java API documentation. Note the difference between keyPressed and keyTyped. If you want to create a typing program, which event do you handle for capturing alphanumeric characters? Which one for handling special keys like Enter and Backspace? You'll need to know this later.

Mouse Events from FrameEvent

If you click in the FrameEvent frame, you'll get the mousePressed, mouseClicked, and mouseReleased mouse events. If you hold the mouse button down and drag it, you'll get the mouseDragged event. If you move the mouse pointer anywhere in the work area of the frame (or any place but the caption [titlebar] and system menus), you'll get the mouseMoved event. Table 15-3 lists the mouse events.

Try these exercises:

  • Click on the frame's work area with both the right and the left buttons. What's the difference in the output?

  • This sample application was written in AWT components, which are similar to Swing components. Convert to JFC/Swing. (The component events must be set up with JFrame's contentPane.)

Table 15-3. Mouse Events and Event Properties
Event Listener Interface Property
MouseEvent MouseListener

mouseClicked

mouseEntered

mouseExited

mousePressed

mouseReleased

MouseEvent mouseMotionListener

mouseDragged

mouseMoved

 

To AWT or Not to AWT

Many of the concepts in Swing and AWT are very similar. Most of the examples in this book are written in Swing because that's the dominant choice for the Java Standard Edition. However, for the Java Micro edition (Windows CE and handheld devices) AWT is still the best. That's why I used it for some of the examples.

Putting Things Together: A Drawing Program

Although not as ubiquitous as the famous "Hello World", the sample application explained in the following sections is quite common. We want it to draw ovals, rectangles, circles, and so forth. I've opted for simplicity rather than perfection in my design. Later I'll point out what's wrong with it.

We'll incorporate ideas from many chapters in our program, which we'll develop in two modules and three phases. The first phase is a simple application that draws ovals and rectangles. The second phase will add circles, squares, rounded rectangles, and text. (I know text isn't a shape, but I don't like the correct terminology, "glyph.") The third phase will correct a serious design flaw introduced in the first and second phases.

The first module, Shapes, contains all of the graphics. The second module, DrawShapes, defines the user interface, that is, the event handling and the component container layout.

Phase 1: Shapes

The Shapes module contains four main classes: Shape, Rectangle, Oval, and Shapes. Shape is the superclass of the others; it defines the interface that all shape classes use so they all can be treated polymorphically (one can be replaced with another). Rectangle inherits most of its functionality from Shape, as does Oval. Shapes represents a collection of shape objects and implements the Shape interface.

The Shape class defines three methods:

  • The constructor stores shape parameters like x, y position, dimensions (width, height), and color

  • The paint() method paints a shape using the given graphics context

  • The draw_filled() method draws a filled shape

  • The draw_outline() method draws the outline of a shape

The constructor stores five values: x,y position, width, height, color, and fill/nofill. Here's some of its code. (All of the following examples are from One\Shape.py.)

class Shape:        def __init__(self, x, y, width=0, height=0, color=None, fill=0):              self.x=x              self.y=y              self.width=width              self.height=height              self.color=color              self.fill=fill

paint() paints on the given graphics context and so takes a graphics object as an argument. It also checks to see if a color was assigned; if so, it sets the graphics color property to the shape color property and saves the original color to be restored later. If the fill attribute is true, paint()calls the draw_filled() method; otherwise, it calls the draw_outline() method.

paint() Code and Breakdown

Here's the code for paint():

def paint (self, graphics):       if not self.color is None:              oldcolor = graphics.color              graphics.color = self.color       if self.fill:              self.draw_filled (graphics)       else:              self.draw_outline(graphics)       if not self.color is None:              graphics.color = oldcolor

draw_filled() and draw_outline()draw the shape as filled or outlined, respectively. They're abstract, meaning that they don't do anything until they're defined by subclasses of Shape. This makes sense when you think about it for example, only the Rectangle class knows how to draw a rectangle.

Here are the two drawing methods as defined by Shape:

def draw_filled(self, graphics):        pass def draw_outline(self, graphics):        pass

The draw_outline() method gets the bounds of the shape: position (x,y) and dimensions (width, height). We'll use these to calculate the area of the window that needs to be redrawn. First, we need the getRect() function.

def getRect(self):        return self.x, self.y, self.width, self.height

Both Rectangle and Oval subclass Shape, and both implement the abstract methods draw_filled() and draw_outline(). To implement them, the Rectangle class uses the java.awt.Graphics methods fillRect() and drawRect(), respectively; the Oval class uses fillOval() and drawOval().

Here are the Rectangle and Oval classes:

class Rectangle(Shape):        def __init__(self, x, y, width, height, color=None, fill=0):               Shape.__init__(self, x, y, width, height, color, fill)        def draw_filled(self,graphics):               graphics.fillRect(self.x, self.y, self.width, self.height)        def draw_outline(self,graphics):               graphics.drawRect(self.x, self.y, self.width, self.height) class Oval(Shape):        def __init__(self, x, y, width, height, color=None, fill=0):               Shape.__init__(self, x, y, width, height, color, fill)        def draw_filled(self,graphics):               graphics.fillOval(self.x, self.y, self.width, self.height)        def draw_outline(self,graphics):               graphics.drawOval(self.x, self.y, self.width, self.height)
The Shapes Class

The Shapes class (note the plural) holds many shapes and, like Rectangle and Oval, subclasses Shape. Shapes overrides only the paint() method, which when overridden iterates through the list of shapes in the Shapes instance and calls each one of their paint() methods. Also, Shapes calls a getRect() method that calculates the union of all of the bounds of all of the shapes it contains.

Here's the Shapes class:

class Shapes (Shape):        def __init__(self):               self.__shapes=[]        def addShape(self, shape):               self.__shapes.append(shape)        def paint(self, graphics):               for shape in self.__shapes:                      shape.paint(graphics)        def getRect(self):                      # Lists to hold x,y, height and width                      # from shape in shapes               xl,yl,wl,hl = [],[],[],[]                      # Iterate through the list gathering each shapes                      # bounding rectangle               for shape in shapes:                      x,y,width, height = shape.getRect()                      xl.append(x)                      yl.append(y)                      wl.append(width)                      hl.append(height)                      # Calculate and return the bounding                      # rectangle for all of the shapes.               return min(xl), min(yl), max(wl), max(hl)
Testing

Before we start building the graphical interface, we need to create a unit level test for this module to test the classes defined in Shape. It's much easier to debug the classes in static than in interactive mode. We also need scaffolding code to test the module. (Our scaffolding code adds three shapes to the TestPanel class.)

       # This is for testing only if __name__ == '__main__':        from javax.swing import JFrame, JPanel        from java.awt import Color, Font        class TestPanel(JPanel):        def __init__(self):                   self.background=Color.white        def addShape(self,shape):                   self.shapes.addShape(shape)        def __init__(self):                   self.shapes = Shapes()                   self.addShape(Rectangle(0,0,100,100,Color.blue, 1))                   self.addShape(Rectangle(100,0,100,100,Color.blue))                   self.addShape(Oval(0,100,100,100,Color.blue,1))                   self.addShape(Oval(100,100,100,100, Color.blue))        def paint(self,graphics):                   self.shapes.paint()(graphics) frame = JFrame("Test Shapes", size=(400,440), visible=1) pane = TestPanel() frame.contentPane = pane frame.validate()

After every major change to the Shapes module, you'll want to update the scaffolding code, if necessary, and retest.

Phase 1: DrawShapes

The DrawShapes module does the actual shape drawing. It defines classes to work with mouse and key events, shows the status of operations, sets a shape's fill attribute, and sets up components that allow the user to select rectangles or ovals.

DrawShapes defines four classes:

  • ShapeButton draws a shape in a button

  • ToolBox holds components; has a preferred size

  • StatusBox displays the status of a drawing operation

  • PaintBox displays shapes the user has drawn

  • DrawShapes as the user interface frame, contains all of the above

Shapes Button

ShapeButton represents a particular shape and extends javax.swing.JButton. It's passed a Shape instance and draws the shape in its overridden paint() method. It also defines the preferred width and height for the button.

class ShapeButton(JButton):        def __init__(self, shape):               self.shape = shape        def paint(self, graphics):               JButton.paint()(self, graphics)               self.shape.paint()(graphics)        def getPreferredSize(self):               d = Dimension(30,30)               return d
ToolBox

ToolBox holds ShapeButton and extends javax.swing.Jpanel. It doesn't do much besides define a preferred size.

class ToolBox (JPanel):        def __init__(self):               JPanel.__init__(self)        def getPreferredSize(self):               d = Dimension(40, 0)               return d
StatusBox

StatusBox holds the current status of events and also extends javax.swing.JPanel. As a helper class, it displays the current status of the drawing options, such as the x,y position of the mouse pointer and the shape being drawn.

class StatusBox (JPanel):        def __init__(self):               JPanel.__init__(self)               self.coordinates = JTextField(15, editable=0)               self.format = 'x = %d, y = %d'               self.add(self.coordinates)               self.event_type = JLabel ('MOUSE STATUS ')               self.add(self.event_type)               self.shape_type = JLabel ('SHAPE STATUS ')               self.add(self.shape_type)        def setXY(self, x,y):               self.coordinates.text = self.format % (x,y,)        def setMouseDrag(self, x, y):               self.event_type.text = 'MOUSE DRAG '               self.setXY(x,y)        def setMouseMove(self, x, y):               self.event_type.text = 'MOUSE MOVE '               self.setXY(x,y)        def setMousePress(self, x, y):               self.event_type.text = 'MOUSE PRESS '               self.setXY(x,y)        def setMouseRelease(self, x, y):               self.event_type.text = 'MOUSE RELEASE '               self.setXY(x,y)        def setShapeType(self, shape_type):                     # Set the label based on the shape type               if shape_type == PaintBox.RECTANGLE:                     self.shape_type.text = 'RECTANGLE'               elif shape_type == PaintBox.OVAL:                     self.shape_type.text = 'OVAL'
PaintBox

PaintBox performs all of the graphics operations and handles the mouse events (later it will handle the key events). It's the bread and butter of this sample application, the center ring in this three-ring circus.

When the user hits the mouse button in the paint box, the shape drawing begins. As he drags the lower corner of the shape, an outline appears (this is called rubberbanding). When he lets go of the mouse, the shape is created, added to the other shapes, and redrawn when PaintBox's paint()method is called.

PaintBox extends JComponent. It can draw either rectangles or ovals and has four attributes that determine how the current shape will be drawn: color, fill, rectangle, and oval. The constructor defines these attributes and sets up the event handlers for the mouseDragged, mouseMoved, mousePressed, and mouseReleased events. It's passed an instance of StatusBox (status), which it uses to show the status of the drawing operations.

Here's the PaintBox constructor:

class PaintBox (JComponent):        RECTANGLE=1        OVAL=2        def __init__(self, status):               JComponent.__init__(self)                  self.opaque=1                  self.background=Color.white               self.status = status               self.shapes = Shapes()               self.shape_type = PaintBox.RECTANGLE               self.mouseDragged = self.handle_mouseDragged               self.mouseMoved = self.handle_mouseMoved               self.mousePressed = self.handle_mousePress               self.mouseReleased = self.handle_mouseRelease               self.fill=0               self.color=Color.red

The drawing mechanism starts off when the user clicks the mouse button, which fires the handle_mousePress method. The event handler sets the shape's starting point (start) and initializes its ending point (last) to that value.

def handle_mousePress(self, event):              # Print the status       self.status.setMousePress(event.x, event.y)              # Save the initial location.              # In addition save the initial              # location as the last.       self.last = self.start = event.point

As the user drags the mouse, she gets visual feedback via rubberbanding. When called, the event handler for mouseDragged draws an outline of the shape and sets the last point to the current point. Then it calls drawRubberShape() twice, once with the last point and once with the current point.

def handle_mouseDragged(self,event):              # Print the status.       self.status.setMouseDrag(event.x, event.y)              # Erase the old rubberband shape at the              # old location. Create the new rubberband shape              # at the new location       self.drawRubberShape(self.last.x, self.last.y)       self.drawRubberShape(event.x, event.y)              # Save the current event.x and              # event.y as the last.       self.last = event.point

drawRubberShape() is smart enough to erase the last shape drawn whenever it's called twice with the same point. It does this with the setXORMode() method of the java.awt.Graphics class instance, which sets the graphic mode to XOR. XOR sees to it that a line drawn more than once will be erased. Essentially, it ensures that the colors of the graphics are those specifed in setXORMode() for the current graphic context.

Notice that we dispose of the graphic when we're done with it, using a finally block. After drawRubberShape() sets the graphics mode, it calculates the shape's width and height and then calls the __drawShape() method, which actually draws the shape in the current mode.

def drawRubberShape(self, x, y):        g = self.graphics              # Set the graphics to XOR mode,              # which allows rubberbanding.              # Calculate the width and height.              # Draw the outline of the shape.        try:              g.setXORMode(self.background)              width = abs(self.start.x - x)              height = abs(self.start.y - y)              self.__drawShape(g, self.start.x, self.start.y, width, height)        finally:              g.dispose()

__drawShape() checks for the shape it's supposed to draw rectangle or oval. It then creates the specified shape and uses it to draw the corresponding shape, which in turn calls drawOval() or drawRect() accordingly. The x and y of __drawShape correspond to the starting point defined in the event handler for the mousePress event. The width and height are the width and height as calculated from the last or current point in the mouseDrag event handler.

def __drawShape(self, g, x, y, width, height):       if self.shape_type == PaintBox.RECTANGLE:              rect=Rectangle(self.start.x,self.start.y, width,height,fill=0)              rect.paint(g)       if self.shape_type == PaintBox.OVAL:              oval = Oval(self.start.x, self.start.y, width, height, fill=0)              oval.paint()(g)

When the user lets go of the mouse, the mouseRelease event is fired. The handler for this event is handle_mouseRelease, which calculates the width and height of the shape and then creates a corresponding rectangle or oval.

The shape's x,y coordinates are the starting point obtained during the handling of the mousePress event. Its width and height are the distance from the starting point at which the mouseRelease event handler is invoked (that is, when the user releases the mouse button). The shape is then added to the shapes attribute. The panel's repaint() method is called with the boundary of the shape so that the shape is drawn with the paint() method.

def handle_mouseRelease(self, event):              # Print the status       self.status.setMouseRelease(event.x, event.y)              # Calculate the width and the height       width = abs(self.start.x - event.x)       height = abs(self.start.y - event.y)       shape = None #to hold the shape we are about to create              # Create the shape based on the current shape type.              # Then add the shape to self.shapes.       if self.shape_type == PaintBox.RECTANGLE:              shape = Rectangle(self.start.x, self.start.y, width, height,                      self.color, self.fill)       elif self.shape_type == PaintBox.OVAL:              shape = Oval(self.start.x, self.start.y, width, height,                      self.color, self.fill)       if not shape is None:              self.shapes.addShape(shape)              x,y,width,height = shape.getRect()              self.repaint(x,y,width+1, height+1)

The paint() method calls the shapes.paint() method, which iterates through the shapes list and calls all of the shapes' paint() methods.

def paint (self, graphics):         ...         ...                #Draw all of the shapes.         self.shapes.paint(graphics)

Since the selection of shape attributes happens in another class, PaintBox exposes the following methods so that the attributes can be set by the class that contains a PaintBox instance:

  • setShapeType() sets the current shape type

  • setFill() turns the shape fill property on or off

  • setShapeColor() sets the current color of the shape to be drawn

    def setShapeType(self, shape_type):        self.status.setShapeType(shape_type)        self.shape_type = shape_type def getShapeType(self):        return shape_type def setFill(self, value):        self.fill = value def setShapeColor(self, color):        self.color = color
DrawShapes

As the parent frame for the drawing application, DrawShapes contains StatusBox, ToolBox, and PaintBox instances. It also handles all of the options for the shapes and allows the user to pick different shapes as represented by ShapeButton in the ToolBox instance.

The DrawShapes constructor creates four panels, options (an instance of Panel), toolbar (an instance of Panel), status (an instance of StatusBox), and paint (an instance of PaintBox). The paint panel is added to the center region of the DrawShapes frame; the status panel is added to the south region.

class DrawShapes(JFrame):        def __init__(self):            JFrame.__init__(self,title='Draw Shapes',visible=1,size=(400,400))            self.__init__toolbar()            self.__init__options()            self.statusPane = StatusBox()            self.contentPane.add(self.statusPane, BorderLayout.SOUTH)            self.PaintPane = PaintBox(self.statusPane)            self.contentPane.add(self.PaintPane, BorderLayout.CENTER)      ...      ...

The constructor also calls __init__toolbar and __init__option , which create the toolbar and option panels, respectively.

__init__toolbar creates two shape buttons and adds them to the toolbar pane. One of the buttons is initialized with an instance of the Rectangle class; the other, with an instance of the Oval class. The ShapeButton class represents changing the drawing mode to either rectangle or oval. Its handlers are set to the rect_pressed() and oval_pressed() methods, which change the shape to an oval or a rectangle.

__init__toolbar also adds the toolbar panel to the west region of the DrawShapes frame and is defined as follows:

def __init__toolbar(self):        toolbar = ToolBox()              # Add the rectangle button to the toolbar        rect = ShapeButton(Rectangle(4, 4, 20, 20))        toolbar.add(rect)        rect.actionPerformed = self.rect_pressed              # Add the oval button to the toolbar        oval = ShapeButton(Oval(4, 4, 10, 20))        toolbar.add(oval)        oval.actionPerformed = self.oval_pressed         self.add(toolbar, BorderLayout.WEST)

__init__options does the equivalent for the options pane. It creates a choice component for color and a checkbox component fill/no fill. It then adds these components to the options pane and places the pane in the north region of the DrawShapes frame.

def __init__options(self):        optionsPane = Panel()              # Set up the checkbox item for the fill option        check = Checkbox('Fill')        optionsPane.add(check)        check.itemStateChanged = self.fill_clicked              # Set up the choice for color        colors = Choice()        self.colors_dict = {'blue':Color.blue,                         'green':Color.green,                         'red':Color.red,                         'yellow':Color.yellow,                         'orange':Color.orange,                         'cyan':Color.cyan,                         'pink':Color.pink,                         'gray':Color.gray                         }        for color in self.colors_dict.keys():              colors.add(color)        optionsPane.add(colors)        colors.itemStateChanged = self.color_changed        self.add(optionsPane, BorderLayout.NORTH)
DrawShapes Event Handler

All of the event handlers for the components added to the options and toolbar panes correspond to setting properties in the paint pane (PaintBox instance).

  • The rect_pressed event handler is called when the rectangle button is pressed; it sets the shape type of the paint pane to PaintBox.RECTANGLE.

  • The oval_pressed event handler is called when the oval button is pressed; it sets the shape type of the paint pane to PaintBox.OVAL.

  • The fill_clicked event handler is called when the user checks or unchecks the fill checkbox. If the box is unchecked, the paint pane fill property is set to false. If the box is checked, the paint pane property is set to true.

  • The color_changed event handler is called when the user selects a new color in the choice box; it sets the color of the paint box's current paint mode to the color selected.

These event handlers are defined as follows:

def rect_pressed(self, event):       self.paintPane.setShapeType(PaintBox.RECTANGLE) def oval_pressed(self, event):       self.paintPane.setShapeType(PaintBox.OVAL) def fill_clicked(self, event):       if(event.stateChange==ItemEvent.SELECTED):              self.PaintPane.setFill(1)       elif (event.stateChange==ItemEvent.DESELECTED):              self.PaintPane.setFill(0) def color_changed(self,event):       colorname = event.item       color = self.colors_dict[colorname]       self.PaintPane.setShapeColor(color)

The Complete Shapes and DrawShapes Modules

We've covered all of the classes. At this point, you may want to run the Shapes and DrawShapes modules to see the drawing package in action. Here's the Shapes module.

class Shape:        def __init__(self, x, y, width=0, height=0, color=None, fill=0):              self.x=x              self.y=y              self.width=width              self.height=height              self.color=color              self.fill=fill        def paint(self, graphics):              if not self.color is None:                     oldcolor = graphics.foregroundColor                     graphics.color = self.color              if self.fill:                     self.draw_filled(graphics)              else:                     self.draw_outline(graphics)              if not self.color is None:                     graphics.color = oldcolor        def draw_filled(self, graphics):              pass        def draw_outline(self, graphics):              pass        def getRect(self):              return self.x, self.y, self.width, self.height class Rectangle(Shape):        def __init__(self, x, y, width, height, color=None, fill=0):              Shape.__init__(self, x, y, width, height, color, fill)        def draw_filled(self,graphics):              graphics.fillRect(self.x, self.y, self.width, self.height)        def draw_outline(self,graphics):              graphics.drawRect(self.x, self.y, self.width, self.height) class Oval(Shape):        def __init__(self, x, y, width, height, color=None, fill=0):              Shape.__init__(self, x, y, width, height, color, fill)        def draw_filled(self,graphics):              graphics.fillOval(self.x, self.y, self.width, self.height)        def draw_outline(self,graphics):              graphics.drawOval(self.x, self.y, self.width, self.height) class Shapes (Shape):        def __init__(self):              self.__shapes=[]        def addShape(self, shape):              self.__shapes.append(shape)        def paint(self, graphics):              for shape in self.__shapes:                     shape.paint()(graphics)        def getRect(self):                     # Lists to hold x,y,height and width from shape in shapes                        xl,yl,wl,hl = [],[],[],[]                     # Iterate through the list gathering each shapes                     # bounding rectangle              for shape in shapes:                     x,y,width, height = shape.getRect()                     xl.append(x)                     yl.append(y)                     wl.append(width)                     hl.append(height)              return min(xl), min(yl), max(wl), max(hl)        # This is for testing only if __name__ == '__main__':        from javax.swing import JFrame, JPanel        from java.awt import Color, Font        class TestPanel(JPanel):              def __init__(self):                          self.background=Color.white              def addShape(self,shape):                          self.shapes.addShape(shape)              def __init__(self):                          self.shapes = Shapes()                          self.addShape( Rectangle( 0, 0, 100, 100,                                       Color.blue, 1))                          self.addShape( Rectangle(100, 0, 100, 100,                                       Color.blue))                          self.addShape( Oval( 0, 100, 100, 100, Color.blue,                                       1))                          self.addShape( Oval(100, 100, 100, 100, Color.blue))              def paint(self,graphics):                          self.shapes.paint(graphics)        frame = JFrame("Test Shapes", size=(400,440), visible=1)        pane = TestPanel()        frame.contentPane = pane        frame.validate()

Here's the DrawShapes module:

from Shapes import * from java.awt import Font, Color, Dimension, BorderLayout from javax.swing import JComboBox, JLabel, JTextField, JComponent from javax.swing import BoxLayout, JCheckBox, JButton, JFrame, JPanel from java.awt.event import KeyEvent, ItemEvent class ShapeButton(JButton):        def __init__(self, shape):               self.shape = shape        def paint(self, graphics):               JButton.paint(self, graphics)               self.shape.paint(graphics)        def getPreferredSize(self):               d = Dimension(30,30)               return d class ToolBox (JPanel):        def __init__(self):               JPanel.__init__(self)        def getPreferredSize(self):               d = Dimension(40, 0)               return d class StatusBox (JPanel):        def __init__(self):               JPanel.__init__(self)               self.coordinates = JTextField(15, editable=0)               self.format = 'x = %d, y = %d'               self.add(self.coordinates)               self.event_type = JLabel ('MOUSE STATUS ')               self.add(self.event_type)               self.shape_type = JLabel ('SHAPE STATUS ')               self.add(self.shape_type)        def setXY(self, x,y):               self.coordinates.text = self.format % (x,y,)        def setMouseDrag(self, x, y):               self.event_type.text = 'MOUSE DRAG '               self.setXY(x,y)        def setMouseMove(self, x, y):               self.event_type.text = 'MOUSE MOVE '               self.setXY(x,y)        def setMousePress(self, x, y):               self.event_type.text = 'MOUSE PRESS '               self.setXY(x,y)        def setMouseRelease(self, x, y):               self.event_type.text = 'MOUSE RELEASE '               self.setXY(x,y)        def setShapeType(self, shape_type):                     # Set the label based on the shape type               if shape_type == PaintBox.RECTANGLE:                     self.shape_type.text = 'RECTANGLE'               elif shape_type == PaintBox.OVAL:                     self.shape_type.text = 'OVAL' class PaintBox (JComponent):        RECTANGLE=1        OVAL=2        def __init__(self, status):               JComponent.__init__(self)                self.opaque=1                self.background=Color.white               self.status = status               self.shapes = Shapes()               self.shape_type = PaintBox.RECTANGLE               self.mouseDragged = self.handle_mouseDragged               self.mouseMoved = self.handle_mouseMoved               self.mousePressed = self.handle_mousePress               self.mouseReleased = self.handle_mouseRelease               self.fill=0               self.color=Color.red        def getPreferredSize(self):               d = Dimension(400,400)               return d        def __drawShape(self, g, x, y, width, height):               if self.shape_type == PaintBox.RECTANGLE:                          rect = Rectangle(self.start.x, self.start.y, width,height,fill=0)                     rect.paint(g)               if self.shape_type == PaintBox.OVAL:                     oval = Oval(self.start.x, self.start.y, width, height,fill=0)                     oval.paint()(g)        def drawRubberShape(self, x, y):               g = self.graphics                     # Set the graphics to XOR mode, which allows rubberbanding.                     # Calculate the width and height.                     # Draw the outline of the shape.               try:                     g.setXORMode(self.background)                     width = abs(self.start.x - x)                     height = abs(self.start.y - y)                     self.__drawShape(g, self.start.x, self.start.y, width,                         height)               finally:                     g.dispose()        def handle_mousePress(self, event):                     # Print the status                          self.status.setMousePress(event.x, event.y)                     # Save the initial location.                     # In addition save the initial location as the last.               self.last = self.start = event.point        def handle_mouseDragged(self,event):                     # Print the status.               self.status.setMouseDrag(event.x, event.y)                     # Erase the old rubberband shape at the old location.                     # Create the new rubberband shape at the new location               self.drawRubberShape(self.last.x, self.last.y)               self.drawRubberShape(event.x, event.y)                     # Save the current event.x and event.y as the last.               self.last = event.point        def handle_mouseMoved(self,event):                     # Print the status.               self.status.setMouseMove(event.x,event.y)        def handle_mouseRelease(self, event):                     # Print the status               self.status.setMouseRelease(event.x, event.y)                     # Calculate the width and the height               width = abs(self.start.x - event.x)               height = abs(self.start.y - event.y)               shape = None #to hold the shape we are about to create                     # Create the shape based on the current shape type.                     # Then add the shape to self.shapes.               if self.shape_type == PaintBox.RECTANGLE:                     shape = Rectangle(self.start.x, self.start.y, width,                             height, self.color, self.fill)               elif self.shape_type == PaintBox.OVAL:                     shape = Oval(self.start.x, self.start.y, width, height,                             self.color, self.fill)               if not shape is None:                     self.shapes.addShape(shape)                     x,y,width,height = shape.getRect()                     self.repaint()(x,y,width+1, height+1)        def paint (self, graphics):               self.fillBackground(graphics)                     #Draw all of the shapes.               self.shapes.paint(graphics)        def fillBackground (self, graphics):                     #Get the background color               background=self.background                     #Get the size               size=self.getSize()               width, height = size.width, size.height                     # Create a rectangle that is as big                     # as the whole drawing area.               rect = Rectangle(0,0, width, height, background, 1)               rect.paint(graphics)        def setShapeType(self, shape_type):               self.status.setShapeType(shape_type)               self.shape_type = shape_type        def getShapeType(self):               return shape_type        def setFill(self, value):               self.fill = value        def setShapeColor(self, color):               self.color = color class DrawShapes(JFrame):        def __init__(self):               JFrame.__init__(self, title='Draw Shapes', visible=1,                   size=(400,400))               self.__init__toolbar()               self.__init__options()               self.statusPane = StatusBox()               self.contentPane.add(self.statusPane, BorderLayout.SOUTH)               self.PaintPane = PaintBox(self.statusPane)               self.contentPane.add(self.PaintPane, BorderLayout.CENTER)               self.pack()               def close(event):                     mainWindow.visible=0                     mainWindow.dispose()                     from java.lang import System                     System.exit(0)               global mainWindow               mainWindow = self               self.windowClosing=close        def __init__toolbar(self):               toolbar = ToolBox()                     # Add the rectangle button to the toolbar               rect = ShapeButton(Rectangle(4, 4, 20, 20))               toolbar.add(rect)               rect.actionPerformed = self.rect_pressed                     # Add the oval button to the toolbar               oval = ShapeButton(Oval(4, 4, 10, 20))               toolbar.add(oval)               oval.actionPerformed = self.oval_pressed               self.contentPane.add(toolbar, BorderLayout.WEST)        def rect_pressed(self, event):               self.PaintPane.setShapeType(PaintBox.RECTANGLE)        def oval_pressed(self, event):               self.PaintPane.setShapeType(PaintBox.OVAL)        def fill_clicked(self, event):               if(event.stateChange==ItemEvent.SELECTED):                     self.PaintPane.setFill(1)               elif (event.stateChange==ItemEvent.DESELECTED):                     self.PaintPane.setFill(0)        def color_changed(self,event):               colorname = event.item               color = self.colors_dict[colorname]               self.PaintPane.setShapeColor(color)        def __init__options(self):               optionsPane = JPanel()                     # Set up the checkbox item for the fill option               check = JCheckBox('Fill')               optionsPane.add(check)               check.itemStateChanged = self.fill_clicked                     # Set up the choice for color               colors = JComboBox()               self.colors_dict = {'blue':Color.blue,                                'green':Color.green,                                'red':Color.red,                                'yellow':Color.yellow,                                'orange':Color.orange,                                'cyan':Color.cyan,                                'pink':Color.pink,                                'gray':Color.gray                                }               for color in self.colors_dict.keys():                     colors.addItem(color)                  colors.setSelectedItem('red')               optionsPane.add(colors)               colors.itemStateChanged = self.color_changed               self.contentPane.add(optionsPane, BorderLayout.NORTH) if __name__ == '__main__':        frame = DrawShapes()

Notice that, for all the functionality we get, the code is relative simple.

Now let's fire up DrawShapes and draw some rectangles and ovals. Figure 15-3 is a screen dump of what we're going to do.

Figure 15-3. Screen Dump of the DrawShapes Example

graphics/15fig03.gif

Phase 2: DrawShapes Adding Text

We've seen how DrawShapes handles mouse events. Now let's extend it to include a text shape that handles keyboard events. In addition to text, this second phase of our example will add squares, rounded rectangles, and circles to show the design flaw of phase 1, which we'll have to wait until phase 3 to correct.

To add a text shape, we need to do the following:

  • Define a text shape in the Shapes module.

  • Add a shape button to the toolbar that represents text.

  • Add support for the text mode to the mousePress event handler.

  • Set up event handling in the paint box for the following user actions:

    1. Clicking a point in the paint box to make it the starting position of the text

    2. Entering text

    3. Pressing the Backspace key to delete the last character entered

    4. Pressing Enter to end the text entry

      The result is that the bounding rectangle of the text is calculated and the area is repainted immediately by force.

The Text Class

The Text class extends the Shape class and is defined in the Shapes module. It has font, string, and color attributes, which are passed via Shape's constructor. Text must be passed a reference to the component where it will be drawn so that it can acquire the font metrics, which it uses to calculate its ascent, descent, width, and height. (Look up java.awt.FontMetrics in the Java API documentation to learn more about fonts.) Ascent and descent determine how far above and below the baseline a character extends (more on this later).

Here's the Text class constructor (from TwoShapes.py).

Class Text(Shape):        def __init__(self, x, y, string, font, component, color=None):              Shape.__init__(self, x, y, color=color)              fm = component.getFontMetrics(font)              self.width = fm.stringWidth(string)              self.height = fm.maxAscent + fm.maxDescent              self.ascent = fm.maxAscent              self.descent = fm.maxDescent              self.font = font              self.string = string

Text overrides both the draw_outline() and draw_filled() methods. Draw_filled() calls draw_outline() because Text doesn't differentiate between filled and unfilled mode. draw_outline() changes the font to Text's font, saving the original to be restored after the call to drawString(), which does the actual drawing. Saving and restoring the graphics context attributes ensure that draw_outline() doesn't adversely affect other graphics operations.

Here's the code for draw_outline() (Two\Shapes.py):

def draw_outline(self, graphics):       if(self.font):                    #Get the original font.              font = graphics.font                    #Set the graphics to this font.              graphics.font = self.font              #Draw the string.       graphics.drawString(self.string, self.x, self.y)       if(self.font):                    # Set the graphics back                    # to the original font.              graphics.font = font def draw_filled(self, graphics):       self.draw_outline(graphics)

Text's final method is getRect(), which, you'll remember, is used by PaintBox to calculate the area that needs to be redrawn. Most of the other shapes simply use the inherited version of this method; however, Text needs it to calculate the bounding rectangle for the string because, unlike the other shape classes, its x,y coordinates don't begin in the upper left corner, as shown in Figure 15-4.

Figure 15-4. Baseline, Starting Point, Width, Ascent, and Descent

graphics/15fig04.gif

GetRect() calculates the rectangle by moving its starting point to the upper left corner. The x,y axis increases down and to the right, so subtracting the ascent moves the point up (if down is positive, up is negative).

def getRect(self):               return self.x, self.y-self.ascent, self.width, self.height
The Text Class Code

Text is the most complex shape we've seen so far, and it isn't as complex as it seems. Here it is in its entirety (from Two\Shapes.py):

class Text(Shape):        def __init__(self, x, y, string, font, component, color=None):              Shape.__init__(self, x, y, color=color)              fm = component.getFontMetrics(font)              self.width = fm.stringWidth(string)              self.height = fm.maxAscent + fm.maxDescent              self.ascent = fm.maxAscent              self.descent = fm.maxDescent              self.font = font              self.string = string        def draw_outline(self, graphics):              if(self.font):                           #Get the original font.                     font = graphics.font                           #Set the graphics to this font.                     graphics.font = self.font                     #Draw the string.              graphics.drawString(self.string, self.x, self.y)              if(self.font):                           #Set the graphics back to the original font.                     graphics.font = font        def draw_filled(self, graphics):              self.draw_outline(graphics)        def getRect(self):              return self.x, self.y-self.ascent, self.width, self.height
Supporting the Text Class

Now let's add support for Text to our DrawShapes module font name and point size options and keyboard event handling. Here are some typical user actions that illustrate the type of things we need:

  1. Clicking the Text shape button in the DrawShapes toolbar

  2. Changing the font name in the options pane

  3. Clicking on the PaintBox panel

  4. Typing characters

  5. Pressing the Enter key

Step 1 fires the event handler for the Shape button, which sets the shape type of the PaintBox panel to TEXT. Step 2 fires the event handler for font choice, which sets PaintBox's font attribute. Step 3 fires the mousePressed event, whose handler sets the x,y position of the text. Then Step 4 fires the keyTyped event, whose handler draws text on the paint box. Step 5 causes a Text class to be instantiated and added to PaintBox's shapes attribute.

PaintBox also handles the Backspace key for those of us who occasionally make mistakes. (I got tired of typing "Hekko World.")

To implement Text we first need to add the text shape button to the toolbar of the DrawShapes frame. We create an instance of Text and pass it to the ShapeButton class.

def __init__toolbar(self):              toolbar = ToolBox()                     #Add the rectangle button to the toolbar              rect = ShapeButton(Rectangle(4, 4, 20, 20))              toolbar.add(rect)              rect.actionPerformed = self.rect_pressed        ...        ...                     # Set the current font to Arial,                     # then get the FontMetrics              font = Font("Arial", Font.PLAIN, 20)              fm = self.getFontMetrics(font)                     #Add the Text button to the toolbar.              text=ShapeButton(Text(2, 2+fm.maxAscent, "A", font, self))              toolbar.add(text)              text.actionPerformed = self.text_pressed

As you can see, we need to get the font metrics to put the text shape in the button. The font metrics calculate where to move the start position of the text so that it's displayed in the button's upper right corner. Then we have to set the event handler for the button's action event to text_pressed.

def text_pressed(self, event):       self.PaintPane.setShapeType(PaintBox.TEXT)       self.PaintPane.requestFocus()

The text_pressed event handler sets the shape type of the paint box to TEXT. Calling requestFocus gives the paint box input focus, which is essential for it to receive key events. There can be no key events without input focus.

Next, the DrawShapes frame needs the font options (point size and typeface) added to its options pane. Here's the code:

def __init__options(self):        optionsPane = Panel()        optionsPane.add(Label("Font"))               #Set up the Font point List.        point_list = Choice()        points = (8,9,10,12,14,16,18,20,24,32)        for point in points:               point_list.add(str(point))        optionsPane.add(point_list)        point_list.itemStateChanged = self.point_changed              #Set up font List.        from java.awt import GraphicsEnvironment        ge = GraphicsEnvironment.getLocalGraphicsEnvironment()        fonts = ge.getAvailableFontFamilyNames()        fonts_list = Choice()        for font in fonts:              fonts_list.add(font)        optionsPane.add(fonts_list)        fonts_list.itemStateChanged = self.font_changed

The __init_options method creates a list of point sizes and adds it to a choice control called point_list. Next it gets a list of fonts from GraphicsEnvironment.getAvailableFontFamilyNames and adds it to a choice control called font_list. font_list and point_list are added to the options pane, and event handlers are set up for both.

The event handler for font_list sets the name property of the paint pane (an instance of PaintBox) when the user selects a font name. event.item contains the font name selected by the user and is passed to the setFontName() method of the PaintBox instance, which sets the instance's font.

The event handler for point_list converts event.item to an integer and then sets the paint pane's point size property.

Here are the event handlers for both font_list and point_list:

def font_changed(self, event):        self.PaintPane.setFontName(event.item) def point_changed(self, event):        self.PaintPane.setFontPoint(int(event.item))

That about does it for the changes made to DrawShapes to support text. Now I'll describe the changes made to PaintBox to add text shape support. This is the interesting part.

Test and PaintBox

PaintBox adds another shape type, TEXT. It adds three attributes to its class instance in its constructor: text (a string), which denotes the text being typed by the user but not yet entered in that is, the transitional text; startText (an integer representing a Boolean value), which denotes whether or not a text operation has begun; and text_in, which holds the transitional instance of the text shape, that is, the text being keyed in and edited. When the shape type is TEXT and the user clicks the paint box, startText is set to true, and PaintBox begins processing key events. The key event handlers control the text operation and add characters to the text attribute.

Here's the definition for PaintBox and its constructor (from two\DrawShapes.py):

class Paint box (Panel):        RECTANGLE=1        OVAL=2 ... ...        TEXT=4        def __init__(self, status):               Panel.__init__(self)               self.status = status               self.shapes = Shapes()               self.shape_type = PaintBox.RECTANGLE               self.mouseDragged = self.handle_mouseDragged ... ...               self.keyPressed = self.handle_keyPressed               self.keyTyped = self.handle_keyTyped               self.point = 20               self.fontname = "Arial"               self.font = Font(self.fontname, Font.PLAIN, self.point)               self.text = ""               self.startText = 0               self.text_in = None               ...               ...

When the user presses the mouse button, the mousePress event handler is called, which sets startText to true and sets the text to an empty string if the shape type is TEXT. This defines the text's starting point. In the following example, code that has been added to two\DrawShapes.py is hightlighted in bold.

     def handle_mousePress(self, event): ... ...            self.last = self.start = event.point            if (self.shape_type == PaintBox.TEXT):                  self.text = ""                  self.startText = 1

PaintBox uses only keyPressed to handle control keys and keyTyped to handle character keys when drawing a text shape.

The keyPressed event handler handles the Enter and Backspace keys. When Enter is pressed, the text shape being defined is added to the paint box's shapes collection. Here's the code:

def handle_keyPressed(self, event):        self.ignore = 0               # Perfom actions for drawing text               # if this in text draw mode.        if self.shape_type == PaintBox.TEXT and self.startText:                     # If the user presses the Enter key,                     # We add this shape to the shape list.               if (event.keyCode == KeyEvent.VK_ENTER):                     self.shapes.addShape(self.text_in)                     self.startText = 0                     self.text_in = None

When the Backspace key is pressed, the last character of the text string is removed, and the ignore attribute of PaintBox is set to true. ignore is needed because the Backspace key generates a character (here '\b') that we don't want to show up in our text shape (it appears as a square). A true value tells the keyTyped event handler to disregard any such character. Also when the Backspace key is pressed, a new text shape is created and assigned to the text_in attribute (whose last character was just removed), which forces a repaint of text_in's bounds (before the backspace operation).

     def handle_keyPressed(self, event):             self.ignore = 0                   # Perform actions for drawing text                   # if this in text draw mode.             if self.shape_type == PaintBox.TEXT and self.startText: ... ...                          # If the user presses the Backspace key,                          # we delete the last character in the text.                   if (event.keyCode == KeyEvent.VK_BACK_SPACE):                          x,y,width,height = self.text_in.getRect()                          self.ignore = 1                          self.text = self.text[:-1]                          self.text_in=Text(self.start.x, self.start.y, \                                 self.text,self.font,self, self.color)                          self.repaint()(x,y,width+1, height+1)

Unlike the keyPressed handler, the keyTyped handler works with characters the user types in, which are collected in the text string. After each character is typed, a new text shape object is created (text_in). The keyTyped event handler uses the bounds of text_in to force a repaint so that the shape is redrawn.

def handle_keyTyped(self,event):              # See if this is in text draw mode,              # and the backspace was not pressed       if self.shape_type == PaintBox.TEXT and self.startText \                            and not self.ignore:                     # Get the character typed, add it to the text.              self.text = self.text + event.keyChar                     # Force the text to be shown.                     # Create the Text instance, and force repaint().              self.text_in = Text(self.start.x, self.start.y, \                            self.text, self.font, self, self.color)              x,y,width,height = self.text_in.getRect()              self.repaint()(x,y,width+1, height+1)

The paint()method checks to see if the text_in shape exists. If so, it draws it. Remember, the text_in attribute corresponds to the current shape, that is, the one being worked on.

def paint(self, graphics):              #Draw all of the shapes.        self.shapes.paint(graphics)              #For showing text while we type        if(self.text_in):              self.text_in.paint(graphics)

PaintBox also creates several methods to change its font and point attributes.

def setFontName(self, font):        self.fontname = font        self.font = Font(self.fontname, Font.PLAIN, self.point) def setFontPoint(self,point):        self.point = point        self.font = Font(self.fontname, Font.PLAIN, point)

So now we've added text shape support. Oh, yes, I said earlier that we would add squares, circles, and rounded rectangles. Well, we won't go into detail about these shapes, but we will take a look at the problem they cause.

If all you wanted out of this chapter was an introduction to graphics programming, you just got it. But if you want to learn what makes dynamic object-oriented programming tick, read on. We're going to modify DrawShapes and its classes and make them significantly smaller and more extensible. In other words, we're going to write tight, dynamic, object-oriented Python code.

DrawShapes' Ugly, Inefficient, Nonextensible Code

Let's see what's wrong with DrawShapes as it stands now. We'll start with the DrawShapes class. First look at __init__toolbar (from two\DrawShapes.py).

def __init__toolbar(self):              toolbar = ToolBox()                    #Add the Rectangle button to the toolbar              rect = ShapeButton(Rectangle(4, 4, 20, 10))              toolbar.add(rect)              rect.actionPerformed = self.rect_pressed                    #Add the Circle button to the toolbar              circle = ShapeButton(Circle(4, 4, 10))              toolbar.add(circle)              circle.actionPerformed = self.circle_pressed                    #Add the Oval button to the toolbar              oval = ShapeButton(Oval(4, 4, 10, 20))              toolbar.add(oval)              oval.actionPerformed = self.oval_pressed                    #Add the Square button to the toolbar              square = ShapeButton(Square(4, 4, 20))              toolbar.add(square)              square.actionPerformed = self.square_pressed                    #Add the RoundRectangle button to the toolbar              rrect = ShapeButton(RoundedRectangle(4, 4, 20, 20))              toolbar.add(rrect)              rrect.actionPerformed = self.round_rect_pressed                    # Set the current font to Arial,                    # then get the FontMetrics              font = Font("Arial", Font.PLAIN, 20)              fm = self.getFontMetrics(font)                    # Add the Text button to the toolbar.              text=ShapeButton(Text(2, 2+fm.maxAscent, "A", font, self))              toolbar.add(text)              text.actionPerformed = self.text_pressed              self.add(toolbar, BorderLayout.WEST)

Every time we define a new shape, we have to write three to five lines of code in __init__toolbar; then we have to write an event handler for each one. As you can see, with just six shapes it gets pretty messy.

def circle_pressed(self, event):        self.PaintPane.setShapeType(PaintBox.CIRCLE) def rect_pressed(self, event):        self.PaintPane.setShapeType(PaintBox.RECTANGLE) def round_rect_pressed(self, event):        self.PaintPane.setShapeType(PaintBox.ROUND_RECTANGLE) def square_pressed(self, event):        self.PaintPane.setShapeType(PaintBox.SQUARE) def oval_pressed(self, event):        self.PaintPane.setShapeType(PaintBox.OVAL) def text_pressed(self, event):        self.PaintPane.setShapeType(PaintBox.TEXT)        self.PaintPane.requestFocus()

They all look pretty similar, don't they? Why not replace all of the code above with this:

class ShapeTool:        def __init__(self, shape_class, toolbar, PaintBox):                      # Use eval to create a shape. shape_class                      # holds a string representing the class.               self.__shape = eval(shape_class+'()')               button = ShapeButton(self.__shape)               toolbar.add(button)               button.actionPerformed = self.actionPerformed               self.PaintBox = PaintBox        def actionPerformed(self, event):               self.PaintBox.requestFocus()               self.PaintBox.setShapeType(self.__shape.__class__)               def getShape(self):return self.__shape
The ShapeTool Class

__init__toolbar uses ShapeTool to create shape buttons, add them to the toolbar, and register them with an event handler. Notice that it uses strings to denote each shape's class. The ShapeTool constructor uses the strings as arguments to the built-in eval() function.

class DrawShapes(Frame): ... ...       def __init__toolbar(self):              toolbar = ToolBox()              Shapes = ['Square', 'Rectangle', 'RoundedRectangle',                       'Oval', 'Text', 'Circle' ]              for shape in Shapes:                       shape_tool=ShapeTool(shape,toolbar, self.PaintPane)                       if (isinstance(shape_tool.getShape(), Text)):                              shape_tool.getShape().initForToolbar('A', self.PaintPane)              self.add(toolbar, BorderLayout.WEST)

The ShapeTool class defines the event handler for each button and adds it to the toolbar. It eliminates thirty lines of code and five function definitions. What's more, it's extensible: Any additional shapes are just added to the shapes list in __init__toolbar with no extra code. With the old way, for thirty shapes we'd need thirty event handlers and about seventy-five additional lines of code. With the new way, all we need is thirty entries in the shapes list.

Less code to write equates to less code to test and fewer bugs. In addition, we can create a text file our program can read that lists the shapes we want to display. This is extremely flexible.

Notice that the event handler for ShapeTool passes the class, not an integer representing the type. The old way passed an integer with the type as defined by constant values in the PaintBox class. This is directly from the Department of Redundancy Department (with thanks to Monty Python). A class is a type, and shapes can be uniquely identified by their class. Now PaintBox uses the class as the shape type.

Making PaintBox Modular

There's a lot wrong with the old PaintBox. First of all, as I mentioned, every time we add a shape, we need to add a shape constant. This tightly couples shapes to PaintBox.

class PaintBox (JComponent):        RECTANGLE=1        OVAL=2        CIRCLE=3        TEXT=4        ROUND_RECTANGLE=5        SQUARE = 6        ...        ...

The shape type integer is passed to PaintBox, making it necessary to add another suite to PaintBox's if statement drawShape() for each new shape.

def setShapeType(self, shape_type):        self.status.setShapeType(shape_type)        self.shape_type = shape_type def __drawShape(self, g, x, y, width, height):       if self.shape_type == PaintBox.RECTANGLE:             g.drawRect(self.start.x, self.start.y, width, height)       if self.shape_type == PaintBox.ROUND_RECTANGLE:             g.drawRoundRect(self.start.x, self.start.y, width, height, 10,10)       if self.shape_type == PaintBox.OVAL:             g.drawOval(self.start.x, self.start.y, width, height)       if self.shape_type == PaintBox.CIRCLE:             g.drawOval(self.start.x, self.start.y, width, width)       if self.shape_type == PaintBox.SQUARE:             g.drawRect(self.start.x, self.start.y, width, width)

There are a couple of things wrong with this. First, the code for drawing a shape should be in a shape object, not in PaintBox. This means that the original code for drawShape() isn't modular, and that's bad. If there's a problem drawing a shape, the developer who inherits this code won't know if she should look in the Shapes module or in the PaintBox class.

Code should be modular; that is, a class should do one set of related things. The shape should know how to do shape things like draw itself, and this knowledge should be hidden from PaintBox in other words, encapsulated.

Here's our modular version (Three\PaintBox.py):

def createShape(self):       """Create a new shape with the shape_type.       The shape_type is a Shape class."""       self.shape = self.shape_type()       if (isinstance(self.shape, Text)):             self.shape.setComponent(self) def setShapeType(self, shape_type):       """Set the current Shape type,       then create an instance of the shape"""       self.status.setShapeType(shape_type)       self.shape_type = shape_type       self.createShape() def __drawShape(self, g, x, y, width, height):       self.shape.fromBounds(self.start.x, self.start.y, width, height)       self.shape.draw_outline()(g)

In the new drawShape(), we initialize the shape with the fromBounds() method, which tells the shape to initialize itself from the given bounding rectangle. If you look at the shape classes in the code above, you'll notice all of them either inherit the default, fromBound(), from the Shape class or implement their own version.

The great thing about our new code is that, if we add a triangle, a hexagon, an octagon, and so forth, it doesn't change. With the old __DrawShapes, each time we add a new shape we have to add another elif suite to the if statement. Thirty shapes equal thirty elif branches.

This is the magic of polymorphism. All we do is tell the shape to initialize itself from the bounds we give it; we don't care if it's a triangle or a parallelogram.

Remember that there was another place where we had a long if statement with lots of elif suites handle_mouseRelease, which created a shape based on the shape type defined by the integer constants. The new handle_mouseRelease does not need to do that, so it's not coupled to adding new shapes. Look at the differences with the few shapes we defined, and imagine what the old mouseRelease handler (from Two\DrawShapes.py) would look like if we had thirty shapes.

def handle_mouseRelease(self, event):                     # Print the status              self.status.setMouseRelease(event.x, event.y)                     # Calculate the width and the height              width = abs(self.start.x - event.x)              height = abs(self.start.y - event.y)              shape = None #hold the shape we are about to create                     # Create shape based on the current shape type.                     # Then add the shape to self.shapes.              if self.shape_type == PaintBox.RECTANGLE:                     shape = Rectangle(self.start.x, self.start.y, width,                             height, self.color, self.fill)              elif self.shape_type == PaintBox.OVAL:                     shape = Oval(self.start.x, self.start.y, width, height,                             self.color, self.fill)              elif self.shape_type == PaintBox.CIRCLE:                     shape = Circle(self.start.x, self.start.y, width/2,                             self.color, self.fill)              elif self.shape_type == PaintBox.SQUARE:                     shape = Square(self.start.x, self.start.y, width,                             self.color, self.fill)              elif self.shape_type == PaintBox.ROUND_RECTANGLE:                     shape = RoundedRectangle(self.start.x, self.start.y,                              width, height, self.color, self.fill)              if not shape is None:                     self.shapes.addShape(shape)                     x,y,width,height = shape.getRect()                     self.repaint(x,y,width+1, height+1)

Here's the new mouseRelease handler (Three\DrawShapes.py):

def handle_mouseRelease(self, event):                     # Print the status              self.status.setMouseRelease(event.x, event.y)                     # Calculate the width and the height              width = abs(self.start.x - event.x)              height = abs(self.start.y - event.y)                     # Set the shape bounds, i.e.,                     # the bounds the shape initializes from.                     # Add the shape to the shape list.                     # Calculate the shape actual bounds,                     # and repaint().              self.shape.fromBounds(self.start.x, self.start.y, width, height, self.color, self.fill)              self.shapes.addShape(self.shape)              x,y,width,height = self.shape.getRect()              self.repaint(x,y,width+1, height+1)                     # Create a new shape.              self.createShape()

I don't want you to confuse the fooling around with objects we're doing with defining an extensible architecture for an application. I just want to highlight those simple changes and show that dynamic object-oriented programming saves us huge headaches.

A Broken Record

Remember to read Object-Oriented Analysis and Design with Applications (Booch, 1994) and then Design Patterns (Gamma et al., 1995).

Try these exercises, which extend the drawing package in directory 3:

  • Add three new shapes: a triangle (use drawPolygon and fillPolygon), a line, and a scribble.

  • Create an Undo button on the options pane that allows users to undo the last shape drawn.

  • Create a button on the options pane that allows a user to change the z-order of the shapes (that is, the order in which the shapes are "stacked" in the view); this might pop up a dialog box that has a list of shapes representing the z-order.

  • Add the ability to drag an already drawn shape to a new position. You'll need to do hit testing for the shape.

  • Add the ability to select a shape and change its color.

  • Create a composite shape, and call it TextBox. It should contain two shapes: the text to be drawn and the bounding rectangle. The user draws the textbox first and adds text to it. The text entered word-wraps; that is, it stays within the bounds of the rectangle, filling the box left to right and then top to bottom, and if the text is too long for one line, it starts a new one. If there are too many lines for the bounds, only the text that fits is shown.

If you don't do at least the first exercise, you won't get much out of this chapter. If you can handle the last one, you're well on your way to programmer stardom.

Summary

This chapter mainly covered events and graphics using Java AWT. We learned about the different events the various graphics components publish, and we got some firsthand experience of the order in which events take place. We prototyped a FrameEvents class that allowed us to see the runtime behavior of many events. With prototyping it's much easier to see how events work.

We dealt with the basic drawing of shapes and the XOR graphics mode for rubberbanding. We were able to put together the things we learned about graphics programming and events in a very simple drawing application, which we extended to incorporate ideas in earlier chapters about object-oriented programming and the dynamic behavior of Jython.

CONTENTS


Python Programming with the JavaT Class Libraries. A Tutorial for Building Web and Enterprise Applications with Jython
Python Programming with the Javaв„ў Class Libraries: A Tutorial for Building Web and Enterprise Applications with Jython
ISBN: 0201616165
EAN: 2147483647
Year: 2001
Pages: 25

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net