user606547
user606547

Reputation:

Progress gauge in status bar, using Cody Precord's ProgressStatusBar

I am attempting to create a progress gauge in the status bar for my application, and I'm using the example in Cody Precord's wxPython 2.8 Application Development Cookbook. I've reproduced it below.

For now I simply wish to show the gauge and have it pulse when the application is busy, so I assume I need to use the Start/StopBusy() methods. Problem is, none of it seems to work, and the book doesn't provide an example of how to use the class.

In the __init__ of my frame I create my status bar like so:

self.statbar = status.ProgressStatusBar( self )
self.SetStatusBar( self.statbar )

Then, in the function which does all the work, I have tried things like:

self.GetStatusBar().SetRange( 100 )
self.GetStatusBar().SetProgress( 0 )
self.GetStatusBar().StartBusy()
self.GetStatusBar().Run()

# work done here

self.GetStatusBar().StopBusy()

And several combinations and permutations of those commands, but nothing happens, no gauge is ever shown. The work takes several seconds, so it's not because the gauge simply disappears again too quickly for me to notice.

I can get the gauge to show up by removing the self.prog.Hide() line from Precord's __init__ but it still doesn't pulse and simply disappears never to return once work has finished the first time.

Here's Precord's class:

class ProgressStatusBar( wx.StatusBar ):
    '''Custom StatusBar with a built-in progress bar'''
    def __init__( self, parent, id_=wx.ID_ANY,
                  style=wx.SB_FLAT, name='ProgressStatusBar' ):
        super( ProgressStatusBar, self ).__init__( parent, id_, style, name )

        self._changed = False
        self.busy = False
        self.timer = wx.Timer( self )
        self.prog = wx.Gauge( self, style=wx.GA_HORIZONTAL )
        self.prog.Hide()

        self.SetFieldsCount( 2 )
        self.SetStatusWidths( [-1, 155] )

        self.Bind( wx.EVT_IDLE, lambda evt: self.__Reposition() )
        self.Bind( wx.EVT_TIMER, self.OnTimer )
        self.Bind( wx.EVT_SIZE, self.OnSize )

    def __del__( self ):
        if self.timer.IsRunning():
            self.timer.Stop()

    def __Reposition( self ):
        '''Repositions the gauge as necessary'''
        if self._changed:
            lfield = self.GetFieldsCount() - 1
            rect = self.GetFieldRect( lfield )
            prog_pos = (rect.x + 2, rect.y + 2)
            self.prog.SetPosition( prog_pos )
            prog_size = (rect.width - 8, rect.height - 4)
            self.prog.SetSize( prog_size )
        self._changed = False

    def OnSize( self, evt ):
        self._changed = True
        self.__Reposition()
        evt.Skip()

    def OnTimer( self, evt ):
        if not self.prog.IsShown():
            self.timer.Stop()

        if self.busy:
            self.prog.Pulse()

    def Run( self, rate=100 ):
        if not self.timer.IsRunning():
            self.timer.Start( rate )

    def GetProgress( self ):
        return self.prog.GetValue()

    def SetProgress( self, val ):
        if not self.prog.IsShown():
            self.ShowProgress( True )

        if val == self.prog.GetRange():
            self.prog.SetValue( 0 )
            self.ShowProgress( False )
        else:
            self.prog.SetValue( val )

    def SetRange( self, val ):
        if val != self.prog.GetRange():
            self.prog.SetRange( val )

    def ShowProgress( self, show=True ):
        self.__Reposition()
        self.prog.Show( show )

    def StartBusy( self, rate=100 ):
        self.busy = True
        self.__Reposition()
        self.ShowProgress( True )
        if not self.timer.IsRunning():
            self.timer.Start( rate )

    def StopBusy( self ):
        self.timer.Stop()
        self.ShowProgress( False )
        self.prog.SetValue( 0 )
        self.busy = False

    def IsBusy( self ):
        return self.busy

Update: Here are my __init__ and Go methods. Go() is called when a button is clicked by the user. It does a lot of work which should be irrelevant here. The Setup* functions are other methods which sets up the controls and bindings, I think they're also irrelevant here.

I can leave out the SetStatusBar, but then the status bar appears at the top rather than the bottom and covers other controls, and the problem remains the same even then, so I've left it in.

I'm using Start/StopBusy here, but it's exactly the same with SetProgress.

def __init__( self, *args, **kwargs ):
    super( PwFrame, self ).__init__( *args, **kwargs )

    self.file = None
    self.words = None

    self.panel = wx.Panel( self )

    self.SetupMenu()      
    self.SetupControls()

    self.statbar = status.ProgressStatusBar( self )
    self.SetStatusBar( self.statbar )

    self.SetInitialSize()
    self.SetupBindings()

def Go( self, event ):
    self.statbar.StartBusy()

    # Work done here

    self.statbar.StopBusy( )

Update 2 I tried your suggested code, below is the entire test application, exactly as is. It still doesn't work, the gauge only shows up at the very end, after the 10 seconds have passed.

import time
import wx

import status

class App( wx.App ):
    def OnInit( self ):
        self.frame = MyFrame( None, title='Test' )
        self.SetTopWindow( self.frame )
        self.frame.Show()
        return True

class MyFrame(wx.Frame):
    def __init__(self, *args, **kargs):
        wx.Frame.__init__(self, *args, **kargs)
        self.bt = wx.Button(self)
        self.status = status.ProgressStatusBar(self)
        self.sizer = wx.BoxSizer(wx.VERTICAL)

        self.Bind(wx.EVT_BUTTON, self.on_bt, self.bt)

        self.sizer.Add(self.bt, 1, wx.EXPAND)
        self.sizer.Add(self.status, 1, wx.EXPAND)

        self.SetSizer(self.sizer)
        self.Fit()
        self.SetSize((500,50))

    def on_bt(self, evt):
        "press the button and it will start" 
        for n in range(100):
            time.sleep(0.1)
            self.status.SetProgress(n)

if __name__ == "__main__":    
    root = App()
    root.MainLoop()

Upvotes: 0

Views: 1937

Answers (2)

EgorZ
EgorZ

Reputation: 81

In case this is any use, I combined your code with the wx.lib.delayedresult demo. GUI stays responsive while the gauge is updated. Tested on XP and Linux.

Key thing to remember is that you cannot update any GUI elements directly from the background thread. Posting events works though, so that's what I'm doing here (ProgressBarEvent and EVT_PROGRESSBAR).

Please post any improvements to the code. Thanks!

import time
import wx
import wx.lib.delayedresult as delayedresult
import wx.lib.newevent

ProgressBarEvent, EVT_PROGRESSBAR = wx.lib.newevent.NewEvent()

class ProgressStatusBar( wx.StatusBar ):
    '''Custom StatusBar with a built-in progress bar'''
    def __init__( self, parent, id_=wx.ID_ANY,
                  style=wx.SB_FLAT, name='ProgressStatusBar' ):
        super( ProgressStatusBar, self ).__init__( parent, id_, style, name )

        self._changed = False
        self.busy = False
        self.timer = wx.Timer( self )
        self.prog = wx.Gauge( self, style=wx.GA_HORIZONTAL )
        self.prog.Hide()

        self.SetFieldsCount( 2 )
        self.SetStatusWidths( [-1, 155] )

        self.Bind( wx.EVT_IDLE, lambda evt: self.__Reposition() )
        self.Bind( wx.EVT_TIMER, self.OnTimer )
        self.Bind( wx.EVT_SIZE, self.OnSize )
        self.Bind( EVT_PROGRESSBAR, self.OnProgress )

    def __del__( self ):
        if self.timer.IsRunning():
            self.timer.Stop()

    def __Reposition( self ):
        '''Repositions the gauge as necessary'''
        if self._changed:
            lfield = self.GetFieldsCount() - 1
            rect = self.GetFieldRect( lfield )
            prog_pos = (rect.x + 2, rect.y + 2)
            self.prog.SetPosition( prog_pos )
            prog_size = (rect.width - 8, rect.height - 4)
            self.prog.SetSize( prog_size )
        self._changed = False

    def OnSize( self, evt ):
        self._changed = True
        self.__Reposition()
        evt.Skip()

    def OnTimer( self, evt ):
        if not self.prog.IsShown():
            self.timer.Stop()

        if self.busy:
            self.prog.Pulse()

    def Run( self, rate=100 ):
        if not self.timer.IsRunning():
            self.timer.Start( rate )

    def GetProgress( self ):
        return self.prog.GetValue()

    def SetProgress( self, val ):
        if not self.prog.IsShown():
            self.ShowProgress( True )

        self.prog.SetValue( val )
        #if val == self.prog.GetRange():
        #    self.prog.SetValue( 0 )
        #    self.ShowProgress( False )

    def OnProgress(self, event):
        self.SetProgress(event.count)

    def SetRange( self, val ):
        if val != self.prog.GetRange():
            self.prog.SetRange( val )

    def ShowProgress( self, show=True ):
        self.__Reposition()
        self.prog.Show( show )

    def StartBusy( self, rate=100 ):
        self.busy = True
        self.__Reposition()
        self.ShowProgress( True )
        if not self.timer.IsRunning():
            self.timer.Start( rate )

    def StopBusy( self ):
        self.timer.Stop()
        self.ShowProgress( False )
        self.prog.SetValue( 0 )
        self.busy = False

    def IsBusy( self ):
        return self.busy

class MyFrame(wx.Frame):
    def __init__(self, *args, **kargs):
        wx.Frame.__init__(self, *args, **kargs)
        self.bt = wx.Button(self)
        self.bt.SetLabel("Start!")
        self.status = ProgressStatusBar(self)

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.Bind(wx.EVT_BUTTON, self.handleButton, self.bt)
        self.sizer.Add(self.bt, 1, wx.EXPAND)

        self.SetStatusBar(self.status)

        self.SetSizer(self.sizer)
        self.Fit()
        self.SetSize((600,200))

        #using a flag to determine the state of the background thread
        self.isRunning = False

        #number of iterations in the delayed calculation
        self.niter = 200

        #from the delayedresult demo
        self.jobID = 0
        self.abortEvent = delayedresult.AbortEvent()
        self.Bind(wx.EVT_CLOSE, self.handleClose)

    def handleButton(self, evt):
        "Press the button and it will start. Press again and it will stop." 
        if not self.isRunning:
            self.bt.SetLabel("Abort!")
            self.abortEvent.clear()
            self.jobID += 1

            self.log( "Starting job %s in producer thread: GUI remains responsive"
                      % self.jobID )

            #initialize the status bar (need to know the number of iterations)
            self.status.SetRange(self.niter)
            self.status.SetProgress(0)

            delayedresult.startWorker(self._resultConsumer, self._resultProducer, 
                                      wargs=(self.jobID,self.abortEvent), jobID=self.jobID)
        else:
            self.abortEvent.set()
            self.bt.SetLabel("Start!")

            #get the number of iterations from the progress bar (approximatively at least one more)
            result = self.status.GetProgress()+1
            self.log( "Aborting result for job %s: Completed %d iterations" % (self.jobID,result) )

        self.isRunning = not self.isRunning

    def handleClose(self, event):
        """Only needed because in demo, closing the window does not kill the 
        app, so worker thread continues and sends result to dead frame; normally
        your app would exit so this would not happen."""
        if self.isRunning:
            self.log( "Exiting: Aborting job %s" % self.jobID )
            self.abortEvent.set()
        self.Destroy()

    def _resultProducer(self, jobID, abortEvent):
        """Pretend to be a complex worker function or something that takes 
        long time to run due to network access etc. GUI will freeze if this 
        method is not called in separate thread."""

        count = 0
        while not abortEvent() and count < self.niter:

            #5 seconds top to get to the end...
            time.sleep(5./self.niter)
            count += 1

            #update after a calculation
            event = ProgressBarEvent(count=count)
            wx.PostEvent(self.status, event)

        #introduce an error if jobID is odd
        if jobID % 2 == 1:
            raise ValueError("Detected odd job!")

        return count

    def _resultConsumer(self, delayedResult):
        jobID = delayedResult.getJobID()
        assert jobID == self.jobID
        try:
            result = delayedResult.get()
        except Exception, exc:
            result_string = "Result for job %s raised exception: %s" % (jobID, exc) 
        else:
            result_string = "Got result for job %s: %s" % (jobID, result)

        # output result
        self.log(result_string)

        # get ready for next job:
        self.isRunning = not self.isRunning
        self.bt.SetLabel("Start!")

        #Use this to hide the progress bar when done.
        self.status.ShowProgress(False)

    def log(self,text):
        self.SetStatusText(text)

if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = MyFrame(None)
    frame.Show()
    app.MainLoop()

Upvotes: 0

user606547
user606547

Reputation:

I was advised to answer my own question, maybe it'll help others. This problem seems to be a platform (Linux?) specific problem. See joaquin's answer and accompanying comments.

It can be solved by calling Update() on the frame itself after each call to SetProgress(), as in this example

import time
import wx

class MyFrame(wx.Frame):
    def __init__(self, *args, **kargs):
        wx.Frame.__init__(self, *args, **kargs)
        self.bt = wx.Button(self)
        self.status = ProgressStatusBar(self)
        self.sizer = wx.BoxSizer(wx.VERTICAL)

        self.Bind(wx.EVT_BUTTON, self.on_bt, self.bt)

        self.sizer.Add(self.bt, 1, wx.EXPAND)
        self.sizer.Add(self.status, 1, wx.EXPAND)

        self.SetSizer(self.sizer)
        self.Fit()
        self.SetSize((500,200))

    def on_bt(self, evt):
        "press the button and it will start" 
        for n in range(100):
            time.sleep(0.1)
            self.status.SetProgress(n)
            self.Update()

if __name__ == '__main__':
    app = wx.PySimpleApp()
    frame = MyFrame(None)
    frame.Show()
    app.MainLoop()

The Update() method immediately repaints the window/frame, rather than waiting for EVT_PAINT. Apparently there is a difference between Windows and Linux in when this event is called and/or processed.

I do not know if this trick will work well with Start/StopBusy() where the gauge is updated continuously rather than in discrete chunks; or if there is a better way entirely.

Upvotes: 1

Related Questions