Wednesday, August 8, 2012

Tkinter Simple Canvas Demo

This code is based on the Tcl cscroll.tcl demo with reference to the Tkinter Tour canvasscroll.py demo. It demonstrates a scrollable canvas containing a number of rectangles displayed in a grid.






# File: canvassimple.py
#    http://infohost.nmt.edu/tcc/help/pubs/tkinter//canvas.html#create_rectangle
#    http://www.tcl.tk/man/tcl8.5/TkCmd/canvas.htm
#
# Ref: canvasscroll.py from
#    http://tkinter.unpythonic.net/wiki/A_tour_of_Tkinter_widgets


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

# ===========================================================================
# Constants
# ===========================================================================
SQUARE = 60     # width and height of rect
PAD = 20        # padding around rectangles
SIDE = SQUARE + PAD

ROWS = 10       # num of rows
COLS = 20       # num of rect in a row

BG_FILL = 'gray90'  # background fill colour
CUR_FILL = 'cyan'   # selected rectangle fill colour

# ===========================================================================
# Class
# ===========================================================================
class SimpleCanvasDemo(ttk.Frame):
        
    def __init__(self, isapp=True, name='simplecanvasdemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Simple Canvas Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:
            MsgPanel(self, 
                     ["This window displays a canvas widget that can be ",
                      "scrolled either by using the scrollbars or by right ",
                      "clicking in a rectangle and dragging the mouse ",
                      "horizontally and/or vertically.\n\n",
                      "Click on a rectangle and its indices will be displayed ",
                      "in the label area below the canvas."])
            
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = Frame(self)
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        # create text label
        lbl = ttk.Label(text='Selected Rectangle: ', 
                        background='LightGoldenrod1',
                        font=('Helv', '10', 'bold'))
        lbl.pack(in_=demoPanel, side=BOTTOM, fill=X, 
                 expand=Y, pady=10, padx=5)
        
        # create a canvas with rectangles
        canvas = self._create_grid(demoPanel)
        self._create_rects(canvas)
        self._create_bindings(canvas, lbl)
        
    # ===========================================================================
    # Canvas
    # ===========================================================================                
    def _create_grid(self, parent):
        # frame to hold canvas
        grid = ttk.Frame(parent)
        grid.pack(expand=Y, fill=BOTH, padx=1, pady=1)
        
        # scrollable canvas (must set scrollregion)
        c = Canvas(relief=SUNKEN, borderwidth=2,
                   background=BG_FILL,
                   scrollregion=(0,0, 
                                 COLS*SIDE+PAD, 
                                 ROWS*SIDE+PAD))  # (W,N,E,S)
        
        hscroll = ttk.Scrollbar(orient=HORIZONTAL, command=c.xview)
        vscroll = ttk.Scrollbar(orient=VERTICAL, command=c.yview)

        c['xscrollcommand'] = hscroll.set
        c['yscrollcommand'] = vscroll.set
                
        # set canvas and scrollbar positions and
        # resize behaviours
        grid.rowconfigure(0, weight=1, minsize=0)
        grid.columnconfigure(0, weight=1, minsize=0)

        c.grid(in_=grid, padx=1, pady=1, row=0, column=0,
               rowspan=1, columnspan=1, sticky='news')
        vscroll.grid(in_=grid, padx=1, pady=1, row=0,
                     column=1, rowspan=1, columnspan=1,
                     sticky='news')
        hscroll.grid(in_=grid, padx=1, pady=1, row=1,
                     column=0, rowspan=1, columnspan=1,
                     sticky='news')
        
        return c

    def _create_rects(self, c):
        # create 10 rows of rectangles,
        # with 20 rectangles per row
        # initial colour matches canvas bg
        # each rectangle is 'tagged' with its name
        # (same as text displayed in the rectangle)
        
        for i in range(COLS): # columns
            x = PAD + ( i * SIDE)
            for j in range(ROWS): # rows
                y = PAD + (j * SIDE)
                name = '{},{}'.format(i,j)  
                c.create_rectangle(x, y, x+SQUARE, y+SQUARE,
                                   outline='black',
                                   fill=BG_FILL,
                                   tags=('rect', name))
                
                c.create_text(x+(SQUARE/2), y+(SQUARE/2),
                              text=name,
                              anchor=CENTER,
                              tags=('text', name))

    # ===========================================================================
    # Canvas Bindings
    # ===========================================================================
    def _create_bindings(self, canvas, lbl):        
        # highlight rectangle under mouse
        canvas.tag_bind('all', '<Any-Enter>', self._enter_rect )
        canvas.tag_bind('all', '<Any-Leave>', self._leave_rect )
        
        # display rectangle indices on 'select'
        canvas.tag_bind('all', '<1>', 
                        lambda evt, l=lbl, c=canvas: self._sel_rect(c, l))
        
        # drag canvas (scroll using right mouse button)
        canvas.tag_bind('all', '<3>', 
                        lambda evt, c=canvas: self._begin_drag(evt, c))
        canvas.tag_bind('all', '<B3-Motion>', 
                        lambda evt, c=canvas: self._drag_canvas(evt, c))

    # ===========================================================================
    # Canvas bound methods (callbacks)
    # ===========================================================================
    def _sel_rect(self, canvas, label):
        item = self._get_current_rect(canvas)  # selected object
        tags = canvas.gettags('current')       # associated tags
        for t in tags:  
            if t not in ('rect', 'current', 'text'): 
                # tag must be the object name               
                label['text'] = 'Selected Rectangle: ({})'.format(t)

    def _enter_rect(self, evt):
        w = self.nametowidget(evt.widget)     # get canvas
        item = self._get_current_rect(w)      # get selected rect
        w.itemconfigure(item, fill=CUR_FILL)  # change fill colour

    def _leave_rect(self, evt):
        w = self.nametowidget(evt.widget)   # get canvas
        item = self._get_current_rect(w)    # get selected rect
        w.itemconfigure(item, fill=BG_FILL) # change fill colour

    def _get_current_rect(self, c):
        item = c.find_withtag('current')  # id of current item
        tags = c.gettags('current')       # associated tags
        if 'text' in tags:                # text item?
            item = c.find_below(item)     # id of containing rect
            
        return item

    # drag canvas (scroll)
    def _begin_drag(self, evt, canvas):
        canvas.scan_mark(evt.x, evt.y)  # initial right-mouse click
        
    def _drag_canvas(self, evt, canvas): 
        canvas.scan_dragto(evt.x, evt.y) # capture dragging

# ===========================================================================
# Main
# ===========================================================================
if __name__ == '__main__':
    SimpleCanvasDemo().mainloop()