尝试在tkinter中运行线程(tkinter在主线程)执行任务,但却导致主线程暂停

0 投票
2 回答
3966 浏览
提问于 2025-04-17 16:19

我刚开始学习Python,现在正在开发一个小应用程序供自己使用。我在用tkinter来做图形界面。

我想做的是创建一个弹出窗口,上面有一个标签,标签的内容会根据登录尝试的情况而变化。所以在主线程中,tk正在运行并显示这个动态文本的弹出窗口时,我想启动一个线程,最多尝试登录5次,并通过设置一个叫做'logindata'的全局变量来向主线程报告结果。

这里最重要的是AuctioneerGUI中的_login()方法和LoginThread类,其他的可以忽略,但可能也有用。

当按下登录按钮时,会调用_login()方法。这个方法的作用就是尝试登录并设置logindata。与此同时,主线程会不断循环,直到它发现LoginThread已经设置了变量,当收集到所有三个变量后,它才会继续执行后面的逻辑(虽然这些逻辑还没有完全实现,但和问题无关)。

现在发生的情况是,主线程在启动LoginThread后会暂停,只有等LoginThread完成后才会继续。尽管LoginThread应该在一个独立的线程中运行,因此不应该阻塞主线程。所以弹出窗口只会在LoginThread完成任务后才显示。我希望弹出窗口能立即出现,并显示更新用户的标签。我该怎么做呢?

我确定问题是线程阻塞了主线程,因为我通过打印信息确认了这一点。

还有一个小问题,popup.destroy()似乎没有任何作用,TopLevel窗口依然存在。

抱歉写了这么多文字,感谢你们的帮助。我已经花了比预期更多的时间尝试不同的方法,但都没能解决问题。

如果有什么不清楚的地方,请告诉我,不用在意我有时不太高效或傻的逻辑,我首先想让它能正常工作,再考虑美观。

-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")

更新:

经过一些研究,我决定通过创建一个继承自标签的ThreadSafeLabel来解决这个问题,它会连接到Widget,并通过队列进行通信,就像这个例子一样:

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

2 个回答

2

启动一个 threading.Thread 的正确方法是调用 start 方法,而不是 run 方法。其实,start 方法是用来创建一个新线程的。如果你直接用 run 方法,那你就是在主线程里运行 LoginThread.run,并没有真正创建新线程。

所以你应该这样做:

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

来自 官方文档

一旦创建了线程对象,就必须通过调用线程的 start() 方法来启动它的活动。这样会在一个单独的控制线程中调用 run() 方法。

2

首先,正如unutbu所说,你实际上是在主线程中运行另一个线程的run函数,所以在它完成之前,什么事情都不会发生。


一旦你解决了这个问题,你绝对不想让一个线程在等待某个变量变化时一直转圈,就像你在这里做的那样:

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

在这里,主线程只能一直转圈,直到后台线程把logindata["counter"]改成其他值。如果你强迫主线程等待另一个线程完成,那你不如直接在主线程中运行其他代码。你的代码效果就跟单线程一样,只是它还会不停地消耗CPU资源,反复检查这个值,完全没有必要。

如果你需要等某件事情完成,你需要使用某种跨线程的信号,比如threading.Condition或者queue.Queue


不过,这样做仍然无法解决你的问题,因为主线程会一直卡在_login函数里,直到登录完成。这意味着它无法做其他事情,比如重新绘制屏幕、处理鼠标点击等等。

所以,即使你解决了前两个问题,让事情运转起来,这仍然和不创建线程、直接在主线程中登录是一样的。

你需要的是一个_login函数,它在启动后台线程后立即返回,然后使用其他机制从后台线程触发tkinter循环中的事件。

撰写回答