[Contents] [Prev] [Next]

Step 12: Using the Doc/View programming model

Step 12 introduces the Doc/View model of programming, which is based on the principle of separating data from the interface for that data. Essentially, the data is encapsulated in a document object, which is derived from the TDocument class, and displayed on the screen and manipulated by the user through a view object, which is derived from the TView class.

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.

Organizing the application source

The source for Step 12 is divided into four source files:

You should divide your Doc/View code this way to distinguish the document and its supporting view from the application code. The application code provides the support framework for the document and view classes, but doesn't contribute directly to the functionality of the Doc/View model. This also demonstrates good design practice for code reusability.

Doc/View model

The Doc/View model is based on three ObjectWindows classes:

The TDocument and TView classes provide the abstract functionality for document and view objects. You must provide the specific functionality for your own document and view classes. You must also explicitly create the document manager and attach it to the application object. You must also provide the document templates for the document manager. These steps are described in the following sections.

TDrawDocument class

The TDrawDocument class is derived from the ObjectWindows class TFileDocument, which is in turn derived from the TDocument class. TDocument provides a number of input and output functions. These virtual functions return dummy values and have no real functionality. TFileDocument provides the basic functionality required to access a data file in the form of a stream.

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.

Creating and destroying TDrawDocument

TDrawDocument's constructor takes a single parameter, a TDocument *, that is a pointer to the parent document. A document can be a parent of a number of other documents, treating the data contained in those documents as if it were part of the parent. The constructor passes the parent pointer on to TFileDocument. The constructor also initializes the Lines data member to 0.

The destructor for TDrawDocument deletes the TLines object pointed to by Lines.

Storing line data

The document class you're going to create controls access to the data contained in a drawing. But you still need some way to store the data. You've already created the TLine class and the TLines array in previous steps. Luckily, this code can be recycled. The line data for each document is stored in a TLines array, and accessed by the document through a protected TLines * data member called 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.

Implementing TDocument virtual functions

TDrawDocument needs to implement a few of the virtual functions inherited from TDocument. These functions provide streaming and the ability to commit changes to the document or to discard all changes made to the document since the last save.

Opening and closing a drawing

Although TFileDocument provides the basic functionality required for stream input and output, it doesn't know how to read the data for a line. To provide this ability, you need to override the Open and Close functions.

Here's the signature of the Open function:

bool Open(int mode, const char far* path=0);

The Open function is similar to the OpenFile function used in earlier steps in the tutorial. There are differences, though:

Here's how the code for your Open function might look:

TDrawDocument::Open(int /*mode*/, const char far* path)
  Lines = new TLines(5, 0, 5);
  if (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;
    delete is;
  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; }

Saving and discarding changes

TDocument provides two functions for saving and discarding changes to a document:

For TDrawDocument, the document is updated as each line is drawn in the view window. The only function of Commit for the TDrawDocument class is to save the data to a file.

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:

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) << '
// 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; }
There's only one thing in the Commit function that you haven't seen before:

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

TDrawDocument::Revert(bool clear)
  if (!TFileDocument::Revert(clear))
    return false;
  if (!clear)
  return true;

Accessing the document's data

There are two main ways to access data in TDrawDocument: adding a line (such as a new line when the user draws in a view) and getting a reference to a line in the document (such as getting a reference to each line when repainting the window). You can add two functions, AddLine and GetLine, to take care of each of these actions.

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:

TDrawDocument::AddLine(TLine& line)
  int index = Lines->GetItemsInContainer();
  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; }

TDrawView class

The TDrawView class is derived from the ObjectWindows TWindowView class, which is in turn derived from the TView and TWindow classes. TView doesn't have any inherent windowing capabilities; a TView-derived class gets these capabilities by either adding a window member or pointer or by mixing in a window class with a view class.

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.

TDrawView data members

The TDrawView class has a number of protected data members.

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.

Creating the TDrawView class

The TDrawView constructor takes two parameters, a TDrawDocument & (a reference to the view's associated document) and a TWindow * (a pointer to the parent window). The parent window defaults to 0 if no value is supplied. The constructor passes its two parameters to the TWindowView constructor, and initializes the DrawDoc member to point at the document passed as the first parameter.

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:

  // Always starts with the File group
  POPUP "&File"
    // Edit group
    // Container group
  // 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:

  // Edit Group
  POPUP "&Tools"
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.

Naming the class

Every view class should define the function StaticName, which takes no parameters and returns a static const char far *. This function should return the name of the view class. Here's how the StaticName function might look:

static const char far* StaticName() {return "Draw View";}

Protected functions

TDrawView has a couple of protected access functions to provide functionality for the class.

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:

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)

Event handling in TDrawView

The TDrawView class handles many of the events that were previously handled by the TDrawWindow class. Most of the other events that TDrawWindow handled that aren't handled by TDrawView are handled by the application object and the document manager; this is discussed later in Step 12.

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:

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:
Function TDrawView version

EvLButtonDown Does not set IsDirty. This is taken care of in EvLButtonUp.

EvRButtonDown No change.

EvMouseMove No change.

EvLButtonUp Checks to see if the mouse was moved after the left button press. If so, calls the document's AddLine function to add the point.

CmPenSize No change.

CmPenColor No change.

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.

The VnRevert function invalidates the display area, clearing it and repainting the drawing in the window. It then returns true.

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.

You can create a document template class using the macro DEFINE_DOC_TEMPLATE_CLASS, which takes three parameters. The first parameter is the name of the document class, the second is the name of the view class, and the third is the name of the document template class. The macro to create a template class for the TDrawDocument and TDrawView classes would look like this:

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:

For the first three of these, you specify them using the REGDATA macro:

REGDATA(key, value)
key indicates what the value string pertains to. There are three different keys you need for creating a document registration table:

A typical document registration table looks something like this:

  REGDATA(description, "Point Files (*.PTS)")
  REGDATA(extension, ".PTS")
  REGDATA(docfilter, "*.pts")
  REGDOCFLAGS(dtAutoDelete | dtHidden)
Once 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);

Here's how the template instance for TDrawDocument and TDrawView classes might look:

DrawTemplate drawTpl(DrawReg);

Supporting Doc/View in the application

STEP12.CPP contains the code for the application object and the definition of the main window. The application object provides a framework for the Doc/View classes defined in STEP12DV.CPP. This section discusses the changes to the TDrawApp class that are required to support the new Doc/View classes. The OwlMain function remains unchanged.

InitMainWindow function

The InitMainWindow function requires some minor changes to support the Doc/View model:

InitInstance function

The InitInstance function is overridden because there are a couple of function calls that need to be made after the main window has been created. InitInstance should first call the TApplication version of InitInstance. That function calls the InitMainWindow function, which constructs the main window object, then creates the main window.

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:


Adding functions to TDrawApp

The TDrawApp class adds a number of new functions. It overrides the TApplication version of InitInstance. It adds a response table and takes the CmAbout function from the TDrawWindow class. It adds drag and drop capability by adding the EV_WM_DROPFILES macro to the response table and adding the EvDropFiles function to handle the event. It also handles a new event, WM_OWLVIEW, that indicates a view request message. Two functions handle this message. EvNewView handles a WM_OWLVIEW message with the dnCreate parameter. EvCloseView handles a WM_OWLVIEW message with the dnClose parameter.

Here's the new declaration of the TDrawApp class, along with its response table definition:

class TDrawApp : public TApplication
    TDrawApp() : TApplication() {}

    // Override methods of TApplication
    void InitInstance();
    void InitMainWindow();

    // Event handlers
    void EvNewView  (TView& view);
    void EvCloseView(TView& view);
    void EvDropFiles(TDropInfo dropInfo);
    void CmAbout();

  EV_OWLVIEW(dnCreate, EvNewView),
  EV_OWLVIEW(dnClose,  EvCloseView),

CmAbout function

The CmAbout function is nearly identical to the TDrawWindow version. The only difference is that the CmAbout function is no longer contained in its parent window class. Instead of using the this pointer as its parent, it substitutes a call to GetMainWindow function. The function should now look like this:

  TDialog(GetMainWindow(), IDD_ABOUT).Execute();

EvDropFiles function

The EvDropFiles function handles the WM_DROPFILES event. This function gets one parameter, a TDropInfo object. The TDropInfo object contains functions to find the number of files dropped, the names of the files, where the files were dropped, and so on.

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:

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)
    delete filePath;

EvNewView function

The WM_OWLVIEW event informs the application when a view-related event has happened. All functions that handle WM_OWLVIEW events return void and take a single parameter, a TView &. When the event's parameter is dnCreate, this indicates that a new view object has been created and requires the application to set up the view's window.

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:

TDrawApp::EvNewView(TView& view)
  if (!view.IsOK())
  else if (view.GetViewMenu())

EvCloseView function

If the parameter for the WM_OWLVIEW event is dnClose, this indicates that a view has been closed. This is handled by the EvCloseView parameter. Like the EvNewView function, the EvCloseView function returns void and takes a TView & parameter.

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:

TDrawApp::EvCloseView(TView& /*view*/)
  GetMainWindow()->SetCaption("Drawing Pad");

Where to find more information

Here's a guide to where you can find more information on the topics introduced in this step:

[Contents] [Prev] [Next]