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 ther 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:
Tk
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.
Tkinter
The Python Tk interface. A Python module that provides a collection of Python classes and methods for accessing the Tk toolkit from within Python.
Tcl
The (mostly hidden) language used by Tk and, hence, used by Tkinter to communicate with the Tk toolkit.
Widget
A user interface element, such as a text box, combo box, or top-level window. On Windows, the common terminology is control or window.
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:
Brevity
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.
Maturity
First released in 1990, the core is well developed and stable.
Extensibility
Many extensions of Tk exist, and more are being frequently distributed on the Web. Any extension is immediately accessible from Tkinter's 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:
Speed
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 communication between the various levels of Python, Tcl, and Tk.
Tcl
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 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.
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 trace-backs printed by Python aren't typically seen. Although Python prints the trace-back 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()
root.mainloop()
Figure 20-1. Tkinter's "Hello World" |
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.
Figure 20-2. tkDemo example in action |
|
|
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
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
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.
Marks
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.
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.
|
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, optional=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',
text='Hello World!',
relief=RAISED,
borderwidth=3)
And provide an annoying blinking effect by periodic execution of:
label.configure(background='red')
And:
label.configure(background='white')
There are dozens of options that can be specified for each widget. Table 20-3 lists a few common properties available for each widget.
|
|
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.
The 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, pack(), grid(), or place().
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 grid().
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 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
Figure 20-3. The Tkinter Doubletalk browser in action |
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 the 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.
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 HWND), DrawText() takes a handle to a device context (an HDC) and so forth. MFC wraps most of these handles in objects and thus provides CWnd and 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 win32ui
The 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 underlaying 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
>>> d=Dialog(win32ui.IDD_SIMPLE_INPUT)
Looking at the object, you can see it's indeed an instance of a Python class:
>>> d
<pywin.mfc.dialog.Dialog instance at 1083c80>
And you can see the underlying win32ui object:
>>> d._obj_
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:
>>> d.CreateWindow()
>>> button=d.GetDlgItem(win32ui.IDC_PROMPT1)
>>> button.SetWindowText("Hello from Python")
>>>
The prompt in the dialog should now read ''Hello form 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 ScribbleTemplate, 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 PyCDocTemplate. SetDocStrings().
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:
# scribble1.py
#
# The starting framework for our scribble application.
import win32ui
import win32con
import pywin.mfc.docview
class ScribbleTemplate(pywin.mfc.docview.DocTemplate):
pass
class ScribbleDocument(pywin.mfc.docview.Document):
def OnNewDocument(self):
" " "Called whenever the document needs initializing.
For most MDI applications, this is only called as the document
is created.
" " "
self.strokes = []
return 1
class ScribbleView(pywin.mfc.docview.ScrollView):
def OnInitialUpdate(self):
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!
try:
win32ui.GetApp() .RemoveDocTemplate(template)
except NameError:
# haven't run this before - that's ok
pass
# 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'
template.SetDocStrings(docs)
# Then register it with MFC.
win32ui.GetApp() .AddDocTemplate(template)
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:
� 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 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:
class ScribbleTemplate(pywin.mfc.docview.DocTemplate):
def MatchDocType(self, fileName, fileType):
doc = self.FindOpenDocument(fileName)
if doc: return doc
ext = string.lower(os.path.splitext(fileName)[1])
if ext =='.psd':
return win32ui.CDocTemplate_Confidence_yesAttemptNative
return win32ui.CDocTemplate_Confidence_noAttempt
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:
class ScribbleDocument(pywin.mfc.docview.Document):
�
def AddStroke(self, start, end, fromView):
self.strokes.append((start, end))
self.SetModifiedFlag()
self.UpdateAllViews( fromView, None )
def GetStrokes(self):
return self.strokes
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 OnOpenDocument() and OnSaveDocument() respectively.
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)
file.close()
win32ui.AddToRecentFileList(filename)
return 1
def OnSaveDocument(self, filename):
file = open(filename, "wb")
pickle.dump(self.strokes, file)
file.close()
self.SetModifiedFlag(0)
win32ui.AddToRecentFileList(filename)
return 1
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.
The 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:
� The View must hook the relevant mouse messages: in this case, the LBUTTONDOWN, LBUTTONUP, and MOUSEMOVE messages.
� When a LBUTTONDOWN message 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 MOUSEMOVE message 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 LBUTTONUP message 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:
class ScribbleView(pywin.mfc.docview.ScrollView):
def OnInitialUpdate(self):
self.SetScrollSizes(win32con.MM_TEXT, (0, 0))
self.HookMessage(self.OnLButtonDown,win32con.WM_LBUTTONDOWN)
self.HookMessage(self.OnLButtonUp,win32con.WM_LBUTTONUP)
self.HookMessage(self.OnMouseMove,win32con.WM_MOUSEMOVE)
self.bDrawing = 0
def OnLButtonDown(self, params):
assert not self.bDrawing, "Button down message while still drawing"
startPos = params[5]
# Convert the startpos to Client coordinates.
self.startPos = self.ScreenToClient(startPos)
self.lastPos = self.startPos
# Capture all future mouse movement.
self.SetCapture()
self.bDrawing = 1
def OnLButtonUp(self, params):
assert self.bDrawing, "Button up message, but not drawing!"
endPos = params[5]
endPos = self.ScreenToClient(endPos)
self.ReleaseCapture()
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:
return
pos = params[5]
dc = self.GetDC()
# Setup for an inverting draw operation.
dc.SetROP2(win32con.R2_NOT)
# "undraw" the old line
dc.MoveTo(self.startPos)
dc.LineTo(self.lastPos)
# Now draw the new position
self.lastPos = self.ScreenToClient(pos)
dc.MoveTo(self.startPos)
dc.LineTo(self.lastPos)
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():
dc.MoveTo(startPos)
dc.LineTo(endPos)
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 CApp.
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:
# scribbleApp.py
#
# The application object for Python.
from pywin.framework.app import CApp
class ScribbleApplication(CApp):
def InitInstance(self):
# All we need do is call the base class,
# then import our document template.
CApp.InitInstance(self)
import scribble2
# And create our application object.
ScribbleApplication()
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.
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:
� 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. 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.
The 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 wxPython.
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 *
class MyApp(wxApp):
def OnInit(self):
frame = wxFrame(NULL, -1, "Hello from wxPython")
frame.Show(true)
* 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. |
self.SetTopWindow(frame)
return true
app = MyApp(0)
app.MainLoop()
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 wxPython library with the from wxPython.wx import * statement. This is common practice for wxPython programs, but you can obviously perform more restrictive imports if you prefer.
Every 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 method. 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
class MyFrame(wxFrame):
def __init__(self, parent, ID, title):
wxFrame.__init__(self, parent, ID, title,
wxDefaultPosition, wxSize(200, 150))
self.CreateStatusBar()
self.SetStatusText("This is the statusbar")
menu = wxMenu()
menu.Append(ID_ABOUT, "&About",
"More information about this program")
menu.AppendSeparator()
menu.Append(ID_EXIT, "E&xit", "Terminate the program")
menuBar = wxMenuBar()
menuBar.Append(menu, "&File");
self.SetMenuBar(menuBar)
class MyApp(wxApp):
def OnInit(self):
frame = MyFrame(NULL, -1, "Hello from wxPython")
frame.Show(true)
self.SetTopWindow(frame)
return true
app = MyApp(0)
app.MainLoop()
Figure 20-7. A wxPython application with menus |
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
class MyFrame(wxFrame):
def __init__(self, parent, ID, title):
wxFrame.__init__(self, parent, ID, title,
wxDefaultPosition, wxSize(200, 150))
self.CreateStatusBar()
self.SetStatusText("This is the statusbar")
menu = wxMenu()
menu.Append(ID_ABOUT, "&About",
"More information about this program")
menu.AppendSeparator()
menu.Append(ID_EXIT, "E&xit", "Terminate the program")
menuBar = wxMenuBar()
menuBar.Append(menu, "&File");
self.SetMenuBar(menuBar)
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"
"message dialog.",
"About Me", wxOK | wxICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()
def TimeToQuit(self, event):
self.Close(true)
class MyApp(wxApp):
def OnInit(self):
frame = MyFrame(NULL, -1, "Hello from wxPython")
frame.Show(true)
self.SetTopWindow(frame)
return true
app = MyApp(0)
app.MainLoop()
Figure 20-8. The application with an About box |
The 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 self.OnAbout."
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.
|
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 OnInit method:
class DoubleTalkBrowserApp(wxApp):
def OnInit(self):
frame = MainFrame(NULL)
frame.Show(true)
self.SetTopWindow(frame)
return true
app = DoubleTalkBrowserApp(0)
app.MainLoop()
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:
class MainFrame(wxMDIParentFrame):
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)
self.SetIcon(self.icon)
# create a statusbar that shows the time and date on the right
sb = self.CreateStatusBar(2)
sb.SetStatusWidths([-1, 150])
self.timer = wxPyTimer(self.Notify)
self.timer.Start(1000)
self.Notify()
menu = self.MakeMenu(false)
self.SetMenuBar(menu)
menu.EnableTop(1, 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_SAVEAS,self.OnMenuSaveAs)
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)
EVT_CLOSE(self, self.OnCloseWindow)
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 wxMDIParentFrame as the base class of MainFrame. By using this class you automatically get everything needed to implement MDI for the appli-
cation 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 SetIcon method.
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 Notify method:
# Time-out handler
def Notify(self):
t = time.localtime(time.time())
st = time.strftime(" %d-%b-%Y %I:%M:%S", t)
self.SetStatusText(st, 1)
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 SetStatusText 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.AppendSeparator()
fmenu.Append(ID_EXIT, "E&xit", "Terminate the program")
dtmenu = wxMenu()
dtmenu.Append(ID_ADD, "&Add Transaction",
"Add a new transaction")
if withEdit:
dtmenu.Append(ID_EDIT, "&Edit Transaction",
"Edit selected transaction in current view")
dtmenu.Append(ID_JRNL, "&Journal view",
"Open or raise the journal view")
dtmenu.Append(ID_DTAIL, "&Detail view",
"Open or raise the detail view")
hmenu = wxMenu()
hmenu.Append(ID_ABOUT, "&About",
"More information about this program")
main = wxMenuBar()
main.Append(fmenu, "&File")
main.Append(dtmenu,"&Bookset")
main.Append(hmenu, "&Help")
return main
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):
self.Close()
def OnCloseWindow(self, event):
self.timer.Stop()
del self.timer
del self.icon
self.Destroy()
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)
dlg.ShowModal()
dlg.Destroy()
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 event.Veto(true).
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)
dlg.SetStyle(wxOPEN)
dlg.SetWildcard("*.dtj")
if dlg.ShowModal() == wxID_OK:
self.path = dlg.GetPath()
self.SetTitle(self.title + ' - ' + self.path)
self.bookset = BookSet()
self.bookset.load(self.path)
self.GetMenuBar().EnableTop(1, true)
win = JournalView(self, self.bookset, ID_EDIT)
self.views.append((win, ID_JRNL))
dlg.Destroy()
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 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_OK and 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 BookSet file.
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()
menu.EnableTop(1, true)
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:
class JournalView(wxMDIChildFrame):
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,
wxDefaultSize, wxLC_REPORT)
## Forces a resize event to get around a minor bug�
self.SetSize(self.GetSize())
self.lc.InsertColumn(0, "Date")
self.lc.InsertColumn(1, "Comment")
self.lc.InsertColumn(2, "Amount")
self.currentItem = 0
EVT_LIST_ITEM_SELECTED(self, tID, self.OnItemSelected)
EVT_LEFT_DCLICK(self.lc, self.OnDoubleClick)
menu = parent.MakeMenu(true)
self.SetMenuBar(menu)
EVT_MENU(self, editID, self.OnEdit)
EVT_CLOSE(self, self.OnCloseWindow)
self.UpdateView()
Figure 20-11 shows the application is progressing nicely and starting to look like a serious Windows application.
The 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
Figure 20-11. The list of Doubletalk transactions |
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):
self.OnEdit()
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 UpdateView method:
def UpdateView(self):
self.lc.DeleteAllItems()
for x in range(len(self.bookset)):
trans = self.bookset[x]
self.lc.InsertStringItem(x, trans.getDateString())
self.lc.SetStringItem(x, 1, trans.comment)
self.lc.SetStringItem(x, 2, str(trans.magnitude()))
self.lc.SetColumnWidth(0, wxLIST_AUTOSIZE)
self.lc.SetColumnWidth(1, wxLIST_AUTOSIZE)
self.lc.SetColumnWidth(2, wxLIST_AUTOSIZE)
self.SetTitle("Journal view - %d transactions" %
len(self.bookset))
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):
if self.currentItem:
trans = self.bookset[self.currentItem]
dlg = EditTransDlg(self, trans,
self.bookset.getAccountList())
if dlg.ShowModal() == wxID_OK:
trans = dlg.GetTrans()
self.bookset.edit(self.currentItem, trans)
self.parent.UpdateViews()
dlg.Destroy()
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 UpdateView() method.
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.
|
Sizers |
box = wxBoxSizer(wxHORIZONTAL) |
Resources |
Brute force |
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:
class EditTransDlg(wxDialog):
def__init__(self, parent, trans, accountList):
Figure 20-12. The wxPython Doubletalk transaction editor |
wxDialog.__init__(self, parent, -1, "")
self.item = -1
if trans:
self.trans = copy.deepcopy(trans)
self.SetTitle("Edit Transaction")
else:
self.trans = Transaction()
self.trans.setDateString(dates.ddmmmyyyy(self.trans.date))
self.SetTitle("Add 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_PNT and 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 wxPoint and 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.
Figure 20-13. The wxPython Doubletalk editor running on Redhat Linux 6.0 |
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),
wxLC_REPORT)
self.lc.InsertColumn(0, "Account")
self.lc.InsertColumn(1, "Amount")
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:
� The balance field is disabled, as you only want to use it to display a value.
� Use a wxStaticLine control for drawing the line across the dialog.
� A wxComboBox is used for selecting existing account names from a list.
� Use the standard IDs wxID_OK and wxID_CANCEL for 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_PNT(self, 190,100)
wxDLG_SZE(self, 40, -1)
self.balance.Enable(false)
wxStaticLine(self, -1, wxDLG_PNT(self, 5,115),
wxDLG_SZE(self, 225,-1))
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_PNT(self, 190,122),
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),
btnSz)
wxButton(self, ID_DEL, "&Delete Line", wxDLG_PNT(self, 142,140),
btnSz)
self.ok = wxButton(self, wxID_OK, "OK", wxDLG_PNT(self, 145,5),
btnSz)
self.ok.SetDefault()
wxButton(self, wxID_CANCEL, "Cancel", wxDLG_PNT(self, 190,5), btnSz
# Resize the window to fit the controls
self.Fit()
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
self.date.SetValue(self.trans.getDateString())
self.comment.SetValue(self.trans.comment)
for x in range(len(self.trans.lines)):
account, amount, dict = self.trans.lines[x]
self.1c.InsertStringItem(x, account)
self.1c.SetStringItem(x, 1, str(amount))
self.Validate()
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 Validate:
def Validate(self, *ignore):
bal = self.trans.balance()
self.balance.SetValue(str(bal))
date = self.date.GetValue()
try:
dateOK = (date == dates.testasc(date))
except:
dateOK = 0
if bal == 0 and dateOK:
self.ok.Enable(true)
else:
self.ok.Enable(false)
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())
self.trans.addLine(account, amount)
# update the list control
idx = len(self.trans.lines)
self.lc.InsertStringItem(idx-1, account)
self.lc.SetStringItem(idx-1, str(amount))
self.Validate()
self.account.SetValue("")
self.amount.SetValue("")
You call 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/.