## ezgraphics.py # (c) 2013 by Rance Necaise # http://ezgraphics.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.2 # 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. User and reference guides are available at http://ezgraphics.org. # # New in v1.1: (Sept 2014) # * Changed how the tkinter module is imported. The name of the module was # changed from Tkinter to tkinter starting with Python version 3.3. # We now use a try/except block to try and handle the correct import. # # New in v1.2: (Oct 2014) # * Added descriptive comments for the Canvas methods that allow for # manipulating items on the canvas. # try: import Tkinter as tk except ImportError : 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 # 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() ## 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) : return self._canvas ## Starts the event loop that processes 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._tkwin.winfo_exists() : self._tkwin.mainloop() ## 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 associated with the # object exist and is valid. The object of an invalid window, which # normally results from being closed, can not be used. # @return True if the window is valid or False otherwise. # def isValid(self) : return self._tkwin.winfo_exists() ## Closes and destroys the window. # The object still exist, but the window is not valid and can not be used. # def close(self) : # We can not close a window that was previously closed. if not self._tkwin.winfo_exists() : return # 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() ## Hides or iconifies the top level window. # The window still exists and can be displayed again using the show method. # def hide(self) : self._tkwin.withdraw() self._tkwin.update_idletasks() ## Shows or deiconifies a previously hidden top level window. # def show(self) : 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) : 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) : 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._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._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, style = None, size = None) : origFamily, origSize, origStyle = self._textOpts["font"] if family is None : family = origFamily elif (family is not None and family not in ('helvetica', 'arial', 'courier', 'times', 'times roman')) : raise GraphicsParamError( "Invalid font family." ) if style is None : style = origStyle elif (style is not None and style not in ('bold', 'normal', 'italic', 'bold italic')) : raise GraphicsParamError( "Invalid font style." ) if size is None : size = origSize elif size is not None and (type(size) != int or size <= 0) : raise GraphicsParamError( "Invalid font size." ) 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._fontOpts["justify"] = tk.LEFT elif style == "center" : self._fontOpts["justify"] = tk.CENTER elif style == "right" : self._fontOpts["justify"] = tk.RIGHT else : raise GraphicsParamError( "Invalid justification value." ) ## The same as setTextFont(). This version is deprecated. # def setFont(self, family = None, style = None, size = None ): self.setTextFont(family, style, size) ## The same as setTextAnchor(). This version is deprecated. # def setAnchor(self, position): self.setTextAnchor(position) ## The same as setTextJustify(). This version is deprecated. # def setJustify(self, style): self.setTextJustify(style) #--- 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 ): 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) : 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) : 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 drawRect(self, x, y, width, height) : obj = self._tkcanvas.create_rectangle(x, y, x + width, y + height, self._polyOpts ) self._tkcanvas.update_idletasks() return obj ## The same as drawRect(). # def drawRectangle(self, x, y, width, height) : return self.drawRect(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.drawPoly(x1, y1, x2, y2, ..., xN, yN) # c.drawPoly(sequence) # # @returns An integer that uniquely identifies the new canvas item. # def drawPoly( self, *coords ): # Unwrap the cooridinates which allows the method to accept individual vertices # or a list of vertices. if len(coords) == 1 and (type(coords[0]) == list or type(coords[0] == tuple)) : expCoords = tuple(*coords) else : expCoords = coords 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 drawPoly(). # def drawPolygon(self, *coords) : return self.drawPoly(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 ): 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 ): 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._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. ## Shifts an item on the canvas. The item to be shifted is indicated by # its id number, which was returned when the item was drawn. # @param itemId The id number of the item to be shifted. # @param dx The amount to shift the item in the horizontal direction. # @param dy The amount to shift the item in the vertical direction. # def shiftItem(self, itemId, dx, dy) : self._tkcanvas.move(itemId, dx, dy) self._tkcanvas.update_idletasks() ## Removes an item from the canvas. The item to be removed is indicated # by its id number, which was returned when the item was drawn. # @param itemId The id number of the item to be removed. # def removeItem(self, itemId) : self._tkcanvas.delete(itemId) self._tkcanvas.update_idletasks() ## Shows or unhides an item that was previously hidden. The item to # unhide is indicated by its id number, which was returned when the item # was drawn. # @param itemId The id number of the item to be shown. # def showItem(self, itemId) : self._tkcanvas.itemconfig(itemId, state = "normal") self._tkcanvas.update_idletasks() ## Hides a canvas item. The item is still part of the canvas, but # it is hidden from view. The item to be removed is indicated by its id # number, which was returned when the item was drawn. # @param itemId The id number of the item to be hidden. # def hideItem(self, itemId) : self._tkcanvas.itemconfig(itemId, state = "hidden") self._tkcanvas.update_idletasks() ## Raises an item to the top of the canvas stack or above another item. # @param itemId The id number of the item to be raised. # @param aboveId If provided, the id number of the item above which an # item is raised, otherwise, the item is raised to the # top of the stack. # def raiseItem(self, itemId, aboveId = None) : if aboveId is None : self._tkcanvas.tag_raise(itemId) else : self._tkcanvas.tag_raise(itemId, aboveId) self._tkcanvas.update_idletasks() ## Lowers an item to the bottom of the canvas stack or below another item. # @param itemId The id number of the item to be lowered. # @param aboveId If provided, the id number of the item below which an # item is lowered. Otherwise, the item is lowered to the # bottom of the stack. # def lowerItem(self, itemId, belowId = None) : if belowId is None : self._tkcanvas.tag_lower(itemId) else : self._tkcanvas.tag_lower(itemId, belowId) self._tkcanvas.update_idletasks() ## Determines if an id number is valid. For an id number to be valid, it # must be associated with an item currently on the canvas. Once an item is # removed, the id number is no longer valid. # @returns True if the id number if valid and False otherwise. # def __contains__(self, itemId): if self._tkcanvas.winfo_ismapped() : return len(self._tkcanvas.find_withtag(itemId)) > 0 else : return False ## Returns the type of item associated with the given id number. # @returns A string indicating the type of item associated with the # given id number. The value will be one of the following: # "arc", "line" (note that a pixel is drawn as a line), # "oval", "polygon", "rectangle", "text". # def itemType(self, itemId) : return self._tkcanvas.type(itemId) ## Returns a list of id numbers. The list will contain the id number of # every item currently on the canvas stack, whether visible or not. # @returns A list of integers that correspond to the id numbers of the # shapes and text on the canvas # def items( self ): return self._tkcanvas.find_all() ## 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 # 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 isValid(self) : return self._tkwin.winfo_exists() ## Hides or iconifies the top level window. # The window still exists and can be displayed again using the show method. # def hide(self) : self._tkwin.withdraw() self._tkwin.update_idletasks() ## Shows or deiconifies a previously hidden top level window. # def show(self) : self._tkwin.deiconify() self._tkwin.update_idletasks() ## Closes and destroys the window. A closed window can not be used. # def close( self ): # We can not close a window that was previously closed. if not self._tkwin.winfo_exists() : return # 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._tkwin.winfo_exists() : 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