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:
def MatchDocType(self, fileName, fileType):
doc = self.FindOpenDocument(fileName)
if doc: return doc
ext = string.lower(os.path.splitext(fileName))
if ext =='.psd':
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:
def AddStroke(self, start, end, fromView):
self.UpdateAllViews( fromView, None )
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
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)
def OnSaveDocument(self, filename):
file = open(filename, "wb")
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
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:
Viewmust hook the relevant mouse messages: in this case, the
- 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:
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
# 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
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
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.