An excerpt from Chapter 20: GUI Development, from Python Programming on Win32.
In this chapter, we examine the various options for developing graphical user interfaces (GUIs) in Python.
We will look in some detail at three of the GUI toolkits that operate on the Windows platform: Tkinter, PythonWin, and wxPython. Each of these GUI toolkits provide a huge range of facilities for creating user interfaces, and to completely cover any of these toolkits is beyond the scope of this single chapter. Each framework would need its own book to do it justice.
Our intent in this chapter is to give you a feel for each of these GUI frameworks, so you can understand the basic model they use and the problems they were designed to address. We take a brief tour of each of these toolkits, describing their particular model and providing sample code along the way. Armed with this information, you can make an informed decision about which toolkit to use in which situation, have a basic understanding of how your application will look, and where it will run when finished.
The authors need to express their gratitude to Gary Herron for the Tkinter section, and Robin Dunn for the wxPython section. Their information helped us complete this chapter.
Tkinter is the Python interface to the Tk GUI toolkit current maintained by Scriptics (http://www.scriptics.com). Tkinter has become the de facto standard GUI toolkit for Python due mainly to its cross-platform capabilities; it presents a powerful and adaptable GUI model across the major Python platforms, including Windows 95/98/NT, the Macintosh, and most Unix implementations and Linux distributions.
This section gives a short description of the capabilities of Tkinter, and provides a whirlwind tour of the more important aspects of the Tkinter framework. To effectively use Tkinter, you need a more thorough description than is provided for here. Fredrik Lundh has made an excellent introduction to Tkinter programming available at http://www.pythonware.com/library.htm, and at time of printing a Tkinter book by Fredrik has just been announced, so may be available by the time you read this. For more advanced uses of Tkinter, you may need to refer directly to the Tk reference manual, available from the Scriptics site.
Two Python applications, tkBrowser and tkDemo, accompany this section. TkBrowser is a Doubletalk application, providing several views and some small editing capabilities of Doubletalk
BookSets; TkDemo demonstrates a simple use of the core Tk user interface elements. Both applications are too large to include in their entirety, so where relevant, we include snippets.
There's some new terminology with Tkinter, defined here for clarity:
Before we launch into Tkinter programming, a brief discussion of the pros and cons of Tkinter will help you decide if Tkinter may be the correct GUI toolkit for your application. The following are often given as advantages of Tkinter:
To balance things, here's a list of what's often mention as weaknesses in Tkinter:
Although many individuals could (and no doubt will) argue with some individual points on this list, it tends to reflects the general consensus amongst the Python community. Use this only as a guide to assist you in your decision-making process.
Tkinter applications are normal Python scripts, but there are a couple of complications worth knowing about when running graphical applications under Windows. These were discussed in Chapter 3, Python on Windows, but are important enough to reiterate here; what we say in this section applies equally to wxPython later in this chapter.
The standard Python.exe that comes with Python is known as a console application (this means it has been built to interact with a Windows console, otherwise known as a DOS box or command prompt). Although you can execute your Tkinter programs using Python.exe, your program will always be associated with a Windows console. It works just fine, but has the following side effects:
To get around this problem, Python comes with a special GUI version called Pythonw.exe. This is almost identical to the standard Python.exe, except it's not a console program, so doesn't suffer the problems described previously.
There are two drawbacks to this approach. The first is that .py files are automatically associated with Python.exe. As we saw in Chapter 3, this makes it simple to execute Python programs, but does present a problem when you want to use Pythonw.exe. To solve this problem, Python automatically associates the .pyw extension with Pythonw.exe ; thus, you can give GUI Python applications a .pyw extension, and automatically execute them from Windows Explorer, the command prompt, and so forth.
The second drawback is that because Pythonw.exe has no console, any tracebacks printed by Python aren't typically seen. Although Python prints the traceback normally, the lack of a console means it has nowhere useful to go. To get around this problem, you may like to develop your application using Python.exe (where the console is an advantage for debugging) but run the final version using Pythonw.exe.
The easiest way to get a feel for Tkinter is with the ever popular "Hello World!" example. The result of this little program is shown in Figure 20-1.
from sys import exit
from Tkinter import *
root = Tk()
Button(root, text='Hello World!', command=exit).pack()
As you can see, apart from the
import statements, there are only three lines of interest. The
root variable is set to the default top-level window automatically created by Tk, although applications with advanced requirements can customize the top-level frames. The code then creates a Tkinter button object, specifying the parent (the
root variable), the text for the button, and the command to execute when clicked. We discuss the
pack() method later in this section. Finally, turn control over to the main event-processing loop, which creates the Windows on the screen and dispatches user-interface events.
From the extreme simplicity of the "Hello World" example, the other end of the scale could be considered the
tkDemo sample included with this chapter. Although space considerations prevent us from examining this sample in detail, Figure 20-2 should give you an indication of the capabilities offered by Tkinter.
Tkinter implements a fairly small set of core widgets, from which other widgets or complete applications can be based. Table 20-1 lists these core widgets with a short description of how they are used.
Toplevel widgets are special in that they have no master widget and don't support any of the geometry-management methods (as discussed later). All other widgets are directly or indirectly associated with a Toplevel widget.
Used as a container widget for other child widgets. For instance, the tkDemo example consists of a number of frames within frames within frames to achieve its particular layout.
Displays text or images.
Displays text with automatic line-break and justification capabilities.
Displays text with advanced formatting, editing, and highly interactive capabilities.
Displays graphical items from a display list, with highly interactive capabilities.
Standard simple entry widgets, also known as the
Widgets for implementing and responding to menus.
Quite a few of these widgets are demonstrated in the tkBrowser sample, and every one gets an exercise in the tkDemo sample, so you are encouraged to experiment with these samples to get a feel for the capabilities of each widget. Of these widgets, we will discuss two of the most popular and powerful in more detail: the Text and Canvas widgets.
The Text widget provides for the display and editing of text, as you would expect from a text control. The Text widget is also capable of supporting embedded images and child windows, but the real power of the text control can be found in its support of indexes, tags, and marks:
insert, which defines the current insertion point.
The Canvas widget displays graphical items, such as lines, arcs, bitmaps, images, ovals, polygons, rectangles, text strings, or arbitrary Tkinter widgets. Like the Text widget, the Canvas widget implements a powerful tagging system, allowing you to associate any items on the canvas with a name.
Many useful widgets are actually built from the core widgets. The most common example is the dialog widget, and recent versions of Tkinter provide some new sophisticated dialog widgets similar to the Windows common dialogs. In many cases when running on Windows, the standard Windows dialog is used.
Many of these dialogs come in their own module. Table 20-2 lists the common dialog box modules and their functionality.
Simple message box related dialogs, such as Yes/No, Abort/Retry/Ignore, and so forth.
Contains base classes for building your own dialogs, and also includes a selection of simple input dialogs, such as asking for a string, integer, or float value.
A dialog with functionality very close to the Windows common file dialogs.
A dialog for choosing a color.
There are many other widgets available; both included with the Tkinter package, and also available externally. One interesting and popular source of Tkinter widgets can be found in the Python megawidgets (
Pmw) package. This package comes with excellent documentation and sample code and can be found at http://www.dscpl.com.au/pmw/.
In most cases, you build your own dialogs by deriving them from the
tkSimpleDialog.Dialog. Our tkBrowser sample defines an
EditTransaction class that shows an example of this.
Tkinter provides a flexible and powerful attribute set for all widgets. Almost all attributes can be set at either widget-creation time or once the widget has been created and displayed. Although Tkinter provides obvious attributes for items such as the color, font, or visible state of a widget, the set of enhanced attributes for widgets is usually the key to tapping the full potential of Tkinter.
Tkinter makes heavy use of Python keyword arguments when specifying widget attributes. A widget of any type (for instance a Label) is constructed with code similar to:
w = Label(master, option1=value1, option2=value2,...)
And once constructed, can be reconfigured at any time with code like:
w.configure(option1=value1, option2=value2, ...)
For a specific example, you can create a label with the following code:
label = Label(parent, background='white',
And provide an annoying blinking effect by periodic execution of:
There are dozens of options that can be specified for each widget. Table 20-3 lists a few common properties available for each widget.
The height and width of the widget in pixels.
The color of the widget as a string. You can specify a color by name (for example, red
A 3D appearance for the object (
Width of the border, in pixels.
The Window text (i.e., the caption) for the widget and additional formatting options for multiline widgets.
The font that displays the text. This can be in a bewildering array of formats: some platform-independent and some platform-dependent. The most common form is a tuple containing font name, point size, and style (for example,
Techniques used by control widgets to communicate back to the application. The
There are also dozens of methods available for each widget class, and the Tkinter documentation describes these in detail, but there is one important method we mention here because it's central to the Tkinter event model.
bind() method is simple, but provides an incredible amount of power by allowing you to bind a GUI event to a Python function. It takes two parameters, the event you wish to bind to (specified as a string) and a Python object to be called when the event fires.
The power behind this method comes from the specification of the event. Tkinter provides a rich set of events, ranging from keyboard and mouse actions to Window focus or state changes. The specification of the event is quite intuitive (for example, <Key> binds any key, <Ctrl-Alt-Key-Z> is a very specific key, <Button-1> is a the first mouse-button click, and so forth) and covers over 20 basic event types. You should consult the Tkinter reference guides for a complete set of events supported by Windows and a full description of the Tkinter event model.
Tkinter provides a powerful concept typically not found in Windows GUI toolkits, and that is geometry management. Geometry management is the technique used to lay out child widgets in their parent (for example, controls in a dialog box). Most traditional Windows environments force you to specify the absolute position of each control. Although this is specified in dialog units rather than pixels and controls can be moved once created, Tkinter provides a far more powerful and flexible model.
Tkinter widgets provide three methods for geometry management,
place() is the simplest mechanism and similar to what most Windows users are used to; each widget has its position explicitly specified, either in absolute or relative coordinates. The
grid() mechanism, as you may expect, automatically aligns the widgets in a grid pattern, while the
pack() method is the most powerful and the most commonly used. When widgets are packed, they are automatically positioned based on the size of the parent and the other widgets already placed. All of these techniques allow customization of the layout process, such as the padding between widgets.
These geometry-management capabilities allow you to define user interfaces that aren't tied to particular screen resolutions and can automatically resize and layout controls as the window size changes, capabilities that most experienced Windows user-interface programmers will know are otherwise difficult to achieve. Our two samples (described next) both make extensive use of the
pack() method, while the tkDemo sample also makes limited use of
We have included a sample Doubletalk browser written in Tkinter. This is a fully functional transaction viewer and editor application and is implemented in tkBrowser.py. This implements a number of features that demonstrate how to build powerful applications in Tkinter. A number of dialogs are presented, including the transaction list, and the detail for each specific transaction. To show how simple basic drawing and charting is, a graphical view of each account is also provided. Rather than labor over the details of this sample, the best thing to do is just to run it. Then once you have a feel for the functionality, peruse the source code to see the implementation. There are ample comments and documentation strings included less than 700 lines of source. Figure 20-3 shows our final application in action.
The second sample is TkDemo.py, which is a demonstration of all the Tkinter core widgets. It is highly animated and provides a good feel for the basic operation of these widgets.
As mentioned previously, Tkinter is the standard GUI for Python applications, therefore you can find a large number of resources both in the standard Python documentation and referenced via the Python web site.
Tkinter is excellent for small, quick GUI applications, and since it runs on more platforms than any other Python GUI toolkit, it is a good choice where portability is the prime concern.
Obviously we haven't been able to give Tkinter the depth of discussion it warrants, but it's fair to say that almost anything that can be done using the C language and Tk can be done using Python and Tkinter. One example is the Python megawidgets (
PMW) package mentioned previously; this is a pure Python package that creates an excellent widget set by building on the core Tkinter widgets.
To learn more about any of the Tkinter topics discussed here, you may like to refer to the following sources:
As mentioned in Chapter 4, Integrated Development Environments for Python, PythonWin is a framework that exposes much of the Microsoft Foundation Classes (MFC) to Python. MFC is a C++ framework that provides an object-based model of the Windows GUI API, as well as a number of services useful to applications.
The term PythonWin is a bit of a misnomer. PythonWin is really an application written to make use of the extensions that expose MFC to Python. This means PythonWin actually consists of two components:
We focus primarily on the MFC functionality exposed to Python so we can build a fully functional GUI application.
As PythonWin mirrors MFC, it's important to understand key MFC concepts to understand how PythonWin hangs together. Although we don't have room for a complete analysis of MFC, an introduction to its concepts is in order.
The Microsoft Foundation Classes are a framework for developing complete applications in C++. MFC provides two primary functions:
The object-oriented wrapping is straightforward. Many Windows API functions take a "handle" as their first parameter; for example, the function
SendMessage() takes a handle to a window (an
DrawText() takes a handle to a device context (an
HDC) and so forth. MFC wraps most of these handles in objects and thus provides
CDC classes, both of which have the relevant methods.
So, instead of writing your C++ code as:
HWND hwnd = CreateWindow(...); // Create a handle to the window...
EnableWindow(hwnd); // and enable it.
You may write code similar to:
CWnd wnd; // Create a window object.
wnd.CreateWindow(...); // Create the Window.
wnd.EnableWindow();// And enable it.
There are a large number of objects, including generic window objects, frame windows, MDI child windows, property pages, fonts, dialogs, etc. It's a large object model, so a good MFC text or the MFC documentation is recommended for anything more than casual use from Python.
The framework aspects of MFC provides some useful utility classes, both for structuring your application and performing many of the mundane tasks a good Windows application should do. The mundane but useful tasks it performs include automatic creation of tool-tip text and status-bar text for menus and dockable toolbars, reading and writing preferences in the registry, maintaining the "recently used files" list, and so forth.
MFC also provides a useful application/template/document/view architecture. You create an application object, then add one or more document templates to the application. A document template knows how to create a specific document, meaning your application can work with many documents. A "document" is a general concept; it holds the data for the object your application manages, but doesn't provide any user interface for viewing that data. The last link in the chain is the view object that's responsible for the user interaction. Each view defines a way of looking at your data. For example, you may have a graphical view and also a tabular view. Included in all of this are many utility functions for managing these objects. For example, when a view notifies its document that data has been changed, the document automatically notifies all other views, so they can be kept up-to-date.
If your application doesn't fit this model, don't be alarmed: you can customize almost all this behavior. But there is no doubt that utilizing this framework is the simplest way to use MFC.
Think of PythonWin as composed of two distinct portions. The
win32ui module is a Python extension that provides access to the raw MFC classes. For many MFC objects, there is an equivalent
win32ui object. For example, the functionality of the MFC
CWnd object is provided by a
PyCWnd Python object; an MFC
CDocument object by a
PyCDocument object, etc. For a full list, see the PythonWin reference (on the PythonWin help menu).
For the MFC framework to be useful, you need to be able to override default methods in the MFC object hierarchy; for example, the method
CView::OnDraw() is generally overridden to draw the screen for a view. But the objects exposed by the
win32ui module are technically Python types (they aren't classes) and a quirk in the Python language prevents these Python types from having their methods overridden.
To this end, the
win32ui module provides a mechanism to "attach" a Python class instance object to a
win32ui type. When MFC needs to call an overridden method, it then calls the method on the attached Python object.
What this means for the programmer is that you can use natural Python classes to extend the types defined in
pywin.mfc package provides Python base classes that interface with many of the
win32ui objects. These base classes handle the interaction with
win32ui and allow you to use Python subclassing to get your desired behavior.
This means that when you use a PythonWin object, there are two Python objects involved (the object of a
win32ui type and the Python class instance), plus an underlying MFC C++ object.
Let's see what this means in practice. We will examine a few of these objects from the PythonWin interactive window and create a dialog object using one of the standard PythonWin dialogs:
>>> import win32ui
>>> from pywin.mfc.dialog import Dialog
Looking at the object, you can see it's indeed an instance of a Python class:
<pywin.mfc.dialog.Dialog instance at 1083c80>
And you can see the underlying
object 'PyCDialog' - assoc is 010820C0, vf=True, notify=0,ch/u=0/0, mh=1, kh=0
It says that the C++ object is at address 0x010820c0 and also some other internal, cryptic properties of the object. You can use any of the underlying
win32ui methods automatically on this object:
>>> button.SetWindowText("Hello from Python")
The prompt in the dialog should now read "Hello from Python."
During the rest of this section, we will develop a sample application using PythonWin. This will lead us through many of the important MFC and PythonWin concepts, while also leveraging the dynamic nature of PythonWin.
MFC itself has a tutorial/sample called Scribble, which delivers a basic drawing application. We will develop a version of Scribble written in Python.
We will make use of some of the features of PythonWin to demonstrate how rapidly you can create such an application. Specifically, we will develop the Scribble framework first to run under the existing PythonWin framework, then make changes to it so it can run standalone. This is in contrast to the traditional technique of developing MFC applications, where the application object is often one of the first entities defined. A key benefit in using the PythonWin application object is that you get the full benefits of the PythonWin IDE, including error handling and reporting in the interactive window. This makes development much easier before we finally plug in our custom application object.
The general design of the Scribble application is simple. Define the document object to keep a list of strokes. A stroke is the start and end coordinates of a line. The document object also can load and store this list of strokes to a file. A view object is also defined that can render these strokes onto a Window.
The first step in the sample is to provide a placeholder for the document template, document, and view objects. Once this skeleton is working, we fill out these objects with a useful implementation.
Our first step is to develop a simple framework with placeholders for the major objects.
We define three objects: a
ScribbleDocument, and a
ScribbleView. These objects derive their implementation from objects in the
pywin.mfc.docview module. The
ScribbleTemplate object remains empty in this implementation. The
ScribbleDocument object has a single method,
OnNew-Document(), which is called as a document object is initialized; the implementation defines an empty list of strokes. The view object is based on a
PyCScrollView (i.e., an MFC
CScrollView) and defines a single method
OnInitialUpdate(). As the name implies, this method is called the first time a view object is updated. This method places the view in the correct mapping mode and disables the scrollbars. For more information on mapping modes and views, see the MFC documentation.
The final part of the skeleton registers the new document template with the MFC framework. This registration process is simple, just a matter of calling
AddDocTemplate() on the application object. In addition, this code associates some doc strings with the template. These doc strings tell the MFC framework important details about the document template, such as the file extensions for the documents, the window title for new documents, etc. For information on these doc strings, see the PythonWin Reference for the function
The term doc strings has a number of meanings. To Python, a doc string is a special string in a Python source file that provides documentation at runtime for specific objects. In the context of an MFC document template, a doc string is a string that describes an MFC document object.
A final note before we look at the code. This application has no special requirement for a frame window. The standard MFC/PythonWin Frame windows are perfectly suitable for the application. Therefore, we don't define a specific Frame window for the sample.
Let's look at the example application with the described functionality:
# The starting framework for our scribble application.
"""Called whenever the document needs initializing.
For most MDI applications, this is only called as the document
self.strokes = 
self.SetScrollSizes(win32con.MM_TEXT, (0, 0))
# Now we do the work to create the document template, and
# register it with the framework.
# For debugging purposes, we first attempt to remove the old template.
# This is not necessary once our app becomes stable!
# haven't run this before - that's ok
# Now create the template object itself...
template = ScribbleTemplate(None, ScribbleDocument, None, ScribbleView)
# Set the doc strings for the template.
docs='\nPyScribble\nPython Scribble Document\nScribble documents (*.psd)\n.psd'
# Then register it with MFC.
Notice there's some code specifically for debugging. If you execute this module multiple times, you'd potentially create multiple document templates, but all for the same class of documents (i.e., the
ScribbleDocument). To this end, each time you execute this module, try to remove the document template added during the previous execution.
What does this sample code do? It has registered the
ScribbleTemplate with MFC, and MFC is now capable of creating a new document. Let's see this in action. To register the template in PythonWin, perform the following steps:
To test this skeleton, select New from the File menu. You will see a list of all the document templates registered in PythonWin. The list should look something like Figure 20-4.
You can now select the Python
ScribbleDocument and see what happens. You should see a new Frame window, with the title
PyScribble1. MFC has given the new document a default name based on the doc strings you supplied the template.
Because you haven't added any code for interacting with the user, your application won't actually do anything yet! We will now develop this skeleton into a usable Scribble application.
Although MFC and PythonWin support multiple document templates, there's a slight complication that isn't immediately obvious. When MFC is asked to open a document file, it asks each registered
DocumentTemplate in turn if it can handle this document type. The default implementation for
DocumentTemplates is to report that it "can possibly open this document." Thus, when you're asked to open a Scribble document, one of the other
DocumentTemplate objects (e.g., the Python editor template) may be asked to handle it, rather than your
ScribbleTemplate. This wouldn't be a problem if this application handled only one document template, but since PythonWin already has some of its own, it could be a problem.
Therefore, it's necessary to modify the
DocumentTemplate so that when asked, it answers "I can definitely open this document." MFC then directs the open request to the template.
You provide this functionality by overriding the MFC method
MatchDocType(). It's necessary for this function to first check if a document of that name is already open; this prevents users from opening the document multiple times. The document template code now looks like:
def MatchDocType(self, fileName, fileType):
doc = self.FindOpenDocument(fileName)
if doc: return doc
ext = string.lower(os.path.splitext(fileName))
if ext =='.psd':
As you can see, you check the extension of the filename, and if it matches, tell MFC that the document is indeed yours. If the extension doesn't match, tell MFC you can't open the file.
As mentioned previously, this
ScribbleDocument object is responsible only for working with the document data, not for interacting with the user. This makes the
ScribbleDocument quite simple. The first step is to add some public methods for working with the strokes. These functions look like:
def AddStroke(self, start, end, fromView):
self.UpdateAllViews( fromView, None )
The first function appends the new stroke to the list of strokes. It also sets the document's "modified flag." This flag is used by MFC to automatically prompt the user to save the document as the program exits. It also automatically enables the File/Save option for the document.
The last thing the document must do is to load and save the data from a file. MFC itself handles displaying of the Save As, etc., dialogs, and calls Document functions to perform the actual save. The function names are
As the strokes are a simple list, you can use the Python
pickle module. The functions become quite easy:
def OnOpenDocument(self, filename):
file = open(filename, "rb")
self.strokes = pickle.load(file)
def OnSaveDocument(self, filename):
file = open(filename, "wb")
OnOpenDocument() loads the strokes from the named file. In addition, it places the filename to the most recently used (MRU) list.
OnSaveDocument() dumps the strokes to the named file, updates the document status to indicate it's no longer modified, and adds the file to the MRU list. And that is all you need to make your document fully functional.
View object is the most complex object in the sample. The
View is responsible for all interactions with the user, which means the
View must collect the strokes as the user draws them, and also draw the entire list of strokes whenever the window requires repainting.
The collection of the strokes is the most complex part. To collect effectively, you must trap the user pressing the mouse button in the window. Once this occurs, enter a drawing mode, and as the mouse is moved, draw a line to the current position. When the user releases the mouse button, they have completed the stroke, so add the stroke to the document. The key steps to coax this behavior are:
Viewmust hook the relevant mouse messages: in this case, the
LBUTTONDOWNmessage is received, remember the start position and enter a drawing mode. Also capture the mouse, to ensure that you get all future mouse messages, even when the mouse leaves the window.
MOUSEMOVEmessage occurs when you are in drawing mode, draw a line from the remembered start position to the current mouse position. In addition, erase the previous line drawn by this process. This gives a "rubber band" effect as you move the mouse.
LBUTTONUPmessage is received, notify the document of the new, completed stroke, release the mouse capture, and leave drawing mode.
After adding this logic to the sample, it now looks like:
self.SetScrollSizes(win32con.MM_TEXT, (0, 0))
self.bDrawing = 0
def OnLButtonDown(self, params):
assert not self.bDrawing, "Button down message while still drawing"
startPos = params
# Convert the startpos to Client coordinates.
self.startPos = self.ScreenToClient(startPos)
self.lastPos = self.startPos
# Capture all future mouse movement.
self.bDrawing = 1
def OnLButtonUp(self, params):
assert self.bDrawing, "Button up message, but not drawing!"
endPos = params
endPos = self.ScreenToClient(endPos)
self.bDrawing = 0
# And add the stroke to the document.
self.GetDocument().AddStroke( self.startPos, endPos, self )
def OnMouseMove(self, params):
# If Im not drawing at the moment, I don't care
if not self.bDrawing:
pos = params
dc = self.GetDC()
# Setup for an inverting draw operation.
# "undraw" the old line
# Now draw the new position
self.lastPos = self.ScreenToClient(pos)
Most of this code should be quite obvious. It's worth mentioning that you tell Windows to draw the line using a
NOT mode. This mode is handy; if you draw the same line twice, the second draw erases the first. Thus, to erase a line you drew previously, all you need is to draw the same line again.
Another key point is that the mouse messages all report the position in "Screen Coordinates" (i.e., relative to the top-left corner of the screen) rather than in "Client Coordinates" (i.e., relative to the top-left corner of our window). You use a member function
PyCWnd.ScreenToClient() to transform these coordinates.
The final step to complete the
View is to draw all your strokes whenever the window requires updating. This code is simple: you iterate over the list of strokes for the document, drawing lines between the coordinates. To obtain this behavior, use the code:
def OnDraw(self, dc):
# All we need to is get the strokes, and paint them.
doc = self.GetDocument()
for startPos, endPos in doc.GetStrokes():
And that's it! You now have a fully functional drawing application, capable of loading and saving itself from disk.
The simplest way to create an application object for Scribble is to subclass one of the standard application objects. The PythonWin application object is implemented in
pywin.framework.intpyapp, and it derives from the
CApp class in
pywin.framework.app. This base class provides much of the functionality for an application, so we too will derive our application from
This makes the application object small and simple. You obviously may need to enhance certain aspects; in this case, you should use the
pywin.framework modules as a guide. The minimal application object looks like:
# The application object for Python.
from pywin.framework.app import CApp
# All we need do is call the base class,
# then import our document template.
# And create our application object.
To run this, use the following command line:
C:\Scripts> start pythonwin /app scribbleApp.py
An instance of Pythonwin.exe now starts, but uses the application object you defined. Therefore, there'a no Interactive Window, the application doesn't offer to open .py files, etc. The Scribble application should look like Figure 20-5.
There is also a technique to avoid this command line, but you need a copy of a resource editor (such as Microsoft Visual C++). You can take a copy of Pythonwin.exe (name it something suitable for your application), then open the .exe in the resource editor and locate an entry in the string table with the value
pywin.framework.startup. This is the name of a module executed to boot the PythonWin application; the default script parses the "/app" off the command line and loads that application. You can change this to any value you like, and PythonWin then loads your startup script. See startup.py in the PythonWin distribution for an example of a startup script.
As we've discussed, MFC provides a framework architecture, and much of this architecture is tied together by resource IDs, integers that identify Windows resources in a DLL or executable.
For example, when you define a
DocumentTemplate, you specify a resource ID. The previous example doesn't specify a resource ID, so the default of
win32ui.IDR_PYTHONTYPE is used. When a document is created, MFC uses the resource ID in the following ways:
Another example of the reliance on resource IDs is in the processing and definition of menu and toolbar items. Each command in the application is assigned an ID. When you define menu or toolbar items, you specify the menu text (or toolbar bitmap) and the command ID. When MFC displays a menu item, it uses a string defined with the same ID and places this text automatically in the application's status bar. When the mouse hovers over a toolbar item, MFC again looks for a string with the specified ID and uses it for the tooltip-text for the button. This architecture has a number of advantages:
However, it also has a number of disadvantages:
PythonWin can use resources from arbitrary DLLs, thus you can define your own DLL containing only resources. PythonWin makes it easy to use these resources; just load the DLL (using
win32ui.LoadLibrary()), and PythonWin locates and uses the resources in this DLL.
If you are writing a large application, you'll probably find it worthwhile to define your own resource DLL when using PythonWin. The benefits offered by the framework make it worth the pain of initially setting everything up. On the other hand, it does make PythonWin somewhat cumbersome for defining these applications purely from Python code.
For the vast majority of Python users in Windows, PythonWin will never be more than an interesting IDE environment for developing Python applications. But many other Windows developers are beginning to use PythonWin to develop Windows applications. When comparing the three GUI toolkits available in this book, you will probably come to the conclusion that PythonWin is the least suitable for simple, small GUI applications written in Python, and this would be fair. However, depending on your particular circumstances (usually either because you have an existing MFC investment or it's important to use some user-interface features offered only by PythonWin), it may be a good choice.
PythonWin suffers from a lack of decent documentation. A Windows help file is included that contains a reference guide for all of the objects and methods exposed by PythonWin, but PythonWin doesn't include a comprehensive overview of the MFC framework. There are many good MFC books available, so a specific recommendation is impossible. Information from Microsoft on MFC can be found at http://msdn.microsoft.com/visualc/.
Another GUI toolkit available for Python is called
wxPython. The current incarnation is fairly new to the Python scene and is rapidly gaining popularity amongst Python developers.
wxPython is a Python extension module that encapsulates the wxWindows C++ class library.
wxPython is a cross-platform GUI framework for Python that is quite mature on the Windows platform. It exposes the popular
wxWindows C++ framework Python to provide an attractive alternative for GUI development.
wxWindows is a free C++ framework designed to make cross-platform programming child's play. Well, almost.
wxWindows 2.0 supports Windows 3.1/95/98/NT, Unix with GTK/Motif/Lesstif, with a Mac version underway. Other ports are under consideration.
wxWindows is a set of libraries that allows C++ applications to compile and run on several different types of computers, with minimal source-code changes. There's one library per supported GUI (such as Motif, or Windows). As well as providing a common API for GUI functionality, it provides functionality for accessing some commonly used operating-system facilities, such as copying or deleting files.
wxWindows is a framework in the sense that it provides a lot of built-in functionality, which the application can use or replace as required, thus saving a great deal of coding effort. Basic data structures such as strings, linked lists, and hash tables are also supplied.
Native versions of controls, common dialogs, and other window types are used on platforms that support them. For other platforms, suitable alternatives are created using
wxWindows itself. For example, on Win32 platforms the native list control is used, but on GTK, a generic list control with similar capabilities was created for use in the
wxWindows class library.
Experienced Windows programmers will feel right at home with the
wxWindows object model. Many of the classes and concepts will be familiar. For example, the Multiple Document Interface, drawing on Device Contexts with GDI objects such as brushes, pens, and so on.
wxPython is a Python extension module that provides a set of bindings from the
wxWindows library to the Python language. In other words, the extension module allows Python programers to create instances of
wxWindows classes and to invoke methods of those classes.
wxPython extension module attempts to mirror the class hierarchy of
wxWindows as closely as possible. This means that there is a
wxFrame class in
wxPython that looks, smells, tastes, and acts almost the same as the
wxFrame class in the C++ version.
wxPython is close enough to the C++ version that the majority of the
wxPython documentation is actually annotations to the C++ documentation that describe the places where
wxPython is different. There is also a series of sample programs included, and a series of documentation pages that assist the programmer in getting started with
The latest version of
wxPython can always be found at http://alldunn.com/wxPython/. From this site you can download a self-installer for Win32 systems that includes a prebuilt extension module, documentation in HTML help format, and a set of demos.
Also available from this site is a Linux RPM,
wxPython sources, documentation in raw HTML, and pointers to other sites, mail lists, the
wxPython FAQ, and so forth.
If you want to build
wxPython from sources yourself, you also need the
wxWindows sources, available from http://www.wxwindows.org/.
The remainder of this chapter gives a basic introduction to using
wxPython, starting with a simple example teaching the basic structure of a
wxPython application. We then build a more involved sample that touches on some of the more advanced features of the toolkit, using classes from the Doubletalk financial modeler you're already familiar with.
We've always found that the best way to learn is by doing and then experimenting and tweaking with what's been done. So download and install
wxPython, fire up your favorite text editor and get ready to play along as you read the next few sections.
Familiarize yourself with this little
wxPython program, and refer back to it as you read through the explanations that follow:
from wxPython.wx import *
frame = wxFrame(NULL, -1, "Hello from wxPython")
app = MyApp(0)
When you run this code, you should see a Window appear similar to Figure 20-6.
The first thing to do is import the entire
wxPython library with the
* statement. This is common practice for
wxPython programs, but you can obviously perform more restrictive imports if you prefer.
wxPython application needs to derive a class from
wxApp and provide an
OnInit method for it. The framework calls this method as part of its initialization sequence, and the usual purpose of
OnInit is to create the windows and essentials necessary for the program to begin operation. In the sample you created a frame with no parent, with a title of "Hello from
wxPython" and then showed it. We could also have specified a position and size for the frame in its constructor, but since we didn't, defaults are used. The last two lines of the
OnInit method will probably be the same for all applications;
SetTopWindow method informs
wxWindows that this frame is one of the main frames (in this case the only one) for the application, and you return
true to indicate success. When all top-level windows have been closed, the application terminates.
The final two lines of the script again will probably be the same for all your
wxPython applications. You create an instance of the application class and call its
MainLoop is the heart of the application: it's where events are processed and dispatched to the various windows, and it returns when the final window is closed. Fortunately,
wxWindows insulates you from the differences in event processing in the various GUI toolkits.
Most of the time you will want to customize the main frame of the application, and so using the stock
wxFrame isn't sufficient. As you might expect, you can derive your own class from
wxFrame to begin customization. This next example builds on the last by defining a frame class and creating an instance in the application's
OnInit method. Notice that except for the name of the class created in
OnInit, the rest of the
MyApp code is identical to the previous example. This code is displayed in Figure 20-7.
from wxPython.wx import *
ID_ABOUT = 101
ID_EXIT = 102
def _ _init_ _(self, parent, ID, title):
wxFrame._ _init_ _(self, parent, ID, title,
wxDefaultPosition, wxSize(200, 150))
self.SetStatusText("This is the statusbar")
menu = wxMenu()
"More information about this program")
menu.Append(ID_EXIT, "E&xit", "Terminate the program")
menuBar = wxMenuBar()
frame = MyFrame(NULL, -1, "Hello from wxPython")
app = MyApp(0)
This example shows some of the built-in capabilities of the
wxFrame class. For example, creating a status bar for the frame is as simple as calling a single method. The frame itself automatically manages its placement, size, and drawing. On the other hand, if you want to customize the status bar, create an instance of your own
wxStatusBar-derived class and attach it to the frame.
Creating a simple menu bar and a drop-down menu is also demonstrated in this example. The full range of expected menu capabilities is supported: cascading submenus, checkable items, popup menus, etc.; all you have to do is create a menu object and append menu items to it. The items can be text as shown here, or other menus. With each item you can optionally specify some short help text, as we have done, which are shown in the status bar automatically when the menu item is selected.
The one thing that the last sample doesn't do is show how to make the menus actually do something. If you run the sample and select Exit from the menu, nothing happens. The next sample takes care of that little problem.
To process events in
wxPython, any method (or standalone function for that matter) can be attached to any event using a helper function from the toolkit.
wxPython also provides a
wxEvent class and a whole bunch of derived classes for containing the details of the event. Each time a method is invoked due to an event, an object derived from
wxEvent is sent as a parameter, the actual type of the event object depends on the type of the event;
wxSizeEvent for when the window changes size,
wxCommandEvent for menu selections and button clicks,
wxMouseEvent for (you guessed it) mouse events, and so forth.
To solve our little problem with the last sample, all you have to do is add two lines to the
MyFrame constructor and add some methods to handle the events. We'll also demonstrate one of the common dialogs, the
wxMessageDialog. Here's the code, with the new parts in bold, and the running code shown in Figure 20-8:
from wxPython.wx import *
ID_ABOUT = 101
ID_EXIT = 102
def _ _init_ _(self, parent, ID, title):
wxFrame._ _init_ _(self, parent, ID, title,
wxDefaultPosition, wxSize(200, 150))
self.SetStatusText("This is the statusbar")
menu = wxMenu()
"More information about this program")
menu.Append(ID_EXIT, "E&xit", "Terminate the program")
menuBar = wxMenuBar()
EVT_MENU(self, ID_ABOUT, self.OnAbout)
EVT_MENU(self, ID_EXIT, self.TimeToQuit)
def OnAbout(self, event):
dlg = wxMessageDialog(self, "This sample program shows off\n"
"frames, menus, statusbars, and this\n"
"About Me", wxOK | wxICON_INFORMATION)
def TimeToQuit(self, event):
frame = MyFrame(NULL, -1, "Hello from wxPython")
app = MyApp(0)
EVT_MENU function called here is one of the helper functions for attaching events to methods. Sometimes it helps to understand what is happening if you translate the function call to English. The first one says, "For any menu item selection event sent to the window
self with an ID of
ID_ABOUT, invoke the method
There are many of these
EVT_* helper functions, all of which correspond to a specific type of event, or events. Some popular ones are listed in Table 20-4. See the
wxPython documentation for details.
Sent to a window when its size has changed, either interactively by the user or programmatically.
Sent to a window when it has been moved, either interactively by the user or programmatically.
Sent to a frame when it has been requested to close. Unless the close is being forced, it can be canceled by calling
This event is sent whenever a portion of the window needs to be redrawn.
Sent for each nonmodifier (Shift key, etc.) keystroke when the window has the focus.
This event is sent periodically when the system isn't processing other events.
The left mouse button has been pressed down.
The left mouse button has been let up.
The left mouse button has been double-clicked.
The mouse is in motion.
A scrollbar has been manipulated. This one is actually a collection of events, which can be captured individually if desired.
A button has been clicked.
A menu item has been selected.
Okay, now let's build something that's actually useful and learn more about the
wxPython framework along the way. As has been shown with the other GUI toolkits, we'll build a small application around the Doubletalk class library that allows browsing and editing of transactions.
We're going to implement a Multiple Document Interface, where the child frames are different views of the transactional data, rather than separate "documents." Just as with previous samples, the first thing to do is create an application class and have it create a main frame in its
frame = MainFrame(NULL)
app = DoubleTalkBrowserApp(0)
Since we are using MDI, there is a special class to use for the frame's base class. Here is the code for the initialization method of the main application frame:
title = "Doubletalk Browser - wxPython Edition"
def _ _init_ _(self, parent):
wxMDIParentFrame._ _init_ _(self, parent, -1, self.title)
self.bookset = None
self.views = 
if wxPlatform == '_ _WXMSW_ _':
self.icon = wxIcon('chart7.ico', wxBITMAP_TYPE_ICO)
# create a statusbar that shows the time and date on the right
sb = self.CreateStatusBar(2)
self.timer = wxPyTimer(self.Notify)
menu = self.MakeMenu(false)
EVT_MENU(self, ID_OPEN, self.OnMenuOpen)
EVT_MENU(self, ID_CLOSE, self.OnMenuClose)
EVT_MENU(self, ID_SAVE, self.OnMenuSave)
EVT_MENU(self, ID_EXIT, self.OnMenuExit)
EVT_MENU(self, ID_ABOUT, self.OnMenuAbout)
EVT_MENU(self, ID_ADD, self.OnAddTrans)
EVT_MENU(self, ID_JRNL, self.OnViewJournal)
EVT_MENU(self, ID_DTAIL, self.OnViewDetail)
Figure 20-9 shows the state of the application so far.
Obviously, we're not showing all the code yet, but we'll get to it all eventually as we go through piece by piece.
Notice the use of
wxMDIParentFrame as the base class of
MainFrame. By using this class you automatically get everything needed to implement MDI for the application without having to worry about what's really happening behind the scenes. The
wxMDIParentFrame class has the same interface as the
wxFrame class, with only a few additional methods. Often changing a single document interface program to a MDI program is as easy as changing the base classes the application's classes are derived from. There is a corresponding
wxMDIChildFrame to be used for the document windows, as we'll see later. If you ever need to have access to the client area (or the background area) of the MDI parent, you can use the
wxMDIClientWindow class. You might use this for placing a background image behind all the child windows.
The next thing the previous code does is create an icon and associate it with the frame. Normally Windows applications load items such as icons from a resource file that is linked with the executable. Since
wxPython programs have no binary executable file, you create the icon by specifying the full path to a .ico file. Assigning the icon to the frame only requires calling the frame's
You may have noticed from Figure 20-9 that the status bar has two sections, with the date and time displayed in the second one. The next bit of code in the initialization method handles that functionality. The frame's
CreateStatusBar method takes an optional parameter specifying the number of sections to create, and
SetStatusWidths can be given a list of integers to specify how many pixels to reserve for each section. The -1 means that the first section should take all the remaining space.
In order to update the date and time, you create a
wxPyTimer object. There are two types of timer classes in
wxPython. The first is the
wxPyTimer used here, which accepts a function or method to use as a callback. The other is the
wxTimer class, which is intended to be derived from and will call a required method in the derived class when the timer expires. In the example you specify that when the timer expires, the
Notify method should be called. Then start the timer, telling it to expire every 1000 milliseconds (i.e., every second). Here is the code for the
# Time-out handler
t = time.localtime(time.time())
st = time.strftime(" %d-%b-%Y %I:%M:%S", t)
You first use Python's
time module to get the current time and format it in to a nice, human-readable formatted string. Then by calling the frame's
SetStatus-Text method, you can put that string into the status bar, in this case in slot 1.
As you can see in the next bit of code, we have moved the building of the menu to a separate method. This is mainly for two reasons. The first is to help reduce clutter in the
_ _init_ _ method and better organize the functionality of the class. The second reason has to do with MDI. As with all MDI applications, each child frame can have its own menu bar, automatically updated as the frame is selected.
The approach taken by our sample is to either add or remove a single item from the
BookSet menu based on whether a view can select transactions for editing. Here's the code for the
MakeMenu method. Notice how the parameter controls whether the Edit Transaction item is added to the menu. It might have made better sense to just enable or disable this item as needed, but then you wouldn't be able to see how
wxPython changes the menus automatically when the active window changes. Also notice that you don't create the Window menu. The
wxMDIParentFrame takes care of that for you:
def MakeMenu(self, withEdit):
fmenu = wxMenu()
fmenu.Append(ID_OPEN, "&Open BookSet", "Open a BookSet file")
fmenu.Append(ID_CLOSE, "&Close BookSet",
"Close the current BookSet")
fmenu.Append(ID_SAVE, "&Save", "Save the current BookSet")
fmenu.Append(ID_SAVEAS, "Save &As", "Save the current BookSet")
fmenu.Append(ID_EXIT, "E&xit", "Terminate the program")
dtmenu = wxMenu()
dtmenu.Append(ID_ADD, "&Add Transaction",
"Add a new transaction")
dtmenu.Append(ID_EDIT, "&Edit Transaction",
"Edit selected transaction in current view")
dtmenu.Append(ID_JRNL, "&Journal view",
"Open or raise the journal view")
"Open or raise the detail view")
hmenu = wxMenu()
"More information about this program")
main = wxMenuBar()
If you skip back to the
_ _init_ _ method, notice that after you create the menu and attach it to the window, the
EnableTop method of the menubar is called. This is how to disable the entire
BookSet submenu. (Since there is no
BookSet file open, you can't really do anything with it yet.) There is also an
Enable method that allows you to enable or disable individual menu items by ID.
The last bit of the
_ _init_ _ method attaches event handlers to the various menu items. We'll be going through them one by one as we explore the functionality behind those options. But first, here are some of the simpler ones:
def OnMenuExit(self, event):
def OnCloseWindow(self, event):
def OnMenuAbout(self, event):
dlg = wxMessageDialog(self,
"This program uses the doubletalk package to\n"
"demonstrate the wxPython toolkit.\n\n"
"by Robin Dunn",
"About", wxOK | wxICON_INFORMATION)
The user selects Exit from the File menu, then the
OnMenuExit method is called, which asks the window to close itself. Whenever the window wants to close, whether it's because its
Close method was called or because the user clicks on the Close button in the titlebar, the
OnCloseWindow method is called. If you want to prompt the user with an "Are you sure you want to exit?" type of message, do it here. If he decides not to quit, just call the method
Most programs will want to have a fancier About box than the
wxMessageDialog provides, but for our purposes here it works out just fine. Don't forget to call the dialog's
Destroy method, or you may leak memory.
Before doing anything with a
BookSet, you have to have one opened. For this, use the common dialog
wxFileDialog. This is the same File Open dialog you see in all your other Windows applications, all wrapped in a nice
wxPython-compatible class interface.
Here's the event handler that catches the File Open menu event, and Figure 20-10 shows the dialog in action:
def OnMenuOpen(self, event):
# This should be checking if another is already open,
# but is left as an exercise for the reader...
dlg = wxFileDialog(self)
if dlg.ShowModal() == wxID_OK:
self.path = dlg.GetPath()
self.SetTitle(self.title + ' - ' + self.path)
self.bookset = BookSet()
win = JournalView(self, self.bookset, ID_EDIT)
Start off by creating the file dialog and tell it how to behave. Next show the dialog and give the user a chance to select a
BookSet file. Notice that this time you're checking the return value of the
ShowModal method. This is how the dialog says what the result was. By default, dialogs understand the
wxID_CANCEL IDs assigned to buttons in the dialog and do the right thing when they are clicked. For dialogs you create, you can also specify other values to return if you wish.
The first thing to do after a successful completion of the file dialog is ask the dialog what the selected pathname was, and then use this to modify the frame's title and to open a
Take a look at the next line. It reenables the
BookSet menu since there is now a file open. It's really two statements in one and is equivalent to these two lines:
menu = self.GetMenuBar()
Since it makes sense to actually let the user see something when they ask to open a file, you should create and show one of the views in the last bits of the
OnMenuOpen handler above. We'll take a look at that next.
The Journal view consists of a
wxListCtrl with a single-line summary for each transaction. It's placed inside a
wxMDIChildFrame and since it's the only thing in the frame, don't worry about setting or maintaining the size, the frame does it automatically. (Unfortunately, since some platforms send the first resize event at different times, sometimes the window shows up without its child sized properly.) Here's a simple workaround:
def _ _init_ _(self, parent, bookset, editID):
wxMDIChildFrame._ _init_ _(self, parent, -1, "")
self.bookset = bookset
self.parent = parent
tID = wxNewId()
self.lc = wxListCtrl(self, tID, wxDefaultPosition,
## Forces a resize event to get around a minor bug...
self.currentItem = 0
EVT_LIST_ITEM_SELECTED(self, tID, self.OnItemSelected)
menu = parent.MakeMenu(true)
EVT_MENU(self, editID, self.OnEdit)
Figure 20-11 shows the application is progressing nicely and starting to look like a serious Windows application.
wxListCtrl has many personalities, but they should all be familiar to you. Underneath its
wxPython wrappers, it's the same control used in Windows Explorer in the right side panel. All the same options are available: large icons, small icons, list mode, and the report mode used here. You define the columns with their headers and then set some events for the list control. You want to be able to edit the transactions when they are double-clicked, so why are both event handlers needed? The list control sends an event when an item is selected, but it doesn't keep track of double-clicks. The base
wxWindow class, on the other hand, reports double-clicks, but it knows nothing about the list control. So by catching both events you can easily implement the functionality you need. Here is the code for the event handlers:
def OnItemSelected(self, event):
self.currentItem = event.m_itemIndex
def OnDoubleClick(self, event):
After creating and setting up the list control, you create a menubar for this frame. Here you call the menu-making method in the parent, asking it to add the Edit Transaction menu item.
The last thing the
_ _init_ _ method does is call a method to fill the list control from the
BookSet. We've split this into a separate method so it can be called independently whenever the
BookSet data changes. Here's the
for x in range(len(self.bookset)):
trans = self.bookset[x]
self.lc.SetStringItem(x, 1, trans.comment)
self.lc.SetStringItem(x, 2, str(trans.magnitude()))
self.SetTitle("Journal view - %d transactions" %
Putting data in a list control is fairly easy; just insert each item. For the report mode, you insert an item for the first column and then set values for the remaining columns. For each column in the example, just fetch some data from the transaction and send it to the list control. If you were using icons or combination of icons and text, there are different methods to handle that.
Now that there's data in the list control, you should resize the columns. You can either specify actual pixel widths or have the list auto-size the columns based on the widths of the data.
The last thing the
JournalView class needs to do is to enable the editing of the transactions. We saw previously that when an item is double-clicked, a method named
OnEdit is invoked. Here it is:
def OnEdit(self, *event):
trans = self.bookset[self.currentItem]
dlg = EditTransDlg(self, trans,
if dlg.ShowModal() == wxID_OK:
trans = dlg.GetTrans()
This looks like what we did with the file dialog in the main frame, and indeed you will find yourself using this pattern quite often when using dialogs. The one item to notice here is the call to
UpdateViews() in the parent window. This is how to manage keeping all the views of the
BookSet up to date. Whenever a transaction is updated, this method is called and then loops through all open views, telling the views to update themselves with their
wxPythonincludes a number of powerful techniques for controlling the layout of your windows and controls. There are several alternative mechanisms provided and potentially several ways to accomplish the same thing. This allows the programmer to use whichever mechanism works best in a particular situation or whichever they are most comfortable with.
- There is a class called
wxLayoutConstraintsthat allows the specification of a window's position and size in relationship to its siblings and its parent. Each
wxLayoutContraintsobject is composed of eight
wxIndividualLayoutConstraintobjects, which define different sorts of relationships, such as which window is above this window, what is the relative width of this window, etc. You usually have to specify four of the eight individual constraints in order for the window to be fully constrained. For example, this button will be positioned in the center of its parent and will always be 50% of the parent's width:
b = wxButton(self.panelA, 100, ' Panel A `)
lc = wxLayoutConstraints()
lc.centreX.SameAs (self.panelA, wxCentreX)
lc.centreY.SameAs (self.panelA, wxCentreY)
lc.width.PercentOf (self.panelA, wxWidth, 50)
- Layout algorithm
- The class named
wxLayoutAlgorithmimplements layout of subwindows in MDI or SDI frames. It sends a
wxCalculateLayoutEventto children of the frame, asking them for information about their size. Because the event system is used this technique can be applied to any window, even those that aren't necessarily aware of the layout classes. However, you may wish to use
wxSashLayoutWindowfor your subwindows since this class provides handlers for the required events and accessors to specify the desired size of the window. The sash behavior in the base class can be used, optionally, to make the windows user-resizable.
wxLayoutAlgorithmis typically used in IDE style of applications, where there are several resizable windows in addition to the MDI client window or other primary editing window. Resizable windows might include toolbars, a project window, and a window for displaying error and warning messages.
- In an effort to simplify the programming of simple layouts, a family of
wxSizerclasses has been added to the
wxPythonlibrary. These are classes that are implemented in pure Python instead of wrapping C++ code from
wxWindows. They are somewhat reminiscent of the layout managers from Java in that you select the type of sizer you want and then add windows or other sizers to it, and they all follow the same rules for layout. For example, this code fragment creates five buttons that are laid out horizontally in a box, and the last button is allowed to stretch to fill the remaining space allocated to the box:
box = wxBoxSizer(wxHORIZONTAL)
box.Add(wxButton(win, 1010, "one"), 0)
box.Add(wxButton(win, 1010, "two"), 0)
box.Add(wxButton(win, 1010, "three"), 0)
box.Add(wxButton(win, 1010, "four"), 0)
box.Add(wxButton(win, 1010, "five"), 1)
wxWindowslibrary has a simple dialog editor available that can assist with the layout of controls on a dialog and generates a portable cross-platform resource file. This file can be loaded into a program at runtime and transformed on the fly into a window with the specified controls on it. The only downfall with this approach is that you don't have the opportunity to subclass the windows that are generated, but if you can do everything you need with existing control types and event handlers, it should work out great. Eventually, there will be a
wxPython-specific application builder tool that will generate either a resource type of file or actual Python source code for you.
- Brute force
- Finally, there is the brute-force mechanism of specifying the exact position of every component programmatically. Sometimes the layout needs of a window don't fit with any of the sizers or don't warrant the complexity of the constraints or the layout algorithm. For these situations, you can fall back on doing it "by hand," but you probably don't want to attempt it for anything much more complex than the Edit Transaction dialog.
The next step is to build a dialog to edit a transaction. As you've seen, the transaction object is composed of a date, a comment, and a variable number of transaction lines each of which has an account name and an amount. We know that all the lines should add up to zero and that the date should be a valid date. In addition to editing the date and comment, you need to be able to add, edit, and delete lines. Figure 20-12 shows one possible layout for this dialog and the one used for this example.
Since there's quite a bit going on here, let's go through the initialization of this class step by step. Here's the first bit:
def _ _init_ _(self, parent, trans, accountList):
wxDialog._ _init_ _(self, parent, -1, "")
self.item = -1
self.trans = copy.deepcopy(trans)
self.trans = Transaction()
This is fairly simple stuff. Just invoke the parent class's
_ _init_ _ method, do some initialization, and determine if you're editing an existing transaction or creating a new one. If editing an existing transaction, use the Python copy module to make a copy of the object. You do this because you will be editing the transaction in-place and don't want to have any partially edited transactions stuck in the
BookSet. If the dialog is being used to add a new transaction, create one, and then fix its date by truncating the time from it. The default date in the transaction includes the current time, but this dialog is equipped to deal only with the date portion.
If you review the sidebar "wxPython Window Layout," you'll see a number of choices available, but we have chosen to use the brute-force mechanism for the Edit Transaction dialog:
# Create some controls
wxStaticText(self, -1, "Date:", wxDLG_PNT(self, 5,5))
self.date = wxTextCtrl(self, ID_DATE, "",
wxDLG_PNT(self, 35,5), wxDLG_SZE(self, 50,-1))
wxStaticText(self, -1, "Comment:", wxDLG_PNT(self, 5,21))
self.comment = wxTextCtrl(self, ID_COMMENT, "",
wxDLG_PNT(self, 35, 21), wxDLG_SZE(self, 195,-1)
The code shows how to create the labels and the text fields at the top of the dialog. Notice the use of
wxDLG_SZE to convert dialog units to a
wxPoint and a
wxSize, respectively. (The -1's used above mean that the default size should be used for the height.) Using dialog units instead of pixels to define the dialog means you are somewhat insulated from changes in the font used for the dialog, so you use dialog units wherever possible. The
wxSize are always defined in terms of pixels, but these conversion functions allow the actual number of pixels used to vary automatically from machine to machine with different fonts. This makes it easy to move programs between platforms that have completely different window managers. Figure 20-13 shows this same program running on RedHat Linux 6.0, and you can see that for the most part, the controls are still spaced appropriately even though a completely different font is used on the form. It looks like the
wxTextCtrl is a few dialog units taller on this platform, so perhaps there should be a bit more space between the rows. We leave this as an exercise for you.
The next control to be defined is the
wxListCtrl that displays the account and amount lines:
self.lc = wxListCtrl(self, ID_LIST,
wxDLG_PNT(self, 5,34), wxDLG_SZE(self, 225,60),
self.lc.SetColumnWidth(0, wxDLG_SZE(self, 180,-1).width)
self.lc.SetColumnWidth(1, wxDLG_SZE(self, 40,-1).width)
It's important to note that the width of this control is 225 dialog units. Since this control spans the entire width of the dialog, you know the space you have to work with. You can use this value when deciding where to place or how to size the other controls.
Instead of auto-sizing the width of the list columns, let's now use explicit sizes. But you can still use dialog units to do it by extracting the
width attribute from the
wxSize object returned from
wxDLG_SZE. We should mention the following points:
wxStaticLinecontrol for drawing the line across the dialog.
wxComboBoxis used for selecting existing account names from a list.
wxID_CANCELfor OK and Cancel buttons, respectively, and force the OK button as the default button.
Fit()method to determine the initial size of the dialog window. This function calculates the total required size based on the size information specified in each of the children.
Here's the rest of the code for creating the controls:
wxStaticText(self, -1, "Balance:", wxDLG_PNT(self, 165,100))
self.balance = wxTextCtrl(self, ID_BAL, "",
wxDLG_SZE(self, 40, -1))
wxStaticLine(self, -1, wxDLG_PNT(self, 5,115),
wxStaticText(self, -1, "Account:", wxDLG_PNT(self, 5,122))
self.account = wxComboBox(self, ID_ACCT, "",
wxDLG_PNT(self, 30,122), wxDLG_SZE(self, 130,-1),
accountList, wxCB_DROPDOWN | wxCB_SORT)
wxStaticText(self, -1, "Amount:", wxDLG_PNT(self, 165,122))
self.amount = wxTextCtrl(self, ID_AMT, "",
wxDLG_SZE(self, 40, -1))
btnSz = wxDLG_SZE(self, 40,12)
wxButton(self, ID_ADD, "&Add Line", wxDLG_PNT(self, 52,140), btnSz)
wxButton(self, ID_UPDT, "&Update Line", wxDLG_PNT(self, 97,140),
wxButton(self, ID_DEL, "&Delete Line", wxDLG_PNT(self, 142,140),
self.ok = wxButton(self, wxID_OK, "OK", wxDLG_PNT(self, 145,5),
wxButton(self, wxID_CANCEL, "Cancel", wxDLG_PNT(self, 190,5), btnSz)
# Resize the window to fit the controls
The last thing to do is set up some event handlers and load the dialog controls with data. The event handling for the controls is almost identical to the menu handling discussed previously, so there shouldn't be any surprises:
# Set some event handlers
EVT_BUTTON(self, ID_ADD, self.OnAddBtn)
EVT_BUTTON(self, ID_UPDT, self.OnUpdtBtn)
EVT_BUTTON(self, ID_DEL, self.OnDelBtn)
EVT_LIST_ITEM_SELECTED(self, ID_LIST, self.OnListSelect)
EVT_LIST_ITEM_DESELECTED(self, ID_LIST, self.OnListDeselect)
EVT_TEXT(self, ID_DATE, self.Validate)
# Initialize the controls with current values
for x in range(len(self.trans.lines)):
account, amount, dict = self.trans.lines[x]
self.lc.SetStringItem(x, 1, str(amount))
The last thing the code snippet does is call a
Validate() method, which as you can probably guess, is responsible for validating the dialog data; in this case, validating the date and that all transaction lines sum to zero. Check the date when the field is updated (via the
EVT_TEXT() call shown in the code) and check the balance any time a line is added or updated. If anything doesn't stack up, disable the OK button. Here is
def Validate(self, *ignore):
bal = self.trans.balance()
date = self.date.GetValue()
dateOK = (date == dates.testasc(date))
dateOK = 0
if bal == 0 and dateOK:
Notice that the balance field is updated. The next thing we demonstrate is the Add Line functionality. To do this, you need to take whatever is in the account and amount fields, add them to the transaction, and also add them to the list control:
def OnAddBtn(self, event):
account = self.account.GetValue()
amount = string.atof(self.amount.GetValue())
# update the list control
idx = len(self.trans.lines)
self.lc.SetStringItem(idx-1, 1, str(amount))
Validate again to check if the transaction's lines are in balance. The event handlers for the Update and Delete buttons are similar and not shown here.
That's about all there is to it!
wxPython takes care of the tab-traversal between fields, auto-completion on the Enter key, auto-cancel on Esc, and all the rest.
This small section has barely touched the surface of what
wxPython is capable of. There are many more window and control types than what have been shown here, and the advanced features lend themselves to highly flexible and dynamic GUI applications across many platforms. Combined with the flexibility of Python, you end up with a powerful tool for quickly creating world-class applications.
For more information on
wxPython, including extensive documentation and sample code, see the
wxPython home page at http://alldunn.com/wxPython/.
For more information on the underlying
wxWindows framework, please visit its home page at http://www.wxwindows.org/.
When getting started, you should probably avoid using PythonWin or IDLE for running
wxPython programs, because the interactions between the various toolkits may have unexpected consequences.
Mark Hammond, Andy Robinson is an independent Microsoft Windows consultant working out of Melbourne, Australia.
Return to Python DevCenter.
Copyright © 2009 O'Reilly Media, Inc.