wxpython: Is it possible to stop the running program when I click the stop button?

Viewed 1135

Suppose I have a large program which takes a while to complete, when the program starts, I found some parameter is not correctly set, so I want to stop it. Instead of terminate the program completely, I want the running calculation to stop but the wx.Frame still shown. Is it possible? Below is the sample code:

import wx
import time

class Test(wx.Frame):

    def __init__(self, title):
        super().__init__(None, title=title)
        self.panel = wx.Panel(self)

        self.initUI()
        self.Centre()

    def initUI(self):
        vbox = wx.BoxSizer(wx.VERTICAL)
        button1 = wx.Button(self.panel, label="START Calculation")
        button1.Bind(wx.EVT_BUTTON, self.start_test)

        button2 = wx.Button(self.panel, label="STOP Calculation")
        button2.Bind(wx.EVT_BUTTON, self.stop_test)

        vbox.Add(button1, 0, wx.ALL, 5)
        vbox.Add(button2, 0, wx.ALL, 5)
        self.panel.SetSizer(vbox)

    def start_test(self, e):
        for i in range(20000):
            print("Program is running...")
            time.sleep(5)

    def stop_test(self, e):
        print("How do I stop the test when click this button?")


if __name__ == '__main__':
    app = wx.App()
    Test("A large program").Show()
    app.MainLoop()

enter image description here

As shown in the program, when I click start button, the program runs. I hope I can stop the program but still keep the app window. For now, when I click the start button, the stop button is not even clickable. Is it possible to achieve the goal I want?

3 Answers

you can't do that with a single thread.

You have to run your computation loop in another thread, and have the stop button set a flag so the loop stops at some point (since you cannot forcefully kill a thread: Is there any way to kill a Thread?. TL;DR? the answer is no)

Something like this:

import threading

...

  def run_code(self):
      for i in range(20000):
          print("Program is running...")
          time.sleep(5)
          if self.__stopped:
             break
      self.__stopped = True

  def start_test(self, e):
      if not self.__stopped:
         self.__stopped = False
         threading.Thread(target=run_code).start()

  def stop_test(self, e):
      self.__stopped = True

now when you click on 'start' it launches the run_code method in a thread, so it yields to the main loop and the 'stop' button is active.

Since the thread runs in another method, you can share the __stopped attribute.

Be careful to put a time.sleep() call (even small) from time to time, because threads don't use time slicing in Python (because of the global interpreter lock that most python versions implement). The running thread has to yield some CPU to the main thread from time to time or that won't work. Well, you can try without first, and see.

Also don't use wx calls in threads. Use wx.CallAfter to schedule wx calls from within a thread, to be executed in the main thread.

I just slightly modified your code. I added a thread to run the calculate method, so it does not blocks the main thread.

import wx
import time
import threading

class Test(wx.Frame):

    def __init__(self, title):
        super().__init__(None, title=title)
        self.panel = wx.Panel(self)

        self.initUI()
        self.Centre()

    def initUI(self):
        vbox = wx.BoxSizer(wx.VERTICAL)
        button1 = wx.Button(self.panel, label="START Calculation")
        button1.Bind(wx.EVT_BUTTON, self.start_test)

        button2 = wx.Button(self.panel, label="STOP Calculation")
        button2.Bind(wx.EVT_BUTTON, self.stop_test)

        vbox.Add(button1, 0, wx.ALL, 5)
        vbox.Add(button2, 0, wx.ALL, 5)
        self.panel.SetSizer(vbox)
        self.stop = False

    def create_thread(self, target):
        thread = threading.Thread(target=target)
        thread.daemon = True
        thread.start()

    def calculate(self):
        for i in range(20000):
            if self.stop:
                break
            print("Program is running...")
            time.sleep(5)
        self.stop = False

    def start_test(self, e):
        self.create_thread(self.calculate)

    def stop_test(self, e):
        print("The calculation has been stopped!")
        self.stop = True


if __name__ == '__main__':
    app = wx.App()
    Test("A large program").Show()
    app.MainLoop()

If self.stop is True, it breaks out from the foor loop. This way you can start and stop the calculation, just by clicking the two buttons.

Some people seem to frown upon this method but I find it simple and useful.
Make use of Yield, which momentarily yields control back to the main loop.
This allows the main loop to still be responsive i.e. your stop button can be activated in between loop iterations.

import wx
import time

class Test(wx.Frame):

    def __init__(self, title):
        super().__init__(None, title=title)
        self.panel = wx.Panel(self)

        self.initUI()
        self.Centre()

    def initUI(self):
        vbox = wx.BoxSizer(wx.VERTICAL)
        button1 = wx.Button(self.panel, label="START Calculation")
        button1.Bind(wx.EVT_BUTTON, self.start_test)

        button2 = wx.Button(self.panel, label="STOP Calculation")
        button2.Bind(wx.EVT_BUTTON, self.stop_test)

        vbox.Add(button1, 0, wx.ALL, 5)
        vbox.Add(button2, 0, wx.ALL, 5)
        self.panel.SetSizer(vbox)

    def start_test(self, e):
        self.running = 0
        loop = 0
        while self.running == 0:
            print("Program is running..."+str(loop))
            time.sleep(1)
            loop +=1
            wx.GetApp().Yield()

    def stop_test(self, e):
        print("Process has stopped")
        self.running = 1

if __name__ == '__main__':
    app = wx.App()
    Test("A large program").Show()
    app.MainLoop()
Related