尝试在tkinter中运行线程(tkinter在主线程)执行任务,但却导致主线程暂停
我刚开始学习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,并通过队列进行通信,就像这个例子一样:
2 个回答
启动一个 threading.Thread
的正确方法是调用 start
方法,而不是 run
方法。其实,start
方法是用来创建一个新线程的。如果你直接用 run
方法,那你就是在主线程里运行 LoginThread.run
,并没有真正创建新线程。
所以你应该这样做:
_thread = LoginThread(self._sm)
_thread.start()
来自 官方文档:
一旦创建了线程对象,就必须通过调用线程的 start() 方法来启动它的活动。这样会在一个单独的控制线程中调用 run() 方法。
首先,正如unutbu所说,你实际上是在主线程中运行另一个线程的run
函数,所以在它完成之前,什么事情都不会发生。
一旦你解决了这个问题,你绝对不想让一个线程在等待某个变量变化时一直转圈,就像你在这里做的那样:
while counter == -1:
counter = logindata["counter"]
print(counter)
在这里,主线程只能一直转圈,直到后台线程把logindata["counter"]
改成其他值。如果你强迫主线程等待另一个线程完成,那你不如直接在主线程中运行其他代码。你的代码效果就跟单线程一样,只是它还会不停地消耗CPU资源,反复检查这个值,完全没有必要。
如果你需要等某件事情完成,你需要使用某种跨线程的信号,比如threading.Condition
或者queue.Queue
。
不过,这样做仍然无法解决你的问题,因为主线程会一直卡在_login
函数里,直到登录完成。这意味着它无法做其他事情,比如重新绘制屏幕、处理鼠标点击等等。
所以,即使你解决了前两个问题,让事情运转起来,这仍然和不创建线程、直接在主线程中登录是一样的。
你需要的是一个_login
函数,它在启动后台线程后立即返回,然后使用其他机制从后台线程触发tkinter循环中的事件。