状态栏中的进度指示器,使用Cody Precord的ProgressStatusBar
我正在尝试为我的应用程序在状态栏中创建一个进度条,并且我参考了Cody Precord的《wxPython 2.8应用开发手册》中的示例。我把示例内容复制到下面。
目前我只是想显示这个进度条,并在应用程序忙碌时让它闪烁,所以我想我需要使用Start/StopBusy()这两个方法。问题是,这些方法似乎都没有效果,而书中也没有提供如何使用这个类的示例。
在我的窗口的__init__方法中,我是这样创建状态栏的:
self.statbar = status.ProgressStatusBar( self )
self.SetStatusBar( self.statbar )
然后,在执行所有工作的函数中,我尝试了以下几种方法:
self.GetStatusBar().SetRange( 100 )
self.GetStatusBar().SetProgress( 0 )
self.GetStatusBar().StartBusy()
self.GetStatusBar().Run()
# work done here
self.GetStatusBar().StopBusy()
我还尝试了这些命令的多种组合,但都没有任何反应,进度条始终没有显示出来。这个工作需要几秒钟,所以不是因为进度条消失得太快我没注意到。
我可以通过去掉Precord的__init__中的self.prog.Hide()这一行来让进度条显示出来,但它仍然不会闪烁,并且在第一次工作完成后就消失了,再也不出现。
这是Precord的类:
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
更新:以下是我的__init__和Go方法。Go()是在用户点击按钮时调用的。它做了很多工作,这里不太相关。Setup*函数是其他设置控件和绑定的方法,我觉得它们在这里也不重要。
我可以不使用SetStatusBar,但这样状态栏就会出现在顶部,而不是底部,遮住其他控件,而且即使这样问题依然存在,所以我还是保留了它。
我在这里使用Start/StopBusy,但使用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( )
更新2 我尝试了你建议的代码,下面是整个测试应用程序,完全没有改动。它仍然不工作,进度条只在10秒结束后才会出现。
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()
2 个回答
如果这对你有帮助的话,我把你的代码和wx.lib.delayedresult的示例结合在了一起。这样在更新进度条的时候,界面依然可以正常使用。这个在XP和Linux上都测试过了。
需要记住的关键点是,你不能直接从后台线程更新任何界面元素。不过,发送事件是可以的,所以我在这里就是这么做的(使用了ProgressBarEvent和EVT_PROGRESSBAR)。
如果你有任何改进的建议,请分享代码。谢谢!
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()
有人建议我自己回答这个问题,也许能帮助到其他人。这个问题似乎是特定于某个平台(可能是Linux)的。可以看看joaquin的回答和相关评论。
解决这个问题的方法是在每次调用SetProgress()之后,直接对框架调用Update(),就像这个例子一样:
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()
Update()方法会立即重新绘制窗口或框架,而不是等到EVT_PAINT事件。显然,Windows和Linux在这个事件被调用和处理的时机上是有区别的。
我不确定这个方法在使用Start/StopBusy()时是否有效,因为在这种情况下,进度条是持续更新的,而不是分段更新的;或者是否有更好的解决办法。