## @package ezgraphics.py # # This module is part of the Python Graphics and GUI Toolkit which is an # ongoing open source project designed to provide simple Python GUI 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. # # (c) 2015 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. # # Version: 2.1 # 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. It also # allows for an optional menu bar displayed at the top of the window and # a status bar displayed at the bottom of the window. Mouse and keyboard # events can be captured and handled using a callback function or # methods. # class GraphicsWindow : # Class variable used to identify the first toplevel window created, # which serves as the main window. _MainWindow = None ## 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) : # The window is initially visible, unless both arguments are None. visible = True if width is None and height is None : width = 400 height = 400 visible = False # If this is the first toplevel window, it has to be created as the # child of Tk's root window. Otherwise, each new toplevel window is # created as the child of the main window. if GraphicsWindow._MainWindow is None : GraphicsWindow._MainWindow = self self._tkwin = tk.Toplevel(_rootWin, padx=0, pady=0, bd=0) else : self._tkwin = tk.Toplevel(GraphicsWindow._MainWindow._tkwin, padx=0, pady=0, bd=0) # Used to implement the event loop for the main window. self._waitVar = None # Create the new window. self._tkwin.protocol("WM_DELETE_WINDOW", self.close) self._tkwin.title("") # A menu bar is created, but not used by default. To use a menu bar, # the user must call showMenu() and configure it by accessing the # menu via the menu() method. self._menubar = GraphicsMenu(self) # Create a canvas inside the top-level window that is used # for drawing the graphical shapes and text. self._canvas = GraphicsCanvas(self, width, height) # Create a status bar that can be used at the bottom of the window. self._tkstatus = tk.Label(self._tkwin, text="", anchor="w", relief=tk.SUNKEN) # Used to implement the local event loops for getMouse(), getKey() # and sleep(). self._eventVar = tk.IntVar() self._eventVar.set(0) self._eventValue = None # References to the callback function or class when the _onXYZ # methods are not used. self._cbRoutine = None self._cbObject = None # A timer can be set that does not stop or pause the event loop. When # the timer is set, we must save it's id number. self._tktimer = None # Keep track of the id number of an image displayed over the entire # canvas. self._imgId = None # If the window is initially visible, then bring the window to the # front of all other windows and force an update. if visible : self._tkwin.lift() self._tkwin.resizable(0, 0) self._tkwin.update_idletasks() else : self._tkwin.withdraw() # Is the window open and valid? self._valid = True ## Closes and destroys the window. The window object still exist, but # the closed window can not be accessed. If the main window is closed, # the event loop is terminated. # def close(self) : # We can not close a window that was previously closed. if not self._valid : return # If this is the main window being closed, then the event loop has # to be terminated. The GUI app remains active and new windows can # be created. To terminate the GUI app, use quit(). if self is GraphicsWindow._MainWindow : self._tkwin.update_idletasks() # If we are in a local event loop (getMouse, getKey, pause), # then we must terminate that loop before destroying the windows. if self._eventVar.get() == 1 : self._eventVar.set(0) # Destroy the main window and terminate the mainloop. self._tkwin.destroy() GraphicsWindow._MainWindow = None self._waitVar.set(1) # Destroy the window and force an update so it will close when # used in IDLE or the Wing IDE. else : self._tkwin.destroy() self._tkwin.update_idletasks() # Indicate that the window has been closed. self._valid = False ## Terminates the GUI app. The program continues execution immediately # after the call to the wait() method, but no new windows can be created. # def quit(self) : _rootWin.destroy() ## 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._valid and self == GraphicsWindow._MainWindow and self._waitVar is None): # We use a variable for the event loop so it can be stopped without # having to destroy the root window. self._waitVar = tk.IntVar() self._waitVar.set(0) self._tkwin.wait_variable(self._waitVar) # Clear the wait variable after the event loop ends. self._waitVar = None def getMouse(self) : def _onMouse(event) : self._eventVar.set(0) self._eventValue = (event.x, event.y) if self._valid : self._tkwin.update() self._eventVar.set(1) self._canvas._tkcanvas.bind("", _onMouse) self._tkwin.wait_variable(self._eventVar) self._canvas._tkcanvas.unbind("") return self._eventValue else : return (-1, -1) def getKey(self) : def _onKey(event) : self._eventVar.set(0) if len(event.char) == 0 : self._eventValue = event.keysym else : num = ord(event.char) if num == 8 or num == 9 or num == 13 or num == 27 or num == 127: self._eventValue = event.keysym else : self._eventValue = event.char if self._valid : self._tkwin.update() self._eventVar.set(1) self._tkwin.bind("", _onKey) self._tkwin.wait_variable(self._eventVar) self._tkwin.unbind("") return self._eventValue else : return "" def sleep(self, msTime) : def _onSleep() : self._eventVar.set(0) if self._tkwin.winfo_exists() : self._tkwin.update() self._eventVar.set(1) self._tkwin.after(msTime, _onSleep) self._tkwin.wait_variable(self._eventVar) ## 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 ## 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 True if the window is closed and False otherwise. # def isValid(self): return self._valid ## Hides or iconifies the top level window. # The window is not destroyed, it's simply hidden from view and # can be displayed again using the show() method. # def hide(self): self._tkwin.withdraw() self._tkwin.update() ## Shows or deiconifies a previously hidden window. # def show(self): self._tkwin.deiconify() self._tkwin.update() ## Returns a reference to the menu bar associated with the window. # A menu bar will be displayed after this method is called the first # time and the menu has been configured. # @return a reference to a GraphicsMenu object. # def menu(self) : return self._menubar ## Displays the menu bar at the top of the window. # The menu() method is used to access the menu for configuration. # def showMenu(self) : self._tkwin.config(menu=self._menubar._tkmenu) ## Hides the menu bar and removes it from the window. # The menu still exists and can be displayed again and/or configured # further. # def hideMenu(self) : self._tkwin.config(menu="") ## Displays a status bar at the bottom of the window that can be # used to display text messages. # def showStatus(self) : self._tkstatus.pack(side=tk.BOTTOM, fill=tk.X) ## Hides the status bar by removing it from the bottom of the window. # Hiding the bar does not destroy it or remove the text contained in # the bar. # def hideStatus(self) : self._tkstatus.pack_forget() ## Sets the text message displayed in the message bar. # By default, the message is aligned to the left side of the bar. # @param text The text string to be displayed in the status bar. # def setStatus(self, text="") : self._tkstatus.config(text=text) ## Modifies one or more configuration options of the status bar. # The options and settings are specified as keyword parameters. # @param options One or more named parameters. The options that can # be configured include the following: # # fg: the text color specified as a string. # bg: the background color specified as a string. # anchor: a string command that specifies how the text is aligned # within the status bar. The valid values are: # "n", "s", "e", "w", "center", "nw", "ne", "sw", "se" # font: a tuple indicating the font used to draw the text message. # justify: a string command that specifies how multiple lines of # text should be aligned. The valid values are: # "left", "center", "right" # padx: extra horizontal padding added around the text. # pady: extra veritcal padding added around the text. # def configStatus(self, **options) : self._tkstatus.config(options) ## Enables one or more action events on the window. # w.setEvent(event1, event2, ...) # # @param events # def enableEvents(self, *events) : for eventType in events : eventType = eventType.lower() if eventType == "keypress" : self._tkwin.bind("", self._handleEvents) elif eventType == "mousemove" : self._canvas._tkcanvas.bind("", self._handleEvents) elif eventType == "mousedown" : self._canvas._tkcanvas.bind("", self._handleEvents) elif eventType == "mouseup" : self._canvas._tkcanvas.bind("", self._handleEvents) ## Clears or deactivates one or more window events that were set using # the enableEvents() method. # @param events # def clearEvents(self, *events) : for eventType in events : eventType = eventType.lower() if eventType == "keypress" : self._tkwin.unbind("") elif eventType == "mousemove" : self._canvas._tkcanvas.unbind("") elif eventType == "mousedown" : self._canvas._tkcanvas.unbind("") elif eventType == "mouseup" : self._canvas._tkcanvas.unbind("") ## Sets the event handler to a user defined function or object instead # of using the onXYZ callback methods. # def setEventHandler(self, handler) : if hasattr(handler, "__call__") : self._cbRoutine = handler self._cbObject = None else : self._cbObject = handler self._cbRoutine = None ## Sets a timer that triggers an alarm after it expires. The time is # given in milliseconds. # def setTimer(self, msTime) : event = tk.Event() event.type = "Alarm" theCommand = lambda: self.onAlarm(event) self._tktimer = self._tkwin.after(msTime, theCommand) ## Clears the timer before it expires. # def clearTimer(self) : if self._tktimer is not None : self._tkwin.after_cancel(self._tktimer) self._tktimer = None #--- Call back routines that can be overridden in a derived class in # order to handle various events. These methods are documented in the # documentation.py file. They should not be called directly. def onMenuSelect(self, event) : if self._cbObject : self._cbObject.onMenuSelect(self, event) elif self._cbRoutine : self._cbRoutine(self, event) def onMouseMove(self, event) : if self._cbObject : self._cbObject.onMouseMove(self, event) elif self._cbRoutine : self._cbRoutine(self, event) def onMouseDrag(self, event) : if self._cbObject : self._cbObject.onMouseDrag(self, event) elif self._cbRoutine : self._cbRoutine(self, event) def onMouseDown(self, event): if self._cbObject : self._cbObject.onMouseDown(self, event) elif self._cbRoutine : self._cbRoutine(self, event) def onMouseUp(self, event): if self._cbObject : self._cbObject.onMouseUp(self, event) elif self._cbRoutine : self._cbRoutine(self, event) def onKeyPress(self, event) : if self._cbObject : self._cbObject.onKeyPress(self, event) elif self._cbRoutine : self._cbRoutine(self, event) def onAlarm(self, event) : if self._cbObject : self._cbObject.onAlarm(self, event) elif self._cbRoutine : self._cbRoutine(self, event) ## Helper method that handles the tkevents. # def _handleEvents(self, event) : if event.type == "2" : event.type = "KeyPress" self.onKeyPress(event) elif event.type == "4" : event.type = "MouseDown" event.button = int(event.num) self.onMouseDown(event) elif event.type == "5" : event.type = "MouseUp" event.button = int(event.num) self.onMouseUp(event) elif event.type == "6" : # If one of the mouse buttons were pressed, this is a mouse drag. if event.state & 0xF00 : if event.state & 0x100 != 0 : event.button = 1 elif event.state & 0x200 != 0 : event.button = 2 elif event.state & 0x400 != 0 : event.button = 3 else : event.button = 0 event.type = "MouseDrag" self.onMouseDrag(event) # Otherwise, it's a mouse move. else : event.button = 0 event.type = "MouseMove" self.onMouseMove(event) ## 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._arcStyle = "pieslice" self._textOpts = {"text" : "", "justify" : tk.LEFT, "anchor" : tk.NW, "fill" : "black", "font" : ("helvetica", 10, "normal")} # Tk requires the application to maintain a reference to the images # that are drawn on the canvas. For convenience, we maintain a # dictionary of the image references. self._images = {} # 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 for the shapes and text. Thus, when a # geometric shape or text is drawn on the canvas, the item is # maintained internally as an object until cleared. # def clear(self): self._tkcanvas.delete(tk.ALL) self._images = {} self._tkcanvas.update_idletasks() self._win._imgId = None ## 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.setColor(colorname) # c.setColor(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 if self._polyOpts["dash"] : self._polyOpts["dash"] = (4 * size, 4 * 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 == "dashed" : width = self._polyOpts["width"] self._polyOpts["dash"] = (4 * width, 4 * width) else : self._polyOpts["dash"] = None ## Sets the style used when drawing an arc on the canvas. # @param style, The style of the arc. It can be one of the strings: # "pieslice", "chord", or "arc". The default is "pieslice". # def setArcStyle(self, style) : if style not in ("pieslice", "chord", "arc") : raise GraphicsParamError("Invalid arc style.") self._arcStyle = "pieslice" ## 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 in ("left", "center", "right") : self._textOpts["justify"] = style else : raise GraphicsParamError("Invalid justification value.") #--- The object 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 four forms of the method: # # c.drawPoly(x1, y1, x2, y2, ..., xN, yN) # c.drawPoly(sequence of ints) # c.drawPoly((x1, y1), (x2, y2), ..., (xN, yN)) # c.drawPoly(sequence of 2-tuples) # # @returns An integer that uniquely identifies the new canvas item. # def drawPoly(self, *coords): minCoords = 6 # 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 type(expCoords[0]) == list or type(expCoords[0]) == tuple : minCoords = 3 if len(expCoords) < minCoords : 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, style=self._arcStyle, 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 ## Draws an image onto the canvas. # def drawImage(self, x, y = None, image = None) : if type(x) == GraphicsImage : image = x self.setWidth(image.width()) self.setHeight(image.height()) obj = self._tkcanvas.create_image(0, 0, anchor="nw", image=image._tkimage) else : obj = self._tkcanvas.create_image(x, y, anchor="nw", image=image._tkimage) # self._images[obj] = image self._tkcanvas.update_idletasks() return obj #--- Methods that can be used to manipulate the items previously drawn #--- on the canvas. Each canvas drawing method returns a unique id number #--- used to identify the resulting object. ## 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. The item is shifted by a given # amount in both the horizontal and vertical directions. # @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() ## Scales or resizes a geometric shape on the canvas. # The coordinates that define the given geometric shape are modified # by a given scale factor. # @param itemId The id number of the item to be resized. # @param xScale, yScale - the horizontal and vertical scale factor. # @param xOffset, yOffset - the horizontal and vertical offset. # def scaleItem(self, itemId, xScale, yScale, xOffset = None, yOffset = None) : if xOffset is None and yOffset is None : bbox = self._tkcanvas.bbox(itemId) xOffset = (bbox[2] - bbox[0]) // 2 + bbox[0] yOffset = (bbox[3] - bbox[1]) // 2 + bbox[1] self._tkcanvas.scale(itemId, xOffset, yOffset, xScale, yScale) 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) if itemId in self._images : self._images.pop(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 an item on the canvas. # 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 belowId 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 containing the id numbers of all items on the canvas, # 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() self._tkcanvas.update_idletasks() ## Returns the id number of the item that is just above the given target # item on the canvas stack. # @param itemId the id number of the target item. # @returns the id number of the item that is just above the target item. # If the target item is at the top of the stack, 0 is returned. # def itemAbove(self, itemId) : idList = self._tkcanvas.find_above(itemId) if len(idList) == 0 : return 0 else : return idList[0] ## Returns the id number of the item that is just below the given target # item on the canvas stack. # @param itemId the id number of the target item. # @returns the id number of the item that is just below the target item. # If the target item is at the bottom of the stack, 0 is returned. # def itemBelow(self, itemId) : idList = self._tkcanvas.find_below(itemId) if len(idList) == 0 : return 0 else : return idList[0] ## This class defines a basic top level window that can display a # digital GraphicsImage. # class ImageWindow(GraphicsWindow) : ## Creates a new window for displaying images. This provides a quick # way to display images without having to access and draw on the # canvas of a graphics window. # def __init__(self) : super().__init__(None, None) self._imgId = None ## Displays an image in the window. The window is resized to fit tightly # around the image. # @param image The GraphicsImage object containing the image to # be displayed. # def display(self, image = None) : if self._imgId is not None : self._canvas._tkcanvas.delete(self._imgId) self._imgId = None if image is None : return width = image.width() height = image.height() self._canvas._tkcanvas.config(width=width, height=height) self._canvas._width = width self._canvas._height = height self._tkwin.deiconify() self._imgId = self._canvas._tkcanvas.create_image( 0, 0, anchor="nw", image=image._tkimage) self._canvas._tkcanvas.update_idletasks() self._tkwin.update() ## This class defines an RGB digital image that is contained within an # ImageWindow. # class GraphicsImage : ## Creates a new graphics image. # def __init__(self, width = None, height = None) : # Create the photo image. if height is None and type(width) == str : filename = width self._tkimage = tk.PhotoImage(file = filename) else : self._tkimage = tk.PhotoImage(width = width, height = height) ## Gets the width of the image in pixels. # @return The width of the image. # def width(self) : return self._tkimage.width() ## Gets the height of the image in pixels. # @return The width of the image. # def height(self) : return self._tkimage.height() ## Sets a pixel to a given RGB color. # There are two forms of the method: # # win.setPixel(row, col, red, green, blue) # win.setPixel(row, col, hexColor) # win.setPixel(row, col, pixel) # # @param row, col (int) The pixel coordinates. # @param red, green, blue (int) The discrete RGB color components in # the range [0..255]. # @param hexColor The RGB color components specified as a hex string. # @param pixel The RGB color components specified as a 3-tuple. # def setPixel(self, row, col, *rgbColor) : if len(rgbColor) == 1 : if type(rgbColor[0]) == str : color = rgbColor[0] else : rgbColor = tuple(*rgbColor) color = "{#%02x%02x%02x}" % rgbColor else : color = "{#%02x%02x%02x}" % rgbColor self._tkimage.put(color, (col, row)) ## Returns a 3-tuple containing the RGB color of a given pixel. # @param row, col (int) The pixel coordinates. # @return An RGB color as a 3-tuple. # def getPixel(self, row, col) : result = self._tkimage.get(col, row) if type(result) == str : parts = result.split() return (int(parts[0]), int(parts[1]), int(parts[2])) else : return result ## Returns the red component of the RGB color of a given pixel. # @param row, col (int) The pixel coordinates. # @return The value of the red component of the given pixel. def getRed(self, row, col) : pixel = self.getPixel(row, col) return pixel[0] ## Returns the green component of the RGB color of a given pixel. # @param row, col (int) The pixel coordinates. # @return The green component of the given pixel. def getGreen(self, row, col) : pixel = self.getPixel(row, col) return pixel[1] ## Returns the blue component of the RGB color of a given pixel. # @param row, col (int) The pixel coordinates. # @return The blue component of the given pixel. def getBlue(self, row, col) : pixel = self.getPixel(row, col) return pixel[2] ## Clears the image and removes all of the pixels but the size of the # image remains the same. # def clear(self) : self._tkimage.blank() ## Creates a duplicate copy of the image. # def copy(self) : image = GraphicsImage(1, 1) image._tkimage = self._tkimage.copy() return image ## Saves the digital image to a file in either the gif or ppm format. # @param filename, The full name of a gif or ppm image file. # @param format, The format in which to save the image. It can be either # "gif" or "ppm". The default is "gif". # def save(self, filename, format="gif") : if format not in ("gif", "ppm") : raise GraphicsParamError( "Invalid image format.") self._tkimage.write(filename, format=format) ## This class defines a menu container into which menu options can be added. # Each option can be associated with a command code or a pull-down menu. # When an option with a command code is selected, the menu callback routine # is called with the given code. # class GraphicsMenu : def __init__(self, win, menu = None) : self._win = win self._menu = menu if menu : self._tkmenu = tk.Menu(menu._tkmenu, tearoff=0) else : self._tkmenu = tk.Menu(win._tkwin, tearoff=0) def addSubMenu(self, label) : submenu = GraphicsMenu(self._win, self) self._tkmenu.add_cascade(label=label, menu=submenu._tkmenu) return submenu def addOption(self, label, cmdCode) : if hasattr(cmdCode, "__call__") : theCommand = cmdCode else : event = tk.Event() event.type = "MenuSelect" event.menutype = "item" event.cmdcode = cmdCode theCommand = lambda: self._win.onMenuSelect(event) self._tkmenu.add_command(label=label, command=theCommand) def addSeparator(self) : self._tkmenu.add_separator() def addCheckButton(self, label, cmdCode, checked = False) : boolVar = tk.BooleanVar() boolVar.set(checked) if hasattr(cmdCode, "__call__") : theCommand = cmdCode else : event = tk.Event() event.type = "MenuSelect" event.menutype = "check" event.cmdcode = cmdCode event.var = boolVar theCommand = lambda: self._win.onMenuSelect(event) self._tkmenu.add_checkbutton(label=label, variable=boolVar, onvalue=1, offvalue=0, command=theCommand) return boolVar ## Adds a group of radio buttons to a menu. # The entries for the radio button group are passed as a sequence of # strings. The entries are added to the menu in the order that they # occur within the sequence. The currently selected entry is indicated # by its position within the group, with the first entry at position 1. # The radio button that is initially selected can be specified, # otherwise, the first entry in the sequence is used. # @param buttonLabels A sequence of string that serve as the labels # for the radio buttons in the group. # @param cmdCode The code associated with the group of radios buttons # that will be passed to the callback routine when a radio # button is selected. # @returns A reference to a special IntVar object that contains the # value of the currently selected radio button. This value # can be accessed using the objects get() method. # def addRadioButtons(self, buttonLabels, cmdCode, initValue=None) : intVar = tk.IntVar() if initValue is None : intVar.set(1) else : intVar.set(initValue) if hasattr(cmdCode, "__call__") : theCommand = cmdCode else : event = tk.Event() event.type = "MenuSelect" event.menutype = "radio" event.cmdcode = cmdCode event.var = intVar theCommand = lambda: self._win.onMenuSelect(event) num = 1 for entry in buttonLabels : self._tkmenu.add_radiobutton(label=entry, variable=intVar, value=num, command=theCommand) num = num + 1 return intVar # --- 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(GraphicsError, 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 the root window and initialize the Tk system. _rootWin = tk.Tk() _rootWin.withdraw()