作為一個自動化測試人員,開發基本的應用桌面程序是必須的!最近在研究wxpython相關知識,目前看到多線程一塊,發現官方文檔介紹說:"在線程中不能修改修改窗口屬性!",但是實際情況是:最近在做一個翻牆的簡單APP。我開了2個線程一個線程用於顯示設置進度(用的是第三方host,所以要下載host再覆蓋本地host) ,一個線程處理下載任務,發現第一個線程中動態的設置self.gauge(value)可以生效,並沒用到wx.CallAfter!! 需要注意的是,wxpython一次只能處理一個事件,避免同時在線程中啟用用wx.CallAfter,這樣wxpython還是一個一個去執行CallAfter傳遞函數。即:啟用N個線程,N-1個用來干事情(不要在線程種使用CallAfter),第N個線程調用CallAfter通知主線程更新界面。上面說過了本人嘗試了在線程中直接更改主窗口控件(不知道是否出現問題)。是可行的!但是,還是建議按照官方說法利用wx.CallAfter,以免發生異常崩潰。
wxpython多線程應用的常景:對於一些復雜任務的處理(比如下載若干文件),如果把這些代碼全部放在主線程中,等你觸發了這個事件后,應用程序會卡死,而且也觸發不了其他時間,幾乎處於一個假死狀態(雖然他還活着...),這樣的程序別說給別人用了,自己用都會崩潰....,所以就用到多線程,觸發了下載事件后,在線程中完成任務,主線程干干嘛干嘛,但是要給一下提示給窗口程序,比如觸發下載按鈕時,主程序有"下載中..."等字樣,下載完成后,線程通知窗口程序更新狀態為"下載完成...",這樣的交互至少是友善的。
wxpython多線程的使用方法:wxpython開發者建議的是使用wx.CallAfter+PubSub。CallAfter負者推送時間給主程序,PubSub實現wxPython應用程序與其他線程進行通信。
其實在wx.CallAfter中直接傳一個主線程的方法,更簡單!但是既然官方這樣說,我們就這樣用!!好吧我們來一個官方的例子,來看看wxpython如何使用的~
import time import wx from threading import Thread from wx.lib.pubsub import Publisher ######################################################################## class TestThread(Thread): """Test Worker Thread Class.""" #---------------------------------------------------------------------- def __init__(self): """Init Worker Thread Class.""" Thread.__init__(self) self.start() # start the thread #---------------------------------------------------------------------- def run(self): """Run Worker Thread.""" # This is the code executing in the new thread. for i in range(6): time.sleep(10) wx.CallAfter(self.postTime, i) time.sleep(5) wx.CallAfter(Publisher().sendMessage, "update", "Thread finished!") #---------------------------------------------------------------------- def postTime(self, amt): """ Send time to GUI """ amtOfTime = (amt + 1) * 10 Publisher().sendMessage("update", amtOfTime) ######################################################################## class MyForm(wx.Frame): #---------------------------------------------------------------------- def __init__(self): wx.Frame.__init__(self, None, wx.ID_ANY, "Tutorial") # Add a panel so it looks the correct on all platforms panel = wx.Panel(self, wx.ID_ANY) self.displayLbl = wx.StaticText(panel, label="Amount of time since thread started goes here") self.btn = btn = wx.Button(panel, label="Start Thread") btn.Bind(wx.EVT_BUTTON, self.onButton) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.displayLbl, 0, wx.ALL|wx.CENTER, 5) sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5) panel.SetSizer(sizer) # create a pubsub receiver Publisher().subscribe(self.updateDisplay, "update") #---------------------------------------------------------------------- def onButton(self, event): """ Runs the thread """ TestThread() self.displayLbl.SetLabel("Thread started!") btn = event.GetEventObject() btn.Disable() #---------------------------------------------------------------------- def updateDisplay(self, msg): """ Receives data from thread and updates the display """ t = msg.data if isinstance(t, int): self.displayLbl.SetLabel("Time since thread started: %s seconds" % t) else: self.displayLbl.SetLabel("%s" % t) self.btn.Enable() #---------------------------------------------------------------------- # Run the program if __name__ == "__main__": app = wx.PySimpleApp() frame = MyForm().Show() app.MainLoop()
上述代碼我們隨便搜索下wxpython多線程的應用多會舉官方這個例子,可能是版本的問題(本人用的是wx-2.8),第一句就給來個錯,網上search一下提出這個Publisher這個moudle不存在的問題是普遍的,但是沒有相應的說法,現在就針對這個經典的wxpython程序剖析一下。這段代碼本身是簡單的,由於版本的問題可能出現如下問題。
1.ImportError: cannot import name Publisher
出現這個錯誤是正常的,我們進入wx.lib.pubsub這個模塊發現並沒有Publisher這個類,但是我們在wx.lib.pubsub這個模塊下面的pub模塊發現了Publisher的影子,原來在2.8版本時已經將這個類私有化了,見126行,_publisher = _Publisher(),同時將Publisher的subscribe與sendMessage復制給了subscribe與sendMessage變量見(128,131)行。所以我們這樣引入頭:from wx.lib.pubsub import pub,同時將所有的Publisher()改為pub。
2.TypeError: sendMessage() takes exactly 2 arguments (3 given)
經過1的修改,以為大功告成,運行依然報錯崩潰。出現這個問題大多數人如果硬找問題原因很難找的,我們debug進出,發現他實例化的是"C:\Python27\Lib\site-packages\wx-2.8-msw-unicode\wx\lib\pubsub\core\kwargs\publisher.py"這個模塊下的Publisher類,仔細一看sendMessage方法果然第二個參數應該傳個字典類型的所有我們將本程序24,32,71行改為wx.CallAfter(pub.sendMessage, "update", msg="Thread finished!"),pub.sendMessage("update", msg=amtOfTime),t = msg。運行一下沒問題~~注意sendMessage的鍵值(這里是msg)與subscribe監聽方法種的接收參數(這里是msg)要相同,看源碼其實就是msgKwargs這個字典參數來傳遞值的。
其實,這段代碼沒有問題的。我們注意到”C:\Python27\Lib\site-packages\wx-2.8-msw-unicode\wx\lib\pubsub\core\__init__.py" 的43行,其實core這個模塊對於Publisher這個類的加載是動態的。我們進入policies.py這個模塊第10行,發現msgDataProtocol = 'kwargs',原來如此...我們知道core模塊下面有2個包一個是arg1,一個是kwargs,我們觀察這2個包publisher模塊下Publisher類的sendMessage方法是不一樣的。原來我們這端代碼用的是arg1下的這個Publisher類,好吧,我們將policies.py的第10行改為msgDataProtocol ='arg1',還原代碼運行正確!!kwargs與arg1不同點是kwargs可以傳遞多個參數給主程序(**kwargs),而arg1只能是傳遞一個參數。
經過以上簡單的論述,我們知道了wxpython是通過wx.CallAfter給主程序推入事件,通過PubSub與主程序傳遞數據。關於wxpython多線程的簡單理解就是這樣了,希望能提供幫助。