2013-02-19 145 views
0

我是Python新手,目前正在開發一個小型應用程序供個人使用。我用我的gui使用tkinter。試圖與tkinter(tkinter是在主線程上)運行線程執行任務,而是它停止主線程

我想要做的是創建一個帶有標籤的Toplevel彈出窗口,根據登錄嘗試的方式更改文本。所以,當tk運行的主線程顯示帶有動態文本的彈出窗口時,我想要啓動一個線程來嘗試登錄至多5次,並通過設置名爲'logindata'的全局變量向主線程報告。

AuctioneerGUI和LoginThread類中的_login()方法實際上是唯一重要的事情,您可以忽略其餘部分,但它們可能認爲相關。

當按下登錄按鈕時調用_login()方法。所有這些都是嘗試登錄並設置logindata。同時主線程正在循環,直到它注意到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 protected][^@]+\.[^@]+", 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解決問題後:

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

+0

理解它的工作原理很有用。您仍在投票處理完成情況,這仍然是浪費,但是您只會每100ms執行一次,而不是儘可能快。而且,更重要的是,你讓主要的'tkinter'事件循環在間隔100ms內運行,所以GUI不會凍結。這仍然不是理想的,但它是你用純tkinter做的最好的。 – abarnert 2013-02-19 02:25:15

+0

是的,我意識到儘可能快地通過輪詢囤積所有cpu資源並不明智。我發佈的代碼片段幾乎是一個正在進行的混亂。我有一些java線程的經驗,但我從來沒有像tkinter一樣處理它。 – 2013-02-19 02:29:34

+0

我可能會記住錯誤,但不是Swing基本相同 - 觸摸另一個線程中的任何Swing對象會導致異常或神祕崩潰等。除非我非常確定Swing有一個'runLaterOnDispatchThread'方法(和''BackgroundWorker'和'WorkerExecutor'),對吧? – abarnert 2013-02-19 18:48:48

回答

2

第零,因爲unutbu指出,你只是運行的其他線程的主線程run功能,所以沒有什麼事情發生,直到其完成。


一旦你解決這個問題,你永遠,永遠想有一個線程自旋等待一個變量發生變化,因爲你在這裏做的:

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

主線程不可能做任何事情但是在這裏旋轉,直到後臺線程將logindata["counter"]設置爲其他東西。如果強制主線程等待另一個線程完成,那麼還可以在主線程中運行其他代碼。你的代碼與單線程處理有相同的效果,除了它燒盡可能多的CPU儘可能檢查值,無論原因。

如果您需要等到某些事情完成,您需要使用某種交叉線程信號,例如threading.Conditionqueue.Queue


然而,這仍然不能解決你的問題,因爲主線程會仍然被卡住的_login函數內,直​​到登錄完成。這意味着它不能做其他事情,如重畫屏幕,處理鼠標點擊等。

所以,即使你解決了前兩個問題,並且得到了工作,這仍然是完全一樣的不產卵線程,只是在主線程中進行登錄。

你需要的是一個_login函數,它在啓動後臺線程後立即返回,然後使用其他一些機制從後臺線程觸發tkinter循環中的事件。

+0

謝謝。我會研究這個,並做更多的研究,聽起來這應該解決我的問題,我現在看到明顯的,即那些其他while循環仍然在主線程中。 – 2013-02-19 00:51:42

+0

@DaanLubbers:'tkinter'的最大問題在於它沒有線程安全的'run_function_in_main_thread',或者甚至可以使用線程安全的'add_event_to_queue'方法來自己實現一個。你必須做一些搜索來找出如何去做(儘管答案可能在右邊的相關問題之一上)。或者搜索mktkinter,它可以讓所有主線程外的調用變爲「在主線程中運行此函數」調用。 – abarnert 2013-02-19 00:54:06

1

正確的方式開始新的threading.Thread是通過調用start方法,而不是run方法。這是產生新線程的start方法。沒有它,你實際上正在主線程中運行。

因此,而不是嘗試:

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

the docs

一旦線程對象被創建,它的活動必須通過調用 線程的start()方法來啓動。這將調用單獨的控制線程中的run()方法。

+0

我不知道那是怎麼發生的。我甚至稱它是從文本開始的,我很確定這是從某個時刻開始的。 使用開始彈出並不顯示,雖然它出現。我按下登錄按鈕,當http的東西完成時,它會保持按住一秒左右,然後它將取消按下並將正確的數據發送到主線程。所以它似乎仍然以某種方式阻止主線程。或者它可能會在彈出窗口呈現之前以某種方式關閉? – 2013-02-19 00:46:08