1

I am building a GUI in Python and using threads to generate multiple countdown timers that work independently. I have a minimized module for testing which i am including bellow. I want the button to start the count down and then when its clicked again stop it, then reset so that it can be started on the next click. Also reset on its own once time runs out. The only problem is that after it stops, I cannot get it to restart. I get the "cannot start a thread twice" error. I have been trying to use a conditional loop to get the thread to exit its self but it hasn't been working. I would really appreciate some insight

There are actually two things that i want this program to be able to do. 1) run all the way through the timer then automatically reset so that it can be restarted 2) be stopped in the middle of a countdown, have it automatically reset so that it can be restarted

I think that solving these issues will be valuable for the community to see because it is an example of a real world solution to the issue that a lot of people talk about on the forum which is how to get around the no restarting threads.

__author__ = 'iKRUSTY'

'''
there are two things i would like this code to be able to do
1) run all the way through the timer and then reset so that it can be restarted
2) be stopped before completing and then reset so that it can be restarted

'''

from tkinter import *
import time
import os
import threading

#Variables
global FRYER_ONE_TIME_VAR       # holds the value for changing label text to update timer
global BASKET_ONE_TARGET_TIME       #this is a user input that will determine the length of the countdown
global timerOneStayAlive        #this is the value that i am attempting to use so that the thread closes after it is set to false
timerOneStayAlive = FALSE       #initializes to false because the the invert function changes it to true once the button is clicked

FRYER_ONE_TIME_VAR=" "        #used to pass time between functiuons

#Font Profiles
SMALLEST_FONT = ("Verdana", 9)
SMALL_FONT = ("Verdana", 10)
LARGE_FONT = ("Verdana", 12)
LARGEST_FONT = ("Verdana", 18)

class timer():
    global BASKET_ONE_TARGET_TIME
    BASKET_ONE_TARGET_TIME = 5      #Just setting it manually for now

    def __init__(self):
        self.s = 0      #these values are used to convert from seconds to a minute:second format
        self.m = 0       #these values are used to convert from seconds to a minute:second format

    def SetTime(self, seconds):
        self.seconds=seconds        #this is a counter that will be used to calculate remaining time

    def TimerReset(self):
        self.seconds = BASKET_ONE_TARGET_TIME       #resets counter to target time

    def StartCountdown(self, FryerLabel): #takes a label as an argumet to tell it where to display the countdown

        global timerOneStayAlive
        print("StartCountdown Started!")        #USED FOR TROUBLE SHOOTING
        self.seconds = BASKET_ONE_TARGET_TIME   #set start value for seconds counter
        self.seconds=self.seconds+1      #makes the full time appear upon countdown start

        while self.seconds > 0:
            FRYER_ONE_TIME_VAR = self.CalculateTime()       #Calculate time reduces the counter by one and reformats it to a minute:second format. returns a string
            FryerLabel.config(text=FRYER_ONE_TIME_VAR)      #Update Label with current value
            print(self.seconds)                               #USED FOR TROUBLE SHOOTING
            time.sleep(1)
            # reset label with default time
            if self.seconds == 0:                           #Reset once counter hits zero
                print("resetting time")                       #USED FOR TROUBLE SHOOTING
                self.seconds = BASKET_ONE_TARGET_TIME + 1
                FRYER_ONE_TIME_VAR = self.CalculateTime()
                FryerLabel.config(text=FRYER_ONE_TIME_VAR)
                break
        print("TimerStayAlive before invert: ", timerOneStayAlive)          #USED FOR TROUBLE SHOOTING
        timerOneStayAlive = invert(timerOneStayAlive)                   #inverts the value back to FALSE so that Ideally the loop breaks
                                                                        #  and the thread completes so that it can be called again
        print("TimerStayAlive after invert: ", timerOneStayAlive)       #USED FOR TROUBLE SHOOTING
        print("end startcountdown")                                 #USED FOR TROUBLE SHOOTING

    def CalculateTime(self):
        #print("CalculateTime has been called")
        lessThanTen=0
        self.seconds = self.seconds - 1
        self.m, self.s = divmod(self.seconds, 60)

        if self.s<10:
            lessThanTen=1
        #create time String Variables
        colonVar=':'
        minutesString = str(self.m)
        secondsString = str(self.s)
        #insert variables into string array
        timeArray = []
        timeArray.append(minutesString)
        timeArray.append(colonVar)
        if lessThanTen == 1:
            timeArray.append("0")
        timeArray.append(secondsString)
        #prepare for output
        self.timeString = ''.join(timeArray)
        return self.timeString

def invert(boolean):
    return not boolean

def BasketOneButtonClicked():
    print("button clicked")             #USED FOR TROUBLE SHOOTING
    global timerOneStayAlive
    timerOneStayAlive = invert(timerOneStayAlive)   #Changes from FALSE to TRUE
    if timerOneStayAlive == TRUE:
        print("timerOneStayAlive: ", timerOneStayAlive)         #USED FOR TROUBLE SHOOTING
        basketOneThread.start()
        updateButtonStatus()                                #changes text of button depending on whether timerOneStayAlive is TRUE or FALSE
    else:
        print("timerOneStayAlive: ", timerOneStayAlive)     #USED FOR TROUBLE SHOOTING
        return


def updateButtonStatus():
    global timerOneStayAlive
    if timerOneStayAlive == FALSE:
        basketOneStartButton.config(text="Start")
    if timerOneStayAlive == TRUE:
        basketOneStartButton.config(text="Stop")


def BasketOneThreadComShell():          # I used this so that i can ideally call multiple functions with a single thread
    '''
    This is where i think the problem may be. this is what is called when the thread is initialized and theoretically
    when this completes the thread should come to a halt so that when the button is reset, the thread can be called again
    I think that the function is completing but for some reason the thread keeps on running.
    '''

    global timerOneStayAlive
    print("ComShell Started")       #USED FOR TROUBLE SHOOTING
    while timerOneStayAlive:
        basketOneTimer.StartCountdown(countdownLabelBasket1)
        updateButtonStatus()
        if timerOneStayAlive == FALSE:      #redundant because while loop should do it. i just tried it because i couldnt get the process to end
            break
    print("Threadshell has ended")      #USED FOR TROUBLE SHOOTING
    return
    print("after return check")     #USED FOR TROUBLE SHOOTING



root = Tk()

'''
the  following is all just building the GUI Stuff
'''

Container = Frame(root)
Container.grid(row=1, column=0, padx=10, pady=10)
countdownContainerBasket1 = Label(Container, width=10, height=5)
countdownContainerBasket1.grid(row=2, column=0, sticky=NSEW)
countdownLabelBasket1 = Label(countdownContainerBasket1, text="Basket 1", background="white", anchor=CENTER, width=10, height=6, font=LARGE_FONT, padx=20)
countdownLabelBasket1.pack()
basketOneTimer = timer()
basketOneTimer.SetTime(5)
basketOneStartButton = Button(Container, text="Start", font=LARGE_FONT, command=BasketOneButtonClicked)
basketOneStartButton.grid(row=3, column=0, padx=10, pady=10)

basketOneThread = threading.Thread(target=BasketOneThreadComShell) #this is where the thread is initialized. start() is called in BasketOneButtonClick

print(threading.active_count)       #tried to use this to see if the thread was exiting but it didnt help

root.mainloop()

You'll probly want to give this a run just to see what the GUI looks like and what i mean when i say timer. Try letting it run all the way through and resetting to get an idea of what im talking about.

Let me know if there is any other info that would be helpful, and keep in mind this is my first week with python so im no expert. Thanks all

  • if all you're doing is a countdown timer, you don't need threads -- they add complexity without adding any real value. – Bryan Oakley Jul 01 '15 at 11:59
  • Thats what everyone keeps saying but without threads how can i have 4 timers which are basically just countdown loops with a 1 second delay. running independently of one another, update the GUI and drive the GPIO on a Ras-P?. When I run a timer wont it lock up until the countdown loop finishes unless its in a separate thread? If i dont use threads even the button locks up and i cant hit it again to cancel. I already have it working with threads and its not complicated at all but Its just frustrating that theres no command to end the thread. if you can name it you should be able to kill it – Holden Greene Jul 01 '15 at 21:44
  • See http://stackoverflow.com/q/2400262/7432 for an example of a timer that doesn't use threads. That example can easily be extrapolated to keep as many timers as you want (well, up to a few hundred, perhaps). – Bryan Oakley Jul 02 '15 at 00:08
  • Thanks Brian, I guess ill just go this route. You've been very helpful. – Holden Greene Jul 02 '15 at 00:25
  • Just got it working with the after(). Didn't want to at first but it was definitely the call. Thanks again for your help! – Holden Greene Jul 02 '15 at 08:51

1 Answers1

1

The python community is very lucky to have a style guide that is very well accepted https://www.python.org/dev/peps/pep-0008/. It is much harder for me to quickly understand the problems you are talking about due to your non pep8 naming and formatting.

Other threads cannot interact with your widgets, only the tkinter mainloop can do that. You can use the after method provided by tkinter to run a function after a given time period in the mainloop.

see this example Mutli-threading python with Tkinter for an example!

To be clear threading with tkinter may work, but it is unreliable and you will get unexpected behavior that is hard to debug. Don't do it.

Suggestions

  1. the tkinter after method would be called to start a function that indefinitely checks a queue for functions to run. It then schedules these functions to be run by tkinter using the after method

  2. I would have a timer class with start, stop and pause methods. When started it would count up/down and simply restart itself on completion. Each timer instance you create would also need a reference to a tkinter button.

  3. to update the button you put a function on the queue that may look like

    tkinter_queue.put(lambda: but.config(text=i))
    

now the function that checks the queue will update your button timers

Also as you are in your first week of python programming you can be sure that most problems you encounter will have answers here if not elsewhere. Please do your research before hand. Gui programming and threading are not the easiest topics so even more reason to do your research.

Community
  • 1
  • 1
timeyyy
  • 654
  • 9
  • 20
  • Thanks for your helpful response! I assure you ive done a fair amount of scouring both here and out. Most of the info I found says similar things to your response. I was hoping to maybe get some insight as to why the while loop method of exiting wasn't working which is not a topic that i have seen discussed yet. But ya most people just caution that threading is hard and you probly shouldn't do it. It just seems to damn convenient so tempting. Thanks by the way for the pep-0008 link that is really helpful and ill be sure to apply it in the future. Let me know if you think of anything else – Holden Greene Jul 01 '15 at 21:53
  • No problem, threading can be a tricky topic so i would recommend asking another question, providing a minimal example showing what is going wrong. If you break out the code it will also be easier to debug and isolate the problem. You can still use threads but you just have to put any code that modifies tkinter on the queue. – timeyyy Jul 02 '15 at 06:38
  • I just went with the after(). Worked like a charm. So relieved. Thanks for your help, and constructive guidance. – Holden Greene Jul 02 '15 at 08:49