Tkinter Canvas Access from a separate Class
Question
I've run into problems while trying to create a program for my frequent biking. I want the program to draw a red line across the designated course when the start button of the stopwatch is pressed. However, I can't seem to access the canvas from the class stopwatch and I need help doing that. The code is below.
from Tkinter import *
import random
import time
from itertools import product
class App():
def __init__(self,master):
menubar = Menu(master)
filemenu = Menu(menubar,tearoff=0)
filemenu.add_command(label="Quit",command=root.destroy)
filemenu.add_command(label="Interval",command=self.Interval)
coursemenu = Menu(menubar,tearoff=0)
coursemenu.add_command(label="New Random Course",command=self.regenerateTerrain)
menubar.add_cascade(label="File",menu=filemenu)
menubar.add_cascade(label="Course",menu=coursemenu)
master.config(menu=menubar)
self.statusbar = Frame(master)
self.statusbar.pack(side=BOTTOM, fill=X)
self.infobar = Frame(master)
self.infobar.pack(side=TOP,fill=X)
self.course = Label(self.infobar, text="Welcome!")
self.course.pack(side=LEFT)
self.action = Label(self.infobar, text="")
self.action.pack(side=RIGHT,fill=X)
#Stopwatch
self.stop = StopWatch(self.infobar)
#Stopwatch buttons
self.button = Button(self.infobar, text="Start", command=self.stop.Start)
self.button2 = Button(self.infobar, text="Stop", command=self.stop.Stop)
self.button3 = Button(self.infobar, text="Reset", command=self.stop.Reset)
self.button4 = Button(self.infobar, text="Quit", command=root.quit)
self.button.pack(side=LEFT)
self.button2.pack(side=LEFT)
self.button3.pack(side=LEFT)
self.button4.pack(side=LEFT)
#Constants for program
#distance is in miles
#height is in feet
self.totalDistance = 25
self.heightInterval = 10
self.canvasHeight = 300
self.canvasWidth = 500
self.c = Canvas(root,width=self.canvasWidth,height=self.canvasHeight,background="white")
self.c.pack(side=TOP,fill=BOTH,expand=YES)
#Call regenerate an initial time, so that terrain gets generated on
#initial creation
self.regenerateTerrain()
def buildTerrain(self,distance=25,topBound=0,
bottomBound=300,width=500):
options=['up','down','flat','flat']
y = (bottomBound-topBound)/2
map = [y]
changer =0
for i in xrange(distance*10):
direction = random.choice(options)
options.pop()
if direction=='up' and y>10:
options.append('up')
map.append(map[len(map)-1]-self.heightInterval)
changer=-self.heightInterval
elif direction=='down' and y<bottomBound-10:
options.append('down')
map.append(map[len(map)-1]+self.heightInterval)
changer=self.heightInterval
else:
options.append('flat')
map.append(map[len(map)-1])
changer=0
y+=changer
return map
def regenerateTerrain(self,distance=25,topBound=0,bottomBound=300,width=500):
self.c.delete(ALL)
x = 0
y = (bottomBound+topBound)/2
self.build = self.buildTerrain()
for i in xrange(1,len(self.build)-1):
self.c.create_line(x,y,x+(self.canvasWidth/(self.totalDistance*10)),self.build[i],fill="black")
x+=(self.canvasWidth/(self.totalDistance*10))
y=self.build[i]
self.c.create_oval(0,self.build[0]-1,4,self.build[0]-5,fill="red")
def Interval(self):
top = Toplevel()
top.title("Interval Mode")
a = Frame(top)
b = Frame(top)
c = Frame(top)
entLabelLow = Label(a, text="# of minutes at low interval: ")
entLabelHigh = Label(b, text="# of minutes at high interval: ")
entLabelTotal = Label(c, text="Total Number of Minutes: ")
entWidgeTotal = Entry(c, width=5)
entWidgeLow = Entry(a, width=5)
entWidgeHigh = Entry(b, width=5)
entLabelTotal.pack(side=LEFT)
entWidgeTotal.pack(side=LEFT)
entLabelLow.pack(side=LEFT)
entWidgeLow.pack(side=LEFT)
entLabelHigh.pack(side=LEFT)
entWidgeHigh.pack(side=LEFT)
a.pack(side=TOP)
b.pack(side=TOP)
c.pack(side=TOP)
self.linesDist = 0
self.minutes = 0.0
self.timeatHL = 0
self.timeatLL = 0
self.currentPos = 0
def drawGraph():
if entWidgeLow.get().strip() == "" or entWidgeHigh.get().strip() == "":
print"Enter a value please"
pass
top.destroy()
elif int(entWidgeLow.get().strip()) not in range(1,11) or int(entWidgeHigh.get().strip()) not in range(1,11):
print"Please enter a number between 1 and 10"
pass
top.destroy()
else: #Get the values
self.LLength = int(entWidgeLow.get().strip())
self.HLength = int(entWidgeHigh.get().strip())
self.TLength = int(entWidgeTotal.get().strip())
top.destroy()
while self.linesDist < self.canvasWidth - 50: #Create the vertical lines
self.c.create_line(10,195,10,205,fill="red")
self.linesDist += 50
self.intervalLength = self.TLength / 10.0
self.minutes += float(self.intervalLength)
self.c.create_line((self.linesDist, 0, self.linesDist, 300), fill="gray")
self.c.create_text(self.linesDist, 290, text=str(self.minutes))
#Now to draw the graph
while self.currentPos < 500:
self.c.create_line(self.currentPos, 200, (((500/self.TLength)*self.LLength)+self.currentPos), 200)
self.currentPos += (float(self.LLength)/self.TLength) * 500
self.c.create_line(self.currentPos, 200, self.currentPos, 100)
self.c.create_line(self.currentPos, 100, (((500/self.TLength)*self.HLength)+self.currentPos), 100)
self.currentPos += (float(self.HLength)/self.TLength) * 500
self.c.create_line(self.currentPos, 100, self.currentPos, 200)
self.stop.Start()
self.submit = Button(top, text="Submit", command = drawGraph)
self.submit.pack(side=BOTTOM)
self.c.delete(ALL)
class StopWatch(Frame):
def __init__(self, parent=App, **kw):
"""Creates the watch widget"""
Frame.__init__(self, parent, kw)
self._start = 0.0
self._elapsed = 0.0
self._running = 0
self.timestr = StringVar()
self.parent=parent
self.makeWidgets()
def makeWidgets(self):
"""Make the label"""
#It doesn't know waht the parent of this label is
l = Label(self.parent, textvariable=self.timestr)
self._setTime(self._elapsed)
l.pack(fill=X, expand=NO, padx=2, pady=2)
def _update(self):
"""Update the label with the correct time"""
self._elapsed=time.time() - self._start
self._setTime(self._elapsed)
self._timer = self.after(50, self._update)
return self._elapsed
def _setTime(self, elap):
"""Set time string"""
minutes = int(elap/60)
seconds = int(elap - minutes*60.0)
hundreths = int(((elap - minutes*60.0 - seconds)*100))
self.timestr.set("%02d:%02d:%02d" % (minutes, seconds, hundreths))
def Start(self):
if not self._running:
self._start = time.time() - self._elapsed
self._update()
self._running = 1
return self._running
def Stop(self):
"""To stop it, DUH"""
if self._running:
self.after_cancel(self._timer)
self._elapsed = time.time() - self._start
self._setTime(self._elapsed)
self._running = 0
def Reset(self):
"""Think about it"""
if self._running:
self.Stop()
self._start = time.time()
self._elapsed = 0.0
self._setTime(self._elapsed)
root=Tk()
root.title("Bike Computer")
myapp=App(root)
root.mainloop()
I want to put the create_line statement in the _update definition but I can't access the canvas.
Solution
class App(object):
def __init__(self):
self.c = Canvas(root, ...)
self.stop = StopWatch(self.infobar, self.c)
class StopWatch(object):
def __init__(self, infobar, canvas):
self.canvas = canvas
def draw_line_on_canvas(self):
self.canvas.create_line(...)
As Bryan Oakley said, you'll need to pass a reference to the canvas, to the StopWatch. We do this the same way as the "infobar". We make a few modifications to the StopWatch's initaliser to accomidate the canvas reference. Then we just bind it to StopWatch instance, as you would do normally. Now you have access to the canvas, and can call its methods and access it's attributes.
OTHER TIPS
Just pass a reference to the canvas to the constructor of the StopWatch class. Or, create a controller class that knows about the canvas and the stopwatch, then the stopwatch will ask the controller to ask the canvas to draw the line.
[edit] How would you do that? First, make the canvas, then make the stopwatch:
self.c = Canvas(root,...)
...
self.stop = StopWatch(self.infobar, self.c)
Then, use that within the stopwatch:
class StopWatch(Frame):
def __init__(parent=App, canvas):
Frame.__init__(self, parent)
self.canvas = canvas
...
def drawGraph():
...
self.canvas.create_line(...)