Automatic Event Binding
Another area of GUI development that is often tedious and error-prone is associating user actions or events, such as clicking on a mouse button, with the action to be performed, such as incrementing a counter. This is known as binding an event to a handler. Because this is such a common operation, PythonCard has established conventions that allow this binding to take place uniformly and with a minimal amount of coding. To understand exactly what we mean, we need to look at some code to see how the PythonCard approach builds on the standard wxPython approach.
The is what the source code to our Counter application would look like if it were coded directly in wxPython:
from wxPython import wx ID_FILE_EXIT = wx.NewId() ID_COUNTER_INCREMENT = wx.NewId() ID_COUNTER_DECREMENT = wx.NewId() ID_COUNTER_RESET = wx.NewId() class MyApp(wx.wxApp): def OnInit(self): frame = wx.wxFrame(wx.NULL, -1, "PythonCard Counter Tutorial", size=(204, 160)) self.frame = frame panel = wx.wxPanel(frame, -1) self.resetBtn = wx.wxButton(panel, -1, "Reset", (10, 68)) self.decrBtn = wx.wxButton(panel, -1, "Decrement", (10, 38)) self.incrBtn = wx.wxButton(panel, -1, "Increment", (10, 8)) # this event binding is done automatically by the PythonCard framework wx.EVT_BUTTON(panel, self.resetBtn.GetId(), self.OnResetMouseClick) wx.EVT_BUTTON(panel, self.decrBtn.GetId(), self.OnDecrMouseClick) wx.EVT_BUTTON(panel, self.incrBtn.GetId(), self.OnIncrMouseClick) # post initialization self.incrBtn.SetDefault() self.field1 = wx.wxTextCtrl(panel, -1, "42", (127, 19), (55, 46), wx.wxTE_READONLY) # After a control is created, PythonCard then does post initialization for # the attributes supported by a component. These are extra steps in wxPython. font = self.field1.GetFont() font.SetPointSize(24) font.SetFaceName('MS Sans Serif') font.SetFamily(wx.wxSWISS) self.field1.SetFont(font) # create the 'File' menu file_menu = wx.wxMenu() file_menu.Append(ID_FILE_EXIT, 'E&xit\tAlt+X') # create the 'Counter' menu counter_menu = wx.wxMenu() counter_menu.Append(ID_COUNTER_INCREMENT, 'Increment') counter_menu.Append(ID_COUNTER_DECREMENT, 'Decrement') counter_menu.Append(ID_COUNTER_RESET, 'Reset') # we now need a menu bar to hold the 2 menus just created menu_bar = wx.wxMenuBar() menu_bar.Append(file_menu, '&File') menu_bar.Append(counter_menu, 'Counter') # set the menu bar (tells the system we're done) frame.SetMenuBar(menu_bar) # Using EVT_MENU, we associate the identifier for each menu # item to a method to be called when the menu item is selected. wx.EVT_MENU(self, ID_FILE_EXIT, self.OnFileExit) # we can reuse the methods defined for the buttons wx.EVT_MENU(self, ID_COUNTER_INCREMENT, self.OnIncrMouseClick) wx.EVT_MENU(self, ID_COUNTER_DECREMENT, self.OnDecrMouseClick) wx.EVT_MENU(self, ID_COUNTER_RESET, self.OnResetMouseClick) frame.Show(1) self.SetTopWindow(frame) return 1 def OnIncrMouseClick(self, event): endValue = int(self.field1.GetValue()) + 1 self.field1.SetValue(str(endValue)) def OnDecrMouseClick(self, event): endValue = int(self.field1.GetValue()) - 1 self.field1.SetValue(str(endValue)) def OnResetMouseClick(self, event): self.field1.SetValue('0') def OnFileExit(self, event): self.frame.Close() app = MyApp(0) app.MainLoop()
You can see that there is a substantial amount of overhead involved in associating events with their handlers. The same application coded using PythonCard is actually divided into two files. One file contains the application logic while the other is a resource file describing the attributes of all the GUI components. In other words, we're separating form and function. The application code looks like this:
from PythonCardPrototype import model class Counter(model.Background): def on_menuFileExit_select(self, event): self.Close() def on_menuCounterIncrement_select(self, event): startValue = int(self.components.field1.text) endValue = startValue + 1 self.components.field1.text = str(endValue) def on_menuCounterDecrement_select(self, event): startValue = int(self.components.field1.text) endValue = startValue - 1 self.components.field1.text = str(endValue) def on_menuCounterReset_select(self, event): self.components.field1.text = "0" def on_incrBtn_mouseClick(self, event): startValue = int(self.components.field1.text) endValue = startValue + 1 self.components.field1.text = str(endValue) def on_decrBtn_mouseClick(self, event): startValue = int(self.components.field1.text) endValue = startValue - 1 self.components.field1.text = str(endValue) def on_resetBtn_mouseClick(self, event): self.components.field1.text = "0" if __name__ == '__main__': app = model.PythonCardApp(Counter) app.MainLoop()
So, how does PythonCard bind events to the appropriate handler? If you look at the wxPython example you'll notice that there is a certain amount of consistency in the naming of the event handlers, such as "OnIncrMouseClick" and "OnDecrMouseClick". This consistency is not required, but it is fairly typical in practice. PythonCard leverages a consistent naming scheme to automatically associate handlers with the events that should trigger them.
The scheme works like this. The PythonCard framework intercepts all wxPython events. For each one, it looks at the name of the component that triggered the event, such as a button named "incrBtn". Then it looks for a method name with three pieces of information separated by underscores: a prefix of "on" followed by the name of the component followed by the name of the event, such as "mouseClick". When it finds a match the code is executed--automatic event binding.