wx.Gauge在Windows中无法超过25%更新,在Linux中正常工作

3 投票
1 回答
979 浏览
提问于 2025-04-16 17:38

我在使用wxPython时遇到了很多麻烦,尤其是在不同平台之间的兼容性问题 :(

下面是一个函数。当用户点击一个按钮时,这个函数会被调用,它会执行一些可能需要时间的工作。在这个过程中,状态栏会显示一个进度条。

def Go(self, event):       
    progress = 0
    self.statbar.setprogress(progress)
    self.Update()

    # ...

    for i in range(1, numwords + 1):
        progress = int(((float(i) / float(numwords)) * 100) - 1)
        self.wrdlst.Append(words.next())
        self.statbar.setprogress(progress)
        self.Update()

    self.wrdlst.Refresh() 

    # ...

    progress = 100
    self.PushStatusText(app.l10n['msc_genwords'] % numwords)        
    self.statbar.setprogress(progress)

在Linux系统下,调用self.Update()是必要的,否则进度条不会更新,直到函数执行完毕,这样就没什么意义了。而在Windows(至少是Win 7)下,这些调用似乎没有效果。

在Linux上,这一切都运行得很好(有调用Update()),但在Windows 7上,进度条在大约20-25%的时候就停止了,距离函数结束还有一段时间。也就是说,进度条应该移动到25%左右,然后就停下来了,没什么明显的原因,但函数依然正常执行并输出正确的结果。

为了找出问题,我尝试在更新进度条之前插入一行print progress,想看看progress的值是不是我想的那样。令我惊讶的是,这样进度条就正常工作了,但一旦我去掉print,它又停止工作了。我也可以用time.sleep(0.001)替代print,但即使是这么短的暂停,程序也几乎停滞不前,如果我把时间再缩短一点,问题又会出现,所以这并没有什么帮助。

我搞不清楚发生了什么,也不知道该怎么解决,但我猜在Windows下,事情的进展太快了,以至于progress在一段时间后没有正确更新,始终保持在一个固定的值(大约25)。不过,我也不知道为什么会这样,这对我来说没有意义。当然,printsleep都不是好的解决方案。即使我打印“nothing”,Windows仍然会为这个不存在的输出打开另一个窗口,这很烦人。

如果你需要更多信息或代码,请告诉我。

编辑:好的,这里有一个工作应用程序(至少对我来说)有这个问题。虽然它还是挺长的,但我尽量删掉了与当前问题无关的部分。

在Linux上它运行正常,就像完整的应用程序一样。在Windows上,它是否正常工作取决于Go函数中numwords的值。如果我把它的值增加到1000000(100万),问题就消失了。我怀疑这可能与系统有关,所以如果它在你那儿正常工作,可以尝试调整numwords的值。也可能是因为我把它改成了Append()一个静态文本,而不是像原始代码那样调用生成器。

不过,当前numwords的值(100000)在我这里在Windows上确实失败了。

import wx

class Wordlist(wx.TextCtrl):    
    def __init__(self, parent):
        super(Wordlist, self).__init__(parent,
                                       style=wx.TE_MULTILINE|wx.TE_READONLY)
        self.words = []
        self.SetValue("")

    def Get(self):
        return '\r\n'.join(self.words)

    def Refresh(self):
        self.SetValue(self.Get())

    def Append(self, value):
        if isinstance(value, list):
            value = '\r\n'.join(value)        
        self.words.append(unicode(value))

class ProgressStatusBar(wx.StatusBar):
    def __init__(self, *args, **kwargs):
        super(ProgressStatusBar, self).__init__(*args, **kwargs)

        self._changed = False

        self.prog = wx.Gauge(self, style=wx.GA_HORIZONTAL)
        self.prog.Hide()

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

        self.Bind(wx.EVT_IDLE, lambda evt: self.__reposition())
        self.Bind(wx.EVT_SIZE, self.onsize)

    def __reposition(self): 
        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 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 showprogress(self, show=True):
        self.__reposition()
        self.prog.Show(show)

class MainFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MainFrame, self).__init__(*args, **kwargs)

        self.SetupControls()

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

        self.panel.Fit()
        self.SetInitialSize()
        self.SetupBindings()

    def SetupControls(self):
        self.panel = wx.Panel(self)

        self.gobtn = wx.Button(self.panel, label="Go")                               
        self.wrdlst = Wordlist(self.panel)

        wrap = wx.BoxSizer()
        wrap.Add(self.gobtn, 0, wx.EXPAND|wx.ALL, 10)
        wrap.Add(self.wrdlst, 0, wx.EXPAND|wx.ALL, 10)
        self.panel.SetSizer(wrap)

    def SetupBindings(self):
        self.Bind(wx.EVT_BUTTON, self.Go, self.gobtn)

    def Go(self, event):       
        progress = 0
        self.statbar.setprogress(progress)
        self.Update()

        numwords = 100000

        for i in range(1, numwords + 1):
            progress = int(((float(i) / float(numwords)) * 100) - 1)
            self.wrdlst.Append("test " + str(i))
            self.statbar.setprogress(progress)
            self.Update()

        self.wrdlst.Refresh()

        progress = 100
        self.statbar.setprogress(progress)

class App(wx.App):
    def __init__(self, *args, **kwargs):
        super(App, self).__init__(*args, **kwargs)
        framestyle = wx.MINIMIZE_BOX|wx.CLOSE_BOX|wx.CAPTION|wx.SYSTEM_MENU|\
                     wx.CLIP_CHILDREN
        self.frame = MainFrame(None, title="test", style=framestyle)
        self.SetTopWindow(self.frame)
        self.frame.Center()
        self.frame.Show()

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

编辑2:下面是一个更简单的代码版本。我觉得我不能再缩小了。对我来说,它仍然有问题。我可以在IDLE中运行它,或者直接通过双击.py文件在Windows中运行,两种方式效果一样。

我尝试了不同的numwords值。似乎问题并没有像我最初说的那样消失,而是当我增加numwords时,进度条在调用print之前能达到更高的进度。在当前值为1,000,000时,这个简化版本能达到大约50%。在上面的长版本中,值为1,000,000时能达到大约90%,值为100,000时能达到大约25%,而值为10,000时只能达到大约10%。

在下面的版本中,一旦调用print,进度就会继续,甚至在循环结束时能达到99%。在原始版本中,调用self.wrdlst.Refresh()时,当numwords值较高时需要几秒钟,这可能导致进度条暂停。所以我认为事情是这样的:在循环中,进度条只能达到某个点,当循环结束时,函数继续执行,而进度条保持不动,等到函数结束时,进度条才继续,直到达到99%。因为print语句不会花太多时间,下面的版本让人感觉进度条从0%平滑移动到99%,但print却暗示了其他情况。

import wx

class MainFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MainFrame, self).__init__(*args, **kwargs)        
        self.panel = wx.Panel(self)        
        self.gobtn = wx.Button(self.panel, label="Go")
        self.prog = wx.Gauge(self, style=wx.GA_HORIZONTAL)
        wrap = wx.BoxSizer()
        wrap.Add(self.gobtn, 0, wx.EXPAND|wx.ALL, 10)
        wrap.Add(self.prog, 0, wx.EXPAND|wx.ALL, 10)
        self.panel.SetSizer(wrap)
        self.panel.Fit()
        self.SetInitialSize()        
        self.Bind(wx.EVT_BUTTON, self.Go, self.gobtn)

    def Go(self, event):        
        numwords = 1000000
        self.prog.SetValue(0)
        for i in range(1, numwords + 1):
            progress = int(((float(i) / float(numwords)) * 100) - 1)
            self.prog.SetValue(progress)
        print "Done"

if __name__ == "__main__":
    app = wx.App()
    frame = MainFrame(None)
    frame.Show()
    app.MainLoop()

1 个回答

2

其实,你的长时间运行的任务正在阻塞图形界面线程。这可能在某些平台和/或电脑上运行得不错,也可能不行。

import wx
from wx.lib.delayedresult import startWorker

class MainFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(MainFrame, self).__init__(*args, **kwargs)        
        self.panel = wx.Panel(self)        
        self.gobtn = wx.Button(self.panel, label="Go")
        self.prog = wx.Gauge(self, style=wx.GA_HORIZONTAL)
        self.timer = wx.Timer(self)

        wrap = wx.BoxSizer()
        wrap.Add(self.gobtn, 0, wx.EXPAND|wx.ALL, 10)
        wrap.Add(self.prog, 0, wx.EXPAND|wx.ALL, 10)
        self.panel.SetSizer(wrap)
        self.panel.Fit()
        self.SetInitialSize()        

        self.Bind(wx.EVT_BUTTON, self.Go, self.gobtn)
        self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)


    def Go(self, event):
        # Start actual work in another thread and start timer which 
        # will periodically check the progress and draw it
        startWorker(self.GoDone, self.GoCompute)
        self.progress = 0
        self.timer.Start(100)

    def OnTimer(self, event):
        # Timer draws the progress
        self.prog.SetValue(self.progress)

    def GoCompute(self):
        # This method will run in another thread not blocking the GUI
        numwords = 10000000
        self.prog.SetValue(0)
        for i in range(1, numwords + 1):
            self.progress = int(((float(i) / float(numwords)) * 100) - 1)

    def GoDone(self, result):
        # This is called when GoCompute finishes
        self.prog.SetValue(100)
        self.timer.Stop()
        print "Done"

if __name__ == "__main__":
    app = wx.App()
    frame = MainFrame(None)
    frame.Show()
    app.MainLoop()

另外,注意和你的例子相反:

  • 按钮在点击后会恢复到未点击状态
  • 你可以移动窗口,它不会卡住

一般来说,像这样的方法 def Something(self, event) 应该只运行几毫秒。

编辑:我还观察到在Windows 7上,进度条开始增长是在你调用 self.prog.SetValue() 的时候,它会在一段时间内慢慢增长到指定的值。它不会“跳”到那个值,而是慢慢增长到设定的值。这似乎是Windows 7的一个特性。我不得不在性能选项中关闭“在窗口内部动画控件和元素”来消除这种行为。

撰写回答