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
- 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
- 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')
- 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()
- 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!!'
- 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!!'
- 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!!'
- 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!!'
- START HERE
- This idiom allows you to treat more than one python module (file) in
- your application as your runtime 'main' (commonly used for unit testing),
- similar to having each Java module (file) have its own main() method.
- Also similar to (in C/C++):
- #ifdef TEST
- int main(int argc, char *argv[]) { ... }
- #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 + '()')