Python DevCenter
oreilly.comSafari Books Online.Conferences.


Python Programming on Win32
Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

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.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)
        return 1
    def OnSaveDocument(self, filename):
        file = open(filename, "wb")
        pickle.dump(self.strokes, file)
        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:

  • 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.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.bDrawing = 1
    def OnLButtonUp(self, params):
        assert self.bDrawing, "Button up message, but not drawing!"
        endPos = params[5]
        endPos = self.ScreenToClient(endPos)
        self.bDrawing = 0
        # And add the stroke to the document.
        self.GetDocument().AddStroke( self.startPos, endPos, self )
    def OnMouseMove(self, params):
        # If Im not drawing at the moment, I don't care
        if not self.bDrawing:
        pos = params[5]
        dc = self.GetDC()
        # Setup for an inverting draw operation.
        # "undraw" the old line
        # Now draw the new position
        self.lastPos = self.ScreenToClient(pos)

Most of this code should be quite obvious. It's worth mentioning that you tell Windows to draw the line using a NOT mode. This mode is handy; if you draw the same line twice, the second draw erases the first. Thus, to erase a line you drew previously, all you need is to draw the same line again.


Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

Next Pagearrow

Sponsored by: