Python Programming on Win32
Help for Windows ProgrammersBy Mark Hammond & Andy Robinson
1st Edition January 2000
1-56592-621-8, Order Number: 6218
672 pages, $34.95
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:
- A GUI toolkit provided as a library of C routines. This library manages and manipulates the windows and handles the GUI events and user interaction.
- The Python Tk interface. A Python module that provides a collection of Python classes and methods for accessing the Tk toolkit from within Python.
- The (mostly hidden) language used by Tk and, hence, used by Tkinter to communicate with the Tk toolkit.
- A user interface element, such as a text box, combo box, or top-level window. On Windows, the common terminology is control or window.
Pros and Cons of Tkinter
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:
- Python programs using Tkinter can be very brief, partly because of the power of Python, but also due to Tk. In particular, reasonable default values are defined for many options used in creating a widget, and packing it (i.e., placing and displaying).
- Cross platform
- Tk provides widgets on Windows, Macs, and most Unix implementations with very little platform-specific dependence. Some newer GUI frameworks are achieving a degree of platform independence, but it will be some time before they match Tk's in this respect.
- First released in 1990, the core is well developed and stable.
- Many extensions of Tk exist, and more are being frequently distributed on the Web. Any extension is immediately accessible from Tkinter, if not through an extension to Tkinter, than at least through Tkinter's access to the Tcl language.
To balance things, here's a list of what's often mention as weaknesses in Tkinter:
- There is some concern with the speed of Tkinter. Most calls to Tkinter are formatted as a Tcl command (a string) and interpreted by Tcl from where the actual Tk calls are made. This theoretical slowdown caused by the successive execution of two interpreted languages is rarely seen in practice and most real-world applications spend little time communicating between the various levels of Python, Tcl, and Tk.
- Python purists often balk at the need to install another (and to some, a rival) scripting language in order to perform GUI tasks. Consequently, there is periodic interest in removing the need for Tcl by using Tk's C-language API directly, although no such attempt has ever succeeded.
- Tk lacks modern widgets
- It's acknowledged that Tk presents a small basic set of widgets and lacks a collection of modern fancy widgets. For instance, Tk lacks paned windows, tabbed windows, progress meter widgets, and tree hierarchy widgets. However, the power and flexibility of Tk is such that you can easily construct new widgets from a collection of basic widgets. This fits in especially well with the object-oriented nature of Python.
- Native look and feel
- One common source of complaints is that Tkinter applications on Windows don't look like native Windows applications. As we shall see, the current version of Tkinter provides an interface that should be acceptable to almost everyone except the Microsoft marketing department, and we can expect later versions of Tkinter to be virtually indistinguishable.
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.
Running GUI Applications
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:
- If you execute Python.exe from Windows Explorer, a new empty console window is created; then the Tkinter windows are created.
- If you execute a Tkinter application under Python.exe from a command prompt, the command prompt doesn't return until the Tkinter application has finished. This will be a surprise for many users, who expect that executing a GUI program returns the command prompt immediately.
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()
Figure 20-1. Tkinter's "Hello World"
As you can see, apart from the
importstatements, there are only three lines of interest. The
rootvariable 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
rootvariable), 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.
The other end of the World
From the extreme simplicity of the "Hello World" example, the other end of the scale could be considered the
tkDemosample 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.
Figure 20-2. tkDemo example in action
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.
Table 20-1: Core Tkinter Widgets
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:
- Indexes provide a rich model for describing positions in the text control. The position specification can be in terms of line and column position (relative or absolute), pixel position, special system index names, and so forth.
- Tags are an association between a name and one or more regions of text. There is no restriction on overlapping regions, so a character may belong to any number of tags, and tags can be created and destroyed dynamically. In addition, the text associated with a tag may be given any number of display characteristics (such as font, color specifications, and so forth). When we combine these capabilities with the Tkinter event model described later, it becomes easy to build highly interactive applications (such as a web browser) around this Text widget.
- A mark is a single position within the text (or more accurately, a position between two characters). Marks flow naturally with the surrounding text as characters are inserted and deleted, making them particularly suitable for implementing concepts such as bookmarks or breakpoints. Tkinter manages a number of predefined marks, such as
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.
Dialog and other noncore widgets
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.
Table 20-2: Tkinter Common Dialog Box Modules
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
EditTransactionclass that shows an example of this.
Widget properties and methods
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.
Table 20-3: Common Tkinter Widget Properties
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
or light blue), or you can specify each of the RGB components in hexadecimal notation prefixed with a # (e.g., #ffffff for white).
A 3D appearance for the object (
GROOVE) or a 2D appearance (FLAT or SOLID).
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,
"bold"). Tkinter guarantees that the fonts
Courierexist on all platforms, and styles can be any combination of bold, roman, italic, underline, and overstrike, which are always available, with Tkinter substituting the closest matching font if necessary.
Techniques used by control widgets to communicate back to the application. The
commandoption allows you to specify an arbitrary Python function (or any callable Python object) to be invoked when the specified action occurs (e.g., when a Button widget is pushed). Alternatively, several widgets take the variable option and, if specified, must be an instance of one of the
BooleanVarclasses (or subclass). Once set up, changes to the widget are immediately reflected in the object, and changes in the object are immediately reflected to the widget. This is demonstrated in tkBrowser.py in a number of places including the
EditTransactionclass, which uses this technique for managing the data shown in the dialog.
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
Tkinter Sample Code
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.
Figure 20-3. The Tkinter Doubletalk browser 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:
- The standard Python documentation is optionally installed with Python on Windows and is also available online at http://www.python.org/doc.
- PythonWare and Fredrik Lundh provide excellent Tkinter resources, including tutorials available at http://www.pythonware.com.
- Tcl and Tk are developed and supported by the Scriptics Corporation, which can be found at http://www.scriptics.com. Tcl and Tk documentation is available from http://www.scriptics.com/resource/doc/. O'Reilly has an excellent book on the subject, Tcl/Tk in a Nutshell by Paul Raines and Jeff Trantor.
- Python megawidgets are available via http://www.dscpl.com.au/pmw/.
- Keep your eye out for O'Reilly's Tkinter Programming by Fredrik Lundh.
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:
- Python modules that provide the raw MFC functionality
- Python code that uses these modules to provide the PythonWin application
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.
Introduction to MFC
The Microsoft Foundation Classes are a framework for developing complete applications in C++. MFC provides two primary functions:
- An object-oriented wrapper for the native Windows user-interface API
- Framework facilities that remove much of the grunge work involved in making a complete, standalone Windows application
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
CDCclasses, 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.
The PythonWin Object Model
Think of PythonWin as composed of two distinct portions. The
win32uimodule is a Python extension that provides access to the raw MFC classes. For many MFC objects, there is an equivalent
win32uiobject. For example, the functionality of the MFC
CWndobject is provided by a
PyCWndPython object; an MFC
CDocumentobject by a
PyCDocumentobject, 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
win32uimodule 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
win32uimodule provides a mechanism to "attach" a Python class instance object to a
win32uitype. 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.mfcpackage provides Python base classes that interface with many of the
win32uiobjects. These base classes handle the interaction with
win32uiand 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
win32uitype 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
win32uimethods automatically on this object:
>>> button.SetWindowText("Hello from Python")
The prompt in the dialog should now read "Hello from Python."
Developing a PythonWin Sample Application
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.
Defining a Simple Framework
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
ScribbleTemplateobject remains empty in this implementation. The
ScribbleDocumentobject 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
ScribbleTemplatewith 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:
- Start PythonWin.
- Open the sample code in PythonWin using the File menu and select Open.
- From the File menu, select Import. This action executes the module in the PythonWin environment.
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.
Figure 20-4. The File/New dialog in PythonWin after executing the sample application
You can now select the Python
ScribbleDocumentand 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.
Enhancing the DocumentTemplate
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
DocumentTemplatein turn if it can handle this document type. The default implementation for
DocumentTemplatesis to report that it "can possibly open this document." Thus, when you're asked to open a Scribble document, one of the other
DocumentTemplateobjects (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
DocumentTemplateso 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.
Enhancing the Document
As mentioned previously, this
ScribbleDocumentobject is responsible only for working with the document data, not for interacting with the user. This makes the
ScribbleDocumentquite 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
picklemodule. 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.
Defining the View
Viewobject is the most complex object in the sample. The
Viewis responsible for all interactions with the user, which means the
Viewmust 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
- When a
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.
- If a
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.
- When a
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
NOTmode. 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
Viewis 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.
Creating the Application Object
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
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.frameworkmodules 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.
Figure 20-5. Our PythonWin Scribble application
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.
PythonWin and Resources
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_PYTHONTYPEis used. When a document is created, MFC uses the resource ID in the following ways:
- The menu with the ID is loaded and used for the document's frame. This allows each document supported in an application to have a unique set of menus as is common in MDI applications.
- The icon with the ID is loaded and used for the document's frame.
- The accelerator with that ID is loaded, providing document-specific shortcut keys to many of the menu functions.
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:
- Hooking the various pieces of your application together becomes simple. You define an icon, menu, accelerators, strings, and so forth with the same resource ID, and MFC automatically ties all these together for your application.
- If you are working with an existing MFC or C++ application, there's a good chance you already use resources in a similar fashion, so PythonWin often fits naturally when embedded in these applications.
- When you need to respond to a GUI command, specify the command ID. The same code then handles the command regardless of whether it was sourced from the keyboard, toolbar or menu.
- Localizing your application for other languages becomes simpler. You define new resources in the new language but use the same IDs, and the application still works regardless of the particular resources in use at the time.
However, it also has a number of disadvantages:
- Python doesn't have a technique for defining resources, such as dialogs, menus, toolbars, or strings. So while this scheme works well using MFC from Microsoft Visual C++ (which does provide this facility), it doesn't work as well from Python.
- It's a pain to move beyond the MFC-provided framework. As soon as you begin manually defining and managing these resources, you aren't much better off than if you simply had used the raw Windows GUI API.
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.
wxPythonis a Python extension module that encapsulates the wxWindows C++ class library.
wxPythonis a cross-platform GUI framework for Python that is quite mature on the Windows platform. It exposes the popular
wxWindowsC++ framework Python to provide an attractive alternative for GUI development.
wxWindowsis a free C++ framework designed to make cross-platform programming child's play. Well, almost.
wxWindows2.0 supports Windows 3.1/95/98/NT, Unix with GTK/Motif/Lesstif, with a Mac version underway. Other ports are under consideration.
wxWindowsis 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.
wxWindowsis 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
wxWindowsitself. 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
Experienced Windows programmers will feel right at home with the
wxWindowsobject 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.
wxWindows + Python = wxPython
wxPythonis a Python extension module that provides a set of bindings from the
wxWindowslibrary to the Python language. In other words, the extension module allows Python programers to create instances of
wxWindowsclasses and to invoke methods of those classes.
wxPythonextension module attempts to mirror the class hierarchy of
wxWindowsas closely as possible. This means that there is a
wxPythonthat looks, smells, tastes, and acts almost the same as the
wxFrameclass in the C++ version.
wxPythonis close enough to the C++ version that the majority of the
wxPythondocumentation is actually annotations to the C++ documentation that describe the places where
wxPythonis different. There is also a series of sample programs included, and a series of documentation pages that assist the programmer in getting started with
Where to get wxPython
The latest version of
wxPythoncan 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,
wxPythonsources, documentation in raw HTML, and pointers to other sites, mail lists, the
wxPythonFAQ, and so forth.
If you want to build
wxPythonfrom sources yourself, you also need the
wxWindowssources, available from http://www.wxwindows.org/.
Where to go from here
The remainder of this chapter gives a basic introduction to using
wxPython, starting with a simple example teaching the basic structure of a
wxPythonapplication. 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.
A simple example
Familiarize yourself with this little
wxPythonprogram, 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.
Figure 20-6. A basic wxPython application
The first thing to do is import the entire
wxPythonlibrary with the
*statement. This is common practice for
wxPythonprograms, but you can obviously perform more restrictive imports if you prefer.
wxPythonapplication needs to derive a class from
wxAppand provide an
OnInitmethod for it. The framework calls this method as part of its initialization sequence, and the usual purpose of
OnInitis 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
OnInitmethod will probably be the same for all applications;
wxWindowsthat this frame is one of the main frames (in this case the only one) for the application, and you return
trueto 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
wxPythonapplications. You create an instance of the application class and call its
MainLoopis 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,
wxWindowsinsulates 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
wxFrameisn't sufficient. As you might expect, you can derive your own class from
wxFrameto begin customization. This next example builds on the last by defining a frame class and creating an instance in the application's
OnInitmethod. Notice that except for the name of the class created in
OnInit, the rest of the
MyAppcode 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)
Figure 20-7. A wxPython application with menus
This example shows some of the built-in capabilities of the
wxFrameclass. 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.
Events in wxPython
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.
wxPythonalso provides a
wxEventclass 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
wxEventis sent as a parameter, the actual type of the event object depends on the type of the event;
wxSizeEventfor when the window changes size,
wxCommandEventfor menu selections and button clicks,
wxMouseEventfor (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
MyFrameconstructor 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)
Figure 20-8. The application with an About box
EVT_MENUfunction 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
selfwith 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
wxPythondocumentation for details.
Table 20-4: Common wxPython Event Functions
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.
Building a Doubletalk Browser with wxPython
Okay, now let's build something that's actually useful and learn more about the
wxPythonframework 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.
Figure 20-9. The first MDI wxPython application
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
wxMDIParentFrameas 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
wxMDIParentFrameclass has the same interface as the
wxFrameclass, 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
wxMDIChildFrameto 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
wxMDIClientWindowclass. 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
wxPythonprograms 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
CreateStatusBarmethod takes an optional parameter specifying the number of sections to create, and
SetStatusWidthscan 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
wxPyTimerobject. There are two types of timer classes in
wxPython. The first is the
wxPyTimerused here, which accepts a function or method to use as a callback. The other is the
wxTimerclass, 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
Notifymethod 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
timemodule to get the current time and format it in to a nice, human-readable formatted string. Then by calling the frame's
SetStatus-Textmethod, 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
BookSetmenu based on whether a view can select transactions for editing. Here's the code for the
MakeMenumethod. 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
wxPythonchanges the menus automatically when the active window changes. Also notice that you don't create the Window menu. The
wxMDIParentFrametakes 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
EnableTopmethod of the menubar is called. This is how to disable the entire
BookSetsubmenu. (Since there is no
BookSetfile open, you can't really do anything with it yet.) There is also an
Enablemethod 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
OnMenuExitmethod is called, which asks the window to close itself. Whenever the window wants to close, whether it's because its
Closemethod was called or because the user clicks on the Close button in the titlebar, the
OnCloseWindowmethod 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
wxMessageDialogprovides, but for our purposes here it works out just fine. Don't forget to call the dialog's
Destroymethod, 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)
Figure 20-10. wxPython browsing for a Doubletalk transaction file
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
BookSetfile. Notice that this time you're checking the return value of the
ShowModalmethod. This is how the dialog says what the result was. By default, dialogs understand the
wxID_CANCELIDs 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
BookSetmenu 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
OnMenuOpenhandler above. We'll take a look at that next.
The Journal view consists of a
wxListCtrlwith a single-line summary for each transaction. It's placed inside a
wxMDIChildFrameand 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.
Figure 20-11. The list of Doubletalk transactions
wxListCtrlhas many personalities, but they should all be familiar to you. Underneath its
wxPythonwrappers, 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
wxWindowclass, 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
BookSetdata 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
JournalViewclass needs to do is to enable the editing of the transactions. We saw previously that when an item is double-clicked, a method named
OnEditis 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
BookSetup 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.
wxDialog and friends
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.
Figure 20-12. The wxPython Doubletalk transaction editor
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_SZEto convert dialog units to 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
wxSizeare 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
wxTextCtrlis 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.
Figure 20-13. The wxPython Doubletalk editor running on Redhat Linux 6.0
The next control to be defined is the
wxListCtrlthat 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
widthattribute from the
wxSizeobject returned from
wxDLG_SZE. We should mention the following points:
- The balance field is disabled, as you only want to use it to display a value.
- Use a
wxStaticLinecontrol for drawing the line across the dialog.
wxComboBoxis used for selecting existing account names from a list.
- Use the standard IDs
wxID_CANCELfor OK and Cancel buttons, respectively, and force the OK button as the default button.
- Call the base class
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))
Validateagain 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!
wxPythontakes 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
wxPythonis 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
wxPythonhome page at http://alldunn.com/wxPython/.
For more information on the underlying
wxWindowsframework, please visit its home page at http://www.wxwindows.org/.
1. When getting started, you should probably avoid using PythonWin or IDLE for running
wxPythonprograms, because the interactions between the various toolkits may have unexpected consequences.
Back to: Sample Chapter Index
Back to: Python Programming on Win32
© 2001, O'Reilly & Associates, Inc.