状态栏中的进度表,使用Cody Precord的ProgressStatusBar

时间:2011-03-11 20:17:59

标签: python wxpython

我正在尝试在我的应用程序的状态栏中创建一个进度表,我正在使用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 :(得分: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(),其中仪表是连续更新而不是离散的块;或者如果有更好的方法。

答案 1 :(得分:0)

如果这是任何用途,我将您的代码与wx.lib.delayedresult演示结合起来。当仪表更新时,GUI保持响应。在XP和Linux上测试过。

要记住的关键是你无法直接从后台线程更新任何GUI元素。发布事件虽然有效,但这就是我在这里所做的事(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()