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 PythonWin

Related Reading

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

by Mark Hammond

As mentioned in Chapter 4, Integrated Development Environments for Python, PythonWin is a framework that exposes much of the Microsoft Foundation Classes (MFC) to Python. MFC is a C++ framework that provides an object-based model of the Windows GUI API, as well as a number of services useful to applications.

The term PythonWin is a bit of a misnomer. PythonWin is really an application written to make use of the extensions that expose MFC to Python. This means PythonWin actually consists of two components:

We focus primarily on the MFC functionality exposed to Python so we can build a fully functional GUI application.

As PythonWin mirrors MFC, it's important to understand key MFC concepts to understand how PythonWin hangs together. Although we don't have room for a complete analysis of MFC, an introduction to its concepts is in order.

Introduction to MFC

The Microsoft Foundation Classes are a framework for developing complete applications in C++. MFC provides two primary functions:

The object-oriented wrapping is straightforward. Many Windows API functions take a "handle" as their first parameter; for example, the function SendMessage() takes a handle to a window (an 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.

The PythonWin Object Model

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.

More from this Chapter:

Using Tkinter

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 underlying MFC C++ object.

Let's see what this means in practice. We will examine a few of these objects from the PythonWin interactive window and create a dialog object using one of the standard PythonWin dialogs:

>>> import win32ui
>>> from pywin.mfc.dialog import Dialog
>>> 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 from Python."

 

Developing a PythonWin Sample Application

During the rest of this section, we will develop a sample application using PythonWin. This will lead us through many of the important MFC and PythonWin concepts, while also leveraging the dynamic nature of PythonWin.

MFC itself has a tutorial/sample called Scribble, which delivers a basic drawing application. We will develop a version of Scribble written in Python.

We will make use of some of the features of PythonWin to demonstrate how rapidly you can create such an application. Specifically, we will develop the Scribble framework first to run under the existing PythonWin framework, then make changes to it so it can run standalone. This is in contrast to the traditional technique of developing MFC applications, where the application object is often one of the first entities defined. A key benefit in using the PythonWin application object is that you get the full benefits of the PythonWin IDE, including error handling and reporting in the interactive window. This makes development much easier before we finally plug in our custom application object.

The general design of the Scribble application is simple. Define the document object to keep a list of strokes. A stroke is the start and end coordinates of a line. The document object also can load and store this list of strokes to a file. A view object is also defined that can render these strokes onto a Window.

The first step in the sample is to provide a placeholder for the document template, document, and view objects. Once this skeleton is working, we fill out these objects with a useful implementation.

Defining a Simple Framework

Our first step is to develop a simple framework with placeholders for the major objects.

We define three objects: a 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().

TIP:  

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:

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.

 

Enhancing the DocumentTemplate

Although MFC and PythonWin support multiple document templates, there's a slight complication that isn't immediately obvious. When MFC is asked to open a document file, it asks each registered 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.

Enhancing the Document

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.

Defining the View

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:

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.

Creating the Application Object

The simplest way to create an application object for Scribble is to subclass one of the standard application objects. The PythonWin application object is implemented in pywin.framework.intpyapp, and it derives from the 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.

PythonWin and Resources

As we've discussed, MFC provides a framework architecture, and much of this architecture is tied together by resource IDs, integers that identify Windows resources in a DLL or executable.

For example, when you define a DocumentTemplate, you specify a resource ID. The previous example doesn't specify a resource ID, so the default of win32ui.IDR_PYTHONTYPE is used. When a document is created, MFC uses the resource ID in the following ways:

Another example of the reliance on resource IDs is in the processing and definition of menu and toolbar items. Each command in the application is assigned an ID. When you define menu or toolbar items, you specify the menu text (or toolbar bitmap) and the command ID. When MFC displays a menu item, it uses a string defined with the same ID and places this text automatically in the application's status bar. When the mouse hovers over a toolbar item, MFC again looks for a string with the specified ID and uses it for the tooltip-text for the button. This architecture has a number of advantages:

However, it also has a number of disadvantages:

PythonWin can use resources from arbitrary DLLs, thus you can define your own DLL containing only resources. PythonWin makes it easy to use these resources; just load the DLL (using win32ui.LoadLibrary()), and PythonWin locates and uses the resources in this DLL.

If you are writing a large application, you'll probably find it worthwhile to define your own resource DLL when using PythonWin. The benefits offered by the framework make it worth the pain of initially setting everything up. On the other hand, it does make PythonWin somewhat cumbersome for defining these applications purely from Python code.

PythonWin Conclusion

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

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.