Question

Please consider the following code:

    import wx
    import time

    class MyFrame(wx.Frame):
        def __init__(self,parent,id,title):
            wx.Frame.__init__(self,parent,id,title)
            self.txtu = wx.TextCtrl(self, -1)
            btnco = wx.Button(self, -1,"Rotate",pos=(0,30))
            self.Bind(wx.EVT_BUTTON, self.OnCo, id = btnco.GetId() )

        def OnCo(self,event):
            self.txtu.write("|")
            chars = ['|','/','-','\\']
            counter = 1
            for i in range(60):
                self.txtu.Replace(0, 1, chars[counter])
                counter += 1
                counter %= 4
                time.sleep(0.1)

    app = wx.App()
    frame = MyFrame(None,-1,"TextCtrl Problem")
    frame.Show()
    app.MainLoop()

My goal is to visualise a rotating bar in the TextCtrl for a few seconds when clicking the button. However, when running this code, the application blocks for a while and finally only the last character in the series is printed after the loop is done. How could I adapt this code see the rotation? Is there some sort of flush method (or other trick) that would allow this?

Thanks

Was it helpful?

Solution 3

Here is an example of the code I finally used. It is a combination of the answers by Mike Driscoll and Yoriz. Both their answers are great and valid for the question that was asked. However they stop working whenever the rotating character needs to indicate a calculation in progress. To prevent this I basically start two threads, one dealing with the actual calculation and another dealing with the rotating character. When the calculation is finished, its thread posts an event to the main frame. This latter can use this event to report the result of the calculation and to abort the progress thread. The progress thread in turn regularly posts events (mediated by a wx.Timer) to update the TextCtrl.

The code is the following:

import wx
from threading import Thread
from itertools import cycle

# Define notification event for thread completion
EVT_RESULT_ID = wx.NewId()
EVT_PROGRESSUPDATE_ID = wx.NewId()

def EVT_RESULT(win, func):
    """Define Result Event."""
    win.Connect(-1, -1, EVT_RESULT_ID, func)

class ResultEvent(wx.PyEvent):
    """Simple event to carry arbitrary result data."""
    def __init__(self, data):
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_RESULT_ID)
        self.data = data

def EVT_PROGRESSUPDATE(win, func):
    win.Connect(-1,-1,EVT_PROGRESSUPDATE_ID,func)

class ProgressUpdateEvent(wx.PyEvent):
    def __init__(self,data):
        wx.PyEvent.__init__(self)
        self.SetEventType(EVT_PROGRESSUPDATE_ID)
        self.data = data

# Thread class that shows progress
class ProgressThread(Thread):
    def __init__(self,notify_window):
        Thread.__init__(self)
        self._notify_window = notify_window
        self.chars = cycle(('|','/','-','\\'))

        self.tinyTimer = wx.Timer(notify_window)
        notify_window.Bind(wx.EVT_TIMER, self.updateTextCtrl, self.tinyTimer)
        self.tinyTimer.Start(100)

    def updateTextCtrl(self,event):
        wx.PostEvent(self._notify_window, ProgressUpdateEvent(next(self.chars)))

    def abort(self):
        self.tinyTimer.Stop()
        return

# Thread class that executes processing
class WorkerThread(Thread):
    """Worker Thread Class."""
    def __init__(self, notify_window):
        """Init Worker Thread Class."""
        Thread.__init__(self)
        self._notify_window = notify_window
        self.start()

    def run(self):
        """Run Worker Thread."""
        x = 0
        for i in range(100000000):
            x += i
        wx.PostEvent(self._notify_window, ResultEvent(x))

class MainFrame(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id, 'Thread Test')

        self.start_button = wx.Button(self, -1, 'Start', pos=(0,0))
        self.progress = wx.TextCtrl(self,-1,'',pos=(0,50))
        self.status = wx.StaticText(self, -1, '', pos=(0,100))
        self.Bind(wx.EVT_BUTTON, self.OnStart, self.start_button)

        # Set up event handlers
        EVT_RESULT(self,self.OnResult)
        EVT_PROGRESSUPDATE(self,self.OnProgressUpdate)

        # And indicate we don't have a worker thread yet
        self.worker = None

    def OnStart(self, event):
        """Start Computation."""
        # Trigger the worker thread unless it's already busy
        if not self.worker:
            self.status.SetLabel('Starting computation')
            self.worker = WorkerThread(self)
            self.p = ProgressThread(self)

    def OnResult(self, event):
        """Show Result status."""
        self.p.abort()
        self.status.SetLabel('Computation Result: %s' % event.data)
        self.worker = None

    def OnProgressUpdate(self,event):
        self.progress.ChangeValue(event.data)

class MainApp(wx.App):
    def OnInit(self):
        self.frame = MainFrame(None, -1)
        self.frame.Show(True)
        self.SetTopWindow(self.frame)
        return True

if __name__ == '__main__':
    app = MainApp(0)
    app.MainLoop()

OTHER TIPS

You can't use time.sleep() as it blocks wxPython's main loop. Instead, you should use a wx.Timer. I modified your code to use them as follows:

import wx

class MyFrame(wx.Frame):

    #----------------------------------------------------------------------
    def __init__(self,parent,id,title):
        wx.Frame.__init__(self,parent,id,title)
        panel = wx.Panel(self)
        self.counter = 1

        self.txtu = wx.TextCtrl(panel)
        btnco = wx.Button(panel, -1,"Rotate",pos=(0,30))
        self.Bind(wx.EVT_BUTTON, self.OnCo, id = btnco.GetId() )

        self.tinyTimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.updateTextCtrl, self.tinyTimer)

        self.sixtyTimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.onSixty, self.sixtyTimer)

    #----------------------------------------------------------------------
    def OnCo(self,event):
        self.tinyTimer.Start(100)
        self.sixtyTimer.Start(6000)

    #----------------------------------------------------------------------
    def onSixty(self, event):
        """
        Stop the timers and the animation
        """
        self.tinyTimer.Stop()
        self.sixtyTimer.Stop()

    #----------------------------------------------------------------------
    def updateTextCtrl(self, event):
        """
        Update the control so it appears to be animated
        """
        self.txtu.write("|")
        chars = ['|','/','-','\\']

        self.txtu.Clear()
        self.txtu.SetValue(chars[self.counter])
        self.counter += 1
        self.counter %= 4

#----------------------------------------------------------------------
app = wx.App()
frame = MyFrame(None,-1,"TextCtrl Problem")
frame.Show()
app.MainLoop()

Note that we need two timers. One to update the display every so often and the other time stop the animation after X seconds. In this case, I told it to stop the animation after 6 seconds. I changed the update a little too as when using your original code, it would put a bunch of characters in the text control instead of just spinning one.

Here are some handy decorators for allowing your methods to be threaded.

import wx
import time
from functools import wraps
from threading import Thread
from itertools import cycle


def runAsync(func):
    '''Decorates a method to run in a separate thread'''
    @wraps(func)
    def wrapper(*args, **kwargs):
        func_hl = Thread(target=func, args=args, kwargs=kwargs)
        func_hl.start()
        return func_hl
    return wrapper


def wxCallafter(target):
    '''Decorates a method to be called as a wxCallafter'''
    @wraps(target)
    def wrapper(*args, **kwargs):
        wx.CallAfter(target, *args, **kwargs)
    return wrapper


class MyFrame(wx.Frame):
    def __init__(self, parent, id_, title):
        wx.Frame.__init__(self, parent, id_, title)
        panel = wx.Panel(self)
        self.txtu = wx.TextCtrl(panel, -1)
        btnco = wx.Button(panel, -1, "Rotate", pos=(0, 30))
        btnco.Bind(wx.EVT_BUTTON, self.onBtn)

    @wxCallafter
    def setTextu(self, value):
        self.txtu.ChangeValue(value)

    @runAsync
    def onBtn(self, event):
        chars = cycle(('|', '/', '-', '\\'))
        for _ in range(60):
            if not self:  # Stops if the frame has been destroyed
                return
            self.setTextu(next(chars))
            time.sleep(0.1)

app = wx.App()
frame = MyFrame(None, -1, "TextCtrl Problem")
frame.Show()
app.MainLoop()
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top