Python Programming on Win32 using PythonWin
Pages: 1, 2, 3, 4
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 docext = string.lower(os.path.splitext(fileName)[1])if ext =='.psd':return win32ui.CDocTemplate_Confidence_yesAttemptNativereturn 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 1def 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:
- The
Viewmust hook the relevant mouse messages: in this case, theLBUTTONDOWN,LBUTTONUP, andMOUSEMOVEmessages. - When a
LBUTTONDOWNmessage is received, remember the start position and enter a drawing mode. Also capture the mouse, to ensure that you get all future mouse messages, even when the mouse leaves the window. - If a
MOUSEMOVEmessage occurs when you are in drawing mode, draw a line from the remembered start position to the current mouse position. In addition, erase the previous line drawn by this process. This gives a "rubber band" effect as you move the mouse. - When a
LBUTTONUPmessage is received, notify the document of the new, completed stroke, release the mouse capture, and leave drawing mode.
After adding this logic to the sample, it now looks like:
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 = 0def 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 = 1def 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 careif not self.bDrawing:returnpos = params[5]dc = self.GetDC()# Setup for an inverting draw operation.dc.SetROP2(win32con.R2_NOT)# "undraw" the old linedc.MoveTo(self.startPos)dc.LineTo(self.lastPos)# Now draw the new positionself.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.




