Slow GUI update of a wx (Python) widget?

心已入冬 提交于 2020-01-16 04:56:07

问题


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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!