HOME/Articles/

Draw

Article Outline

Example Python program Draw.py Python version 3.x or newer. To check the Python version use:

python --version

Modules

  • import time
  • import os
  • import sys
  • import string
  • import tkinter
  • from tkinter import font
  • import sys

Methods

  • def color(r, g, b):
  • def _on_closing():
  • def setCanvasSize(w=_DEFAULT_CANVAS_SIZE, h=_DEFAULT_CANVAS_SIZE):
  • def setBackground(c=WHITE):
  • def setColor(c=_DEFAULT_PEN_COLOR):
  • def setFontFamily(f=_DEFAULT_FONT_FAMILY):
  • def availableFonts():
  • def setFontSize(s=_DEFAULT_FONT_SIZE):
  • def setFontBold(b = None):
  • def setFontItalic(i = None):
  • def _makeSureWindowCreated():
  • def line(x0, y0, x1, y1):
  • def oval(x, y, width, height):
  • def filledOval(x, y, width, height):
  • def rect(x, y, width, height):
  • def filledRect(x, y, width, height):
  • def polygon(pts):
  • def filledPolygon(pts):
  • def string(s, x, y):
  • def _getPhotoImage(name):
  • def _getPhotoImageReference(w, h):
  • def picture(pic, x=0, y=0, mag=1):
  • def clear():
  • def _doUpdate(inShow = False):
  • def show(msec=0):
  • def _leftButtonCallback(event):
  • def _rightButtonCallback(event):
  • def _keyCallback(event):
  • def hasNextKeyTyped():
  • def nextKeyTyped():
  • def mousePressed():
  • def mouseX():
  • def mouseY():
  • def mouseLeft():
  • def mouseRight():
  • def _regressionTest():
  • def _main():

Code

Python tkinter example

"""
Draw.py

The Draw module defines functions that allow the user to create a
drawing.  A drawing appears on the canvas.  The canvas appears
in the window.  

This module has been recoded by AJB to remove references to 
pygame, and solely uses tkinter.

Original version is from Sedgwick, Princeton University
"""
import time
import os
import sys
import string
import tkinter

from tkinter import font


#-----------------------------------------------------------------------

# create a custom color
def color(r, g, b):
    return '#%02x%02x%02x' % (r, g, b)

# Handy pre-defined colors

WHITE      = '#ffffff'
BLACK      = '#000000'
GRAY       = '#888888'
RED        = '#ff0000'
GREEN      = '#00ff00'
BLUE       = '#0000ff'
CYAN       = '#00ffff'
MAGENTA    = '#ff00ff'
YELLOW     = '#ffff00'
DARK_RED   = '#800000'
DARK_GREEN = '#008000'
DARK_BLUE  = '#000080'
DARK_GRAY  = '#404040'
LIGHT_GRAY = '#C0C0C0'
ORANGE     = '#ffc800'
VIOLET     = '#ee82ee'
PINK       = '#ffafaf'


# Default Sizes and Values


_DEFAULT_CANVAS_SIZE = 512

_DEFAULT_PEN_COLOR = BLACK

_DEFAULT_FONT_FAMILY = 'Helvetica'
_DEFAULT_FONT_SIZE = 12
_DEFAULT_FONT_WEIGHT = 'normal'
_DEFAULT_FONT_SLANT = 'roman'

_validFontFamilies = None
_fontFamily = _DEFAULT_FONT_FAMILY
_fontSize   = _DEFAULT_FONT_SIZE
_fontWeight = _DEFAULT_FONT_WEIGHT
_fontSlant  = _DEFAULT_FONT_SLANT

_canvasWidth = float(_DEFAULT_CANVAS_SIZE)
_canvasHeight = float(_DEFAULT_CANVAS_SIZE)
_penColor = _DEFAULT_PEN_COLOR



_canvas = None
_tkWindow = None

_images = []
_imageRefs = []

# Has the window been created?
_windowCreated = False

# Do we want to update the display only upon calling show() ?
_showMode = False

# the queue (really a list) of keys that have been typed, awaiting processing
_keysTyped = []

# the queue of mouse clicks. Each element in the queue consists of
# a tuple containing x, y, "left" or "right"
_clicks = []

# The position of the mouse as of the most recent mouse click
_mousePos = None



def _on_closing():
    global _tkWindow
    _tkWindow.destroy()


def setCanvasSize(w=_DEFAULT_CANVAS_SIZE, h=_DEFAULT_CANVAS_SIZE):
    """
    Set the size of the canvas to w pixels wide and h pixels high.
    Calling this function is optional. If you call it, you must do
    so before calling any drawing function.
    """
    global _background
    # global _surface
    global _tkWindow
    global _canvas
    global _canvasWidth
    global _canvasHeight
    global _windowCreated
    global _validFontFamilies

    if _windowCreated:
        raise Exception('The Draw window already was created')

    if (w < 1) or (h < 1):
        raise Exception('width and height must be positive')

    _canvasWidth = w
    _canvasHeight = h

    _tkWindow = tkinter.Tk()
    _tkWindow.title("Draw")
    _tkWindow.protocol("WM_DELETE_WINDOW", _on_closing)
    _canvas = tkinter.Canvas(_tkWindow, width=w, height=h, 
                             bd=0, highlightthickness=0)
    _canvas.pack()
    _canvas.config(bg=WHITE)
    #print("Doing the bindings")
    _canvas.bind("<Button-1>", _leftButtonCallback)
    _canvas.bind("<Button-2>", _rightButtonCallback)
    _canvas.bind("<Button-3>", _rightButtonCallback)
    #print("Done with the bindings")
    _canvas.bind_all("<Key>",  _keyCallback)

    _validFontFamilies = list(tkinter.font.families())

    _windowCreated = True

    _tkWindow.update()

def setBackground(c=WHITE):
    global _canvas
    _makeSureWindowCreated()
    _canvas.config(bg=c)

def setColor(c=_DEFAULT_PEN_COLOR):
    """
    Set the pen color to c
    c defaults to BLACK.
    """
    global _penColor
    _makeSureWindowCreated()    
    _penColor = c

def setFontFamily(f=_DEFAULT_FONT_FAMILY):
    """
    Set the font family to f (e.g. 'Helvetica' or 'Courier' or 'Times').
    """
    global _fontFamily
    global _validFontFamilies

    _makeSureWindowCreated()    
    _fontFamily = f

    if not (f in _validFontFamilies):
        errStr = "Invalid font " + f + ", not one of " + str(_validFontFamilies)
        raise Exception(errStr)

def availableFonts():
    _makeSureWindowCreated()  
    return _validFontFamilies


def setFontSize(s=_DEFAULT_FONT_SIZE):
    """
    Set the font size to s (e.g. 12 or 16).
    """
    global _fontSize
    _fontSize = s

def setFontBold(b = None):
    """
    Set the bolding as desired. The default weight is no bolding.
    """
    global _fontWeight
    if not b:
        _fontWeight = _DEFAULT_FONT_WEIGHT

    else:
        _fontWeight = 'bold'

def setFontItalic(i = None):
    """
    Set the italic as desired. The default italic is none.
    """
    global _fontSlant
    if not i:
        _fontSlant = _DEFAULT_FONT_SLANT

    else:
        _fontSlant = 'italic'



#-----------------------------------------------------------------------

def _makeSureWindowCreated():
    global _windowCreated
    if not _windowCreated:
        setCanvasSize()
        _windowCreated = True

#-----------------------------------------------------------------------

# Functions to draw shapes, text, and images

def line(x0, y0, x1, y1):
    """
    Draw a line from (x0, y0) to (x1, y1).
    """

    global _canvas
    global _penColor

    _makeSureWindowCreated()

    _canvas.create_line(x0, y0, x1, y1, capstyle=tkinter.ROUND, fill = _penColor)

    _doUpdate()


def oval(x, y, width, height):
    """
    Outline largest oval that fits in a box of size width x height
    with top left at (x,y)
    """

    global _canvas
    global _penColor

    _makeSureWindowCreated()

    # it appears that the -1 is needed for x1 and y1 when drawing an outline, 
    # similar to when doing an outline of a rectangle. This was confirmed 
    # by magnified visual inspection of a rectangle and oval.
    _canvas.create_oval(x, y, x+width-1, y+height-1, outline = _penColor)

    _doUpdate()

def filledOval(x, y, width, height):
    """
    Fill largest oval that fits in a box of size width x height
    with top left at (x,y)
    """
    global _canvas
    global _penColor

    _makeSureWindowCreated()

    _canvas.create_oval(x, y, x+width, y+height, fill = _penColor, width=0)

    _doUpdate()

def rect(x, y, width, height):
    """
    Outline a rectangle of width and height whose upper left point is (x, y).
    """
    global _canvas
    global _penColor

    _makeSureWindowCreated()

    # Note that the -1 is needed, since the right and bottom 
    # borders actually are 1 pixel outside the bounding
    # box of the rectangle
    _canvas.create_rectangle(x, y, x+width-1, y+height-1, 
                             outline = _penColor, width=1)

    _doUpdate()

def filledRect(x, y, width, height):
    """
    Fill a rectangle of width and height whose upper left point is (x, y).
    """
    global _canvas
    global _penColor

    _makeSureWindowCreated()

    _canvas.create_rectangle(x, y, x+width, y+height, 
                             fill = _penColor, width=0)

    _doUpdate()


def polygon(pts):
    """
    Outline a polygon with x,y coordinates appearing in pts.
    """
    global _canvas
    global _penColor

    _makeSureWindowCreated()

    _canvas.create_polygon(pts, fill='', outline = _penColor, width = 1)

    _doUpdate()


def filledPolygon(pts):
    """
    Fill a polygon with x,y coordinates appearing in pts.
    """
    global _canvas
    global _penColor

    _makeSureWindowCreated()

    _canvas.create_polygon(pts, fill = _penColor, outline = _penColor)

    _doUpdate()

def string(s, x, y):
    """
    Draw string s at (x, y).
    """
    global _tkWindow
    global _canvas
    global _penColor
    global _fontFamily
    global _fontSize
    global _fontSlant
    global _fontWeight

    _makeSureWindowCreated()

    style = _fontSlant + " " + _fontWeight

    id = _canvas.create_text(x, y, text = s, 
                            fill = _penColor, 
                            anchor=tkinter.NW,
                            font=(_fontFamily, str(_fontSize), style)
                            )
    _doUpdate()

    return _canvas.bbox(id)

# return a PhotoImage object associated with the file "name". 
# Caches the last 50 images loaded.

def _getPhotoImage(name):
    global _images
    for p in _images:
        if p[0] == name:
            return p[1]

    if len(_images) > 50:
        _images.pop(0)

    ans = tkinter.PhotoImage(file = name)

    _images.append([name, ans])
    return ans

# keeps up to 50 photo images alive at any given time. 
# We need to do this since when the image is rendered later,
# the local variable will have gone out of scope.
# See https://tkinter.unpythonic.net/wiki/PhotoImage - 
#  "Disappearing Photoimage"
def _getPhotoImageReference(w, h):
    global _imageRefs

    # We're only going to cache 50 of these at any given time
    if len(_imageRefs) >= 50: _imageRefs.pop(0)

    # Create the photoimage and store a reference to keep it from being gc'ed
    ans = tkinter.PhotoImage(width=w, height=h)
    _imageRefs.append(ans)

    return ans


# The mag parameter only applies to images specified via a 2d array
def picture(pic, x=0, y=0, mag=1):
    """
    Draw pic placing the upper left corner at (x,y).  pic is the 
    name of a .gif, .pgm, or .ppm format image. 
    """
    global _canvas
    _makeSureWindowCreated()

    # picture was specified by a file name
    if type(pic) is str:
        im = _getPhotoImage(pic)

    # picture is, presumably, a 2D grid of colors
    else:
        numRows = len(pic)
        numCols = len(pic[0])
        im = _getPhotoImageReference(numCols*mag, numRows*mag)
        line = ""
        if mag == 1:
            for row in range(numRows):
                line += '{' + ' '.join(pic[row]) + '} '
        else:
            for row in range(numRows):
                magRow = [pic[row][j//mag] for j in range(numCols * mag)]
                theLine = '{' + ' '.join(magRow) + '} '
                for dup in range(mag):
                    line += theLine

        im.put(line)

    _canvas.create_image((x, y), image = im, anchor = tkinter.NW, state='normal')

    _doUpdate()

def clear():
    """
    Clear the canvas to the background color
    """
    global _canvas
    _makeSureWindowCreated()
    _canvas.delete(tkinter.ALL)

    _doUpdate()


#-----------------------------------------------------------------------

def _doUpdate(inShow = False):
    global _showMode

    # if we're in showMode, then only update if inShow is True
    # otherwise, update always

    if (not _showMode) or inShow:
        _tkWindow.update()
        #_tkWindow.update_idletasks()        

def show(msec=0):
    """
    Force an update of the display, and then wait the specified number
    of milliseconds.
    """
    global _tkWindow
    global _canvas
    global _showMode

    _makeSureWindowCreated()

    _showMode = True
    _doUpdate(True)

    if msec > 0: _canvas.after(int(msec))

def _leftButtonCallback(event):
    """
    Check if a left mouse button event has occurred
    """
    global _clicks

    _makeSureWindowCreated()

    _clicks = [(event.x, event.y, "left")] + _clicks


def _rightButtonCallback(event):
    """
    Check if a right mouse button event has occurred
    """
    global _clicks

    _makeSureWindowCreated()

    _clicks = [(event.x, event.y, "right")] + _clicks

def _keyCallback(event):
    """
    Check if a key has been typed, and if so, put that key in a queue.
    """
    global _surface
    global _keysTyped

    _makeSureWindowCreated()

    _keysTyped = [event.keysym] + _keysTyped


# Functions for retrieving keys

def hasNextKeyTyped():
    """
    Return True if the queue of keys the user typed is not empty.
    Otherwise return False.
    """
    global _keysTyped
    _doUpdate(True)  # AJB 
    return _keysTyped != []

def nextKeyTyped():
    """
    Remove the first key from the queue of keys that the the user typed,
    and return that key.
    """
    global _keysTyped
    return _keysTyped.pop()

# Functions for dealing with mouse clicks 

def mousePressed():
    """
    Returns True if a mouse click is available in the queue, False otherwise
    """
    global _clicks
    global _mousePos

    _mousePos = None
    if _clicks != []:
        _mousePos = _clicks.pop()

    _doUpdate(True)
    return _mousePos != None

def mouseX():
    """
    Return the x coordinate in user space of the location at
    which the mouse was most recently left-clicked. If a left-click
    hasn't happened yet, raise an exception, since mouseX() shouldn't
    be called until mousePressed() returns True.
    """
    global _mousePos
    if _mousePos:
        return _mousePos[0]      
    raise Exception(
        "Can't determine mouse position if a click hasn't happened")

def mouseY():
    """
    Return the y coordinate in user space of the location at
    which the mouse was most recently left-clicked. If a left-click
    hasn't happened yet, raise an exception, since mouseY() shouldn't
    be called until mousePressed() returns True.
    """
    global _mousePos
    if _mousePos:
        return _mousePos[1] 
    raise Exception(
        "Can't determine mouse position if a click hasn't happened")

def mouseLeft():
    """
    Return True if the most recent mouse click was a left button click
    """
    global _mousePos
    if _mousePos:
        return _mousePos[2] == "left"
    raise Exception(
        "Can't determine mouse button if click hasn't happened")

def mouseRight():
    """
    Return True if the most recent mouse click was a right button click
    """
    global _mousePos
    if _mousePos:
        return _mousePos[2] == "right"
    raise Exception(
        "Can't determine mouse button if click hasn't happened")

def _regressionTest():
    """
    Perform regression testing.
    """

    clear()
    setBackground(YELLOW)

    # Test handling of mouse and keyboard events.
    setColor(BLACK)
    print('Left click with the mouse or type a key')
    while True:
        if mousePressed():
            filledOval(mouseX()-20, mouseY()-20, 40, 40)
        if hasNextKeyTyped():
            print(nextKeyTyped())
        show(0)

    # Never get here.
    show()

#-----------------------------------------------------------------------

def _main():
    """
    Dispatch to a function that does regression testing, or to a
    dialog-box-handling function.
    """
    import sys
    if len(sys.argv) == 1:
        _regressionTest()
    elif sys.argv[1] == 'getFileName':
        _getFileName()
    elif sys.argv[1] == 'confirmFileSave':
        _confirmFileSave()
    elif sys.argv[1] == 'reportFileSaveError':
        _reportFileSaveError(sys.argv[2])

if __name__ == '__main__':
    _main()