问题
Consider this example (tried on python2.7, Ubuntu 11.04):
import wx
import wx.lib.agw.knobctrl as KC
# started from: http://wxpython.org/Phoenix/docs/html/lib.agw.knobctrl.html
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "KnobCtrl Demo")
self.panel = wx.Panel(self)
self.knob1 = KC.KnobCtrl(self, -1, size=(100, 100))
self.knob1.SetTags(range(0, 151, 10))
self.knob1.SetAngularRange(-45, 225)
self.knob1.SetValue(45)
# explicit sizes here - cannot figure out the expands ATM
self.text_ctrl_1 = wx.TextCtrl(self, -1, "0", size=(50, -1))
self.slider_1 = wx.Slider(self, -1, 0, -12, 12, style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_INVERSE, size=(150, -1))
self.text_ctrl_2 = wx.TextCtrl(self, -1, "0", size=(50, -1))
main_sizer = wx.BoxSizer(wx.VERTICAL)
main_sizer.Add(self.knob1, 0, wx.EXPAND | wx.ALL, 20)
main_sizer.Add(self.text_ctrl_1, 0, wx.EXPAND, 20)
main_sizer.Add(self.slider_1, 0, wx.EXPAND , 20)
main_sizer.Add(self.text_ctrl_2, 0, wx.EXPAND, 20)
self.panel.SetSizer(main_sizer)
main_sizer.Layout()
self.knob1.Bind(KC.EVT_KC_ANGLE_CHANGED, self.OnAngleChanged)
self.slider_1.Bind(wx.EVT_SCROLL, self.OnSliderScroll)
def OnAngleChanged(self, e):
theknob = e.EventObject
x = theknob._mousePosition.x
y = theknob._mousePosition.y
ang = theknob.GetAngleFromCoord(x, y)
self.text_ctrl_1.SetValue("%.2f" % (ang))
self.text_ctrl_1.Refresh() # no dice
def OnSliderScroll(self, e):
obj = e.GetEventObject()
val = obj.GetValue()
self.text_ctrl_2.SetValue(str(val))
# our normal wxApp-derived class, as usual
app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()
It results with something like this:

The thing is: if you move the slider very fast, you will notice the bottommost text box updates also rather fast; but if you move the rotary knob very fast, it seems its text box (below it) changes with much reduced frequency ?!
Why is this; and would it be possible to get the response speed of the rotary knob's text box to be as fast as slider's one?
回答1:
Ok, I think I got something working, but I'm not 100% sure, so a more erudite answer would be appreciated. First, note that:
- The
wx.Slider
widget is (I think) implemented in C; so the only time Python "knows" anything about its execution is when the widget broadcasts an event - The
wx.lib.agw.knobctrl.KnobCtrl
is implemented in Python; thus the Python interpreter "knows" (as it has to run it) each line of code of the widget's execution.
So, what I did, is I tried to trace the execution, with:
python -m trace --trace --timing test.py > test.pylog
What I could notice is that: OnSliderScroll
appeared in the log apparently without being triggered by a mouse event, while multiple OnMouseEvents
would appear from knobctrl.py (the mouseovers), and only some would trigger SetTrackPosition()
which eventually calls OnAngleChanged()
. But even more importantly, there was a ton of _gdi_.DC_DrawLine
in the log! Then, looking at the knobctrl.py
source, I realised that the gradient is actually painted in a Python for
loop, line-by-line:
def DrawDiagonalGradient(self, dc, size):
...
for ii in xrange(0, maxsize, 2):
currCol = (r1 + rf, g1 + gf, b1 + bf)
dc.SetPen(wx.Pen(currCol, 2))
dc.DrawLine(0, ii+2, ii+2, 0)
...
... so I thought, this must be hogging the time! So, what is done in the code below, is that a subclass is derived from KnobCtrl
, where DrawDiagonalGradient()
so it uses a plain fill instead of a gradient, which works much faster.
So, the code below compares the original and the derived variant, using the same event handler and the same textfield; you can check out the video at https://vid.me/kM8V, which looks something like this:

You'll notice the textctrl barely changes when the original knob is turned (even if, notably, the printouts are emitted with the expected speed) ; but updates pretty decently when the derived knob with "plain" background is turned (nearly as fast as the slider). I think the "plain" goes even faster when there is no draw of any kind in the overloaded method, but then the previous positions of the knob dot are not erased. My guess is, the draws of the original knob's gradient take up so much time in the allocated drawing timeframe, that Python needs to drop other queued updates for that frame, here notably the update of the text control.
Note that the knob emits both KC.EVT_KC_ANGLE_CHANGING
(which will not refresh the draw unless e.Skip()
is present in the handler), and KC.EVT_KC_ANGLE_CHANGED
; however, as far as I can see, they always follow each other, so below *CHANGED
is used for both.
Of course, if I've misunderstood the problem and the solution, I'd love to know...
import wx
import wx.lib.agw.knobctrl as KC
# started from: http://wxpython.org/Phoenix/docs/html/lib.agw.knobctrl.html
class KnobCtrlPlain(KC.KnobCtrl):
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
size=wx.DefaultSize,
agwStyle=KC.KC_BUFFERED_DC):
super(KnobCtrlPlain, self).__init__(parent, id, pos, size, agwStyle)
def DrawDiagonalGradient(self, dc, size):
col1 = self._startcolour
r1, g1, b1 = int(col1.Red()), int(col1.Green()), int(col1.Blue())
sizex, sizey = size
# must have a filled draw here, to erase previous draws:
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetBrush(wx.Brush(col1, wx.SOLID))
#~ dc.DrawCircle(self.Width/2, self.Height/2, sizex)
dc.DrawRectangle(0, 0, sizex, sizey) # same effect as circle; prob. faster?
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "KnobCtrl DemoB")
self.panel = wx.Panel(self)
self.knob1 = KC.KnobCtrl(self.panel, -1, size=(100, 100))
self.knob1.SetTags(range(0, 151, 10))
self.knob1.SetAngularRange(-45, 225)
self.knob1.SetValue(45)
self.knob2 = KnobCtrlPlain(self.panel, -1, size=(100, 100))
self.knob2.SetTags(range(0, 151, 10))
self.knob2.SetAngularRange(-45, 225)
self.knob2.SetValue(45)
self.text_ctrl_1 = wx.TextCtrl(self.panel, -1, "0", size=(50, -1))
self.slider_1 = wx.Slider(self.panel, -1, 0, -12, 12, style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_INVERSE, size=(150, -1))
self.text_ctrl_2 = wx.TextCtrl(self.panel, -1, "0", size=(50, -1))
main_sizer = wx.BoxSizer(wx.VERTICAL)
main_sizer.Add(self.knob1, 0, wx.EXPAND | wx.ALL, 20)
main_sizer.Add(self.text_ctrl_1, 0, wx.EXPAND, 20)
main_sizer.Add(self.knob2, 0, wx.EXPAND | wx.ALL, 20)
main_sizer.Add(self.slider_1, 0, wx.EXPAND , 20)
main_sizer.Add(self.text_ctrl_2, 0, wx.EXPAND, 20)
self.panel.SetSizer(main_sizer)
main_sizer.Layout()
self.knob1.Bind(KC.EVT_KC_ANGLE_CHANGED, self.OnAngleChanged)
self.knob2.Bind(KC.EVT_KC_ANGLE_CHANGED, self.OnAngleChanged)
self.slider_1.Bind(wx.EVT_SCROLL, self.OnSliderScroll)
def OnAngleChanged(self, e):
theknob = e.EventObject
x = theknob._mousePosition.x
y = theknob._mousePosition.y
ang = theknob.GetAngleFromCoord(x, y)
strval = str("%.2f" % (ang))
print("ac: " + strval)
self.text_ctrl_1.SetValue(strval)
def OnSliderScroll(self, e):
obj = e.GetEventObject()
val = obj.GetValue()
strval = str(val)
print("ss: " + strval)
self.text_ctrl_2.SetValue(strval)
# our normal wxApp-derived class, as usual
app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()
来源:https://stackoverflow.com/questions/27684059/slow-gui-update-of-a-wx-python-widget