试图用tkinter运行一个线程(tkinter在主线程上)执行一个任务但是却停止了主线程

时间:2013-02-19 00:33:22

标签: python multithreading python-3.x tkinter

我是Python新手,目前正在开发一个供个人使用的小应用程序。我正在使用tkinter为我的gui。

我要做的是创建一个Toplevel弹出窗口,其上带有一个标签,根据登录尝试的进行情况更改文本。因此,当运行tk的主线程显示带有动态文本的弹出窗口时,我想启动一个线程,尝试最多登录5次,并通过设置名为'logindata'的全局变量向主线程报告。

AuctioneerGUI中的_login()方法和LoginThread类实际上是唯一重要的事情,你可以忽略其余部分,但他们可能认为相关。

按下登录按钮时会调用_login()方法。所有这一切都是尝试登录并设置logindata。主线程同时循环,直到它注意到LoginThread已设置变量,当它收集了所有三个时,它将进入逻辑的其余部分(未完全实现但与问题无关)

现在发生的事情是主线程在LoginThread启动后暂停,并且仅在完成后继续。即使LoginThread应该在一个单独的线程中运行,因此不会停止主线程。因此,只有在LoginThread执行任务完成后才会显示弹出窗口。我希望弹出窗口只显示并显示给用户更新的标签。我该怎么做?

我确定问题是线程暂停主线程,因为我使用print确定了这一点。

我还有一个小问题。 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来解决问题,该ThreadSafeLabel继承自通过管道连接到Widget的标签并通过如下例子中的队列进行通信:

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

2 个答案:

答案 0 :(得分:2)

启动threading.Thread的正确方法是调用start方法,而不是run方法。生成新线程的是start方法。没有它,您实际上在主线程中运行LoginThread.run

所以请尝试:

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

来自the docs

  

创建线程对象后,必须通过调用启动其活动   线程的start()方法。这将调用a中的run()方法   单独的控制线。

答案 1 :(得分:2)

Zeroth,正如unutbu指出的那样,你只是在主线程中运行另一个线程的run函数,所以在完成之前不会发生任何事情。


一旦你解决了这个问题,你就不会想要在等待变量改变时让线程旋转,就像你在这里一样:

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

主线程除了在此处旋转之外不能做任何其他操作,直到后台线程将logindata["counter"]设置为其他内容。如果强制主线程等到另一个线程完成,那么您也可以在主线程中运行其他代码。您的代码与单线程的代码具有相同的效果,除了它尽可能多地烧掉CPU,无缘无故地反复检查值。

如果你需要等到某事完成,你需要使用某种跨线程信号 - 例如threading.Conditionqueue.Queue


然而,这仍然无法解决您的问题,因为主线程仍然被卡在_login函数内,直​​到登录完成。这意味着它不能做其他事情,如重绘屏幕,处理鼠标点击等。

所以,即使你解决了前两个问题并使事情有效,这仍然与不产生线程完全相同,只是在主线程中进行登录。

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