Reputation:
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
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
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