Monday, August 13, 2012

Tkinter Canvas Items Demo

The following code is based on the Tcl items.tcl demo. A canvas widget is created, a 3x3 grid is drawn and examples of each of the canvas object types (arc, bitmap, line, oval, polygon, rectangle, text, window) are drawn. The items are highlighted on a mouse-over and can be dragged.

Note that under Windows, stippled arcs, ovals and text are not drawn as expected. The Tk manual page warns:
"Note that stipples are not well supported on platforms that do not use X11 as their drawing API."
Canvas text is dragged using the same method as other canvas objects but stippled text is not  dragged cleanly (at least, not using the demo'd drag method).



# File: canvasitems.py
#    http://infohost.nmt.edu/tcc/help/pubs/tkinter//canvas.html
#    http://www.tcl.tk/man/tcl8.5/TkCmd/ttk_scale.htm#M-value
# Ref: 
#    'Python and Tkinter Programming' by John Grayson, p44
#        http://manning.com/grayson/
#    Active state recipe with example of labelled ttk.Scale
#        http://code.activestate.com/recipes/577636-color-study-1/
#
# The following behaviours didn't work in Windows 7 using the
# original Tcl demo and are not implemented here:
#
#    Button-2 drag:  reposition view
#    Button-3 drag:  overstroke of a selected area
#    Ctrl-f:         print of overstroke area

from tkinter import *
from tkinter import ttk
from demopanels import MsgPanel, SeeDismissPanel

class CanvasItemsDemo(ttk.Frame):

    # define colours
    BLUE = 'DeepSkyBlue'
    RED = 'red'
    BISQUE = 'bisque3'
    GREEN = 'SeaGreen3'
    HIGHLIGHT = 'SteelBlue2'
    
    # define fonts
    FONT1 = ('Helv', 12)
    FONT2 = ('Helv', 24, 'bold')
    
    def __init__(self, isapp=True, name='canvasitemsdemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Canvas Items Demo')
        self.isapp = isapp
        
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:
            MsgPanel(self, 
                     ["This window contains a canvas widget with examples of the ",
                      "various kinds of items supported by canvases.  The following ",
                      "operations are supported:\n",
                      "\tItem Highlight:\titem under mouse pointer is highlighted.\n"
                      "\tButton-1 drag:\tmoves item under pointer."])
            
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = Frame(self)
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        self.canvas = self._create_canvas(demoPanel)
        
        # draw items
        self._draw_lines()
        self._draw_curves()
        self._draw_polygons()
        self._draw_rectangles()
        self._draw_ovals()
        self._draw_text()
        self._draw_arcs()
        self._draw_bitmaps()
        self._draw_windows()
        
        self._add_bindings()

    def _create_canvas(self, parent):
        c = Canvas(scrollregion='0c 0c 30c 24c', width='15c',
                   height='10c', relief=SUNKEN, borderwidth=2)

        # Draw a 3x3 rectangular grid.
        c.create_rectangle('0c 0c 30c 24c', width=2)
        c.create_line('0c 8c 30c 8c', width=2)
        c.create_line('0c 16c 30c 16c', width=2)
        c.create_line('10c 0c 10c 24c', width=2)
        c.create_line('20c 0c 20c 24c', width=2)

        # add scrollbars        
        hscroll = Scrollbar(orient=HORIZONTAL, command=c.xview)
        vscroll = Scrollbar(orient=VERTICAL, command=c.yview)
        c['xscrollcommand'] = hscroll.set
        c['yscrollcommand'] = vscroll.set

        # position and define resize behaviour
        c.grid(in_=parent, sticky='news',
               row=0, rowspan=1, column=0, columnspan=1)
        vscroll.grid(in_=parent, row=0, column=1,
                     rowspan=1, columnspan=1, sticky='news')
        hscroll.grid(in_=parent, row=1, column=0,
                     rowspan=1, columnspan=1, sticky='news')
        parent.grid_rowconfigure(0, weight=1, minsize=0)
        parent.grid_columnconfigure(0, weight=1, minsize=0)
        
        return c

    # ===================================================================================
    # Canvas item drawing routines
    # ===================================================================================
    def _draw_lines(self):
        c = self.canvas
        
        c.create_text('5c .2c', text='Lines', anchor=N)
        c.create_line('1c 1c 3c 1c 1c 4c 3c 4c', width='2m',
                      fill=self.BLUE, cap='butt', join='miter',
                      tags=('item', ))  # Z
        c.create_line('4.67c 1c 4.67c 4c', arrow=LAST, 
                      tags=('item', ))  # line w/one arrow
        c.create_line('6.33c 1c 6.33c 4c', arrow=BOTH, 
                      tags=('item'))  # line with two arrows

        # maze
        line = ['5c 6c 9c 6c 9c 1c 8c 1c 8c 4.8c 8.8c 4.8c 8.8c 1.2c ',
                '8.2c 1.2c 8.2c 4.6c 8.6c 4.6c 8.6c 1.4c 8.4c 1.4c 8.4c 4.4c']
        c.create_line(''.join(line), width=3, fill=self.RED, tags=('item', ))
        
        # stippled line
        c.create_line('1c 5c 7c 5c 7c 7c 9c 7c', width='.5c', stipple='gray25',
                      arrow=BOTH, arrowshape=(15, 15, 7), tags=('item', ))

        # 'M' shape
        c.create_line('1c 7c 1.75c 5.8c 2.5c 7cc 3.25c 5.8c 4c 7c',
                      width='.5c', cap=ROUND, join=ROUND, tags=('item', ))
        
    def _draw_curves(self):
        c = self.canvas
        
        c.create_text('15c .2c', text='Curves (smoothed lines', anchor=N)
        
        # curved
        c.create_line('11c 4c 11.5c 1c 13.5c 1c 14c 4c', smooth='on',
                      fill=self.BLUE, tags=('item', ))
        
        # snake with arrows
        c.create_line('15.5c 1c 19.5c 1.5c 15.5c 4.5c 19.5c 4c',
                      smooth='on', arrow=BOTH, width=3, tags=('item',))
        
        # stippled infinity
        line = ['12c 6c 13.5c 4.5c 16.5c 7.5c 18c 6c ',
                '16.5c 4.5c 13.5c 7.5c 12c 6c']
        c.create_line(''.join(line), smooth='on', width='3m', stipple='gray25', 
                      cap=ROUND, fill=self.RED, tags=('item', ))        


    def _draw_polygons(self):
        c = self.canvas
        
        c.create_text('25c .2c', text='Polygons', anchor=N)
        
        # 4 pointed star
        poly = ['21c 1.0c 22.5c 1.75c 24c 1.0c 23.25c 2.5c ',
                '24c 4.0c 22.5c 3.25c 21c 4.0c 21.75c 2.5c']
        c.create_polygon(''.join(poly), fill=self.GREEN,
                         outline='black', width=4, tags=('item', ))

        # roller-coaster
        poly = ['25c 4c 25c 4c 25c 1c 26c 1c 27c 4c 28c 1c ',
                '29c 1c 29c 4c 29c 4c']
        c.create_polygon(''.join(poly), fill=self.RED, smooth='on',
                         tags=('item', ))

        # stippled blocks
        poly = ['22c 4.5c 25c 4.5c 25c 6.75c 28c 6.75c ',
                '28c 5.25c 24c 5.25c 24c 6.0c 26c 6c 26c 7.5c 22c 7.5c ']
        c.create_polygon(''.join(poly), stipple='gray25', 
                         outline='black', tags=('item', ))
        
    
    def _draw_rectangles(self):
        c = self.canvas
        
        c.create_text('5c 8.2c', text='Rectangles', anchor=N)
        
        # empty square
        c.create_rectangle('1c 9.5c 4c 12.5c', outline=self.RED,
                           width='3m', tags=('item', ))

        # filled, outlined block
        c.create_rectangle('0.5c 13.5c 4.5c 15.5c', fill=self.GREEN,
                           tags=('item', ))
        
        # stipple filled, non-outlined block
        c.create_rectangle('6c 10c 9c 15c', outline='',
                           stipple='gray25', fill=self.BLUE,
                           tags=('item', ))
        
    def _draw_ovals(self):
        c = self.canvas
        
        c.create_text('15c 8.2c', text='Ovals', anchor=N)
        
        # empty circle
        c.create_oval('11c 9.5c 14c 12.5c', outline=self.RED,
                      width='3m', tags=('item', ))

        # filled, outlined oval
        c.create_oval('10.5c 13.5c 14.5c 15.5c', fill=self.GREEN,
                      tags=('item', ))

        # stipple filled, non-outlined oval
        # Note: the stipple does not work under Windows
        #       (Grayson, p289)
        c.create_oval('16c 10c 19c 15c', outline='',
                      fill=self.BLUE, stipple='gray25',
                      tags=('item', ))

    def _draw_text(self):
        c = self.canvas
        
        c.create_text('25c 8.2c', text='Text', anchor=N)
        c.create_rectangle('22.4c 8.9c 22.6c 9.1c')
        txt = ["A short string of text, word-wrapped, justified left, ",
               "and anchored north (at the top).  The rectangles show ",
               "the anchor points for each piece of text."]
        c.create_text('22.5c 9c', anchor=N, font=self.FONT1, width='4c',
                      text=''.join(txt), tags=('item', ))

        c.create_rectangle('25.4c 10.9c 25.6c 11.1c')
        txt = ["Several lines,\n each centered\n",
               "individually,\nand all anchored\nat the left edge."]
        c.create_text('25.5c 11c', anchor=W, font=self.FONT1,
                      fill=self.BLUE, text=''.join(txt), tags=('item', ))

        # Note: the stipple does not work well under Windows,
        #       you get a stippled box with no colour rather
        #       than purely stippled text; also, it will not
        #       drag cleanly. The Tk manual states:
        #        "Note that stipples are not well supported on platforms 
        #         that do not use X11 as their drawing API."
        c.create_rectangle('24.9c 13.9c 25.1c 14.1c')        
        c.create_text('25c 14c', font=self.FONT2, anchor=CENTER,
                      fill=self.RED, stipple='gray50',
                      text='Stippled characters', tags=('item', ))

    def _draw_arcs(self):
        c = self.canvas
        
        c.create_text('5c 16.2c', text='Arcs', anchor=N)
        
        # ellipse with missing wedge
        c.create_arc('0.5c 17c 7c 20c', fill=self.GREEN,
                     outline='black', start=45, extent=270,
                     style='pieslice', tags=('item', ))

        # open-ended circle with stippled outline 
        # Note: stipple does not work under Windows
        c.create_arc('6.5c 17c 9.5c 20c', width='4m', style='arc',
                     outline=self.BLUE, start=135, extent=270,
                     outlinestipple='gray25',
                     tags=('item', ))

        # rounded pie-slice
        c.create_arc('0.5c 20c 9.5c 24c', width='4m', style='pieslice',
                     fill='', outline=self.RED, start=225, extent=-90,
                     tags=('item', ))

        # partial ellipse
        c.create_arc('5.5c 20.5c 9.5c 23.5c', width='4m', style='chord',
                     fill=self.BLUE, outline='', start=45, extent=270,
                     tags=('item', ))
        
    
    def _draw_bitmaps(self):
        c = self.canvas
        
        c.create_text('15c 16.2c', text='Bitmaps', anchor=N)
        
        c.create_bitmap('13c 20c', tags=('item', ),
                        bitmap='@images/face.xbm')
        
        c.create_bitmap('17c 18.5c', tags=('item', ),
                        bitmap='@images/noletter.xbm')

        c.create_bitmap('17c 21.5c', tags=('item', ),
                        bitmap='@images/letters.xbm')
        

    def _draw_windows(self):
        c = self.canvas
        
        c.create_text('25c 16.2c', text='Windows', anchor=N)
        c.create_text('21c 17.9c', text='Button:', anchor='sw')
        btn = ttk.Button(c, text='Press Me', command=self._button_press) 
        c.create_window('21c 18c', window=btn, anchor='nw', tags=('item', ))

        c.create_text('21c 20.9c', text='Entry:', anchor='sw')
        entry = ttk.Entry(c, width=20)
        entry.insert(END, 'Edit this string')
        c.create_window('21c 21c', window=entry, anchor='nw', tags=('item', ))
        
        c.create_text('28.5c, 17.4c', text='Scale:', anchor=S)
        scale = ttk.Scale(c, from_=0, to=100, length='6c', orient=VERTICAL,
                  command=self._update_scale_value) 
        c.create_text('27.5c, 17.4c', text='000', anchor=N, tags=('scalevalue',))
        c.create_window('28.5c 17.5c', window=scale, anchor=N, tags=('item', ))

    # ===================================================================================
    # Commands - the 'evt' object passed in is the originating widgets value or None,
    #            not the full Event object generated by a bind
    # ===================================================================================
    def _button_press(self):
        # display some text, wait a few seconds, then remove it
        item = self.canvas.create_text('25c 18.1c', text='Ouch!!',
                                       fill='red', anchor=N)
        self.canvas.after(500, self.canvas.delete, item)
                
        
    def _update_scale_value(self, evt):
        # update value as slider is moved
        value = int(float(evt))
        self.canvas.itemconfigure('scalevalue', text='{:>3}'.format(value))
 

    # ===================================================================================
    # Bindings
    # ===================================================================================
    def _add_bindings(self):
        c = self.canvas

        # behaviour confined to canvas objects with 'item' tag
        c.tag_bind('item', '<Any-Enter>', self._item_enter)
        c.tag_bind('item', '<Any-Leave>', self._item_leave)

        c.tag_bind('item', '<1>', self._item_start_drag) 
        c.tag_bind('item', '<B1-Motion>', self._item_drag)


    # ===================================================================================
    # Bound methods
    # ===================================================================================
    def _item_enter(self, evt):
        # highlight the item under the mouse pointer
        c = self.canvas
        c.__restoreItem = None    # ID of item to be restored
        c.__restoreOpts = None    # item options to be restored  
        
        itemType = c.type(CURRENT)      # current object's type
        if itemType == 'window': return # ignore windows
        
        c.__restoreItem = c.find_withtag('current') 
        opt = ''    # option to be used for highlight
        
        if itemType == 'bitmap':
            bg = c.itemconfigure(CURRENT, 'background')[4]
            opt = 'background'
            c.__restoreOpts = {opt: bg} 
        else:
            fill = c.itemconfigure(CURRENT, 'fill')[4]
            
            # if the item has no fill, highlight it's outline
            if not fill and itemType in ('rectangle', 'arc', 'oval'):
                outline = c.itemconfigure(CURRENT, 'outline')[4]
                opt = 'outline'
                c.__restoreOpts = {opt: outline}
            else:   # use it's fill option for the highlight
                opt = 'fill'
                c.__restoreOpts = {opt: fill}

        # apply highlight colour to current item
        c.itemconfigure(CURRENT, {opt: self.HIGHLIGHT})

    def _item_leave(self, evt):
        # remove highlight from item 
        c = self.canvas

        if c.__restoreItem == c.find_withtag(CURRENT):
            c.itemconfigure(c.__restoreItem, c.__restoreOpts)

    # Note: under Windows, the stippled text item will not
    #       drag cleanly. The Tk manual states:
    #         "Note that stipples are not well supported on platforms 
    #          that do not use X11 as their drawing API."
    def _item_start_drag(self, evt):
        # save drag start coordinates
        c = self.canvas
        
        c.__lastX = c.canvasx(evt.x)
        c.__lastY = c.canvasy(evt.y)
                
    def _item_drag(self, evt):
        # the item moves (is dragged) with the mouse
        c = self.canvas
        
        x = c.canvasx(evt.x)
        y = c.canvasy(evt.y)
        c.move(CURRENT, x-c.__lastX, y-c.__lastY)
        c.__lastX = x
        c.__lastY = y
                
        
if __name__ == '__main__':
    CanvasItemsDemo().mainloop()