状态栏中的进度指示器,使用Cody Precord的ProgressStatusBar

0 投票
2 回答
1922 浏览
提问于 2025-04-16 13:32

我正在尝试为我的应用程序在状态栏中创建一个进度条,并且我参考了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 个回答

0

如果这对你有帮助的话,我把你的代码和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()
1

有人建议我自己回答这个问题,也许能帮助到其他人。这个问题似乎是特定于某个平台(可能是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()时是否有效,因为在这种情况下,进度条是持续更新的,而不是分段更新的;或者是否有更好的解决办法。

撰写回答