【问题标题】:wxPython: Problems with threadwxPython:线程问题
【发布时间】:2011-06-09 09:34:57
【问题描述】:

我的计划不断扩大。

在 MainPanel 类的 MainProgram 方法中,我使用了来自其他程序的一些函数。使用此方法时,GUI 会挂起,直到完成,我想通过为此方法使用新线程来解决此问题。

执行此操作时,我在执行 OnRun 时遇到错误。它说:

Unhandled exception in thread started by <bound method MainPanel.OnIndex of <__main__.MainPanel; proxy of <Swig Object of type 'wxPanel *' at 0x526e238> >>

它认为这与 OnIndex 将 som 值设置为 self.textOutput 有关。现在,我该如何解决我的这个小问题?

非常感谢您的帮助! =)

import wx, thread 

ID_EXIT = 110

class MainPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)

        self.buttonRun = wx.Button(self, label="Run")
        self.buttonRun.Bind(wx.EVT_BUTTON, self.OnRun )
        self.buttonExit = wx.Button(self, label="Exit")
        self.buttonExit.Bind(wx.EVT_BUTTON, self.OnExit)

        self.labelChooseRoot = wx.StaticText(self, label ="Root catalog: ") 
        self.labelScratchWrk = wx.StaticText(self, label ="Scratch workspace: ")
        self.labelMergeFile = wx.StaticText(self, label ="Merge file: ")

        self.textChooseRoot = wx.TextCtrl(self, size=(210, -1))
        self.textChooseRoot.Bind(wx.EVT_LEFT_UP, self.OnChooseRoot)
        self.textScratchWrk = wx.TextCtrl(self, size=(210, -1))
        self.textMergeFile = wx.TextCtrl(self, size=(210, -1))
        self.textOutput = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)

        self.sizerF = wx.FlexGridSizer(3, 2, 5, 5)
        self.sizerF.Add(self.labelChooseRoot)  #row 1, col 1
        self.sizerF.Add(self.textChooseRoot)   #row 1, col 2
        self.sizerF.Add(self.labelScratchWrk)  #row 2, col 1
        self.sizerF.Add(self.textScratchWrk)   #row 2, col 2
        self.sizerF.Add(self.labelMergeFile)   #row 3, col 1
        self.sizerF.Add(self.textMergeFile)    #row 3, col 2

        self.sizerB = wx.BoxSizer(wx.VERTICAL)
        self.sizerB.Add(self.buttonRun, 1, wx.ALIGN_RIGHT|wx.ALL, 5)
        self.sizerB.Add(self.buttonExit, 0, wx.ALIGN_RIGHT|wx.ALL, 5)

        self.sizer1 = wx.BoxSizer()
        self.sizer1.Add(self.sizerF, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL, 10)
        self.sizer1.Add(self.sizerB, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)

        self.sizer2 = wx.BoxSizer()
        self.sizer2.Add(self.textOutput, 1, wx.EXPAND | wx.ALL, 5)

        self.sizerFinal = wx.BoxSizer(wx.VERTICAL)
        self.sizerFinal.Add(self.sizer1, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
        self.sizerFinal.Add(self.sizer2, 1, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)

        self.SetSizerAndFit(self.sizerFinal)


    def OnChooseRoot(self, event):
        dlg = wx.DirDialog(self, "Choose a directory:", style=wx.DD_DEFAULT_STYLE)
        if dlg.ShowModal() == wx.ID_OK:
            root_path = dlg.GetPath()
            self.textChooseRoot.SetValue(root_path)
        dlg.Destroy()

    def OnRun(self, event):
        #Check first if input values are
        thread.start_new_thread(self.OnIndex, ())

    def OnIndex(self):
        #Do something and post to self.textOutput what you do.

    def OnExit(self, event):
        self.GetParent().Close()


class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="IndexGenerator", size=(430, 330), 
                          style=((wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | 
                                  wx.STAY_ON_TOP) ^ wx.RESIZE_BORDER))
        self.CreateStatusBar() 

        self.fileMenu = wx.Menu()
        self.fileMenu.Append(ID_EXIT, "E&xit", "Exit the program")
        self.menuBar = wx.MenuBar()
        self.menuBar.Append(self.fileMenu, "&File")
        self.SetMenuBar(self.menuBar)
        wx.EVT_MENU(self, ID_EXIT, self.OnExit)                    

        self.Panel = MainPanel(self)

        self.CentreOnScreen()
        self.Show()

    def OnExit(self,  event):
        self.Close()

if __name__ == "__main__":
    app = wx.App(False)
    frame = MainWindow()
    app.MainLoop()

[编辑:] 这是 OnRun 和 OnIndex 方法的摘录。是否有一个 () 或一个 , ?

    def OnRun(self, Event=None):
        #---Check input values, continue if not wrong
        if self.CheckValid() == 0:
            #Get root directory
            root_path = self.textChooseRoot.GetValue()
            #Get scratch workspace
            scratch_workspace =self.textScratchWrk.GetValue()
            #Get merge file
            merge_fil = self.textMergeFile.GetValue()

            thread.start_new_thread(self.OnIndex, (root_path,scratch_workspace,merge_fil))

    def showmsg(self, msg):
        self.textOutput.AppendText(msg + "\n")


    def OnIndex(self,root_path,scratch_workspace,merge_fil):
            #---PUBSUB - publishes a message to the "show.statusbar"
##            msg = "Please wait...Program is running..."
##            Publisher().sendMessage(("show.statusbar"), msg)
            #---START INDEX GENERATOR CODE
            gp.overwriteoutput = 1
            gp.OutputMFlag = "DISABLED"
            gp.OutputZFlag = "DISABLED"
            fc_List = {}


            #Get log file. For now a constant. Needs to be changed.
            logfil = open("C:\\Python26\\Code\\log.txt", mode = "w")

            fold_nr = 0
            for root_fold, dirs, files in os.walk(root_path):
                root_fold_low = root_fold.lower()
                if not root_fold_low.find(".gdb") > -1:
                    fold_nr += 1
                    tot_t = time.clock()

                    wx.CallAfter(self.textOutput.AppendText, ("Mappe : " + str(fold_nr) + " : " + root_fold + "\n"))

【问题讨论】:

    标签: multithreading wxpython


    【解决方案1】:

    所有与 wx 对象的交互都应该在主线程中。

    一个简单的解决方法是使用wx.CallAfter(self.textOutput.SetValue, "output") 之类的东西而不是self.textOutput.SetValue("output")

    wx.CallAfter 向主循环发送要执行的内容,因为主循环在主线程中,所以一切正常。

    更新:工作合并代码sn-ps:

    import wx, thread, os, time
    
    ID_EXIT = 110
    
    class Dummy:
        pass
    
    gp = Dummy()
    
    class MainPanel(wx.Panel):
        def __init__(self, parent):
            wx.Panel.__init__(self, parent)
    
            self.buttonRun = wx.Button(self, label="Run")
            self.buttonRun.Bind(wx.EVT_BUTTON, self.OnRun )
            self.buttonExit = wx.Button(self, label="Exit")
            self.buttonExit.Bind(wx.EVT_BUTTON, self.OnExit)
    
            self.labelChooseRoot = wx.StaticText(self, label ="Root catalog: ") 
            self.labelScratchWrk = wx.StaticText(self, label ="Scratch workspace: ")
            self.labelMergeFile = wx.StaticText(self, label ="Merge file: ")
    
            self.textChooseRoot = wx.TextCtrl(self, size=(210, -1))
            self.textChooseRoot.Bind(wx.EVT_LEFT_UP, self.OnChooseRoot)
            self.textScratchWrk = wx.TextCtrl(self, size=(210, -1))
            self.textMergeFile = wx.TextCtrl(self, size=(210, -1))
            self.textOutput = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY)
    
            self.sizerF = wx.FlexGridSizer(3, 2, 5, 5)
            self.sizerF.Add(self.labelChooseRoot)  #row 1, col 1
            self.sizerF.Add(self.textChooseRoot)   #row 1, col 2
            self.sizerF.Add(self.labelScratchWrk)  #row 2, col 1
            self.sizerF.Add(self.textScratchWrk)   #row 2, col 2
            self.sizerF.Add(self.labelMergeFile)   #row 3, col 1
            self.sizerF.Add(self.textMergeFile)    #row 3, col 2
    
            self.sizerB = wx.BoxSizer(wx.VERTICAL)
            self.sizerB.Add(self.buttonRun, 1, wx.ALIGN_RIGHT|wx.ALL, 5)
            self.sizerB.Add(self.buttonExit, 0, wx.ALIGN_RIGHT|wx.ALL, 5)
    
            self.sizer1 = wx.BoxSizer()
            self.sizer1.Add(self.sizerF, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL, 10)
            self.sizer1.Add(self.sizerB, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
    
            self.sizer2 = wx.BoxSizer()
            self.sizer2.Add(self.textOutput, 1, wx.EXPAND | wx.ALL, 5)
    
            self.sizerFinal = wx.BoxSizer(wx.VERTICAL)
            self.sizerFinal.Add(self.sizer1, 0, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
            self.sizerFinal.Add(self.sizer2, 1, wx.ALIGN_RIGHT | wx.EXPAND | wx.ALL)
    
            self.SetSizerAndFit(self.sizerFinal)
    
    
        def OnChooseRoot(self, event):
            dlg = wx.DirDialog(self, "Choose a directory:", style=wx.DD_DEFAULT_STYLE)
            if dlg.ShowModal() == wx.ID_OK:
                root_path = dlg.GetPath()
                self.textChooseRoot.SetValue(root_path)
            dlg.Destroy()
    
        def CheckValid(self):
            return 0
    
        def OnRun(self, Event=None):
            #---Check input values, continue if not wrong
            if self.CheckValid() == 0:
                #Get root directory
                root_path = self.textChooseRoot.GetValue()
                #Get scratch workspace
                scratch_workspace =self.textScratchWrk.GetValue()
                #Get merge file
                merge_fil = self.textMergeFile.GetValue()
    
                thread.start_new_thread(self.OnIndex, (root_path,scratch_workspace,merge_fil))
    
        def showmsg(self, msg):
            self.textOutput.AppendText(msg + "\n")
    
        def OnIndex(self,root_path,scratch_workspace,merge_fil):
                #---PUBSUB - publishes a message to the "show.statusbar"
    ##            msg = "Please wait...Program is running..."
    ##            Publisher().sendMessage(("show.statusbar"), msg)
                #---START INDEX GENERATOR CODE
                gp.overwriteoutput = 1
                gp.OutputMFlag = "DISABLED"
                gp.OutputZFlag = "DISABLED"
                fc_List = {}
    
    
                #Get log file. For now a constant. Needs to be changed.
                #logfil = open("C:\\Python26\\Code\\log.txt", mode = "w")
    
                fold_nr = 0
                for root_fold, dirs, files in os.walk(root_path):
                    root_fold_low = root_fold.lower()
                    if not root_fold_low.find(".gdb") > -1:
                        fold_nr += 1
                        tot_t = time.clock()
    
                        wx.CallAfter(self.textOutput.AppendText, ("Mappe : " + str(fold_nr) + " : " + root_fold + "\n"))
    
        def OnExit(self, event):
            self.GetParent().Close()
    
    
    class MainWindow(wx.Frame):
        def __init__(self):
            wx.Frame.__init__(self, None, title="IndexGenerator", size=(430, 330), 
                              style=((wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | 
                                      wx.STAY_ON_TOP) ^ wx.RESIZE_BORDER))
            self.CreateStatusBar() 
    
            self.fileMenu = wx.Menu()
            self.fileMenu.Append(ID_EXIT, "E&xit", "Exit the program")
            self.menuBar = wx.MenuBar()
            self.menuBar.Append(self.fileMenu, "&File")
            self.SetMenuBar(self.menuBar)
            wx.EVT_MENU(self, ID_EXIT, self.OnExit)                    
    
            self.Panel = MainPanel(self)
    
            self.CentreOnScreen()
            self.Show()
    
        def OnExit(self,  event):
            self.Close()
    
    if __name__ == "__main__":
        app = wx.App(False)
        frame = MainWindow()
        app.MainLoop()
    

    【讨论】:

    • 你好。谢谢,但我遇到了另一个问题。我在方法中也有这个: root_path = self.textChooseRoot.GetValue() 如何将它与 wx.CallAfter 一起使用?没有要传递的参数....
    • 试过 root_path = wx.CallAfter(self.textChooseRoot.GetValue(), ()) 但这没有用。无论哪种方式,您的答案都取决于我认为 root_path 是否正确..
    • 我会将 root_path 作为参数传递给 OnIndex
    • 试过这样做。还是行不通。似乎 OnIndex 中的 wx.CallAfter 存在问题。使用这个: wx.CallAfter(self.textOutput.AppendText, ("Catalog :" + str(cat_nr) + "\n")) 。那应该有效吗?在我看来,布莱恩说的是对的。
    • 它应该而且确实有效。至于 root_path,请注意在为线程目标提供参数时必须使用元组,如下所示: thread.start_new_thread(self.OnIndex, (root_path,)) 并将 OnIndex 定义为: def OnIndex(self, root_path)
    【解决方案2】:

    您只需要使用线程安全的方法将信息发送回主线程。正如其他人所说,您不能直接与主线程交互,否则会发生奇怪的事情。这是一篇非常好的文章,介绍了您可以通过各种方式完成这一壮举:

    http://wiki.wxpython.org/LongRunningTasks

    还有一篇关于这个主题的文章:

    http://www.blog.pythonlibrary.org/2010/05/22/wxpython-and-threads/

    【讨论】:

      【解决方案3】:

      您不能从另一个线程与 GUI 交互。你的回调应该是这样的:

      def OnRun(self, event):
          <get any data you need from the GUI>
          thread.start_new_thread(self.WorkerThread, (parameter1, parameter2, ...))
      
      def WorkerThread(self, parameter1, parameter2, ...):
          # do time-consuming work here. To send data to
          # the GUI use CallAfter:
          wx.CallAfter(self.textOutput.AppendText, "whatever") 
      

      关键是在主线程中产生工作线程之前完成所有非耗时的GUI交互。一旦工作线程启动,它应该拥有它需要的所有数据。然后它可以继续工作,并使用CallAfter 与主线程通信。

      【讨论】:

      • 你好,布莱恩。那么,从另一个程序传递 textOutput 发生的事情不是一种选择吗?要么我有它导致GUI在其他程序运行时挂起的方式,但更新了textOutput。或者我可以让 GUI 不挂起,但失去从其他程序获取 textOutput 的能力?我理解对了吗?
      • @sekstiseks:不,你没有正确理解它。我的示例显示您的工作线程可以更新 GUI——它只是通过CallAfter 来完成此操作,而不是直接调用一些小部件方法。
      • 你好。添加了上面的 OnIndex 和 OnRun 方法的摘录。我究竟做错了什么?如果我删除 thread.start_new+++ 和 wx.CallAfter+++,一切正常。
      • @sekstiseks:我不可能知道你做错了什么。我看不到您的代码,也看不到您的错误。