The Doc/View model permits a greater degree of flexibility in how you present data than does a model that links data encapsulation and user interface into a single class. Using the Doc/View model, you can define a document class to contain any type of data, such as a simple text file, a database file, or in this tutorial, a line drawing. You can then create a number of different view classes, each one of which displays the same data in a different manner or lets the user interact with that data in a different way.
For Step 12, however, you'll simply convert the application from its current model to the Doc/View model. Step 12 uses the SDI model so that you can more easily see the changes necessary for converting to Doc/View without being distracted by the extra code added in Step 11 to support MDI functionality. (You'll create an MDI Doc/View application in Step 13.) But even though the code for Step 12 will look very different from the code from Step 10, the running application for Step 12 will look nearly identical to that of Step 10. You can find the source for Step 12 in the files STEP12.CPP, STEP12.RC, STEP12DV.CPP, and STEP12DV.RC in the directory EXAMPLES\OWL\TUTORIAL.
TDrawDocument uses the functionality contained in TFileDocument to access line data stored in a file. It uses a TLines array to contain the lines, the same as in earlier steps. The array is referenced through a pointer called Lines.
The destructor for TDrawDocument deletes the TLines object pointed to by Lines.
The TPoints and TLines arrays, their iterators, and the TLine class are now defined in the STEP12DV.CPP file. In the Doc/View model, these classes are an integral part of the document class you're about to build. The code for these classes doesn't change at all from Step 10.
Here's the signature of the Open function:
bool Open(int mode, const char far* path=0);where:
bool TDrawDocument::Open(int /*mode*/, const char far* path) { Lines = new TLines(5, 0, 5); if (path) SetDocPath(path); if (GetDocPath()) { TInStream* is = InStream(ofRead); if (!is) return false; unsigned numLines; char fileinfo[100]; *is >> numLines; is->getline(fileinfo, sizeof(fileinfo)); while (numLines-) { TLine line; *is >> line; Lines->Add(line); } delete is; } SetDirty(false); NotifyViews(vnRevert, false); return true; }Closing the drawing is less complicated. The Close function discards the document's data and cleans up. In this case, it deletes the TLines array referenced by the Lines data member and returns true. Here's how the code for your Close function should look:
bool TDrawDocument::Close() { delete Lines; Lines = 0; return true; } Lines is set to 0, both in the constructor and after closing the document, so that you can easily tell whether the document is open. If the document is open, Lines points to a TLines array, and is therefore not 0. But setting Lines to 0 makes it easy to check whether the document is open. The IsOpen function lets you check this from outside the document object:
bool IsOpen() { return Lines != 0; }
Commit checks to see if the document is dirty. If not, and if the force parameter is false, Commit returns true, indicating that the operation was successful.
If the document is dirty, or if the force parameter is true, Commit saves the data. The procedure to save the data is similar to the SaveFile function in previous steps, but, as with the Open function, there are a few differences.
Commit calls the OutStream function to open an output stream. This function is defined in TFileDocument and returns a TOutStream *. Commit then writes the data to the output stream. The procedure for this is almost exactly identical to that used in the old SaveFile function.
After writing the data to the output stream, Commit turns the IsDirty flag off by calling SetDirty with a false parameter. It then returns true, indicating that the operation was successful.
Here's how the code for your Commit function might look:
bool TDrawDocument::Commit(bool force) { if (!IsDirty() && !force) return true; TOutStream* os = OutStream(ofWrite); if (!os) return false; // Write the number of lines in the figure *os << Lines->GetItemsInContainer(); // Append a description using a resource string *os << ' ' << string(*GetDocManager().GetApplication(),IDS_FILEINFO) << 'There's only one thing in the Commit function that you haven't seen before:
// Get an iterator for the array of lines TLinesIterator i(*Lines); // While the iterator is valid (i.e. you haven't run out of lines) while (i) { // Copy the current line from the iterator and increment the array. *os << i++; } delete os; SetDirty(false); return true; }
// Append a description using a resource string *os << ' ' << string(*GetDocManager().GetApplication(), IDS_FILEINFO) << 'This uses a special constructor for the ANSI string class:
string(HINSTANCE instance, uint id, int len = 255);This constructor lets you get a string resource from any Windows application. You specify the application by passing an HINSTANCE as the first parameter of the string constructor. In this case, you can get the current application's instance through the document manager. The GetDocManager function returns a pointer to the document's document manager. In turn, the GetApplication function returns a pointer to the application that contains the document manager. This is converted implicitly into an HINSTANCE by a conversion operator in the TModule class. The second parameter of the string constructor is the resource identifier of a string defined in STEP12DV.RC. This string contains version information that can be used to identify the application that created the document.
The Revert function takes a single parameter, a bool indicating whether the document's views need to refresh their display from the document's data. Revert calls the TFileDocument version of the Revert function, which in turn calls the TDocument version of Revert. The base class function calls the NotifyViews function with the vnRevert event. The second parameter of the NotifyViews function is set to the parameter passed to the TDrawDocument::Revert function. TFileDocument::Revert sets IsDirty to false and returns. If TFileDocument::Revert returns false, the TDrawDocument should also return false.
If TFileDocument::Revert returns true, the TDrawDocument function should check the parameter passed to Revert. If it is false (that is, if the view needs to be refreshed), Revert calls the Open function to open the document file, reload the data, and display it.
Here's how the code for your Revert function might look:
bool TDrawDocument::Revert(bool clear) { if (!TFileDocument::Revert(clear)) return false; if (!clear) Open(0); return true; }
The AddLine function adds a new line to the document's TLines array. The line is passed to the AddLines function as a TLine &. After adding the line to the array, AddLine sets the IsDirty flag to true by calling SetDirty. It then returns the index number of the line it just added. Here's how the code for your AddLines function might look:
int TDrawDocument::AddLine(TLine& line) { int index = Lines->GetItemsInContainer(); Lines->Add(line); SetDirty(true); return index; }The GetLine function takes an int parameter. This int is the index of the desired line. GetLine should first check to see if the document is open. If not, it can try to open the document. If the document isn't open and GetLine can't open it, it returns 0, meaning that it couldn't find a valid document from which to get the line.
Once you know the document is valid, you should also check to make sure that the index isn't too high. Compare the index to the return value from the GetItemsInContainer function. As long as the index is less, you can return a pointer to the TLine object. Here's how the code for your GetLine function might look:
TLine* TDrawDocument::GetLine(int index) { if (!IsOpen() && !Open(ofRead | ofWrite)) return 0; return index < Lines->GetItemsInContainer() ? &(*Lines)[index] : 0; }
TWindowView takes the latter approach, mixing TWindow and TView to provide a single class with both basic windowing and viewing capabilities. By deriving from this general-purpose class, TDrawView needs to add only the functionality required to work with the TDrawDocument class.
The TDrawView is similar to the TDrawWindow class used in previous steps. In fact, you'll see that a lot of the functions from TDrawWindow are brought directly to TDrawView with little or no modifications.
TDC *DragDC; TPen *Pen; TLine *Line; TDragDocument *DrawDoc;Three of these should look familiar to you. DragDC, Pen, and Line perform the same function in TDrawView as they did in TDrawWindow.
Although a document can exist with no associated views, the opposite isn't true. A view must be associated with an existing document. TDrawView is attached to its document when it is constructed. It keeps track of its document through a TDrawDocument * called DrawDoc. The base class TView has a TDocument * member called Doc that serves the same basic purpose. In fact, during base class construction, Doc is set to point at the TDrawDocument object passed to the TDrawView constructor. DrawDoc is added to force proper type compliance when the document pointer is accessed.
The constructor also sets DragDC to 0 and initializes Line with a new TLine object.
The last thing the constructor does is set up the view's menu. You can use the TMenuDescr class to set up a menu descriptor from a menu resource. Here's the TMenuDescr constructor:
TMenuDescr(TResId id);where id is the resource identifier of the menu resource.
The TMenuDescr constructor takes the menu resource and divides it up into six groups. It determines which group a particular menu in the resource goes into by the presence of separators in the menu resource. The only separators that actually divide the resource into groups are at the pop-up level; that is, the separators aren't contained in a menu, but they're at the level of menu items that appear on the menu bar. For example, the following code shows a small snippet of a menu resource:
COMMANDS MENU { // Always starts with the File group POPUP "&File" { MENUITEM "&Open", CM_FILEOPEN MENUITEM "&Save", CM_FILESAVE } MENUITEM SEPARATOR // Edit group MENUITEM SEPARATOR // Container group MENUITEM SEPARATOR // This one is in the Object group POPUP "&Objects" { MENUITEM "&Copy object", CM_OBJECTCOPY MENUITEM "Cu&t object", CM_OBJECTCUT } // No more items, meaning the Window group and Help group are also empty }A menu descriptor would separate this resource into groups like this: the File menu would be placed in the first group, called the File group. The second group (Edit group) and the third group (Container group) are empty, because there' s no pop-up menus between the separators that delimit those groups. The Tools menu is in the Object group. Because there are no menu resources after the Tools menu, the last two groups, the Object group and Help group, are also empty.
Although the groups have particular names, these names just represent a common name for the menu group. The menu represented by each group does not necessarily have that name. The document manager provides a default File menu, but the other menu names can be set in the menu resource.
In this case, the view supplies a menu resource called IDM_DRAWVIEW, which is contained in the file STEP12DV.RC. This menu is called Tools, which has the same choices on it as the Tools menu in earlier steps: Pen Size and Pen Color. To insert the Tools menu as the second menu on the menu bar when the view is created or activated, the menu resource is set up to place the Tools menu in the second group, the Edit group, so that the menu resource looks something like this:
IDM_DRAWVIEW MENU { // Edit Group MENUITEM SEPARATOR POPUP "&Tools" { MENUITEM "Pen &Size", CM_PENSIZE MENUITEM "Pen &Color", CM_PENCOLOR } }You can install the menu descriptor as the view menu using the TView function SetViewMenu function, which takes a single parameter, a TMenuDescr *. SetViewMenu sets the menu descriptor as the view's menu. When the view is created, this menu is merged with the application menu.
Here's how the call to set up the view menu should look:
SetViewMenu(new TMenuDescr(IDM_DRAWVIEW));The destructor for the view deletes the device context referenced by DragDC and the TLine object referenced by Line.
static const char far* StaticName() {return "Draw View";}
The GetPenSize function is identical to the TDrawWindow function GetPenSize. This function opens a TInputDialog, gets a new pen size from the user, and changes the pen size for the window and calls the SetPen function of the current line.
The Paint function is a little different from the Paint function in the TDrawWindow class, but it does basically the same thing. Instead of using an iterator to go through the lines in an array, TDrawView::Paint calls the GetLine function of the view's associated document. The return from GetLine is assigned to a const TLine * called line. If line is not 0 (that is, if GetLine returned a valid line), Paint then calls the line's Draw function. Remember that the TLine class is unchanged from Step 10. The line draws itself in the window.
Here's how the code for the Paint function might look:
void TDrawView::Paint(TDC& dc, bool, TRect&) { // Iterates through the array of line objects. int i = 0; const TLine* line; while ((line = DrawDoc->GetLine(i++)) != 0) line->Draw(dc); }
In addition, TDrawView handles two new messages: VN_COMMIT and VN_REVERT. These view notification messages are sent by the view's document when the document's Commit and Revert functions are called.
Here's the response table definition for TDrawView:
DEFINE_RESPONSE_TABLE1(TDrawView, TWindowView) EV_WM_LBUTTONDOWN, EV_WM_RBUTTONDOWN, EV_WM_MOUSEMOVE, EV_WM_LBUTTONUP, EV_COMMAND(CM_PENSIZE, CmPenSize), EV_COMMAND(CM_PENCOLOR, CmPenColor), EV_VN_COMMIT, EV_VN_REVERT, END_RESPONSE_TABLE;The following functions are nearly the same in TDrawView as the corresponding functions in TDrawWindow. Any modifications to the functions are noted in the right column of the table:
The VnCommit function always returns true. In a more complex application, this function would add any cached data to the document, but in this application, the data is added to the document as each line is drawn.
Defining document templates
Once you've created a document class and an accompanying view class, you have to associate them so they can function together. An association between a document class and a view class is known as a document template class. The document template class is used by the document manager to determine what view class should be opened to display a document.
DEFINE_DOC_TEMPLATE_CLASS(TDrawDocument, TDrawView, DrawTemplate);Once you've created a document template class, you need to create a document registration table. Document registration tables contain information about a particular Doc/View template class instance, such as what the template class does, the default file extension, and so on. A document registration table is actually an object of type TRegList, although you don't have to worry about what the object actually looks; you'll very rarely need to directly access a document registration table object.
Start creating a document registration table by declaring the BEGIN_REGISTRATION macro. This macro takes a single parameter, the name of the document registration class, which is used as the name of the TRegList object.
The next lines in your document registration table create entries in the document registration table. For a Doc/View template, you need to enter four items into this table:
REGDATA(key, value)key indicates what the value string pertains to. There are three different keys you need for creating a document registration table:
BEGIN_REGISTRATION(DrawReg) REGDATA(description, "Point Files (*.PTS)") REGDATA(extension, ".PTS") REGDATA(docfilter, "*.pts") REGDOCFLAGS(dtAutoDelete | dtHidden) END_REGISTRATIONOnce you've created a document registration table, all you need to do is create an instance of the class. The class type is the name of the document template class. You also should give the instance a meaningful name. The constructor for any document template class looks like this:
TplName name(TRegList& reglist);where:
DrawTemplate drawTpl(DrawReg);
GetMainWindow()->SetMenuDescr(TMenuDescr("COMMANDS"));
SetDocManager(new TDocManager(dmSDI | dmMenu));
After the base class InitInstance function has been called, you need to call the main window's DragAcceptFiles function, specifying the true parameter. This enables the main window to accept files that are dropped in the window. Drag and drop functionality is handled through the application's response table, as discussed in the next section.
To enable the user to begin drawing in the window as soon as the application starts up, you also need to call the CmFileNew function of the document manager. This creates a new untitled document and view in the main window.
The InitInstance function should look something like this:
void TDrawApp::InitInstance() { TApplication::InitInstance(); GetMainWindow()->DragAcceptFiles(true); GetDocManager()->CmFileNew(); }
Here's the new declaration of the TDrawApp class, along with its response table definition:
class TDrawApp : public TApplication { public: TDrawApp() : TApplication() {} protected: // Override methods of TApplication void InitInstance(); void InitMainWindow(); // Event handlers void EvNewView (TView& view); void EvCloseView(TView& view); void EvDropFiles(TDropInfo dropInfo); void CmAbout(); DECLARE_RESPONSE_TABLE(TDrawApp); }; DEFINE_RESPONSE_TABLE1(TDrawApp, TApplication) EV_OWLVIEW(dnCreate, EvNewView), EV_OWLVIEW(dnClose, EvCloseView), EV_WM_DROPFILES, EV_COMMAND(CM_ABOUT, CmAbout), END_RESPONSE_TABLE;
void TDrawApp::CmAbout() { TDialog(GetMainWindow(), IDD_ABOUT).Execute(); }
Because this is a SDI application, if the number of files is greater than one, you need to warn the user that only one file can be dropped into the application at a time. To find the number of files dropped in, you can call the TDropInfo function DragQueryFileCount, which takes no parameters and returns the number of files dropped. If the file count is greater than one, pop up a message box to warn the user.
Now you need to get the name of the file dropped in. You can find the length of the file path string using the TDropInfo function DragQueryFileNameLen, which takes a single parameter, the index of the file about which you're inquiring. Because you know there's only one file, this parameter should be a 0. This function returns the length of the file path.
Allocate a string of the necessary length, then call the TDropInfo function DragQueryFile. This function takes three parameters. The first is the index of the file. Again, this parameter should be a 0. The second parameter is a char *, the file path. The third parameter is the length of the file path. This function fills in the file path in the char array from the second parameter.
Once you've got the file name, you need to get the proper template for the file type. To do this, call the document manager's MatchTemplate function. This function searches the document manager's list of document templates and returns a pointer to the first document template with a pattern that matches the dropped file. This pointer is a TDocTemplate *. If the document manager can't find a matching template, it returns 0.
Once you've located a template, you can call the template's CreateDoc function with the file path as the parameter to the function. This creates a new document and its corresponding view, and opens the file into the document.
Once the file has been opened, you must make sure to call the DragFinish function. This function releases the memory that Windows allocates during drag and drop operations.
Here's how the EvDropFiles function should look:
void TDrawApp::EvDropFiles(TDropInfo dropInfo) { if (dropInfo.DragQueryFileCount() != 1) ::MessageBox(0,"Can only drop 1 file in SDI mode","Drag/Drop Error",MB_OK); else { int fileLength = dropInfo.DragQueryFileNameLen(0)+1; char* filePath = new char [fileLength]; dropInfo.DragQueryFile(0, filePath, fileLength); TDocTemplate* tpl = GetDocManager()->MatchTemplate(filePath); if (tpl) tpl->CreateDoc(filePath); delete filePath; } dropInfo.DragFinish(); }
In this case, you need to set the view's window as the client of the main window. There are two functions you need to call to do this: GetWindow and SetClientWindow.
The GetWindow function is member of the view class. It takes no parameters and returns a TWindow *. This points to the view's window.
Once you have a pointer to the view's window, you can set that window as the client window with the main window's SetClientWindow function, which takes a single parameter, a TWindow *, and sets that window object as the client window. This function returns a TWindow *. This return value is a pointer to the old client window, if there was one.
Before continuing, you should check that the new client window was successfully created. TView provides the IsOK function, which returns false if the window wasn't created successfully. If IsOK returns false, you should call SetClientWindow again, passing a 0 as the window pointer, and return from the function.
If the window was created successfully, you need to check the view's menu with the GetViewMenu function. If the view has a menu, use the MergeMenu function of the main window to merge the view's menu with the window's menu.
The code for EvNewView should look like this:
void TDrawApp::EvNewView(TView& view) { GetMainWindow()->SetClientWindow(view.GetWindow()); if (!view.IsOK()) GetMainWindow()->SetClientWindow(0); else if (view.GetViewMenu()) GetMainWindow()->MergeMenu(*view.GetViewMenu()); }
To close a view, you need to remove the view's window as the client of the main window. To do this, call the main window's SetClientWindow function, passing a 0 as the window pointer. You can then restore the menu of the frame window to its former state using the RestoreMenu function of the main window.
When the EvNewView function creates a new view, the caption of the frame window is set to the file path of the document. You need to reset the main window's caption using the SetCaption function.
Here's the code for the EvCloseView function:
void TDrawApp::EvCloseView(TView& /*view*/) { GetMainWindow()>SetClientWindow(0); GetMainWindow()->RestoreMenu(); GetMainWindow()->SetCaption("Drawing Pad"); }