ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: Python Programming on Win32

Python Programming on Win32 using WxPython

Related Reading

Python Programming On Win32
Help for Windows Programmers
By Mark Hammond, Andy Robinson

by Mark Hammond

An excerpt from Chapter 20 of O'Reilly's book Python Programming on Win32.

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

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.

wxWindows + Python = wxPython

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.

More from this Chapter:

Using Tkinter

Using PythonWin

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.

 

Where to get 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/.

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 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.

Using wxPython

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[1] and get ready to play along as you read the next few sections.

A simple example

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)
        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.

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. 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.

Table 20-4: Common wxPython Event Functions

Event Function

Event Description

EVT_SIZE

Sent to a window when its size has changed, either interactively by the user or programmatically.

EVT_MOVE

Sent to a window when it has been moved, either interactively by the user or programmatically.

EVT_CLOSE

Sent to a frame when it has been requested to close. Unless the close is being forced, it can be canceled by calling event.Veto(true).

EVT_PAINT

This event is sent whenever a portion of the window needs to be redrawn.

EVT_CHAR

Sent for each nonmodifier (Shift key, etc.) keystroke when the window has the focus.

EVT_IDLE

This event is sent periodically when the system isn't processing other events.

EVT_LEFT_DOWN

The left mouse button has been pressed down.

EVT_LEFT_UP

The left mouse button has been let up.

EVT_LEFT_DCLICK

The left mouse button has been double-clicked.

EVT_MOTION

The mouse is in motion.

EVT_SCROLL

A scrollbar has been manipulated. This one is actually a collection of events, which can be captured individually if desired.

EVT_BUTTON

A button has been clicked.

EVT_MENU

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 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.

MDI Frame

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 application without having to worry about what's really happening behind the scenes. The wxMDIParentFrame class has the same interface as the wxFrame class, with only a few additional methods. Often changing a single document interface program to a MDI program is as easy as changing the base classes the application's classes are derived from. There is a corresponding wxMDIChildFrame to be used for the document windows, as we'll see later. If you ever need to have access to the client area (or the background area) of the MDI parent, you can use the wxMDIClientWindow class. You might use this for placing a background image behind all the child windows.

Icons

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.

Timers

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 SetStatus-Text method, you can put that string into the status bar, in this case in slot 1.

Main menu

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.

wxFileDialog

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.

wxListCtrl

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.

Figure 20-11. The list of Doubletalk transactions

 

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 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.

wxPython Window Layout

wxPython includes 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.

Constraints
There is a class called wxLayoutConstraints that allows the specification of a window's position and size in relationship to its siblings and its parent. Each wxLayoutContraints object is composed of eight wxIndividualLayoutConstraint objects, 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.height.AsIs ()
lc.width.PercentOf (self.panelA, wxWidth, 50)
b.SetConstraints(lc);

Layout algorithm
The class named wxLayoutAlgorithm implements layout of subwindows in MDI or SDI frames. It sends a wxCalculateLayoutEvent to 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 wxSashLayoutWindow for 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. wxLayoutAlgorithm is 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.

Sizers
In an effort to simplify the programming of simple layouts, a family of wxSizer classes has been added to the wxPython library. 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)

Resources
The wxWindows library 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:

class EditTransDlg(wxDialog):
    def _    _init_    _(self, parent, trans, accountList):
        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:

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.lc.InsertStringItem(x, account)
        self.lc.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, 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.

wxPython Conclusion

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/.


1. When getting started, you should probably avoid using PythonWin or IDLE for running wxPython programs, because the interactions between the various toolkits may have unexpected consequences.

Mark Hammond is an independent Microsoft Windows consultant working out of Melbourne, Australia.

Andy Robinson is a London-based consultant specializing in business analysis, object-oriented design, and Windows development.


Discuss this article in the O'Reilly Network Python Forum.

Return to the Python DevCenter.

Copyright © 2009 O'Reilly Media, Inc.