CSC111 Necaise's Graphics Library
Revision as of 11:42, 17 April 2014 by Thiebaut (talk | contribs) (Thiebaut moved page CSC111 Horstmann's Graphics Library to CSC111 Necaise's Graphics Library)
--D. Thiebaut (talk) 21:20, 6 April 2014 (EDT)
Download
This is a modified (fixed some bugs) version of Horstmann's graphics library. The original is available here, but you are encouraged to download the one below.
Source
The edited zipped version for this course is available here, in case your are having problem copy/pasting the code below in Idle.
# graphics.py
# (c) 2013 by Rance Necaise
# http://graphics.necaiseweb.org
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
## graphics.py
# version 1.0
# This module is part of the PyWare open source project that provides modules and
# and tools for use in the classroom. This module provides classes for creating
# top-level GUI windows that can be used for creating and displaying simple
# geometric shapes and color digital images. Complete documentation of the module
# is available at http://graphics.necaseweb.org.
## Version 1.1
# modified by D. Thiebaut to allow better use of drawPoly()
#
## Version 1.2
# modified by D. Thiebaut to allow resizing of font.
# replace font_opts by text_opts everywhere and commented out
# replacement of size by original size in setFont
import tkinter as tk
## This class defines a basic top level window that can be opened on the
# desktop and used to produce simple graphical drawings. It contains a
# canvas on which geometric shapes can be drawn and manipulated.
#
class GraphicsWindow :
## Creates a new graphics window with an empty canvas.
# @param width The horizontal size of the canvas in pixels.
# @param height The vertical size of the canvas in pixels.
#
def __init__(self, width = 400, height = 400) :
global TheMainWindow
# Is the window open or closed. It must be open to use most operations.
self._isClosed = False
# If this is the first toplevel window, remember it as the main window. The
# event loop is only terminated when the main window is closed.
if TheMainWindow is None :
TheMainWindow = self
# Create a top-level window for the graphics window.
self._tkwin = tk.Toplevel(rootWin, padx=0, pady=0, bd=0)
self._tkwin.protocol("WM_DELETE_WINDOW", self.close)
self._tkwin.title("")
# Create a canvas inside the top-level window which is used for
# drawing the graphical objects and text.
self._canvas = GraphicsCanvas( self, width, height )
# Bring the window to the front of all other windows and force an update.
self._tkwin.lift()
self._tkwin.resizable(0, 0)
self._tkwin.update_idletasks()
## Closes and destroys the window. A closed window can not be used.
#
def close(self) :
# Set the closed flag to true so no other ops can be performed on
# the window object.
if self._isClosed : return
self._isClosed = True
# Destroy the window and force an update so it will close when
# used in IDLE or Wing IDE.
self._tkwin.destroy()
self._tkwin.update_idletasks()
# If the main window is being closed, then the mainloop has to be terminated.
if self is TheMainWindow :
self._tkwin.quit()
## Starts the event loop which handles various window events. This causes
# the sequential execution of the program to stop and wait for the user
# to click the close button on the main window or to call the quit method
# on any window. This method should only be called on the main window.
#
def wait(self) :
if not self._isClosed :
self._tkwin.mainloop()
## Returns a reference to the canvas contained within the window.
# The canvas can be used to draw and manipulate geometric shapes and text.
# @return A reference to the GraphicsCanvas contained in the window.
#
def canvas(self) :
if self._isClosed : raise GraphicsWinError()
return self._canvas
## Sets the title of the window. By default, the window has no title.
# @param title A text string to which the title of the window is set. To
# remove the title, use pass an empty string to the method.
#
def setTitle(self, title) :
self._tkwin.title( title )
## Returns a Boolean indicating whether the window exists or was previously closed.
# Window operations can not be performed on a closed window.
# @return @c True if the window is closed and @c False otherwise.
#
def isClosed(self) :
return self._isClosed
## Hides or iconifies the top level window.
# The window still exists and can be displayed again using the show method.
#
def hide(self) :
if self._isClosed : raise GraphicsWinError()
self._tkwin.withdraw()
self._tkwin.update_idletasks()
## Shows or deiconifies a previously hidden top level window.
#
def show(self) :
if self._isClosed : raise GraphicsWinError()
self._tkwin.deiconify()
self._tkwin.update_idletasks()
#--- The graphics canvas.
## This class defines a canvas on which geometric shapes and text can be
# drawn. The canvas uses discrete Cartesian coordinates >= 0 with (0,0)
# being in the upper-left corner of the window. Unlike a canvas that a
# painter might use, shapes drawn on a graphics canvas are stored as
# objects that can later be reconfigured without having to redraw them.
# A collection of shape properties are also maintained as part of the
# canvas. These properties, which can be changed by calling specific
# methods, are used in drawing the various shapes and text. All shapes
# and text are drawn using the current context or the property settings
# at the time the shape is first drawn.
#
class GraphicsCanvas :
## Creates a new empty graphics canvas. A graphics canvas is
# automatically created as part of a GraphicsWindow. Thus, there should
# be no need for the user of this module to explicitly create one.
# @param win, A reference to the GraphicsWindow in which the canvas is used.
# @param width, (int) The width of the canvas in pixels.
# @param height, (int) The height of the canvas in pixels.
#
def __init__(self, win, width, height) :
# The GraphicsWindow that contains the canvas.
self._win = win
# Keep track of the size of the canvas.
self._width = width
self._height = height
# Maintain the options used for drawing objects and text.
self._polyOpts = {"outline" : "black", "width" : 1, "dash" : None, "fill" : ""}
self._textOpts = {"text" : "", "justify" : tk.LEFT, "anchor" : tk.NW,
"fill" : "black",
"font" : ("helvetica", 10, "normal") }
# Create the tk canvas inside the given window.
self._tkcanvas = tk.Canvas(self._win._tkwin, highlightthickness = 0,
width = width, height = height, bg = "white" )
self._tkcanvas.pack()
## Changes the height of the canvas.
# The window is resized to fit the size of the canvas.
# @param size (int) The new height of the canvas in number of pixels.
#
def setHeight(self, size) :
self._checkValid()
if type(size) != int or size <= 0 :
raise GraphicsParamError( "The window height must be >= 1." )
self._tkcanvas.config(height=size)
self._height = size
self._tkcanvas.update_idletasks()
## Changes the width of the canvas.
# The window is resized to fit the size of the canvas.
# @param size (int) The new width of the canvas in number of pixels.
#
def setWidth(self, size) :
self._checkValid()
if type(size) != int or size <= 0 :
raise GraphicsParamError( "The window width must be >= 1." )
self._tkcanvas.config(width=size)
self._width = size
self._tkcanvas.update_idletasks()
## Returns the height of the canvas.
# @return The canvas height in number of pixels.
#
def height( self ):
return self._height
## Returns the width of the canvas.
# @return The canva width in number of pixels.
#
def width( self ):
return self._width
## Clears the canvas by removing all items previously drawn on it. The canvas
# acts as a container of shapes and text. Thus, when a geometric shape or
# text is drawn on the canvas, it is maintained internally as an object
# until cleared.
#
def clear(self):
self._checkValid()
self._tkcanvas.delete(tk.ALL)
self._tkcanvas.update_idletasks()
## Sets the current background color of the canvas. The color can either
# be specified as a string that names a color or as three integer values
# in the range [0..255].
#
# c.setBackground(colorname)
# c.setBackground(red, green, blue)
#
def setBackground(self, red, green = None, blue = None) :
if type(red) == int :
color = "#%02X%02X%02X" % (red, green, blue)
elif type(red) != str :
raise GraphicsParamError( "Invalid color." )
else :
color = red
self._checkValid()
self._tkcanvas.config(bg = color)
self._tkcanvas.update_idletasks()
## Sets the fill color used when drawing new polygon shapes. The color
# can be specified either as a string that names the color or as three
# integer values in the range [0..255]. If no argument is provided, it
# clears the fill color and the shapes will be drawn in outline form only.
#
# c.setFill()
# c.setFill(colorname)
# c.setFill(red, green, blue)
#
def setFill( self, red = None, green = None, blue = None) :
if red is None :
color = ""
elif type(red) == int :
color = "#%02X%02X%02X" % (red, green, blue)
elif type(red) != str :
raise GraphicsParamError( "Invalid color." )
else :
color = red
self._polyOpts["fill"] = color
## Sets the outline color used when drawing new polygon shapes and the
# color used to draw lines, pixels, and text. The color can be specified
# either as a string that names the color or as three integer values in
# the range [0..255]. If no argument is provided, it clears the outline
# color. A cleared outline color is only meant for drawing polygon type
# shapes that are only filled, without an outline.
#
# c.setOutline()
# c.setOutline(colorname)
# c.setOutline(red, green, blue)
#
def setOutline( self, red = None, green = None, blue = None) :
if red is None :
color = ""
elif type(red) == int :
color = "#%02X%02X%02X" % (red, green, blue)
elif type(red) != str :
raise GraphicsParamError( "Invalid color." )
else :
color = red
self._polyOpts["outline"] = color
self._textOpts["fill"] = color
## Sets both the fill and outline colors used when drawing shapes and text
# on the canvas. The color can be specified either as a string that names
# the color or as three integer values in the range [0..255].
#
# c.setFill(colorname)
# c.setFill(red, green, blue)
#
def setColor( self, red, green = None, blue = None ) :
if type(red) == int :
color = "#%02X%02X%02X" % (red, green, blue)
elif type(red) != str :
raise GraphicsParamError( "Invalid color." )
else :
color = red
self._polyOpts["outline"] = color
self._polyOpts["fill"] = color
self._textOpts["fill"] = color
## Sets the width of lines drawn on the canvas. This includes the line and
# vector shapes and the outlines of polygons.
# @param size (int) The new line width in number of pixels.
#
def setLineWidth(self, size) :
if type(size) != int or size <= 0 :
raise GraphicsParamError( "Invalid line width." )
self._polyOpts["width"] = size
## Sets the style used to drawn lines on the canvas. This includes the line
# and vector shapes and the outlines of polygons.
# @param style (str) The style to use for new lines. It can be either
# "solid" or "dashed".
#
def setLineStyle(self, style) :
if style == "solid" :
self._polyOpts["dash"] = None
elif style == "dashed" :
self._polyOpts["dash"] = (4,)
else :
raise GraphicsParamError("Invalid line style.")
## Sets the font used to draw text on the canvas.
# @param family (str) The font family. It can be one of:
# "arial", "courier", "times", "helvetica".
# @param size (int) The point size of the font.
# @param style (string) The font style. It can be one of:
# "normal", "bold", "italic", or "bold italic".
#
def setTextFont(self, family = None, size = None,
style = None) :
origFamily, origSize, origStyle = self._textOpts["font"]
if (family is not None and
family not in ('helvetica', 'arial', 'courier', 'times', 'times roman')) :
raise GraphicsParamError( "Invalid font family." )
else :
family = origFamily
if (style is not None and
style not in ('bold', 'normal', 'italic', 'bold italic')) :
raise GraphicsParamError( "Invalid font style." )
else :
style = origStyle
if size is not None and (type(size) != int or size <= 0) :
raise GraphicsParamError( "Invalid font size." )
#else :
# size = origSize
self._textOpts["font"] = (family, size, style)
## Sets the position that text is drawn in relation to a bounding box.
# The (x, y) coordinate provided with drawText() is anchored to a spot on
# the bounding box that surrounds the text and the text is positioned
# relative to the anchor.
# @param position A string indicating the anchor position on the
# bounding box. It can be one of:
# "n", "s", "e", "w", "center", "nw", "ne", "sw", "se".
#
def setTextAnchor(self, position) :
if position not in ('n', 's', 'e', 'w', 'nw', 'ne', 'sw', 'se', 'center') :
raise GraphicsParamError( "Invalid anchor position." )
self._textOpts["anchor"] = position
## Sets the justification used to draw new multiline text on the canvas..
# @param style A string specifying the justification. It can be one of:
# "left", "center", or "right".
#
def setTextJustify(self, style) :
if style == "left" :
self._textOpts["justify"] = tk.LEFT
elif style == "center" :
self._textOpts["justify"] = tk.CENTER
elif style == "right" :
self._textOpts["justify"] = tk.RIGHT
else :
raise GraphicsParamError( "Invalid justification value." )
#--- The shape drawing methods.
## Draws or plots a single point (pixel) on the canvas.
# @param x, y Integers indicating the (x, y) pixel coordinates at which
# the point is drawn.
# @return An integer that uniquely identifies the new canvas item.
#
def drawPoint( self, x, y ):
self._checkValid()
obj = self._tkcanvas.create_line( x, y, x+1, y,
fill=self._polyOpts["outline"],
width=self._polyOpts["width"] )
self._tkcanvas.update_idletasks()
return obj
## Draws a line segment on the canvas. The line is drawn between two
# discrete end points.
# @param x1, y1 The coordinates of the starting point.
# @param x2, y2 The coordinates of the ending point.
# @return An integer that uniquely identifies the new canvas item.
#
def drawLine(self, x1, y1, x2, y2) :
self._checkValid()
obj = self._tkcanvas.create_line( x1, y1,
x2, y2,
fill=self._polyOpts["outline"],
width=self._polyOpts["width"],
dash=self._polyOpts["dash"] )
self._tkcanvas.update_idletasks()
return obj
## Draws an arrow or vector on the canvas. The same as a line segment,
# except an arrow head is drawn at the end of the segment.
# @returns An integer that uniquely identifies the new canvas item.
#
def drawArrow(self, x1, y1, x2, y2) :
self._checkValid()
obj = self._tkcanvas.create_line( x1, y1, x2, y2,
fill=self._polyOpts["outline"],
width=self._polyOpts["width"],
dash=self._polyOpts["dash"],
arrow=tk.LAST )
self._tkcanvas.update_idletasks()
return obj
## Draws a rectangle on the canvas. The rectangle is defined by the coordinates
# of the upper left corner of the rectangle and its width and height.
# @param x, y The coordinates of the upper-left corner of the rectangle.
# @param width, height The dimensions of the rectangle.
# @returns An integer that uniquely identifies the new canvas item.
#
def drawRectangle(self, x, y, width, height) :
self._checkValid()
obj = self._tkcanvas.create_rectangle(x, y, x + width, y + height, self._polyOpts )
self._tkcanvas.update_idletasks()
return obj
## The same as drawRectangle().
#
def drawRect(self, x, y, width, height) :
return self.drawRectangle(x, y, width, height)
## Draws a polygon on the canvas. The polygon is defined by three or more vertices
# specified in counter-clockwise order. There are two forms of the method:
#
# c.drawPolygon(x1, y1, x2, y2, ..., xN, yN)
# c.drawPolygon(sequence)
#
# @returns An integer that uniquely identifies the new canvas item.
#
def drawPolygon( self, *coords ):
# Unwrap the cooridinates which allows the method to accept individual vertices
# or a list of vertices.
print( "drawPolygon: coords = ", coords )
if len(coords) == 1 and (type(coords[0]) == list or type(coords[0] == tuple)) :
expCoords = tuple(*coords)
else :
expCoords = coords
#print( "expCoords = ", expCoords )
self._checkValid()
if len(expCoords) < 6 :
raise GraphicsParamError( "At least 3 vertices must be provided." )
obj = self._tkcanvas.create_polygon( expCoords, self._polyOpts )
self._tkcanvas.update_idletasks()
return obj
## The same as drawPolygon().
#
def drawPoly(self, *coords) :
# code added by DFT, April 2014
if len(coords) == 1 and (type(coords[0]) == list or type(coords[0] == tuple)) :
expCoords = tuple(*coords)
else :
expCoords = coords
#print( "drawPoly: coords = ", coords )
return self.drawPolygon(coords)
## Draws an oval on the canvas. The oval is defined by a bounding rectangle
# that is specified by the coordinates of its upper-left corner and its
# dimensions.
# @param x, y The upper-left coordinates of the bounding rectangle.
# @param width, height The dimensions of the bounding rectangle.
# @returns An integer that uniquely identifies the new canvas item.
#
def drawOval( self, x, y, width, height ):
self._checkValid()
obj = self._tkcanvas.create_oval( x, y, x + width, y + height, self._polyOpts )
self._tkcanvas.update_idletasks()
return obj
## Draws an arc or part of a circle on the canvas. The arc is defined by a
# bounding square and two angles. The angles are specified in degrees with
# zero degrees corresponding to the x-axis.
# @param x, y The upper-left coordinates of the bounding square.
# @param diameter The dimensions of the bounding rectangle.
# @param startAngle The angle in degrees at which the arc begins.
# @param extent The extent of the arc given as an angle in degrees.
# @returns An integer that uniquely identifies the new canvas item.
#
def drawArc( self, x, y, diameter, startAngle, extent ):
self._checkValid()
obj = self._tkcanvas.create_arc( x, y, x + diameter, y + diameter, self._polyOpts,
start=startAngle, extent=extent )
self._tkcanvas.update_idletasks()
return obj
## Draws text on the canvas. The text is drawn such that an anchor point on a
# bounding box is positioned at a given point on the canvas. The default
# position of the anchor is in the upper-left (northwest) corner of the
# bounding box. The anchor position can be changed using the setTextAnchor()
# method. The text is drawn using the default font family, size, and style.
# The setTextFont() method can be used to change those characteristics. The
# text to be drawn can consists of multiple lines, each separated by a
# newline character. The justification of the text can be set when drawing
# multiple lines of text.
# @param x, y The position on the canvas at which the anchor point of the
# bounding box is positioned.
# @param text A string containing the text to be drawn on the canvas.
#
def drawText(self, x, y, text) :
self._checkValid()
self._textOpts["text"] = text
obj = self._tkcanvas.create_text( x, y, self._textOpts )
self._tkcanvas.update_idletasks()
return obj
#--- Methods that can be used to manipulate the item previously drawn on the
#--- canvas. Each drawing method returns a unique id number used to identify
#--- the resulting shape. See the online documentation for more information.
def shiftItem(self, itemId, dx, dy) :
self._checkContains(itemId)
self._tkcanvas.move(itemId, dx, dy)
self._tkcanvas.update_idletasks()
def removeItem(self, itemId) :
self._checkContains(itemId)
self._tkcanvas.delete(itemId)
self._tkcanvas.update_idletasks()
def showItem(self, itemId) :
self._checkContains(itemId)
self._tkcanvas.itemconfig(itemId, state = "normal")
self._tkcanvas.update_idletasks()
def hideItem(self, itemId) :
self._checkContains(itemId)
self._tkcanvas.itemconfig(itemId, state = "hidden")
self._tkcanvas.update_idletasks()
def raiseItem(self, itemId, above = None) :
self._checkContains(itemId)
self._tkcanvas.tag_raise(itemId)
self._tkcanvas.update_idletasks()
def lowerItem(self, itemId, below = None) :
self._checkContains(itemId)
self._tkcanvas.tag_lower(itemId)
self._tkcanvas.update_idletasks()
def __contains__(self, itemId):
if self._tkcanvas.winfo_ismapped() :
return len(self._tkcanvas.find_withtag(itemId)) > 0
else :
return False
def itemType(self, itemId) :
self._checkContains(itemId)
return self._canvas.type(itemId)
def items( self ):
self._checkValid()
return self.find_all()
#--- Helper methods.
def _checkValid( self ):
if self._win.isClosed() :
raise GraphicsWinError()
def _checkContains(self, itemId) :
if self._win.isClosed() : raise GraphicsWinError()
if itemId in self : raise GraphicsObjError()
## This class defines a basic top level window that can contains a digital
# image, the pixels of which can be accessed or set.
#
class ImageWindow :
## Creates a new image window with an empty image.
# @param width The horizontal size of the image in pixels.
# @param height The vertical size of the image in pixels.
#
def __init__(self, width = 400, height = 400):
global TheMainWindow
# Is the window open or closed. It must be open to use most operations.
self._isClosed = False
# If this is the first toplevel window, remember it as the main window. The
# event loop is only terminated when the main window is closed.
if TheMainWindow is None :
TheMainWindow = self
# Create a top-level window for the graphics window.
self._tkwin = tk.Toplevel(rootWin, width=width, height=height,
borderwidth=0, padx=0, pady=0, bd=0)
self._tkwin.protocol("WM_DELETE_WINDOW", self.close)
self._tkwin.title("")
# Create the photo image and tk canvas inside the window.
self._tkimage = tk.PhotoImage(width=width, height=height)
self._tkcanvas = tk.Canvas(self._tkwin, width=width, height=height,
bg = "white", bd = 0)
# Add the photo image object to the canvas.
self._tkcanvas.create_image(0, 0, anchor="nw", image=self._tkimage)
self._tkcanvas.pack()
# Bring the window to the front of all other windows and force an update.
self._tkwin.lift()
self._tkwin.resizable(0, 0)
self._tkwin.update_idletasks()
## Sets the title of the window. By default, the window has no title.
# @param title A text string to which the title of the window is set. To
# remove the title, use pass an empty string to the method.
#
def setTitle(self, title) :
self._tkwin.title( title )
## Returns a Boolean indicating whether the window exists or was previously closed.
# Window operations can not be performed on a closed window.
# @return @c True if the window is closed and @c False otherwise.
#
def isClosed(self) :
return self._isClosed
## Hides or iconifies the top level window.
# The window still exists and can be displayed again using the show method.
#
def hide(self) :
if self._isClosed : raise GraphicsWinError()
self._tkwin.withdraw()
self._tkwin.update_idletasks()
## Shows or deiconifies a previously hidden top level window.
#
def show(self) :
if self._isClosed : raise GraphicsWinError()
self._tkwin.deiconify()
self._tkwin.update_idletasks()
## Closes and destroys the window. A closed window can not be used.
#
def close( self ):
# Set the closed flag to true so no other ops can be performed on
# the window object.
if self._isClosed : return
self._isClosed = True
# Destroy the window and force an update so it will close when
# used in IDLE or Wing IDE.
self._tkwin.destroy()
self._tkwin.update_idletasks()
# Terminate the mainloop so the program will exit.
self._tkwin.quit()
## Starts the event loop which handles various window events. This causes
# the sequential execution of the program to stop and wait for the user
# to click the close button on the main window or to call the quit method
# on any window. This method should only be called on the main window.
#
def wait( self ):
if self._isClosed : raise GraphicsWinError()
self._tkwin.mainloop()
## Sets a pixel to a given RGB color.
# @param row, col The pixel coordinates.
# @param red, green, blue The discrete RGB color components in the range [0..255].
#
def setPixel(self, row, col, red, green, blue) :
self._tkimage.put("#%02x%02x%02x" % (red, green, blue), (col, row))
## Returns a 3-tuple containing the RGB color of a given pixel.
# @param row, col The pixel coordinates.
# @returns An RGB color as a 3-tuple.
#
def getPixel(self, row, col) :
string = self._tkimage.get(col, row)
parts = string.split()
return (int(parts[0]), int(parts[1]), int(parts[2]))
# --- Defines special graphics exceptions that are raised when an error
# --- occurs in a GraphicsWindow method.
class GraphicsError( Exception ) :
def __init__( self, message ):
super(GraphicsError, self).__init__( message )
class GraphicsObjError( GraphicsError ) :
def __init__( self ):
super(GraphicsObjectError, self).__init__( "Invalid object id." )
class GraphicsWinError( GraphicsError ) :
def __init__( self ):
super(GraphicsWinError, self).__init__(
"Operation can not be performed on a closed window." )
class GraphicsParamError( GraphicsError ) :
def __init__( self, message ):
super(GraphicsParamError, self).__init__( message )
# --- Create an invisible root window and initialize the Tk system.
rootWin = tk.Tk()
rootWin.withdraw()
# --- Remember the first toplevel window created which serves as the main window.
TheMainWindow = None