Python and GUI

Python module consists of some general utility functions that are used by the demonstrated GUI toolkits. Each toolkit is demonstrated as a separate 'class' with the name 'Basic' prefixed. its main event loop.

In GUI programming, the 'main event loop' is a common way to interact with the user. Think of it as a 'service' that runs continuously, just waiting to generate events with the mouse, keyboard, other external devices, and then any widgets that is haved set up within user's application to 'listen' to these events will be notified that the event has occurred, and they respond according to the 'callback' function user have written for that specific event.

Each toolkit provides an idiom for connecting events to callback functions.

Once user has an idea of how user wants your GUI to behave and respond to user input and actions, user will want to place the widgets in user's window(s) in some aesthetic manner. Not only do user wants the initial look-and-feel to be intuitive for the user, but if the user resizes the window, automatic resizing, stretching, filling of the widgets makes the GUI more presentable and usable. All the GUI toolkits provide some kind of layout management idiom, and most are a combination of horizontal, vertical, row/column ('grid') decomposition of widget groups.

User will need to layout user's window in these 'widget groups,' using a 'container' widget provided by the toolkits. For example, Tkinter provides the 'Frame' as a container, WxPy provides the 'wxBoxSizer' to group widgets together in a container that can be resized as an entire group. Filling and stretching of these containers and their contained widgets (typically referred to as 'children') is done according to how the container is configured. For example, wxBoxSizer takes an orientation argument telling it which direction it should expand. The arguments used to configure the container widgets are typically referred to as 'layout constraints.'

Some toolkits have different naming conventions for how they refer to 'events' and 'callback functions':

 :- PyQt: events ==> Signals, callbacks ==> Slots
 :- FXPy: events ==> Sessages, callbacks ==> FXFUNCMAP'd Targets

Layout constraints demonstrated for each library: when the application window is resized, the appropriate 'layout management' will be in place such that the following happens for the different widgets:

- button: no resizing in any dimension
- textEntry: expand/fill in horizontal dimension only
- multiText: expand/fill in both horizontal and vertical dimensions

Event handling

- button: when pressed, change the label of the button to include

            the number of times the button was pressed

- textEntry: when Enter key is pressed, if text entered is a valid

            filename, insert the text of the file into the multi-line
            text widget, otherwise append the entered text

For a more advanced design using objects and methods, and the use of files and directories, take a look at oogui.OOGui and its derived classes: TkinterGui, WxGui, PyQtGui, PyGtkGui, and FxPyGui in the similarly named modules found at www.metaslash.com/python10.

"""

import oogui

_GUI_TYPES = ('Tkinter',

             'WxPy',
             'PyQt',
             'PyGtk',
             'FXPy')

_BUTTON_LABEL = 'Push Me' _TITLE = 'Python GUI Basics Example' _BORDER_WIDTH = 10


  1. module utility functions

def initializeWidgets(gui):

   """initialize the members used in all the guiBasics examples"""
   gui._topLevel    = None           # top level application window
   gui._button      = None           # push button
   gui._textEntry   = None           # single-line text entry field
   gui._multiText   = None           # multi-line text area widget
   gui._pushedCount = 0              # button shows number times pushed
   
   # return the title this gui example can use, using its class name
   return '%s: %s' % (_TITLE, gui.__class__.__name__)

def buttonPushed(gui)

   """Increment the number of times button pushed, and return new label"""
   gui._pushedCount += 1
   return '%s (%i)' % (_BUTTON_LABEL, gui._pushedCount)

def getText(text)

   """If text is a valid filename, return the text of the file and true;
   otherwise, return same text and false.
   """
   value  = text
   isfile = false
   try:
       file   = open(text)
       value  = file.read()
       isfile = true
       file.close()
   except:
       pass
   return value, isfile


  1. class BasicTkinter

import Tkinter from Tkconstants import * import tkinterGui # need some of the general Tkinter utility functions

class BasicTkinter

   """Provides basic widget layout and event handling example for
   Tkinter and Pmw.
   """

   _NUM_GRID_COLS = 10     # number of grid columns on main top level window
   _NUM_GRID_ROWS =  5     # number of grid rows on main top level window

   def __init__(self):
       # initialize the child widgets used, and get this example's title
       title = initializeWidgets(self)

       # create the application top level window
       self._topLevel = Tkinter.Tk()

       # create a StringVar to hold the changing value of the button label
       self._buttonLabel = Tkinter.StringVar()
       self._buttonLabel.set(_BUTTON_LABEL)   # set initial value

       # build the GUI
       self._buildGUI()

       # set the title of the main window
       self._topLevel.title(title)
       
       # start the main event loop
       self._topLevel.mainloop()

   def _buildGUI(self):
       """Builds the GUI consisting of the button, text, and textArea."""

       # When using the grid() layout manager, you need to assign a weight
       # to the rows and columns where the child widgets are managed. Our
       # window is broken into a top and bottom portion, so configure with
       # 2 rows and _NUM_GRID_COLS (reused by other widgets when anchoring)
       tkinterGui.anchor(self._topLevel, 2, BasicTkinter._NUM_GRID_COLS)

       row = 0     # keep track of the row as we build the GUI
       
       # create a frame to manage the button and text entry so that they
       # remain in the NORTH part of the main window when resized
       topFrame = Tkinter.Frame(self._topLevel)
       tkinterGui.anchor(topFrame, 1, BasicTkinter._NUM_GRID_COLS)
       topFrame.grid(row=row, col=0, columnspan=BasicTkinter._NUM_GRID_COLS,
                     sticky=N+E+W)
       
       # create the button, tying it to the variable whose value may change
       self._button = Tkinter.Button(topFrame, text=_BUTTON_LABEL,
                                     textvariable=self._buttonLabel)

       # connect the button pressed event handler to the button
       # NOTE: you can also pass key=value params when button is created
       # pady here changes the border width within the widget itself
       self._button.configure(command=self._buttonPushed,
                              pady=_BORDER_WIDTH/2)
       
       # manage the button on its parent container per documented constraints;
       # use the 'grid' layout manager, specifying row and column where to
       # place, and its relative placement in that cell location via 'sticky'.
       # pady here sets the padding around the outside of the widget.
       self._button.grid(row=0, col=0, sticky=NW, pady=_BORDER_WIDTH/2)

       # create the text entry field to the right of the button
       # (allow the text entry field to span multiple columns ... the 'grid'
       # manager forces each cell to be of the same
       textcols = BasicTkinter._NUM_GRID_COLS-1
       self._textEntry = Tkinter.Entry(topFrame)
       tkinterGui.anchor(self._textEntry, 1, textcols)
       self._textEntry.grid(row=0, col=1, columnspan=textcols, sticky=N+E+W,
                            padx=_BORDER_WIDTH, pady=_BORDER_WIDTH)

       # connect the event handler to the text entry field
       self._textEntry.bind('', self._textEntered)

       row += 1     # increment so next child moves down to next row

       rows = BasicTkinter._NUM_GRID_ROWS - row   # num rows in multiline text
       cols = BasicTkinter._NUM_GRID_COLS  # allow to span width of window
       frame = Tkinter.Frame(self._topLevel)
       frame.grid(row=row, col=0, rowspan=rows, columnspan=cols, sticky=NSEW)
       tkinterGui.anchor(frame, rows, cols)
       
       # create the multi-line text widget. The scrollbars are managed
       # separate from the scrollable widget in Tkinter, so it is often
       # useful to create a convenience function to create the scrollbars
       # as we have done in tkinterGui
       self._multiText = Tkinter.Text(frame)
       self._multiText.grid(row=0, col=0, rowspan=rows, columnspan=cols,
                            sticky=NSEW)
       tkinterGui.anchor(self._multiText, rows, cols)
       xsb, ysb = tkinterGui.setScrollbar(frame, self._multiText, 'multiText')
       xsb.grid(row=rows, col=0, sticky=S+E+W, columnspan=cols)
       ysb.grid(row=0, col=cols, sticky=N+S+E, rowspan=rows)
       self._topLevel.config(bg='blue')
       self._multiText.config(bg='red')
       

   def _buttonPushed(self, *unusedEvent):
       """Event handler called when the button is pushed."""
       self._buttonLabel.set(buttonPushed(self))

   def _textEntered(self, *unusedEvent):
       """Event handler called when the Enter button is pressed in the
       single-line text entry field.
       """
       text, isFile = getText(self._textEntry.get())
       if isFile:
           self._multiText.delete(1.0, END)
       self._multiText.insert(END, text+'\n')


  1. class BasicWxPy

from wxPython import * from wxPython.wx import * class BasicWxPy:

   """Provides basic widget layout and event handling example for
   WxPy, the Python bindings for the WxWindows C++ toolkit by Robin Dunn.
   NOTE: we deviate slightly from the generic approach to basic guis
   since WxWindows requires a derived wxApp class where the OnInit() method
   is implemented by the base class to specialize the application window.
   """

   _ANCHOR    = wxEXPAND | wxALL   # flags used to set resizing preferences

   class BasicWxApp(wxApp):
       """Hide the required derived class for the wxPython implementation."""

       def OnInit(self):
           """Must override this method to initialize the application."""
           # initialize the child widgets used, and get this example's title
           title = initializeWidgets(self)

           # create the application top level window
           self._topLevel = wxFrame(NULL, -1, title)

           # one of the managed windows needs to be designated as the parent
           # top level so that when it is destroyed, the app is also closed
           self.SetTopWindow(self._topLevel)

           # build the GUI
           self._buildGUI()

           # need to do this explicitly to show the top level; this will
           # show all the children of this window
           self._topLevel.Show(true)
           
           # ok to continue
           return true

       def _buildGUI(self):
           """Builds the GUI consisting of the button, text, and textArea."""
           # first get a vertical panel to construct contain 'rows'
           vsizer, vpanel = self._getSizedPanel(self._topLevel)

           # create a horizontal panel to contain the [button] [text entry]
           tsizer, tpanel = self._getSizedPanel(vpanel, wxHORIZONTAL)

           # create the button widget
           buttonID = wxNewId()
           self._button = wxButton(tpanel, buttonID, label=_BUTTON_LABEL)

           # associate the button with its event handler
           EVT_BUTTON(self._topLevel, buttonID, self._buttonPushed)

           # create the text entry widget
           entryID = wxNewId()
           self._textEntry = wxTextCtrl(tpanel, entryID,
                                        style=wxTE_PROCESS_ENTER)

           # associate the button with its event handler
           EVT_TEXT_ENTER(self._textEntry, entryID, self._textEntered)

           # position the widgets on the panel using the wxBoxSizer
           # so the widgets resize appropriately. An alternate way to
           # 'manage' control widgets is to use SetPosition(wxPoint)
           #
           # tsizer is a horizontal box, so widgets are placed side-by-side;
           # the 2nd value designates whether to stretch the widget in the
           # direction of the box's orientation
           # file:///home/mm/utils/wxPython-2.3.1/docs/wx/wx331.htm#wxsizeradd
           #
           tsizer.Add(self._button, 0)
           tsizer.Add(self._textEntry, 1)

           # explicitly set the height 
           theight = self._textEntry.GetSize().height + oogui.BORDER
           tpanel.SetSize(wxSize(tpanel.GetSize().width, theight))

           # create the multi-text area widget
           self._multiText = wxTextCtrl(vpanel, -1,
                                        style = wxTE_MULTILINE|wxTE_RICH)
           
           # add the 'rows' to the vertical panel
           # - don't stretch the first one vertically (0)
           vsizer.Add(tpanel, 0, BasicWxPy._ANCHOR, oogui.BORDER)
           vsizer.Add(self._multiText, 1, BasicWxPy._ANCHOR, oogui.BORDER)

       def _buttonPushed(self, *unusedEvent):
           """Event handler called when the button is pushed."""
           self._button.SetLabel(buttonPushed(self))

       def _getSizedPanel(self, parent, orientation=wxVERTICAL):
           """CREATES a wxBoxSizer and panel container on parent widget.
           A wxBoxSizer can grow in both directions and can distribute the
           amount of 'growth' in the box's main direction unevenly among
           the child widgets. See description in the API docs:
           file:///home/mm/utils/wxPython-2.3.1/docs/wx/wx41.htm#wxboxsizer
           """
           panel = wxPanel(parent, -1)
           sizer = wxBoxSizer(orientation)
           panel.SetAutoLayout(true)
           panel.SetSizer(sizer)

           return sizer, panel

       def _textEntered(self, *unusedEvent):
           """Event handler called when the Enter button is pressed in the
           single-line text entry field.
           """
           text, isFile = getText(self._textEntry.GetValue())
           if isFile:
               # replace if contents of a file
               self._multiText.SetValue(text)
           else:
               self._multiText.AppendText(text+'\n')
       

       # ------------------------------------------------------
       # end class BasicApp
       # ------------------------------------------------------

   def __init__(self):
       """Initialize the wrapper for wxApp so we can start its event loop."""
       _app = self.BasicWxApp()
       _app.MainLoop()
       


  1. class BasicPmw

class BasicPmw

   """Provides basic widget layout and event handling example for
   Pmw, the Python wrapper around Tkinter, providing a richer set of
   widgets than Tkinter alone. Can be used together with Tkinter.
   """
   def __init__(self):
       pass
   

   def _buildGUI(self):
       """Builds the GUI consisting of the button, text, and textArea."""
       pass
       
   def _buttonPushed(self):
       """Event handler called when the button is pushed."""
       print 'pushed button'

   def _textEntered(self):
       """Event handler called when the Enter button is pressed in the
       single-line text entry field.
       """
       print 'entered text!!'
       


  1. class BasicPyQt

class BasicPyQt

   """Provides basic widget layout and event handling example for
   PyQt, the Python bindings for the Qt C++ toolkit by TrollTech.
   """
   def __init__(self):
       # initialize the child widgets used, and get this example's title
       title = initializeWidgets(self)

       # create the application top level window

       # allow the application window to manage the layout of its children

       # build the GUI
       self._buildGUI()

       # set the title of the main window
       print title # FIXME (quiet pychecker)
       
       # start the main event loop

   def _buildGUI(self):
       """Builds the GUI consisting of the button, text, and textArea."""
       pass

   def _buttonPushed(self):
       """Event handler called when the button is pushed."""
       print 'pushed button'

   def _textEntered(self):
       """Event handler called when the Enter button is pressed in the
       single-line text entry field.
       """
       print 'entered text!!'
       


  1. class BasicPyGtk

class BasicPyGtk

   """Provides basic widget layout and event handling example for
   PyGtk, the Python bindings for the GTK+ (GNOME) C++ windowing toolkit.
   """
   def __init__(self):
       # initialize the child widgets used, and get this example's title
       title = initializeWidgets(self)

       # create the application top level window

       # allow the application window to manage the layout of its children

       # build the GUI
       self._buildGUI()

       # set the title of the main window
       print title # FIXME (quiet pychecker)

       # start the main event loop

   def _buildGUI(self):
       """Builds the GUI consisting of the button, text, and textArea."""
       pass
       
   def _buttonPushed(self):
       """Event handler called when the button is pushed."""
       print 'pushed button'

   def _textEntered(self):
       """Event handler called when the Enter button is pressed in the
       single-line text entry field.
       """
       print 'entered text!!'
       


  1. class BasicFXPy

class BasicFXPy:

   """Provides basic widget layout and event handling example for
   FXPy, the Python bindings for the FOX C++ windowing toolkit.
   """
   def __init__(self):
       # initialize the child widgets used, and get this example's title
       title = initializeWidgets(self)

       # create the application top level window

       # build the GUI
       self._buildGUI()

       # set the title of the main window
       print title # FIXME (quiet pychecker)
       
       # start the main event loop

   def _buildGUI(self):
       """Builds the GUI consisting of the button, text, and textArea."""
       pass
       
   def _buttonPushed(self):
       """Event handler called when the button is pushed."""
       print 'pushed button'

   def _textEntered(self):
       """Event handler called when the Enter button is pressed in the
       single-line text entry field.
       """
       print 'entered text!!'
       


  1. START HERE

  2. This idiom allows you to treat more than one python module (file) in
  3. your application as your runtime 'main' (commonly used for unit testing),
  4. similar to having each Java module (file) have its own main() method.
  5. Also similar to (in C/C++):
  6. #ifdef TEST
  7. int main(int argc, char *argv[]) { ... }
  8. #endif

===if __name__ == '__main__'===

   """Instantiate each GUI example defined in the list."""
   for guitype in _GUI_TYPES:
       # use 'eval' to instantiate the object, with the name of the
       # object built from the list of _GUI_TYPES supported in this tutorial
       print guitype
       eval('Basic' + guitype + '()')