如何使用线程来创建多个独立的可重用对象实例?

时间:2015-07-01 07:20:35

标签: python multithreading timer tkinter

我正在使用Python构建GUI并使用线程生成多个独立工作的倒计时器。我有一个最小化的模块用于测试我包括下面。我希望按钮开始倒计时,然后当它再次单击时停止它,然后重置以便可以在下次单击时启动它。一旦耗尽,也会自行重置。唯一的问题是它停止后,我无法让它重新启动。我得到“无法启动线程两次”错误。我一直在尝试使用条件循环来让线程退出自己,但它没有工作。我真的很感激一些见解

实际上我希望这个程序可以做两件事。 1)通过计时器一直运行然后自动重置,以便可以重新启动 2)在倒计时中间停止,让它自动重置,以便重新启动

我认为解决这些问题对于社区来说是有价值的,因为它是一个真实世界解决问题的例子,很多人在论坛上谈论这是如何绕过没有重新启动的线程。

__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()

你可能想要给它一个运行只是为了看看GUI看起来是什么样的,当我说计时器时我的意思。尝试让它一直运行并重置以了解我正在谈论的内容。

让我知道是否有任何其他信息会有所帮助,请记住这是我第一周使用python所以我不是专家。谢谢大家

1 个答案:

答案 0 :(得分:1)

python社区非常幸运地拥有一个非常受欢迎的样式指南https://www.python.org/dev/peps/pep-0008/。由于你的非命名和格式化,我很难快速理解你所讨论的问题。

其他线程无法与您的小部件交互,只有tkinter主循环才能这样做。您可以使用tkinter提供的after方法在mainloop中的给定时间段之后运行函数。

看这个例子 Mutli-threading python with Tkinter举个例子!

要清楚使用tkinter进行线程化可能会有效,但它不可靠并且您将获得难以调试的意外行为。不要这样做。

建议

  1. 调用方法之后的tkinter来启动一个无限期地检查队列以查找要运行的函数的函数。然后它使用after方法

  2. 安排这些函数由tkinter运行
  3. 我会有一个带有start,stop和pause方法的计时器类。 启动时,它将向上/向下计数,并在完成时自行重启。您创建的每个计时器实例也需要对tkinter按钮的引用。

  4. 更新您在队列中放置可能看起来像

    的按钮
    tkinter_queue.put(lambda: but.config(text=i))
    
  5. 现在检查队列的功能将更新按钮计时器

    另外,当你在python编程的第一周时,你可以确定你遇到的大多数问题都会在这里得到答案,如果不是在其他地方。请事先做好您的研究。 Gui编程和线程不是最简单的主题,因此更有理由进行研究。