Search code examples
pythonpython-multiprocessingwxpython

How to Properly Handle Multiple Calls to wx.App() using wxPython Phoenix


I am trying to troubleshoot/de-bug an issue I came across with my application using wxPython 4.0.7.

I re-wrote my entire program that was functioning with Python 2.7 and wxPython 2.8 on a Windows 7 32-bit system to now work with 64 bit Python 3.7.4 and wxPython 4.0.7 on a 64 bit Windows 10 system.

The problem I am having is that my program requires that it iterate multiple times based on the number of loops specified by the user, and it calls an instance of wx.App() from two different python scripts utilized.

I have read that calling multiple instances of wx.App() is a "no-no" (see creating multiple instances of wx.App)

Clearly this is a problem with this version of wxPython as my application crashes after the first iteration now, when it worked fine before.

Okay, so I understand this now, but I am not certain what the "fix" is for my particular issue.

The basic outline of my application is this: A "runner.py" script is launched which contains the main wx.frame() gui and the following code is appended to the end of the script:

app = wx.App()
frame = Runner(parent=None, foo=Foo)
frame.Show()
app.MainLoop()

When the user clicks on the "execute" button in the wxPython GUI, I have a progress dialog that initiates using this code:

pd = wx.ProgressDialog(title = "Runner.py", message= "Starting Model", parent=self, style=wx.PD_AUTO_HIDE | wx.PD_SMOOTH | wx.PD_CAN_ABORT )
pd.Update(15)

The runner.py script executes a "for loop" that does a bunch of stuff (actually reads in some inputs from R scripts) and then once it's done, it opens up a second python script ("looping.py") and iterates through a set of processes based on the number of loops the user specifies in the GUI launched from runner.py.

As the user needs to visually see what loop process the model run is going through, I have inside this second "looping.py" script, yet another instance of wx.App() that calls up another wx.ProgressDialog(), And the script looks like this:

#Progress Bar to user to start model
app = wx.App()
pd = wx.ProgressDialog("looping.py", "Setup Iteration", parent=None, style=wx.PD_AUTO_HIDE | wx.PD_SMOOTH |  wx.PD_CAN_ABORT )
pd.Update(15)

My specific question is this: How do I initiate the wx.ProgressDialog() successfully within the "looping.py" script without it crashing my application past the first iteration?


Solution

  • You will probably have to sub-class wx.ProgressDialog, arguably it may be easier to write your own progress bar display.
    Something like this, may give you some ideas.
    I've included the ability to run multiple threads doing different things, with pause and stop buttons. The main frame has a button to test whether the Gui is still active, whilst running the threads.
    Updates from the thread are driven by an event You may wish to reduce or increase its options.

    import time
    import wx
    from threading import Thread
    import wx.lib.newevent
    progress_event, EVT_PROGRESS_EVENT = wx.lib.newevent.NewEvent()
    
    class MainFrame(wx.Frame):
    
        def __init__(self):
            wx.Frame.__init__(self, None, title='Main Frame', size=(400,400))
            panel = MyPanel(self)
            self.Show()
    
    class MyPanel(wx.Panel):
    
        def __init__(self, parent):
            wx.Panel.__init__(self, parent)
            self.text_count = 0
            self.parent=parent
            self.btn_start = wx.Button(self, wx.ID_ANY, label='Start Long running process', size=(180,30), pos=(10,10))
            btn_test = wx.Button(self, wx.ID_ANY, label='Is the GUI still active?', size=(180,30), pos=(10,50))
            self.txt = wx.TextCtrl(self, wx.ID_ANY, style= wx.TE_MULTILINE, pos=(10,90),size=(300,100))
    
            self.btn_start.Bind(wx.EVT_BUTTON, self.Start_Process)
            btn_test.Bind(wx.EVT_BUTTON, self.ActiveText)
    
        def Start_Process(self, event):
            process1 = ProcessingFrame(title='Threaded Task 1', parent=self, job_no=1)
            process2 = ProcessingFrame(title='Threaded Task 2', parent=self, job_no=2)
            self.btn_start.Enable(False)
    
        def ActiveText(self,event):
            self.text_count += 1
            txt = "Gui is still active " + str(self.text_count)+"\n"
            self.txt.write(txt)
    
    class ProcessingFrame(wx.Frame):
    
        def __init__(self, title, parent=None,job_no=1):
            wx.Frame.__init__(self, parent=parent, title=title, size=(400,400))
            panel = wx.Panel(self)
            self.parent = parent
            self.job_no = job_no
            self.btn = wx.Button(panel,label='Stop processing', size=(200,30), pos=(10,10))
            self.btn.Bind(wx.EVT_BUTTON, self.OnExit)
            self.btn_pause = wx.Button(panel,label='Pause processing', size=(200,30), pos=(10,50))
            self.btn_pause.Bind(wx.EVT_BUTTON, self.OnPause)
            self.progress = wx.Gauge(panel,size=(200,10), pos=(10,90), range=60)
            self.process = wx.TextCtrl(panel,size = (200,250), pos=(10,120), style = wx.TE_MULTILINE)
            #Bind to the progress event issued by the thread
            self.Bind(EVT_PROGRESS_EVENT, self.OnProgress)
            #Bind to Exit on frame close
            self.Bind(wx.EVT_CLOSE, self.OnExit)
            self.Show()
    
            self.mythread = TestThread(self)
    
        def OnProgress(self, event):
            self.progress.SetValue(event.count)
            self.process.write(event.process+"\n")
            self.Refresh()
            if event.count >= 60:
                self.OnExit(None)
    
        def OnExit(self, event):
            if self.mythread.isAlive():
                self.mythread.terminate() # Shutdown the thread
                self.mythread.join() # Wait for it to finish
                self.parent.btn_start.Enable(True)
            self.Destroy()
    
        def OnPause(self, event):
            if self.mythread.isAlive():
                self.mythread.pause() # Pause the thread
    
    class TestThread(Thread):
        def __init__(self,parent_target):
            Thread.__init__(self)
            self.target = parent_target
            self.stopthread = False
            self.process = 1 # Testing only - mock process id
            self.start()    # start the thread
    
        def run(self):
            # A selectable test loop that will run for 60 loops then terminate
            if self.target.job_no == 1:
                self.run1()
            else:
                self.run2()
    
        def run1(self):
            curr_loop = 0
            while self.stopthread != True:
                if self.stopthread == "Pause":
                    time.sleep(1)
                    continue
                curr_loop += 1
                self.process += 10 # Testing only - mock process id
                if curr_loop <= 60: # Update progress bar
                    time.sleep(1.0)
                    evt = progress_event(count=curr_loop,process="Envoking process "+str(self.process))
                    #Send back current count for the progress bar
                    try:
                        wx.PostEvent(self.target, evt)
                    except: # The parent frame has probably been destroyed
                        self.terminate()
            self.terminate()
    
        def run2(self):
            curr_loop = 0
            while self.stopthread != True:
                if self.stopthread == "Pause":
                    time.sleep(1)
                    continue
                curr_loop += 1
                self.process += 100 # Testing only - mock process id
                if curr_loop <= 60: # Update progress bar
                    time.sleep(1.0)
                    evt = progress_event(count=curr_loop,process="Checking process"+str(self.process))
                    #Send back current count for the progress bar
                    try:
                        wx.PostEvent(self.target, evt)
                    except: # The parent frame has probably been destroyed
                        self.terminate()
            self.terminate()
    
        def terminate(self):
            self.stopthread = True
    
        def pause(self):
            if self.stopthread == "Pause":
                self.stopthread = False
                self.target.btn_pause.SetLabel('Pause processing')
            else:
                self.stopthread = "Pause"
                self.target.btn_pause.SetLabel('Continue processing')
    
    if __name__ == '__main__':
        app = wx.App(False)
        frame = MainFrame()
        app.MainLoop()
    

    enter image description here