ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Building GUI Applications with PythonCard and PyCrust

by Patrick O'Brien
07/18/2002

Developing the Graphical User Interface (GUI) for a Python application is often a tedious, time-consuming, and opaque process. This is the exact opposite of how Python programmers would describe most other aspects of software development using Python. So what is it about GUI applications that causes them to be so hard? Perhaps more importantly, can anything be done to improve the situation? This article attempts to answer that question and describe how the PythonCard project is addressing these issues.

PythonCard is a cross-platform application framework for the wxPython GUI toolkit (which is a Python version of wxWindows). The goal of the PythonCard project is to bring the power, simplicity, and expressiveness of Python into the realm of GUI development. The project is currently in the prototype stage, but plenty of applications are being developed with it even in this early form. At this point, however, things are in a state of flux as we explore various approaches and develop tools to make GUI application development easier, faster, and more "Pythonic."

Let's start with some of the challenges programmers face during GUI development, such as widget creation, component layout, event handling, and runtime interactions. As we do so, we'll focus on characteristics of the wxPython toolkit. But it's worth noting that while all GUI toolkits are different and no characterization will apply equally to each of them, several of our observations will hold true for just about any GUI toolkit. The real issue for this article is how the PythonCard project is addressing these issues and whether you see a benefit for the kinds of applications you develop. It's also worth noting that we don't consider wxPython to be flawed. In fact, we choose to build on wxPython because we think it is the strongest cross-platform GUI toolkit available for Python. Our intent is simply to make wxPython even better and more accessible to novice GUI programmers, without closing off the ability to write straight wxPython code as a fallback when you need access to features or capabilities PythonCard may not have yet encapsulated.

As we explore these issues, we'll show the source code for a simple application, called Counter. To highlight certain differences, we're going to show the code as it would appear in wxPython and as it appears in PythonCard. Creation of the Counter application is described in Dan Shafer's excellent PythonCard tutorial. The resulting application has buttons and menu choices that increment, decrement, and reset a numeric value displayed in a text control and looks like the following figure:

Embedded Python Shell

We said the GUI development process is opaque and we probably need to explain that one first. What we mean is simply that it can be difficult to figure out what is going on inside the various widgets that make up a graphical interface. Python has wonderful runtime introspection capabilities and for most non-GUI applications it is a relatively simple matter to load modules into the Python shell, type a few commands, and look at the resulting state of any object. But the event-driven nature of GUI applications makes it difficult to do this, resulting in objects and actions that are opaque. We thought it would be nice if you could inspect the state of any graphical object just as easily. We thought it would be helpful to be able to manipulate GUI components from a Python shell, while the application was running. Those abilities were among the first features added to PythonCard, in the form of a graphical Python shell called PyCrust.

Because PyCrust is written in Python using the wxPython toolkit, it can be embedded in any wxPython application without conflicting with the application's event loop. PyCrust's modular design allows you to embed a Python command shell and/or a namespace viewer and expose any part of your wxPython application. A minimal application that includes the PyCrust shell looks like this:


from wxPython.wx import * 
 
from PyCrust.shell import Shell 
 
class ShellFrame(wxFrame): 
    def __init__(self, parent=None, id=-1, title='PyCrust Minimus'): 
        wxFrame.__init__(self, parent, id, title) 
        self.shell = Shell(parent=self) 
 
class App(wxApp): 
    def OnInit(self): 
        self.frame = ShellFrame() 
        self.frame.Show(true) 
        self.SetTopWindow(self.frame) 
        return true 
 
def main(): 
    application = App(0) 
    application.MainLoop() 
 
if __name__ == '__main__': 
    main() 

PythonCard takes this one step further by automatically including the PyCrust shell and namespace viewer in every PythonCard application, along with an event message watcher, logger, and property editor. Users of the application built with PythonCard won't normally see these runtime tools, but a standard set of command-line options exposes one or all of them. They are particularly handy during development and debugging. Together they provide a level of transparency that is all too often missing from GUI development environments. For example, this figure shows the Counter example with all of the runtime tools loaded:

The code that makes the PyCrust shell available to the application (and vice versa) is as simple as this:


class PyCrustFrame(MyMiniFrame): 
    def __init__(self, parent, ID, title, pos, size, parentApp): 
        MyMiniFrame.__init__(self, parent, ID, title, pos, size) 
         
        parentApp.shell = PyCrust.shell.Shell(self, -1) 
        parentApp.shell.interp.locals['pcapp'] = parentApp 
        self.parentApp = parentApp 
 
        wx.EVT_CLOSE(self, self.onCloseMe) 
 
    def onCloseMe(self, evt): 
        self.Show(0) 

The shell frame is created as a wxPython MiniFrame. The PyCrust shell is added to that frame. The application itself is added to the shell's local namespace as "pcapp". As you can see in the figure above, from this we now have access to the application, the window, the menu and buttons on the window, the attributes and methods of the menus and buttons, and so forth, while the application is running. This gives us the ability to run nearly any Python command in the PyCrust Python shell.

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.

Resource Editing

The resource file for the Counter application appears below. While the structure of the resource file will be recognizable to any Python programmer, and can be edited by hand if necessary, it is typically maintained using the graphical Resource Editor tool supplied with PythonCard.


{'stack':{'type':'Stack', 
          'name':'Counter', 
    'backgrounds': [ 
    {'type':'Background', 
         'name':'bgcounter', 
          'title':'PythonCard Counter Tutorial', 
          'size':(204, 160), 
 
        'menubar': {'type':'MenuBar', 
         'menus': [ 
             {'type':'Menu', 
             'name':'menuFile', 
             'label':'&File', 
             'items': [ 
                  {'type':'MenuItem', 
                   'name':'menuFileExit', 
                   'label':'E&xit\tAlt+X', 
                  }, 
              ] 
             }, 
             {'type':'Menu', 
             'name':'menuCounter', 
             'label':'Counter', 
             'items': [ 
                  {'type':'MenuItem', 
                   'name':'menuCounterIncrement', 
                   'label':'Increment', 
                  }, 
                  {'type':'MenuItem', 
                   'name':'menuCounterDecrement', 
                   'label':'Decrement', 
                  }, 
                  {'type':'MenuItem', 
                   'name':'menuCounterReset', 
                   'label':'Reset', 
                  }, 
              ] 
             }, 
         ] 
     }, 
         'components': [ 
 
{'type':'Button',  
    'name':'resetBtn',  
    'position':(10, 68),  
    'label':'Reset',  
    }, 
 
{'type':'Button',  
    'name':'decrBtn',  
    'position':(10, 38),  
    'label':'Decrement',  
    }, 
 
{'type':'Button',  
    'name':'incrBtn',  
    'position':(10, 8),  
    'default':1,  
    'label':'Increment',  
    }, 
 
{'type':'TextField',  
    'name':'field1',  
    'position':(127, 19),  
    'size':(55, 46),  
    'editable':0,  
    'font':{'size': 24, 'family': 'sansSerif'},  
    'text':'42',  
    }, 
 
] # end components 
} # end background 
] # end backgrounds 
} } 

By separating PythonCard applications into two parts, the application code and the application resource file, we've removed some of the time and tedium involved in GUI development. At the same time, storing resource information as standard Python structures (dictionaries, lists, tuples, and so on) allows for the creation of additional editing tools or the manipulation of the resource file by custom utilities written in Python. Let's take a look at the Resource Editor and see how it works.

The Resource Editor is one of the development tools included with PythonCard, in addition to a Code Editor, Text Editor, and File Finder. The idea behind the Resource Editor is fairly simple--read and write to a resource file where GUI objects are described in general terms using standard Python structures. Inside the Editor, allow the creation and modification of GUI widgets and their properties. And automatically associate a resource file with an application so that the resource file is loaded and used to build the interface when the application is launched.

This figure shows the Resource Editor with the Counter application resource file loaded:

And here is another screen shot showing the Component menu options:

(In case you're curious, the answer is yes, the Resource Editor is itself a PythonCard application with its own resource file. In fact, the Resource Editor can be used to edit its very own resource file.)

Conclusion

The development of graphical user interfaces is a complex undertaking and we think we're headed in the right direction with PythonCard. Credit Alan Kay with originating the expression that "Simple things should be simple and complex things should be possible." We're doing our best to apply that motto to GUI development in Python. At the same time, there are plenty of areas that need to be explored. Recent discussions on the PythonCard list have centered on the use of sizers to control layout, the creation of compound components, and the ability to persist data transparently between application sessions. So we'd like to invite you to join in our efforts. Complete project information can be found at www.pythoncard.org. If you're attending OSCON 2002, plan to come to our tutorial . And, finally, we'd like to thank Python and wxPython for making our project possible.

Programming Python

Related Reading

Programming Python
Object-Oriented Scripting
By Mark Lutz

Patrick O'Brien is an independent software developer and trainer, specializing in the Python programming language. He is the creator of PyCrust, a developer on the PythonCard project, and leader of the PyPerSyst project. He may be reached at pobrien@orbtech.com.


Return to Python DevCenter.

Copyright © 2009 O'Reilly Media, Inc.