self.Bind vs. self.button.Bind

A common question/confusion that wxPython developers have is what's the difference between

    self.Bind(wx.EVT_BUTTON, self.OnButton, self.button)

and

    self.button.Bind(wx.EVT_BUTTON, self.OnButton)

where self is some window further up the containment hierarchy than the button. On first glance the answer is "Nothing. They both result in self.OnButton being called when the button is clicked by the user." But there are some deeper currents in this stream and to truly understand how things flow on the surface you need to understand how they flow underneath as well.

wx.Event vs. wx.CommandEvent

First things first. When searching for a binding for a particular event, wxPython will search up the containment hierarchy (parent windows) for events that are instances of wx.CommandEvent, but will not for those that are not. There are several reasons for this, which we won't go into here. Just treat it as a fact of life; be happy and move on.

Okay, I know that isn't going to satisfy some of you, so here is a metaphor. A human parent doesn't need to know every time his child's heart beats, and most of the time the child doesn't care much about it either. But when the child does want to know she can simply put her fingers on her carotid artery and feel the pulse. Of course there is always a way around the limitations if the parent wants to be informed of each heartbeat. He can ask his child to feel her pulse and tap her foot for every heartbeat. Now suppose that the powers that be decided that every child should always feel her pulse and tap her foot for every heartbeat 24x7 just in case a parent or grandparent might be interested in knowing about it. The world would be full of cranky kids and nothing would get done because of all the work needed to tap feet and to report those foot taps to Daddy while he is asleep at work (because he was up all night listening to foot taps), and also to Grandma Betty who lives across town. That's why some events propagate and some don't.

Skipping stones

When an event binding is found for a particular event the application's associated handler function is called. If that handler calls event.Skip() anywhere in the handler wxPython continues searching for more event handlers after that event handler finishes. It doesn't matter at what point in the app's event handler code the event.Skip() statement is executed; it's a "deferred call."

Think of it as skipping stones across a pond. When a binding is found the stone touches the water (the application's event handler is called) and there is a splash and a ring of waves generated (the handler can do something that may affect the state of the application.) At this point the stone may be caught by the water and will sink ( the handler does not call Skip() ) or it may bounce and touch the water again at some other place (the handler does call Skip() and so other bindings are searched for.) If the stone is a wx.CommandEvent then the next place it touches down could be in a parent window, or the parent's parent or so on. Or, it may just land on the opposite bank and not touch the water at all (no other higher level event handlers were found to call).

The Bind method

The Bind() method is how we specify an event binding for wxPython. Not only does it tell the system which kind of event we are looking for, but it also tells the system where to examine that criteria. This "where" part is something that usually takes people a while to understand. For example, in this code where self is a wx.Frame or another container control:

    self.Bind(wx.EVT_BUTTON, self.OnButton, self.button)

the binding tells the system to eventually call self.OnButton when it sees a wx.EVT_BUTTON event delivered first to the frame, that originates from self.button. Since self.button is presumably a few layers down the container hierarchy then the frame is potentially not the first splash (EVT_BUTTON handler call) that this stone (event) will make. In other words, the event may also be caught in the panel or in the button first. But in any case, the thing to learn here is that event bindings not only have a matching criteria, but also a hierarchy in which the event handlers are searched for.

Now let's look at the other binding:

    self.button.Bind(wx.EVT_BUTTON, self.OnButton)

This tells the system to immediately call self.OnButton for a wx.EVT_BUTTON event (it matches because self.button is being bound) that is caused by self.button. Now for all practical purposes the only button events that self.button is going to get are the ones generated by clicking on the button itself, so it is almost the same as saying that it is only catching events that are coming from this button, just like above. What is different in this case however is where in the containment hierarchy that the event handler search matching is done first. This time it is happening at the position of the button, and unless the handler calls event.Skip() nothing else in the hierarchy will get a chance to see the event. In other words, this is the first and only splash that this stone will make. Most of the time this is perfectly okay. But sometimes it is real handy to be able to allow events to be seen and perhaps intercepted by other handlers before it gets to the one that implements the normal functionality.

wx.Events

So given what you now know from reading the above, it should be clear why bindings for non-command events must be done on the specific window and not on a window further up the containment hierarchy. Since non-command events don't propagate they simply won't be seen if the binding is not done on the window to which the event is first delivered.

Fruit catching robots

Here's one more metaphor to help you picture how things work. This was a response to a series of questions asked on the wxPythonInAction forum, so you may want to read some of the preceding messages to get the whole story.

Suppose you have a large apple tree, and you are interested in collecting the apples from a specific branch when they are ripe, (and assume that they drop from the branch the moment they are ripe.) One way to do this is to install a robot on the ground beneath the branch and each time an apple hits the ground (after having left the branch and passed through the branches in between) the robot will ask the apple if it came from the branch you are interested in and if so will toss the apple into the basket you are collecting them in. The other way to do it is to install a robot directly on the branch in question. When the apple ripens the robot catches it before it can go anywhere else, and tosses it directly to your basket, bypassing the branches in between. Apples that don't have a robot looking for them just pass straight through the branches, hit the ground and then fade away. If a branch isn't directly beneath the source branch then robots attached to those branches don't have a chance to catch the apple.

In this analogy the apples are the events, the robots are the event bindings and the basket is the event handler function. The branch is the button, the ground is the frame, and the branches in between represent the containment hierarchy. So in the case of the self.button.Bind it really isn't the frame reaching down and grabbing messages directed at one of its child widgets. You're just attaching an event binding directly to the child, and in this case the basket it sends the apples to happens to still be located in the frame, but it could just have easily been located on the same branch, a different branch, or even in a different tree.

In the case of non-command events our apples have to take on another magical property, in that they evaporate if they are not caught within the scope of their own branch, but once they are caught by the robot the basket they are sent to can still exist anywhere.

Which is better?

So to get back to "self.Bind vs. self.button.Bind" some of you may be asking which is better. Some people have been suggesting in the mail list to always bind to the widget that generates the event (in other words, the self.button.Bind style) in order to not have to worry about the differences between command events and non-command events. I think that is perfectly okay as long as you understand what you are giving up. It's not often that you need to intercept an event at multiple points of the containment hierarchy, but it is a real handy feature when you do need it. So to answer the question, there is no real definitive answer. Just use what makes the most sense for you, and keep in mind that there are deeper currents in this stream.

This demo may clear up questions you may have. Experiment with it by un-commenting or commenting out one binding at a time.

   1 #!/usr/bin/python
   2 # -*- encoding: utf-8
   3 
   4 """ A demo to illustrate event handler search and execution hierarchy. """
   5 import sys, os
   6 import wx
   7 
   8 #------------------------------------------------------------------------------
   9 
  10 class MainFrame( wx.Frame ) :
  11 
  12     def __init__( self, title='Event Handler Hierarchy', pos=(10, 10), size=(350, 200) ) :
  13 
  14         wx.Frame.__init__( self, None, id=-1, title=title, pos=pos, size=size )
  15 
  16         #----- Frame's controls:
  17 
  18         # An initial Frame panel is needed for tab-traversal
  19         #   and platform background color capabilities.
  20         # The first control instantiated in a Frame automatically expands
  21         #   to the extent of the Frame's client area.  This is unique to Frames.
  22         frm_pnl = wx.Panel( self )
  23 
  24         btn1 = wx.Button( frm_pnl, label='Btn1' )
  25         btn2 = wx.Button( frm_pnl, label='Btn2' )
  26 
  27         #----- Button centering:
  28 
  29         # Automatically center the button position as simply as possible.
  30         pnl_horzSizer = wx.BoxSizer( wx.HORIZONTAL )
  31         pnl_horzSizer.AddStretchSpacer()
  32         pnl_horzSizer.Add( btn1, flag=wx.ALIGN_CENTER )  # Center horizontally.
  33         pnl_horzSizer.Add( btn2, flag=wx.ALIGN_CENTER )  # Center horizontally.
  34         pnl_horzSizer.AddStretchSpacer()    # 2# StretchSpacers assure btn's vertical centering.
  35 
  36         frm_pnl.SetSizer( pnl_horzSizer )
  37         frm_pnl.Layout()
  38 
  39         #----- Buttons' click event handlers:
  40 
  41         ## Comment out or un-comment these handler bindings
  42         #   to see the effects of each remaining combination.
  43 
  44         #-----
  45 
  46         # Bind the button-to-Frame handler.
  47         # The 3rd argument specifies which button's events to handle.
  48         self.Bind( wx.EVT_BUTTON, self.OnButton_FrameHandler, btn1 )
  49 
  50         ## Bind both button's events because there is no 3rd argument.
  51         #self.Bind( wx.EVT_BUTTON, self.OnButton_FrameHandler )
  52 
  53         #-----
  54 
  55         # Bind the button-to-Panel handler.
  56         frm_pnl.Bind( wx.EVT_BUTTON, self.OnButton_PanelHandler, btn1 )
  57 
  58         ## Bind both button's events because there is no 3rd argument.
  59         #frm_pnl.Bind( wx.EVT_BUTTON, self.OnButton_PanelHandler )
  60 
  61         #-----
  62 
  63         # Bind the button's own handler. No 3rd argument is necessary because "btn1.Bind".
  64         btn1.Bind( wx.EVT_BUTTON, self.OnButton_ButtonHandler )
  65 
  66     #end __init__
  67 
  68     #------------------------
  69 
  70     def OnButton_FrameHandler( self, event ) :
  71         # The button that generated this event:
  72         btn = event.GetEventObject()
  73         print '\n----  OnButton_FrameHandler() for', btn.GetLabelText()
  74 
  75         # There is nowhere to .Skip() up to.
  76     #end def
  77 
  78     def OnButton_PanelHandler( self, event ) :
  79         # The button that generated this event:
  80         btn = event.GetEventObject()
  81         print '\n----  OnButton_PanelHandler() for', btn.GetLabelText()
  82 
  83         event.Skip()    # Search for handler upwards in the child-parent hierarchy tree.
  84     #end def
  85 
  86     def OnButton_ButtonHandler( self, event ) :
  87         # The button that generated this event:
  88         btn = event.GetEventObject()
  89         print '\n----  OnButton_ButtonHandler() for', btn.GetLabelText()
  90 
  91         event.Skip()    # Search for handler upwards in the child-parent hierarchy tree.
  92     #end def
  93 
  94 #end class
  95 
  96 #==============================================================================
  97 
  98 if __name__ == '__main__' :
  99     app = wx.PySimpleApp( redirect=False )
 100     appFrame = MainFrame().Show()
 101     app.MainLoop()
 102 #end def

bindings_demo.py


Comments

A diagram of the apple metaphor
wxpython_events_and_apples.png
I really liked the metaphor and had to do a drawing, I hope there are no bugs!

If anyone wants the SVG for this, just ask.

Donn Ingle (South Africa)


Very nice!

Franz Steinhaeusler (Austria)


What? No animation? ;-) Looks great, thanks for creating this diagram.

RobinDunn


Here's another example based on a problem I just ran into. This was my first experience where I had to use the more "specific" method. Here's the code:

class MyFrame(wx.Frame):
    def __init__(self, title=''):
        wx.Frame.__init__(self, None, wx.ID_ANY, title)
        self.panel = wx.Panel(self)
        self.panel.Bind(wx.EVT_LEFT_UP, self.OnLeftClick)

    def OnLeftClick(self, event):
        print event.GetPosition()
        event.Skip()

At first I tried:

self.Bind(wx.EVT_LEFT_UP, self.OnLeftClick, self.panel)

but that didn't work. Finally I realized the problem was that wx.EVT_LEFT_UP is not a wx.CommandEvent, meaning that the event was not being passed up the widget hierarchy. So the left-click event was caught in the panel, but I was checking for it in the frame itself. So by changing the code to check for the event in the panel specifically, it caught it and ran the handler.

JohnSalerno


I've noticed there are differences between operating systems also. For example in trying to capture keyboard events in a modal dialog with a text control.

In Linux (wxGTK), this worked great :

self.Bind(wx.EVT_KEY_DOWN, self.keyboardEvents)

But under Windows it was not working at all, and I had to do this :

self.textCtrl_1.Bind(wx.EVT_KEY_DOWN, self.keyboardEvents)

ianaré


self.Bind vs. self.button.Bind (last edited 2010-12-16 15:19:50 by hn)