简体   繁体   English

尝试使用tkinter运行线程(tkinter在主线程上)执行任务,但会暂停主线程

[英]Trying to run a thread with tkinter (tkinter is on the main thread) performing a task but instead it halts the main thread

I am new to Python and currently developing a small application for personal use. 我是Python的新手,目前正在开发一个供个人使用的小型应用程序。 I am using tkinter for my gui. 我正在使用tkinter作为我的gui。

What I am trying to do is create a Toplevel popup with a Label on it that changes text depending on how the login attempts are going. 我想要做的是创建一个带有标签的顶级弹出窗口,该弹出窗口根据登录尝试的方式更改文本。 So while the main thread where tk is running on displays the popup with the dynamic text I want to start a thread to attempt to login at most 5 times and report back to the main thread by setting global variables called 'logindata'. 因此,当运行tk的主线程显示带有动态文本的弹出窗口时,我想启动一个线程以尝试最多登录5次,并通过设置名为“ logindata”的全局变量向主线程报告。

The _login() method in AuctioneerGUI and the LoginThread class are really the only things that matter here you can ignore the rest but they may deem relevant. AuctioneerGUI中的_login()方法和LoginThread类实际上是唯一重要的事情,在这里您可以忽略其余的事情,但它们可能被认为是相关的。

The _login() method is called when the login button is pressed. 按下登录按钮时,将调用_login()方法。 All this does is attempt to login and set the logindata. 所有这一切都是尝试登录并设置logindata。 The main thread meanwhile is looping until it notices that the LoginThread has set the variable and when it has collected all three it will go through to the rest of the logic (which is not fully implemented but is irrelevant to the issue) 同时,主线程一直循环运行,直到它注意到LoginThread设置了变量,并且在收集到所有三个变量后,它将遍历其余逻辑(尚未完全实现,但与问题无关)

What happens now is the main thread halts after the LoginThread has been started and only continues when it has finished. 现在发生的是,在LoginThread启动之后主线程暂停,只有在完成后才继续。 Even though the LoginThread should be running in a seperate thread and thus not halt the main thread. 即使LoginThread应该在单独的线程中运行,因此也不要暂停主线程。 So the popup only gets displayed after the task LoginThread performs is done. 因此,仅在完成LoginThread执行的任务后才显示弹出窗口。 I would like the popup to just appear and show the label that gives update to the user. 我希望弹出窗口仅显示并显示向用户提供更新的标签。 How do I do this? 我该怎么做呢?

I am certain the problem is the thread halting the main thread because I determined this using prints. 我确定问题是线程暂停了主线程,因为我是使用打印来确定的。

Also I have one more small question. 我还有一个小问题。 popup.destroy() doesn't seem to do anything. popup.destroy()似乎没有任何作用。 The TopLevel just stays there. TopLevel停留在那里。

Sorry for the wall of text and thanks in advance for helping me. 对不起,我在此先谢谢您的帮助。 I have already spent more time than I should have trying several different things but I didn't manage to get it working. 我已经花了更多的时间去尝试几种不同的方法,但是我没有设法使它起作用。

Let me know if something is unclear and don't mind the sometimes inefficient or silly logic, I first want to make it at least functional before making it pretty. 让我知道是否有不清楚的地方,并且不介意有时效率低下或愚蠢的逻辑,我首先想使其变得更漂亮,然后再使其变得漂亮。

-Daan -Daan

global logindata
logindata = {"counter": -1, "status": -1, "success": -1}

class AuctioneerGUI:
    def __init__(self):
        root = Tk()
        root.title("Path of Exile Auctioneer")
        self._setupGUI(root)
        self._loggingin = False

        root.protocol("WM_DELETE_WINDOW", lambda: root.quit())
        root.mainloop()           

    def _setupGUI(self, root):            
        frame = Frame(root)

        email = StringVar()
        pass_ = StringVar()
        thread = StringVar()

        email.set("email")
        pass_.set("password")
        thread.set("76300")

        email_label = Label(frame, text="email")
        self._email_box = Entry(frame, takefocus=True, width=50, textvariable=email)
        self._email_box.focus_set()
        pass_label = Label(frame, text="password")
        self._pass_box = Entry(frame, takefocus=True, show="*", width=50, textvariable=pass_)
        thread_label = Label(frame, text="thread id")
        self._thread_box = Entry(frame, takefocus=True, width=10, textvariable=thread)
        self._login_button = Button(frame, text="login", command=lambda: self._login(root), takefocus=True)

        frame.pack()
        email_label.pack()
        self._email_box.pack()
        pass_label.pack()
        self._pass_box.pack()
        thread_label.pack()
        self._thread_box.pack()
        self._login_button.pack()

    def _login(self, root):
        self._login_button.configure(command=None)
        email = self._email_box.get()
        pass_ = self._pass_box.get()
        thread = self._thread_box.get()
        # Check email validity
        # no whitespaces, 1 @ sign 1 . after the @ sign
        try:
            thread = int(thread)
        except ValueError:
            return -1
            #invalid thread

        if not re.match(r"[^@]+@[^@]+\.[^@]+", email) or not email.find(" ") == -1:
            return -1
            #invalid mail

        self._sm = SessionManager(email, pass_, thread)    

        self._message = StringVar()
        self._message.set("Attempt 1/5.")

        popup = Toplevel(root)
        popup.title("Logging in...")
        message_label = Label(popup, text = self._message.get(), textvariable = self._message)
        message_label.pack()

        _thread = LoginThread(self._sm)        
        _thread.start()

        loop = True                

        while loop:
            counter = -1
            success = -1
            status = -1
            while counter == -1:
                counter = logindata["counter"]
                print(counter)
            while success == -1:
                success = logindata["success"]
            print(success)
            while status == -1:
                status = logindata["status"]
            print(status)
            if success:
                self._message.set("Attempt {}/5. Success.".format(counter))
            elif status == 200:
                self._message.set("Attempt {}/5. Failed: wrong password.".format(counter))
            else:
                self._message.set("Attempt {}/5. Failed: connection error. {}".format(counter, status))
            updatebar = not success
            logindata["counter"] = -1
            logindata["status"] = -1
            logindata["success"] = -1
            if counter == 5:
                break

        popup.destroy()
        self._login_button["command"] = lambda: self._login(root)
        self._setup_main_layout(root)

    def _setup_main_layout(self, root):
        pass

class LoginThread(threading.Thread):

    def __init__(self, sessionmanager):
        threading.Thread.__init__(self)
        self._sm = sessionmanager

    def run(self):
        success = False
        counter = 1
        while not success:
            if counter > 5:
                break

            data = self._sm.login()
            status = data[1]
            success = data[0]
            logindata["counter"] = counter
            logindata["success"] = success
            logindata["status"] = status
            counter += 1
            print("done")

update: 更新:

After some research I will solve the problem by creating a ThreadSafeLabel that inherits from label that is piped to Widget and communicates through a queue like in this example: 经过一些研究,我将通过创建一个ThreadSafeLabel来解决该问题,该线程继承自传递给Widget的标签并通过队列进行通信,如本例所示:

http://effbot.org/zone/tkinter-threads.htm http://effbot.org/zone/tkinter-threads.htm

The proper way to start a threading.Thread is by calling the start method, not the run method. 启动threading.Thread的正确方法是调用start方法,而不是run方法。 It is the start method that spawns a new thread. 它是生成新线程的start方法。 Without it, you are actually running LoginThread.run in the main thread. 没有它,您实际上是在主线程中运行LoginThread.run

So instead try: 因此,请尝试:

    _thread = LoginThread(self._sm)        
    _thread.start()

From the docs : 文档

Once a thread object is created, its activity must be started by calling the thread's start() method. 创建线程对象后,必须通过调用线程的start()方法来启动其活动。 This invokes the run() method in a separate thread of control. 这将在单独的控制线程中调用run()方法。

Zeroth, as unutbu points out, you're just running the other thread's run function in the main thread, so nothing's going to happen until its done. 正如unutbu所指出的,零是,您只是在主线程中运行另一个线程的run函数,因此在完成之前什么都不会发生。


Once you solve that, you never, ever want to have a thread spin while waiting for a variable to change, as you do here: 一旦解决了这个问题,您就永远不会像在这里那样在等待变量更改的同时进行线程旋转:

while counter == -1:
    counter = logindata["counter"]
    print(counter)

The main thread cannot possibly do anything else but spin here, until the background thread sets logindata["counter"] to something else. 主线程只能在此处旋转,而不能做其他任何事情,直到后台线程将logindata["counter"]为其他内容为止。 If you force the main thread to wait until the other thread is done, you might as well run the other code in the main thread. 如果强制主线程等待另一个线程完成,则最好在主线程中运行其他代码。 Your code has the same effect as doing things single-threaded, except that it also burns as much CPU as possible checking the value over and over for no reason. 您的代码与单线程处理具有相同的效果,除了它还会无偿地消耗尽可能多的CPU反复检查该值。

If you need to wait until something is done, you need to use some kind of cross-thread signal—eg, a threading.Condition or a queue.Queue . 如果您需要等待直到完成某件事,则需要使用某种跨线程信号,例如threading.Conditionqueue.Queue


However, that still won't solve your problem, because the main thread will still be stuck inside the _login function until login is done. 但是,这仍然不能解决您的问题,因为在完成登录之前,主线程仍将停留在_login函数中。 This means it can't do other things like redraw the screen, handle mouse clicks, etc. 这意味着它无法执行其他操作,例如重绘屏幕,处理鼠标单击等。

So, even if you solved the first two problems and got things working, this would still be exactly the same as not spawning a thread, and just doing the login in the main thread. 因此,即使您解决了前两个问题并使工作正常,这仍然与不产生线程,而仅在主线程中进行登录完全相同。

What you need is a _login function that returns immediately after kicking off the background thread, and then use some other mechanism to trigger an event on the tkinter loop from the background thread. 您需要一个_login函数,该函数在启动后台线程后立即返回,然后使用其他机制从后台线程触发tkinter循环上的事件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM