2

I've been slowly learning Tkinter and object-oriented programming but I've programmed myself into a corner with this one. please forgive my lack of critical thinking on this one, but i've asked everyone I know who knows python better than me and we can't get to a working solution here.

I've got a gui app im working on that is meant to allow the user to input stock symbols, create new labels for each symbol, and then update each label periodically. (kinda like a really basic etrade app or something). I've found it's really easy to do this without a gui because I can just say:

while True:
   sPrice = get_stock_price(s)
   print sPrice

but i've bound my get_stock_price(s) function to a button, which spawns a sub-frame and a label contained inside it. The problem i've faced is the label will not update. A friend recommended to add another method solely to update the label, however the only way I know how to continuously update it is do a

while True:
    # get new price
    # update the label
    # time.sleep(~1 minute?)

this causes my gui window to freeze and spin forever. I've been reading up on all the other threads related to this particular situation, and I've seen many different advices; don't call sleep in your main thread, don't use root.update, use events, call root.something.after(500, function) and i've tried to implement. What i've been left with is a frankenstein of code that will still retrieve my stock values, but wont update them, and a few methods that I don't know how to update, or where to call in my code.

What im hoping for is a (potentially long, I know. Sorry!) explanation of what i'm doing wrong, and suggestions on how to fix it. I'm really looking to understand and fix the issue myself, but code-solutions would be awesome so long as they are explained.

Thanks so much in advance!!!

PS: Here is my code so far:

from Tkinter import *
import urllib
import re
import time

class MyApp(object):
    def __init__(self, parent):
        self.myParent = parent
        self.myContainer1 = Frame(parent)
        self.myContainer1.pack()
        self.createWidgets()
        button1 = Button(self.myContainer1, command = self.addStockToTrack)
        self.myContainer1.bind("<Return>", self.addStockToTrack)
        button1.configure(text = "Add Symbol")
        button1.pack()  

    def createWidgets(self):
        # title name
        root.title("Stock App")
        # creates a frame inside myContainer1
        self.widgetFrame = Frame(self.myContainer1)
        self.widgetFrame.pack()
        # User enters stock symbol here:
        self.symbol = Entry(self.widgetFrame) 
        self.symbol.pack()
        self.symbol.focus_set()

    def addStockToTrack(self):
        s = self.symbol.get()
        labelName = str(s) + "Label"
        self.symbol.delete(0, END)
        stockPrice = get_quote(s)
        self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
        self.labelName.pack()
        self.myContainer1.after(500, self.get_quote)

    def updateStock(self):
        while True:
            labelName = str(s) + "Label"
            stockPrice = get_quote(s)
            self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
            self.labelName.pack()
            time.sleep(10)

def get_quote(symbol):
    base_url = 'http://finance.google.com/finance?q='
    content = urllib.urlopen(base_url + symbol).read()
    m = re.search('id="ref_\d*_l".*?>(.*?)<', content)
    if m:
        quote = m.group(1)
    else:
        quote = 'Not found: ' + symbol
    return quote

root = Tk()
myapp = MyApp(root)
root.mainloop()
ford
  • 180
  • 1
  • 3
  • 11

2 Answers2

6

You already have an infinite loop running, so you shouldn't be trying to add another one. Instead, you can use the after method to cause a function to be repeatedly called every so often. In your case, you can replace this:

def updateStock(self):
   while True:
        labelName = str(s) + "Label"
        stockPrice = get_quote(s)
        self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
        self.labelName.pack()
        time.sleep(10)

... with this:

def updateStock(self):
    labelName = str(s) + "Label"
    stockPrice = get_quote()
    self.labelName = Label(self.myContainer1, text = s.upper() + ": " + str(stockPrice))
    self.labelName.pack()
    self.after(10000, self.updateStock)

This will get a quote, add a label, then arrange for itself to be called again in 10 seconds (10,000 ms).

However, I doubt that you want to create a new label every 10 seconds, do you? Eventually the window will fill up with labels. Instead, you can create a label once, then update the label in each iteration. For example, create self.label once in the init, then in the loop you can do:

self.labelName.configure(text=s.upper() + ": " + str(stockPrice))
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Bryan, this definitely set me back on the right path, thanks a bunch for explaining this as you did. It seems though, if I were going to add the self.label to my init function, would it be easier if I instead created another class for my stock information, where I would init the label there? This would also give me some expandability for delete buttons, etc/ – ford Sep 08 '13 at 19:53
  • Also, as per your code, where would I be calling updateStock() now? Still linked to my button1? – ford Sep 08 '13 at 20:09
0

You are looking for threading. Put the event you want to run in another thread. See this example:

import thread, time
def myfunc(a1,a2):
    while True:
      print a1,a2
      time.sleep(1)
thread.start_new_thread(myfunc,("test","arg2")
tkroot.mainloop()

Now you have a function running along with the Tkinter window that prints the args every second.

EDIT: I don't know why so many down votes. Tkinter DOES work well with threads, I've already used this trick several times without problems. See this example:

Download a 10 MB file and log the progress to a Tkinter window.

Without threading:

import urllib2,thread
import Tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)   

        self.parent = parent        
        self.initUI()

    def initUI(self):

        self.pack(fill=tk.BOTH, expand=1)

        canvas = tk.Canvas(self)

        self.text = canvas.create_text(18,18,anchor=tk.W,font="Purisa",text="Status: Press start to download...")
        but=tk.Button(text="Start",command=self.start)
        canvas.create_window((270,18),window=but)

        canvas.pack(fill=tk.BOTH, expand=1)
        self.canvas=canvas

    def start(self):
        #thread.start_new_thread(
        self.download("http://ipv4.download.thinkbroadband.com/10MB.zip","10mb.zip")
        #)

    def onEnd(self):
            self.canvas.itemconfig(self.text, text="Status: done!")

    def download(self,url,file_name):
        u = urllib2.urlopen(url) 
        f = open(file_name, 'wb')
        meta = u.info()
        file_size = int(meta.getheaders("Content-Length")[0])
        print "Downloading: %s Bytes: %s" % (file_name, file_size)

        file_size_dl = 0
        block_sz = 1024*50 #50 kb
        while True:
            buffer = u.read(block_sz)
            if not buffer:
                break

            file_size_dl += len(buffer)
            f.write(buffer)
            status = r"[%3.2f%%]" % (file_size_dl * 100. / file_size)
            self.canvas.itemconfig(self.text,text="Status: downloading..."+status)

        f.close()

        self.onEnd()

def main():
    root = tk.Tk()
    root.resizable(0,0)
    ex = Example(root)
    root.geometry("300x70")
    root.mainloop()  

main()

The window freezes till the download is done.

With thread:

import urllib2,thread
import Tkinter as tk

class Example(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent)   

        self.parent = parent        
        self.initUI()

    def initUI(self):

        self.pack(fill=tk.BOTH, expand=1)

        canvas = tk.Canvas(self)

        self.text = canvas.create_text(18,18,anchor=tk.W,font="Purisa",text="Status: Press start to download...")
        but=tk.Button(text="Start",command=self.start)
        canvas.create_window((270,18),window=but)

        canvas.pack(fill=tk.BOTH, expand=1)
        self.canvas=canvas

    def start(self):
        thread.start_new_thread(
        self.download("http://ipv4.download.thinkbroadband.com/10MB.zip","10mb.zip")
        )

    def onEnd(self):
            self.canvas.itemconfig(self.text, text="Status: done!")

    def download(self,url,file_name):
        u = urllib2.urlopen(url) 
        f = open(file_name, 'wb')
        meta = u.info()
        file_size = int(meta.getheaders("Content-Length")[0])
        print "Downloading: %s Bytes: %s" % (file_name, file_size)

        file_size_dl = 0
        block_sz = 1024*50 #50 kb
        while True:
            buffer = u.read(block_sz)
            if not buffer:
                break

            file_size_dl += len(buffer)
            f.write(buffer)
            status = r"[%3.2f%%]" % (file_size_dl * 100. / file_size)
            self.canvas.itemconfig(self.text,text="Status: downloading..."+status)

        f.close()

        self.onEnd()

def main():
    root = tk.Tk()
    root.resizable(0,0)
    ex = Example(root)
    root.geometry("300x70")
    root.mainloop()  

main()

Doesn't freeze and the text is updated normally.

Nacib Neme
  • 859
  • 1
  • 17
  • 28
  • Tkinter doesn't play well with threads. Printing, like in your example, works well enough. However, the OP is wanting to update the GUI which is less straight-forward. – Bryan Oakley Sep 08 '13 at 02:16
  • 1
    Your code hangs for me as soon as I click the start button, which is not unexpected. This code _will_ crash or hang non-deterministically. It could run for hours, or it could crash in a few seconds. Tkinter simply isn't thread-safe. The only robust solution is to put work to update the GUI on a thread-safe queue that the main thread can poll and perform. – Bryan Oakley Sep 08 '13 at 15:26
  • I'm not saying threading isn't right for anything. In this case, however, the OP is very clearly downloading tiny bits of information, not 10MB zip files. Admittedly, threading is a reasonable solution when downloading 10mb zip files, but it's also quite possible to *not* use threading to do the same work under many circumstances. – Bryan Oakley Sep 08 '13 at 15:34
  • Nacib, thank you for suggesting threads. I had originally intended to use threading with this program but it seemed like a complicated solution to something I wanted to figure out with tkinter. my main goal is comprehension and understanding of the framework. Eventually though, what I will probably have to do is implement threads in this program anyways, for each stock have the stock object pull from the threads pool. – ford Sep 08 '13 at 19:57