3

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?

an offer can't refuse
  • 4,245
  • 5
  • 30
  • 50

3 Answers3

1

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.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • Is there any better way to do it, since the calculation may not be a for loop, It may be a large bunch of code and put the flag everywhere is not possible. Maybe there is a way to kill a thread directly? – an offer can't refuse Dec 22 '18 at 10:32
  • I have a non-professional question about thread: since the created thread is not the main thread. If the calculation part is resource consuming(e.g. need a lot of cpu power), will the calculation be as fast as the case which I didn't add the thread, and use my original program? – an offer can't refuse Dec 22 '18 at 15:25
  • yes, provided that the `sleep` that you insert doesn't take too long/isn't too frequent. Yield some milliseconds every second (average) and that won't show – Jean-François Fabre Dec 22 '18 at 16:26
1

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.

Attila Toth
  • 469
  • 3
  • 11
  • Is there a way to kill the thread directly, may be by the thread id or something? with that, I don't need any flag to insert to my main execution code. – an offer can't refuse Dec 22 '18 at 10:35
0

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()
Rolf of Saxony
  • 21,661
  • 5
  • 39
  • 60
  • Thanks, but suppose `time.sleep(100)` (which should be the time scale of a large program takes). This method will not respond until 100 seconds passed. – an offer can't refuse Dec 23 '18 at 15:29
  • Why 100 seconds? However you code the long running task, if it is only going to test for a stop event once every 100 seconds, you might as well spin it off as a seperate process and `kill` it, if the stop button is pressed. – Rolf of Saxony Dec 24 '18 at 10:16