wx.Gauge fails to update beyond 25% in Windows, works in Linux
I seem to have nothing but trouble with wxPython and cross-platform compatibility :(
I have the function below. It's called when the user clicks a button, it does some work which may take a while, during which a progress gauge is shown in the status bar.
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)
The calls to self.Update()
are apparently needed under Linux, otherwise the gauge doesn't update until the function exits which makes it kinda pointless. These calls seem to have no effect under Windows (Win 7 at least).
The whole thing works perfectly under Linux (with the calls to Update()), but on Windows 7 the gauge seems to stop around the 20-25% mark, a while before the function exits. So it moves as it should until it reaches ~25%, then the gauge stops moving for no apparent reason but the function continues on just fine and exits with the proper output.
In my attempt to find out the problem, I tried inserting a print progress
line just before updating the gauge inside the loop, thinking maybe the value of progress
wasn't what I thought it should be. To my big surprise, the gauge now worked as it should, but the moment I remove that print
it stops working. I can also replace the print with a call to time.sleep(0.001)
, but even with such a short sleep the process still grinds to almost a halt, and if I lower it even further the problem returns, so it's hardly very helpful.
I can't figure out what is going on or how to fix it, but I guess somehow things move too fast under Windows so that progress
doesn't get updated properly after a while and just stays at a fixed value (~25). I have no idea why that would be, however, it makes no sense to me. And of course, neither print
nor sleep
are good solutions. Even if I print out "nothing", Windows still opens another window for the non-existent output, which is annoying.
Let me know if you need further info or code.
Edit: Ok, here's a working application which (for me at least) has the problem. It's still pretty long, but I tried to cut out everything not related to the problem at hand.
It works on Linux, just like the complete app. Under Windows it either fails or works depending on the value of numwords
in the Go function. If I increase its value to 1000000 (1 million) the problem goes away. I suspect this may depend on the system, so if it works for you try to tweak the value of numwords
. It may also be because I changed it so it Append()
s a static text rather than calling a generator as it does in the original code.
Still, with the current value of numwords
(100000) it does fail on Windows for me.
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()
Edit 2: Below is an even simpler version of the code. I don't think I can make it much smaller. It still has the problem for me. I can run it from within IDLE, or directly by double clicking the .py file in Windows, either way works the same.
I tried with various values of numwords
. It seems the problem doesn't actually go away as I first said, instead when I increase numwords
the gauge just reaches further and further before the print
is called. At the current value of 1.000.000 this shorter version reaches around 50%. In the longer version above, a value of 1.000.000 reaches around 90%, a value of 100.000 reaches around 25%, and a value of 10.000 only reaches around 10%.
In the version below, once the print
is called, the progress continues on and reaches 99% even though the loop must have ended by then. In the original version the call to self.wrdlst.Refresh()
, which takes a few seconds when numwords is high, must have caused the gauge to pause. So I think that what happens is this: In the loop the gauge only reaches a certain point, when the loop exits the function continues on while the gauge stays still, and when the function exits the gauge continues on until it reaches 99%. Because a print statement doesn't take a lot of time, the version below makes it seem like the gauge moves smoothly from 0% to 99%, but the print
suggests otherwise.
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.S开发者_JS百科etValue(progress)
print "Done"
if __name__ == "__main__":
app = wx.App()
frame = MainFrame(None)
frame.Show()
app.MainLoop()
So, actually, you are blocking the GUI thread by your long running task. It may and may not run fine on some platforms and/or computers.
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()
Also notice that contrary your example:
- Button goes back to unclicked state after clicked
- You can move the window and it will not freeze
As a rule of thumb, every method which looks like this def Something(self, event)
should run just a few milliseconds.
EDIT: Another thing what I have observed on Windows 7. The gauge starts to grow at the time you call self.prog.SetValue()
and grows in some time to specified value. It does not "jump" to that value, rather it grows slowly to hit set value. It seems to be Windows 7 feature. I had to switch off "Animate controls and element inside windows" in performance options to get rid of this behavior.
精彩评论