#!/usr/bin/env python """This tutorial intends to provide the *basics* for using various GUI toolkits with a simple construction of a panel with a button, single and multi-line text widgets, with different layout and resizing constraints defined for each. Simple event handling is described as well. This 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. Take a look at the constructor (__init__) method for each class to find out how each toolkit initializes itself, creates a top level window, and then starts 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 for you to generate events with the mouse, keyboard, other external devices, and then any widgets you have set up within your application to 'listen' to these events will be notified that the event has occurred, and they respond according to the 'callback' function you have written for that specific event. 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 Regardless of their naming convention, each toolkit provides an idiom for connecting events to callback functions. [For specific comments on each toolkit, refer to the COMMENTS and IDIOMS notes in the derived class modules at www.metaslash.com/python10.] Once you have an idea of how you want your GUI to behave and respond to user input and actions, you will want to place the widgets in your window(s) in some aesthetic manner. Not only do you want 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. As the designer of the GUI, you will need to layout your 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.' 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 + '()')