Search code examples
pythonpython-2.7wxpythonpython-multithreading

Which EvtHandler to use for PostEvent in wxPython


This question addresses the very specific question of the EvtHandler required to post en event using wxPython.

Background

I'm using Python 2.7. In the example below I have two kinds of events:

  1. StartMeausuringEvent is triggered within a wx.Panel derived object (DisplayPanel), hence I use self.GetEventHandler(), this works, even though the binding is in the parent object.
  2. NewResultEvent is triggered from a threading.Thread derived object (MeasurementThread), and it has no event handler, hence I have been forced to send an event handler along, and I chose the event handler of the wx.Frame derived object (MeasurementFrame), as this is also the object that "cathes" the event eventually.

Question

WHY does the first one work, since the object ? And more generally, how tight has the connection between the event handler and the object that "catches" the event has to be?

Code example

import wx
import time
import threading
import numpy as np
import wx.lib.newevent

# Create two new event types
StartMeasuringEvent, EVT_START_MEASURING = wx.lib.newevent.NewCommandEvent()
NewResultEvent,      EVT_NEW_RESULT      = wx.lib.newevent.NewEvent()



class MeasurementFrame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title="Lets measure!", size=(300, 300))
        # Layout
        self.view = DisplayPanel(self)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.view, 1, wx.ALIGN_CENTER)
        self.SetSizer(sizer)
        self.SetMinSize((300, 300))
        self.CreateStatusBar()
        # Create a new measuring device object to embody a physical measuring device
        self.device = MeasuringDevice(self.GetEventHandler(), amplification=10)
        # Bind events to the proper handlers
        self.Bind(EVT_START_MEASURING, self.OnStartMeasurement)
        self.Bind(EVT_NEW_RESULT, self.OnNewResult)

    def OnStartMeasurement(self, evt):
        self.view.SetStatus("Measuring")
        self.device.start_measurement()

    def OnNewResult(self, evt):
        self.view.SetStatus("New Result!")
        print evt.result_data



class DisplayPanel(wx.Panel):
    def __init__(self, parent):
        wx.Panel.__init__(self, parent)
        # Attributes
        self._result_display = wx.StaticText(self, label="0")
        self._result_display.SetFont(wx.Font(16, wx.MODERN, wx.NORMAL, wx.NORMAL))
        self._status_display = wx.StaticText(self, label="Ready!")
        self._status_display.SetFont(wx.Font(8, wx.MODERN, wx.NORMAL, wx.NORMAL))
        # Layout
        sizer = wx.BoxSizer(wx.VERTICAL)
        button1 = wx.Button(self, wx.NewId(), "Increment Counter")
        # button2 = wx.Button(self, wx.NewId(), "Decrease Counter")
        sizer.AddMany([(button1, 0, wx.ALIGN_CENTER),
                       # (button2, 0, wx.ALIGN_CENTER),
                       ((15, 15), 0),
                       (self._result_display, 0, wx.ALIGN_CENTER),
                       (self._status_display, 0, wx.ALIGN_LEFT)])
        self.SetSizer(sizer)
        # Event Handlers
        button1.Bind(wx.EVT_BUTTON, self.OnButton)

    def OnButton(self, evt):
        """ Send an event ... but to where? """
        wx.PostEvent(self.GetEventHandler(), StartMeasuringEvent(self.GetId()))

    def SetStatus(self, status=""):
        """ Set status text in the window"""
        self._status_display.SetLabel(status)



class MeasuringDevice:
    def __init__(self, event_handler, amplification=10):
        self.amplification = amplification
        self.event_handler = event_handler # The object to which all event are sent

    def start_measurement(self, repetitions=1):
        """ Start a thread that takes care of obtaining a measurement """
        for n in range(repetitions):
            worker = MeasurementThread(self.event_handler, self.amplification)
            worker.start()



class MeasurementThread(threading.Thread):
    def __init__(self, event_handler, amplification):
        threading.Thread.__init__(self)
        self.event_handler = event_handler
        self.amplification = amplification

    def run(self):
        print("Beginning simulated measurement")
        time.sleep(1) # My simulated calculation time
        result = np.random.randn()*self.amplification
        evt = NewResultEvent(result_data=result)
        wx.PostEvent(self.event_handler, evt)
        print("Simulated Measurement done!")



if __name__ == '__main__':
    my_app = wx.App(False)
    my_frame = MeasurementFrame(None)
    my_frame.Show()
    my_app.MainLoop()

Solution

  • The first one works because command events automatically propagate up the containment hierarchy (a.k.a the window parent/child connections) until there is a matching binding found or until it reaches a top-level parent window like a frame or dialog. See http://wiki.wxpython.org/self.Bind%20vs.%20self.button.Bind and also http://wxpython.org/OSCON2006/wxPython-intro-OSCON2006.pdf starting at slide 53 for more explanation.

    There is a specific path that the event processor will search when looking for a matching event binding. In a nutshell: as mentioned above, event types that derive directly or indirectly from wx.CommandEvent will continue searching up through the parents until a match is found, and for other types of events it will only look for bindings in the window that the event was sent to and will not propagate to parent windows. If a handler calls event.Skip() then when the handler returns the event processor will continue looking for another matching handler (still limited to the same window for non-command events.) There is a lot more to it than that, (see slide 52 in the PDF linked above) but if you understand this much then it will be enough for almost every event binding/handling situation you'll likely encounter.

    BTW, the wx.Window class derives from wx.EvtHandler so unless you've done something like PushEventHandler then window.GetEventHandler() will return the window itself. So unless you need to push new event handler instances (most Python programs don't, it is used more often in C++) then you can save some typing by just using window instead of window.GetEventHandler().